23 · 变形之后如何修正法线
顶点位置控制几何轮廓,法线控制表面朝向。GPU 修改 position 后不会重新执行 geometry.computeVertexNormals(),所以一块起伏的水面可能仍用“处处朝上”的旧法线计算光照。修正法线要来自变形后的曲面,而不是随便给 normal 加同样偏移。
1. 从切线推导水面法线
在 XZ 平面上定义高度场:
沿 x 和 z 的两条切线为:
选择朝上的叉积顺序,
幅度 A 决定高低,频率 k 也影响坡度。即便振幅不变,加密波纹也会让法线偏得更厉害,因此视觉上高光更破碎。这是公式与视觉效果的直接联系。
2. r185 中法线先于位置计算
Standard/Physical 顶点主函数的相关顺序是:
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#include <normal_vertex>
#include <begin_vertex>
在 beginnormal_vertex 后,objectNormal 已声明,但 transformed 尚未声明。若在这里使用 transformed.x,将产生编译错误。反过来,在 begin_vertex 后改 objectNormal 又太晚:transformedNormal 和输出 varying 已经计算完成。
对本章没有 morph、skinning 的平面,应在 beginnormal_vertex 后用原始 position 计算法线,在 begin_vertex 后用相同 position 对应的函数计算位移。
// 紧接 beginnormal_vertex,仍是局部空间法线。
float demoSlope = uDemoAmplitude * uDemoFrequency *
cos(uDemoFrequency * position.x + uDemoSpeed * uDemoTime);
objectNormal = normalize(vec3(-demoSlope, 1.0, 0.0));
保留 defaultnormal_vertex,让 Three.js 执行实例/批次相关处理、normalMatrix 变换及背面处理。不要再手动乘一次 normalMatrix,否则会重复变换。
完整安装函数在 deformation.js,可运行场景在 water-waves.js。这些函数明确只适用于相应平面参数化,不是任意网格通用法线修正器。
3. 法线如何到达片元
顶点阶段的局部 objectNormal 经过 normalMatrix 进入观察空间,再作为 varying 送到片元。GPU 对三角形内部插值,插值后的长度通常不为 1。内置 normal_fragment_begin 会建立片元法线,后续 normal_fragment_maps 还可能叠加 normalMap 或 bumpMap。
因此“我已经在顶点 normalize 过”并不能取消片元归一化。也不能把世界空间法线直接赋给片元中的观察空间 normal。
如果存在显式 tangent attribute,顶点变形后 tangent 也可能需要更新。只更新 objectNormal 而保留原切线,会让切线空间法线贴图方向失真。本系列基础波面不附切线属性或法线贴图;添加它们时必须扩展整套切线基底,而不只是复制一个 normalMap 参数。
4. 复杂函数:有限差分
解析求导太复杂时,可以对同一个变形函数 D 采样邻近点。以 XZ 参数化平面为例:
vec3 demoCenter = demoDeform(position);
vec3 demoTx = demoDeform(position + vec3(uDemoEpsilon, 0.0, 0.0)) - demoCenter;
vec3 demoTz = demoDeform(position + vec3(0.0, 0.0, uDemoEpsilon)) - demoCenter;
objectNormal = normalize(cross(demoTz, demoTx));
epsilon 的单位是局部长度。太大漏掉细节,太小会因浮点相减损失精度;把模型尺寸、波长和数值范围纳入选择。中心差分使用正负两侧采样更准确,但函数调用更多。复杂噪声的每次调用都可能很贵,还要把阴影 pass 的位置变形成本计入预算。
任意网格不能固定使用局部 X、Z 作为表面切线。应从原表面切线基底取两个方向,或针对特定变形推导雅可比
5. 片元导数的作用与限制
dFdx、dFdy 可以从观察空间表面位置导出几何法线,适合获得三角面法线;内置 flatShading 分支便使用相关思想。它反映的是实际三角形,不会凭空补出顶点网格没有表示的波面细节,也不能让轮廓变细腻。导数位于片元阶段,应避免在非一致控制流里计算依赖导数的表达式。
验证与练习
用单盏斜向 DirectionalLight 和低粗糙度材质观察高光;幅度设 0 时应恢复平面法线。再将 Mesh 做非均匀缩放,正确保留的 normalMatrix 链路应该继续提供相容结果。
- 推导
的法线。 - 将解析法线换为有限差分,比较三个 epsilon 的结果和开销。
- 删除法线补丁,只保留位移,解释哪种观察角度最容易看出错误。
- 找出
transformed在法线阶段不可用的原因,并设计不依赖它的共享计算函数。