URP 渲染实战 / 第 7 章 法线贴图与视差 / 案例 7.6
案例 7.6没有切线的网格:在像素里构造 TBN高阶
目标:网格没有 TANGENT 数据,仍然正确地使用切线空间法线贴图。
7.2 节里求切线的公式用的是三角形的“边”和对应的 UV 差。在像素着色器里,ddx/ddy 正好提供了“相邻像素之间”的位置差 dp 和 UV 差 duv——它们就是一个无穷小三角形的两条边。套用同一个公式:
Assets/Cookbook/Ch07/DerivativeTBN.shader(节选:frame)
// 由 dp = T·du + B·dv 在屏幕 x、y 两个方向各写一次,解出 T = ∂p/∂u、B = ∂p/∂v。 // 乘 sign(det) 保证结果与屏幕 y 轴朝向(平台差异)无关,并自动处理镜像 UV。 half3x3 DerivativeTangentFrame(float3 N, float3 positionWS, float2 uv) { float3 dp1 = ddx(positionWS), dp2 = ddy(positionWS); float2 duv1 = ddx(uv), duv2 = ddy(uv); float det = duv1.x * duv2.y - duv1.y * duv2.x; float s = det < 0 ? -1.0 : 1.0; float3 T = (dp1 * duv2.y - dp2 * duv1.y) * s; float3 B = (dp2 * duv1.x - dp1 * duv2.x) * s; // Gram-Schmidt:去掉与插值法线平行的分量,避免三角形之间出现折痕 T = normalize(T - N * dot(N, T)); B = normalize(B - N * dot(N, B)); return half3x3(T, B, N); }
// 由 dp = T·du + B·dv 在屏幕 x、y 两个方向各写一次,解出 T = ∂p/∂u、B = ∂p/∂v。 // 乘 sign(det) 保证结果与屏幕 y 轴朝向(平台差异)无关,并自动处理镜像 UV。 // (WebGL 的 dFdy 朝上、D3D 的 ddy 朝下,正好靠这个 sign 抵消) mat3 DerivativeTangentFrame(vec3 N, vec3 positionWS, vec2 uv) { vec3 dp1 = dFdx(positionWS), dp2 = dFdy(positionWS); vec2 duv1 = dFdx(uv), duv2 = dFdy(uv); float det = duv1.x * duv2.y - duv1.y * duv2.x; float s = det < 0.0 ? -1.0 : 1.0; vec3 T = (dp1 * duv2.y - dp2 * duv1.y) * s; vec3 B = (dp2 * duv1.x - dp1 * duv2.x) * s; // Gram-Schmidt:去掉与插值法线平行的分量,避免三角形之间出现折痕 T = normalize(T - N * dot(N, T)); B = normalize(B - N * dot(N, B)); return mat3(T, B, N); // GLSL 按列放:列 = T、B、N(HLSL 的 half3x3(T, B, N) 按行放) }
代码里有两个值得注意的处理。第一,公式的分母(UV 雅可比的行列式 det)被省略了,因为最后要归一化,但它的符号不能丢:镜像 UV 会让 det 变号,而不同平台上屏幕 y 轴朝向不同(第 1 章讲过 UNITY_UV_STARTS_AT_TOP),ddy 的符号也会随之翻转。乘上 sign(det) 之后,dp 和 duv 的符号变化会互相抵消,结果与平台无关,镜像 UV 也自然得到正确的副切线方向。第二,屏幕导数在一个三角形内部是常量,直接使用会让 T、B 在三角形之间出现折痕;用插值后的平滑法线 N 做一次 Gram-Schmidt 正交化,可以把折痕基本消除。

这个技巧的代价是每个像素多几次导数和叉乘,以及与离线烘焙器的切线“并不严格一致”——烘焙器按 MikkTSpace 的顶点切线工作,像素导数得到的是逐三角形的切线,在 UV 拉伸剧烈的区域两者会有偏差。所以它最适合程序化网格、三平面映射之外的运行时贴花、以及“没办法拿到切线”的场合;对美术烘焙的角色模型,仍应导入 MikkTSpace 切线。
深入更系统的做法是 Morten Mikkelsen 提出的“表面梯度(Surface Gradient)”框架:把所有凹凸来源(法线贴图、高度图、细节、三平面投影)都转换成表面梯度后相加,再统一还原成法线。HDRP 的分层材质就用了这一框架。思想与本节一脉相承:法线混合的本质是斜率(梯度)的叠加。