cg notes

kindergarten math and some cg notes

这篇文章受到了启发。一般来说我们看到点积都能很快反应出它们和 cos 的关系,相较之下叉积和 sin 的关系经常被忽视。所以决定来推导并复习一下基础数学知识。

首先是定义:

$$\mathbf{a} \times \mathbf{b} = \lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)} \mathbf{n}$$

问题就在于为什么它会 encode sine。为了节省输入公式的时间,我直接从维基百科偷来了公式代码:

$$\mathbf{a}\times\mathbf{b} = (a_2 b_3 – a_3 b_2)\mathbf{i} + (a_3 b_1 – a_1 b_3)\mathbf{j} + (a_1 b_2 – a_2 b_1)\mathbf{k}$$

这意味着右边的长度是 $\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)}$。

可以从两个角度来理解:

  1. 叉积的模代表了两个向量围成平行四边形的面积,而平行四边形的面积很显然是 $\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)}$。
  2. $\frac{\sqrt{(a_2 b_3 – a_3 b_2)^2 + (a_3 b_1 – a_1 b_3)^2 + (a_1 b_2 – a_2 b_1)^2}}{\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert} = 1 – \frac{(a_1 b_1 + a_2 b_2 + a_3 b_3)^2}{(\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert)^2} = 1 – \cos{(\theta)}^2$

我不是故意跳掉那么多步骤的,但输入公式实在太麻烦了。

最后,点积表示了两个向量的“平行程度”,而叉积代表它们的“垂直程度”。也许可以认为这两者都是某向量在对应空间里的长度投影,一如 sine 和 cosine 也是圆半径在两个坐标轴上的投影。

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

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。$$B = N \times T$$,但实际操作的时候好像把 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^(-1) UnityWorldToObjectNormal /