Nintendo 64 上的调色板光照技巧
30fps.net - Pekka Väänänen 的计算机图形和编程
Nintendo 64 上的调色板光照技巧
2025 年 5 月 17 日 本文是我四月份在 Bluesky thread 上的内容的延续。 我们为 Revision 2025 制作了一个 Nintendo 64 的演示! 它具有烘焙光照、法线贴图和实时镜面反射着色,嗯,某种程度上是这样。稍后会详细介绍。优美的歌曲由 noby 创作,吉他由 Moloko (https://soundcloud.com/sou_andrade) 演奏。 下面是我开发的定向环境光和法线贴图技术的一些笔记。 最终它们都非常简单,但我还没有看到它们在其他地方被使用过。
等等,N64 上的法线贴图?
我知道 N64 上可以实现法线贴图,这要归功于其他自制游戏开发者 WadeTyhon 和 Spooky Iluha 早期的实验。 我自己也做过一些浮雕凹凸贴图的 hack。
本文中解释的方法并不新鲜:渲染器在运行时直接将光照计算到纹理中。 它的优点是不需要专门的硬件支持,并且可以在 CPU 上运行任意着色代码。 可惜它太慢了……
调色板着色
所以现在的想法是在 CPU 上进行纹理空间着色。 但是,如果我们对调色板纹理进行着色呢? 无论如何,这些在 N64 上非常常见。 在这种情况下,只需更新_调色板_,纹理就会响应,就好像我们为每个纹素计算了光照一样。 立竿见影的节省!
“调色板空间”着色的演示。 当调色板更新时,完整的纹理也会更新。 当映射到对象时,它看起来好像着色已更改。
原始调色板被阴影调色板替换,并将调色板纹理作为常规纹理应用于对象。 仅使用漫反射 “dot(N,L)” 光照,结果看起来非常好:
上面土豆形岩石网格的另一个视图。
在上面的示例中,我还通过撤消颜色纹理的伽马校正来在线性空间中进行着色 :) 在最终的演示中,这是不可能的,因为我将环境光和直射光项分开,由 N64 的 RDP 单元在硬件中组合。
对象空间法线贴图
通常,法线贴图是在切线空间中完成的。 这样你就可以使用重复纹理,并且精细的法线可以平滑地调节变化的顶点法线。 单色的切线空间法线贴图代表一个光滑的表面。
对象空间法线更简单但限制更多。 现在,法线贴图的纹素不代表与顶点法线的偏差,而是代表绝对表面法线。 运行时数学变得更简单 - 只需从纹理中读取颜色 - 但现在所有表面点都需要唯一的纹素,就像在光照贴图中一样。
早期实验,用于验证高分辨率法线贴图上的方法。**左图:**原始对象空间法线贴图。 **右图:**压缩为 32 色调色板。
设计共享的漫反射和法线调色板
这些对象同时具有漫反射纹理(底色 * ao)和法线贴图。 实际上,两种纹理共享相同的调色板索引,这些索引是我使用 scikit-learn 的 K-means clustering 生成的。 为了使其正常工作,这些图像被解释为单个六通道图像。
下面是一个示例,说明了使用切线空间法线贴图的压缩效果。
屋顶瓦片纹理压缩示例。 RGB 漫反射纹理和法线贴图都以共享调色板索引的方式压缩为 16 色调色板图像。 因此,实际的图像数据只需以每像素 4 位存储一次。
在着色时(可以在加载时或每帧发生),每个调色板颜色都在一个 for 循环中处理。 单个索引用于获取法线和表面漫反射颜色。 然后,CPU 侧的着色器代码为该索引生成新的 RGB 颜色。 循环的结果是一个新的调色板,但应用了着色。
不幸的是,这种方法只适用于定向光。 仅使用调色板也很难表示任何类型的阴影。 这就是为什么我开始研究烘焙光照如何融入等式。
烘焙定向环境光和太阳光
我希望演示文稿有一个具有逼真光照的建筑物。 也许这有点太雄心勃勃😅 经过一番深思熟虑,我将环境光和直射太阳光分别放入顶点颜色 RGB 和 alpha 通道中。 环境光项进一步分为方向强度(灰度环境贴图)和颜色(顶点 RGB,具有饱和度提升)。 太阳是一种定向光,其可见性在顶点 alpha 中传输。
因此,着色公式如下:
ambient = vertex_rgb * grey_irradiance_map(N)
direct = vertex_alpha * sun_color * dot(N, sun_dir)
color = diffuse_texture * (ambient + direct)
以下是不同术语的外观:
光照分解。
请注意,杂乱的“太阳可见性”顶点颜色是如何被右下角的太阳 (N.L) 计算整齐地屏蔽掉的。 最后,将环境光项和直射光项相加,得到下面的阴影结果。
阴影结果。
关于定向环境光,即使烘焙光照很粗糙,纹理中的细节仍然使其看起来非常高端。 考虑一下这个场景,它只有一个彩色模糊的环境贴图和每个顶点的环境光遮蔽:
基于图像的环境光照。 在此图像中,仅启用了环境天空光。 还显示了使用的调色板(左上角)。
它真的很突出! 我喜欢基于图像的光照。
对于模糊的环境贴图,为了简单起见,我使用了等矩形投影。 Polyhaven 的 HDRIs 已经使用了投影。 由于我预先计算了加载时的着色,因此复杂的采样数学不是问题。
64x32 环境贴图(右)在模糊为辐照度贴图之前的可视化。 左侧的点球显示了映射到单位球方向的图像像素。
使用重复纹理着色更大的模型
我最初为单个对象设计了着色算法,并且仅使用您在开始时看到的 potato_rock.obj
对其进行了测试。 对于演示,城堡网格的重复纹理构成了一个问题。 作为一种解决方法,我将大型网格拆分为子网格,每个子网格在概念上共享相同的对象空间法线贴图。
这项任务主要由我自己在 Blender 中手动完成,方法是按材质和表面方向对几何体进行分组。 计算机通过为每个组计算基于多边形法线的世界到模型矩阵来完成其工作。 这几乎是一个近似的切线空间。 所以我最终无法逃脱它们!
这些组中的每一个都共享一个调色板,因此作为一个整体,它们的光照仅在平均意义上是正确的。
简单立方体的切线空间基向量可视化。 在最终模型中,许多指向大致相同方向的多边形必须共享相同的切线空间。
切线空间在运行时_未_进行插值,这表现为刻面光照。 这可能是这项技术的最大缺点。
由于切线空间在多边形上是恒定的,因此光照在这个拱门上没有平滑地插值,这与适当的切线空间法线贴图不同。
镜面反射着色
由于现在许多表面点共享相同的阴影颜色,因此无法正确计算点光或镜面反射着色。 “调色板空间”方法仅真正适用于漫反射定向光,因为着色公式不需要 “to camera” 向量 V ,这取决于着色表面点的位置。 但我仍然尝试对其进行 hack,以用于镜面反射 :)
如果我们将要着色的对象近似为一个球体,那么要着色的点 p 仅仅是 p=radius*normal
。 我们还必须接受结果看起来会呈刻面,因为许多表面点共享相同的调色板索引。
菲涅尔着色。 在光照计算中,雕塑被近似为一个拉伸的球体。
在演示中,镜面高光看起来有点滑稽,但它们似乎仍然欺骗了大多数人。 我认为这是一个成功。
这是未来吗?
在演示中,我试图隐藏该技术的主要局限性:着色不连续、仅支持灰度纹理 (!)、没有点光。 因此,它实际上仅适用于详细的预处理。 我很乐意看到以某种方式解决着色不连续问题(Spooky Iluha 的技术没有它),而不会失去对环境光和直射光的支持。 我不知道是否有可能,但这就是这个爱好的乐趣所在 :)
PAL 兼容的 N64 ROM 可用,但请注意它经常崩溃。
我还打算写一本书。如果您有兴趣,请在此处注册。
Mastodon Bluesky YouTube Feed
30fps.net