<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>cg notes</title>
    <link>https://matterofti.me/graphics/</link>
    <description>kindergarten math and some cg notes</description>
    <pubDate>Sun, 03 May 2026 19:28:53 +0000</pubDate>
    <item>
      <title>Cross products encode sines</title>
      <link>https://matterofti.me/graphics/cross-products-encode-sines</link>
      <description>&lt;![CDATA[看这篇文章受到了启发。一般来说我们看到点积都能很快反应出它们和 cos 的关系，相较之下叉积和 sin 的关系经常被忽视。所以决定来推导并复习一下基础数学知识。 &#xA;&#xA;首先是定义：&#xA;&#xA;$$\mathbf{a} \times \mathbf{b} = \lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)} \mathbf{n}$$&#xA;&#xA;问题就在于为什么它会 encode sine。为了节省输入公式的时间，我直接从维基百科偷来了公式代码：&#xA;&#xA;$$\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}$$&#xA;&#xA;这意味着右边的长度是 $\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)}$。&#xA;&#xA;可以从两个角度来理解：&#xA;&#xA;叉积的模代表了两个向量围成平行四边形的面积，而平行四边形的面积很显然是 $\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)}$。&#xA;$\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$&#xA;&#xA;我不是故意跳掉那么多步骤的，但输入公式实在太麻烦了。&#xA;&#xA;最后，点积表示了两个向量的“平行程度”，而叉积代表它们的“垂直程度”。也许可以认为这两者都是某向量在对应空间里的长度投影，一如 sine 和 cosine 也是圆半径在两个坐标轴上的投影。&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>看<a href="https://www.iquilezles.org/www/articles/noacos/noacos.htm" rel="nofollow">这篇文章</a>受到了启发。一般来说我们看到点积都能很快反应出它们和 cos 的关系，相较之下叉积和 sin 的关系经常被忽视。所以决定来推导并复习一下基础数学知识。</p>

<p>首先是定义：</p>

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

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

<p>$$\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}$$</p>

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

<p>可以从两个角度来理解：</p>
<ol><li>叉积的模代表了两个向量围成平行四边形的面积，而平行四边形的面积很显然是 $\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert \sin{(\theta)}$。</li>
<li>$\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$</li></ol>

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

<p>最后，点积表示了两个向量的“平行程度”，而叉积代表它们的“垂直程度”。也许可以认为这两者都是某向量在对应空间里的长度投影，一如 sine 和 cosine 也是圆半径在两个坐标轴上的投影。</p>
]]></content:encoded>
      <guid>https://matterofti.me/graphics/cross-products-encode-sines</guid>
      <pubDate>Mon, 07 Sep 2020 07:02:10 +0000</pubDate>
    </item>
    <item>
      <title>Unity 法线之旅</title>
      <link>https://matterofti.me/graphics/unity-fa-xian-zhi-lu</link>
      <description>&lt;![CDATA[最近遇到一个需求，处理法线的流程大致如下：&#xA;&#xA;image.png&#xA;&#xA;两张切线空间的法线，在切线空间里做一个混合，再转换到世界空间做一些操作。&#xA;&#xA;而且这个流程会拆成两半，一半放在 surface shader 里，另一半丢进 LightingModel。&#xA;&#xA;起初，我把整个流程写在 LightingModel，传进 s.Normal 做计算，很快发现一个问题：unity 的 surface shader 处理会自动把 s.Normal 转成世界空间的。&#xA;&#xA;这就意味着我们必须拥有自己的 output struct field，例如新写一个 s.NormalT，然后把 binormal, tangent 这些都传到 LightingModel 里……等等，为什么不直接在 surface shader 里计算世界空间的混合结果呢？&#xA;&#xA;也就是说：&#xA;&#xA;image.png&#xA;&#xA;想清楚就可以开工了，首先在 input struct 里加入下面的内容：&#xA;&#xA;struct Input&#xA;{&#xA;&#x9;half4 tangentT; // 注意不能直接用 tangent，会报错&#xA;&#x9;half3 normalW; // 也不能直接用 worldNormal，因为 o.Normal 被写了&#xA;&#x9;...&#xA;}&#xA;&#xA;然后是 output struct：&#xA;&#xA;struct SurfaceOutput {&#xA;  half3 BlendedNormalW;&#xA;  ...&#xA;}&#xA;&#xA;vert:&#xA;&#xA;o.tangentT.xyz = UnityObjectToWorldDir(v.tangent.xyz);&#xA;o.tangentT.w = v.tangent.w; // specifies tangent direction&#xA;o.normalW = UnityObjectToWorldNormal(v.tangent.xyz);&#xA;&#xA;surf: &#xA;&#xA;half3 noiseNormal = tex2D(NoiseTex, uv);&#xA;noiseNormal = 2.h  noiseNormal - 1.h;&#xA;half3 binormal = cross(IN.tangentT.xyz, IN.normalW)  IN.tangentT.w;&#xA;half3x3 rotation = half3x3(IN.tangentT.xyz, binormal, IN.normalW);&#xA;o.BlendedNormalW = mul(rotation, noiseNormal + o.Normal);&#xA;&#xA;最后把 output struct 传进 LightingModel 就可以了。&#xA;&#xA;总结&#xA;&#xA;在 unity surface shader 里：&#xA;&#xA;| 位置          | 名称      | 空间    |&#xA;| ------------- | --------- | ------- |&#xA;| vert          | v.normal  | object  |&#xA;| surf          | IN.normal | tangent |&#xA;| LightingModel | s.Normal  | world   |&#xA;&#xA;TODO&#xA;&#xA;这篇文还没写完，几个还需要细讲的地方记录一下：&#xA;&#xA;UNITYINITIALIZEOUTPUT&#xA;TANGENTSPACEROTATION&#xA;worldNormal 的写入问题&#xA;INTERNALDATA&#xA;怎么辨别法线所在的空间（基础）&#xA;怎么灵活转换各种空间的法线&#xA;&#xA;---&#xA;&#xA;5.15 修订&#xA;&#xA;勘误和补记&#xA;&#xA;之前构造 rotation matrix 时，使用了切线空间的法线和世界空间的切线，铸成大错。&#xA;&#xA;所以我们需要自己做一个世界空间法线。worldNormal 这个名字被污染了，用不了，重定义一个 normalW 来转换。&#xA;&#xA;另外，关于 binormal 的详细解释请参见这个 post。$$B = N \times T$$，但实际操作的时候好像把 tangent 放在前面也没事（待进一步验证）。tangent.w 这个分量则是为了调整左右镜像模型的切线方向设置的。&#xA;&#xA;辨别&#xA;&#xA;tangent space normal 和普通的法线图长得一样&#xA;object space normal 和世界法线差不多，呈现出十字形，但相对于物体固定&#xA;world space normal 相对于世界坐标旋转&#xA;&#xA;转换&#xA;&#xA;| ⇒       | tangent           | object                     | world                      |&#xA;| ------- | ----------------- | -------------------------- | -------------------------- |&#xA;| tangent | /                 | tTw ⸰ wTo          | rotation                   |&#xA;| object  | oTw ⸰ wTt | /                          | UnityObjectToWorldNormal |&#xA;| world   | rotation^(-1) | UnityWorldToObjectNormal | /                          |&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>最近遇到一个需求，处理法线的流程大致如下：</p>

<p><img src="https://i.loli.net/2020/01/22/jGk7f5KrFpd8SAD.png" alt="image.png"></p>

<p>两张<strong>切线空间</strong>的法线，在<strong>切线空间</strong>里做一个混合，再转换到<strong>世界空间</strong>做一些操作。</p>

<p>而且这个流程会拆成两半，一半放在 surface shader 里，另一半丢进 LightingModel。</p>

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

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

<p>也就是说：</p>

<p><img src="https://i.loli.net/2020/01/22/2l74hOrk1sJgXtj.png" alt="image.png"></p>

<p>想清楚就可以开工了，首先在 input struct 里加入下面的内容：</p>

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

<p>然后是 output struct：</p>

<pre><code>struct SurfaceOutput {
  half3 BlendedNormalW;
  ...
}
</code></pre>

<p>vert:</p>

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

<p>surf:</p>

<pre><code>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);
</code></pre>

<p>最后把 output struct 传进 LightingModel 就可以了。</p>

<h3 id="总结">总结</h3>

<p>在 unity surface shader 里：</p>

<table>
<thead>
<tr>
<th>位置</th>
<th>名称</th>
<th>空间</th>
</tr>
</thead>

<tbody>
<tr>
<td>vert</td>
<td>v.normal</td>
<td>object</td>
</tr>

<tr>
<td>surf</td>
<td>IN.normal</td>
<td>tangent</td>
</tr>

<tr>
<td>LightingModel</td>
<td>s.Normal</td>
<td>world</td>
</tr>
</tbody>
</table>

<h3 id="todo">TODO</h3>

<p>这篇文还没写完，几个还需要细讲的地方记录一下：</p>
<ul><li><code>UNITY_INITIALIZE_OUTPUT</code></li>
<li><code>TANGENT_SPACE_ROTATION</code></li>
<li>worldNormal 的写入问题</li>
<li><code>INTERNAL_DATA</code></li>
<li>怎么辨别法线所在的空间（基础）</li>
<li>怎么灵活转换各种空间的法线</li></ul>

<hr>

<p>5.15 修订</p>

<h3 id="勘误和补记">勘误和补记</h3>

<p>之前构造 <code>rotation</code> matrix 时，使用了切线空间的法线和世界空间的切线，铸成大错。</p>

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

<p>另外，关于 binormal 的详细解释请参见<a href="https://catlikecoding.com/unity/tutorials/rendering/part-6/" rel="nofollow">这个 post</a>。$$B = N \times T$$，但实际操作的时候好像把 tangent 放在前面也没事（待进一步验证）。<code>tangent.w</code> 这个分量则是为了调整左右镜像模型的切线方向设置的。</p>

<h3 id="辨别">辨别</h3>
<ul><li>tangent space normal 和普通的法线图长得一样</li>
<li>object space normal 和世界法线差不多，呈现出十字形，但相对于物体固定</li>
<li>world space normal 相对于世界坐标旋转</li></ul>

<h3 id="转换">转换</h3>

<table>
<thead>
<tr>
<th>⇒</th>
<th>tangent</th>
<th>object</th>
<th>world</th>
</tr>
</thead>

<tbody>
<tr>
<td>tangent</td>
<td>/</td>
<td>tTw ⸰ wTo</td>
<td>rotation</td>
</tr>

<tr>
<td>object</td>
<td>oTw ⸰ wTt</td>
<td>/</td>
<td><code>UnityObjectToWorldNormal</code></td>
</tr>

<tr>
<td>world</td>
<td>rotation^(-1)</td>
<td><code>UnityWorldToObjectNormal</code></td>
<td>/</td>
</tr>
</tbody>
</table>
]]></content:encoded>
      <guid>https://matterofti.me/graphics/unity-fa-xian-zhi-lu</guid>
      <pubDate>Tue, 19 May 2020 02:22:51 +0000</pubDate>
    </item>
  </channel>
</rss>