最近遇到一个需求,处理法线的流程大致如下:

image.png

两张切线空间的法线,在切线空间里做一个混合,再转换到世界空间做一些操作。

而且这个流程会拆成两半,一半放在 surface shader 里,另一半丢进 LightingModel。

起初,我把整个流程写在 LightingModel,传进 s.Normal 做计算,很快发现一个问题:unity 的 surface shader 处理会自动把 s.Normal 转成世界空间的。

这就意味着我们必须拥有自己的 output struct field,例如新写一个 s.NormalT,然后把 binormal, tangent 这些都传到 LightingModel 里……等等,为什么不直接在 surface shader 里计算世界空间的混合结果呢?

也就是说:

image.png

想清楚就可以开工了,首先在 input struct 里加入下面的内容:

struct Input
{
	half4 tangentT; // 注意不能直接用 tangent,会报错
	half3 normalW; // 也不能直接用 worldNormal,因为 o.Normal 被写了
	...
}

然后是 output struct:

struct SurfaceOutput {
  half3 BlendedNormalW;
  ...
}

vert:

o.tangentT.xyz = UnityObjectToWorldDir(v.tangent.xyz);
o.tangentT.w = v.tangent.w; // specifies tangent direction
o.normalW = UnityObjectToWorldNormal(v.tangent.xyz);

surf:

half3 noiseNormal = tex2D(_NoiseTex, uv);
noiseNormal = 2.h * noiseNormal - 1.h;
half3 binormal = cross(IN.tangentT.xyz, IN.normalW) * IN.tangentT.w;
half3x3 rotation = half3x3(IN.tangentT.xyz, binormal, IN.normalW);
o.BlendedNormalW = mul(rotation, noiseNormal + o.Normal);

最后把 output struct 传进 LightingModel 就可以了。

总结

在 unity surface shader 里:

位置 名称 空间
vert v.normal object
surf IN.normal tangent
LightingModel s.Normal world

TODO

这篇文还没写完,几个还需要细讲的地方记录一下:

  • UNITY_INITIALIZE_OUTPUT
  • TANGENT_SPACE_ROTATION
  • worldNormal 的写入问题
  • INTERNAL_DATA
  • 怎么辨别法线所在的空间(基础)
  • 怎么灵活转换各种空间的法线

5.15 修订

勘误和补记

之前构造 rotation matrix 时,使用了切线空间的法线和世界空间的切线,铸成大错。

所以我们需要自己做一个世界空间法线。worldNormal 这个名字被污染了,用不了,重定义一个 normalW 来转换。

另外,关于 binormal 的详细解释请参见这个 post,但实际操作的时候好像把 tangent 放在前面也没事(待进一步验证)。tangent.w 这个分量则是为了调整左右镜像模型的切线方向设置的。

辨别

  • tangent space normal 和普通的法线图长得一样
  • object space normal 和世界法线差不多,呈现出十字形,但相对于物体固定
  • world space normal 相对于世界坐标旋转

转换

tangent object world
tangent / tTw wTo rotation
object oTw wTt / UnityObjectToWorldNormal
world rotation UnityWorldToObjectNormal /