39 · csm_Normal 与 csm_FragNormal:两种不同层次的法线
法线表示表面朝向,而不只是一个配合位置使用的装饰变量。光照中的点积依赖它;位置变了但法线没变,轮廓和明暗便可能互相矛盾。本章依据 CSM 6.4.0 实现,特别纠正该版本 README 的类型笔误:csm_FragNormal 是 vec3,不是 float。
1. 两个输出的职责
| 输出 | 阶段与空间 | 默认值 | 实际接管位置 |
|---|---|---|---|
csm_Normal |
顶点,物体局部空间 | attribute normal |
beginnormal_vertex 中的 objectNormal 初值 |
csm_FragNormal |
片元,观察空间 | 平滑法线 normalize(vNormal),或平面着色的导数法线 |
normal_fragment_maps 执行后覆盖 normal |
csm_Normal 的替换还保留了 USE_TANGENT 下 objectTangent = tangent.xyz 的声明。它没有根据新法线重新生成切线;如果几何形变同时改变了切线基底,不能假设使用旧 tangent 的法线贴图仍准确。
支持范围也不能只看 README 的“有 normalMap 的材质”:Normal、Phong、Standard、Physical 等带对应法线流程的材质可以使用片元法线,即使没有提供 normalMap。Basic/Points 没有本章的光照法线消费链路,不应拿它们演示这个输出。
2. 顶点法线会继续参与内置变换
CSM 输出进入 objectNormal 后,还会通过 morph 法线、骨骼、实例/模型变换,再传到片元阶段。对于普通 Mesh,法线从局部到观察空间使用模型观察矩阵的逆转置(normalMatrix):
非均匀缩放下不能直接乘普通位置矩阵,否则不再与切平面垂直。片元接收的是顶点 varying 的插值结果,它通常不再是单位长度;内置法线流程会再次归一化。
手动写 objectNormal = ... 的对应 CSM 写法是 csm_Normal = ...。它发生在法线变换前,所以不要先乘 normalMatrix,否则会重复转换。
3. 从水面偏导得到 csm_Normal
第 37 章曲面为
为了朝向局部 +Y,取
float slopeX = uCsmWaveAmplitude * 0.65 * 1.7 * cos(phaseX);
float slopeZ = uCsmWaveAmplitude * 0.35 * 1.2 * cos(phaseZ);
csm_Normal = normalize(vec3(-slopeX, 1.0, -slopeZ));
这是解析法线:无需额外纹理采样,也不用每帧在 CPU 调用 computeVertexNormals()。后者只知道 CPU 几何,无法看到纯 GPU 位移。
解析法线表达连续曲面的切面,低分段网格的实际三角形仍是折线近似。法线再平滑也不能修复粗糙的轮廓;应根据波长与观察距离选细分数。
4. csm_FragNormal 覆盖的是最终普通表面法线
实际补丁为:
#include <normal_fragment_maps>
normal = csm_FragNormal;
这意味着原本的 normalMap/bumpMap 已经计算,但会被这里的值覆盖。自定义 main 在原片元主逻辑前运行,不能写 csm_FragNormal = normal 来读取尚未计算的内置贴图结果。
默认 csm_FragNormal 由 vNormal 或 vViewPosition 的导数构造,并非 normalMap 的结果。源码默认对平滑双面法线乘 gl_FrontFacing ? 1.0 : -1.0;自己全量赋值时,要自己处理所需的朝向逻辑。
覆写后没有替你保证单位长度,因此请输出归一化结果。该输出也不等于清漆层法线,Physical 的 clearcoat 有独立流程。
5. 实例拓展:给平面添加解析细波光照
在水面例子的顶点代码全局声明 varying vec3 vCsmSurfaceNormal;,并在其 main 尾部增加:
vCsmSurfaceNormal = normalMatrix * csm_Normal;
再给 CSM 传入以下 fragmentShader,便得到一个不使用 normalMap 的观察空间法线入口:
varying vec3 vCsmSurfaceNormal;
void main() {
vec3 surfaceNormal = normalize(vCsmSurfaceNormal);
#ifdef DOUBLE_SIDED
surfaceNormal *= gl_FrontFacing ? 1.0 : -1.0;
#endif
csm_FragNormal = surfaceNormal;
}
此变体适用于本例的普通无骨骼 Mesh。它重新做了内置 varying 已能做的事,目的是验证空间;生产代码保留 csm_Normal 即可。若要增加更细的程序化凹凸,可把局部 xz 通过 varying 传到片元,在片元计算额外高度偏导,再将所得局部法线转换到观察空间。不得把局部法线直接写给 csm_FragNormal。
对于蒙皮或实例对象,上面直接 normalMatrix * csm_Normal 的自定义 varying 不包含全部内置法线变换,不能照搬。需要保留原法线贴图并在它之后加工时,手动补丁或精确的 patchMap 往往比这个覆盖输出更适合。
练习(暂不提供答案)
- 对水面施加非均匀 scale,比较
csm_Normal与错误的观察空间提前转换结果。 - 将叉积顺序反转,解释法线与双面渲染所呈现的变化。
- 为片元增加局部高频高度函数,同时明确它只改变光照、不改变轮廓。
- 给材质添加 normalMap,再分别使用与不使用
csm_FragNormal,分析贴图结果为何被覆盖。