<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://xueqingzhe.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://xueqingzhe.github.io/" rel="alternate" type="text/html" hreflang="zh-CN"/><updated>2026-04-23T03:46:33+00:00</updated><id>https://xueqingzhe.github.io/feed.xml</id><title type="html">blank</title><subtitle>Technical Artist specializing in real-time rendering, toon shading, and PBR techniques. </subtitle><entry><title type="html">URP - RendererFeature ：ScreenSpaceOutline</title><link href="https://xueqingzhe.github.io/blog/2026/URP-RendererFeature-ScreenSpaceOutline/" rel="alternate" type="text/html" title="URP - RendererFeature ：ScreenSpaceOutline"/><published>2026-04-02T00:00:00+00:00</published><updated>2026-04-02T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/URP%20-%20RendererFeature%20%EF%BC%9AScreenSpaceOutline</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/URP-RendererFeature-ScreenSpaceOutline/"><![CDATA[<h1 id="继承类别">继承类别</h1> <p><strong>对于RendererFeature就是C#脚本，不过继承类别是ScriptableRendererFeature和ScriptableRenderPass</strong> <strong>一个负责注册，一个负责执行</strong></p> <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// ========== Feature 类：负责创建和注册 Pass ==========</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ScreenSpaceOutline</span> <span class="p">:</span> <span class="n">ScriptableRendererFeature</span>
<span class="c1">// ========== Pass 类：负责实际渲染指令 ==========</span>
<span class="k">internal</span> <span class="k">class</span> <span class="nc">ScreenSpaceOutlinePass</span> <span class="p">:</span> <span class="n">ScriptableRenderPass</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="固定必须实现的函数">固定必须实现的函数</h1> <h2 id="feature类">Feature类：</h2> <ul> <li><code class="language-plaintext highlighter-rouge">Create()</code> — 初始化。只跑一次（或资源变动时重跑）。在这里new Material、new Pass，做一切”准备工作”。</li> <li><code class="language-plaintext highlighter-rouge">AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)</code> — 注册。每帧跑一次。在这里把Pass加入渲染队列，顺便把当帧参数传给Pass。不做初始化，不发GPU指令。</li> </ul> <h2 id="pass类">Pass类：</h2> <ul> <li><code class="language-plaintext highlighter-rouge">Execute(ScriptableRenderContext context, ref RenderingData renderingData)</code> — 每帧到了指定的<code class="language-plaintext highlighter-rouge">renderPassEvent</code>时机就跑。在这里写CommandBuffer，发真正的GPU渲染指令。</li> </ul> <p>这三个不写会报编译错误，因为基类里是<code class="language-plaintext highlighter-rouge">abstract</code>的。</p> <h1 id="feature类-1">Feature类</h1> <h2 id="create">Create()</h2> <h3 id="初始化数据">初始化数据</h3> <p><strong>例如：</strong></p> <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">outlineShader</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> <span class="c1">// shader 没赋值就跳过</span>
<span class="n">outlineMaterial</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Material</span><span class="p">(</span><span class="n">outlineShader</span><span class="p">);</span>
<span class="n">m_Pass</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ScreenSpaceOutlinePass</span><span class="p">(</span><span class="n">outlineMaterial</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="指定插入时机">指定插入时机</h3> <p><img src="/assets/img/TAMonth04/Pasted image 20260402221720.png" alt=""/> <strong>除了初始化，还需要指定Feature的插入时机，这个成员是ScriptableRenderPass类下的renderPassEvent</strong></p> <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">//控制 Pass 插入渲染队列的时机</span>
<span class="n">m_Pass</span><span class="p">.</span><span class="n">renderPassEvent</span> <span class="p">=</span> <span class="n">RenderPassEvent</span><span class="p">.</span><span class="n">AfterRenderingTransparents</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <ul> <li>需要深度图 → 至少<code class="language-plaintext highlighter-rouge">AfterRenderingPrePasses</code>之后</li> <li>需要完整场景画面 → <code class="language-plaintext highlighter-rouge">AfterRenderingOpaques</code>之后</li> <li>想让被Bloom等晕染 → <code class="language-plaintext highlighter-rouge">AfterRenderingTransparents</code></li> <li>想让不受后处理影响 → <code class="language-plaintext highlighter-rouge">AfterRenderingPostProcessing</code></li> </ul> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="n">BeforeRendering</span>                 <span class="c1">// 最早，什么都还没渲染</span>
    <span class="err">↓</span>
<span class="n">BeforeRenderingShadows</span>          <span class="c1">// Shadow Pass之前</span>
<span class="n">AfterRenderingShadows</span>           <span class="c1">// Shadow Pass之后</span>
    <span class="err">↓</span>
<span class="n">BeforeRenderingPrePasses</span>        <span class="c1">// 深度预Pass之前</span>
<span class="n">AfterRenderingPrePasses</span>         <span class="c1">// 深度预Pass之后（深度图已可用）</span>
    <span class="err">↓</span>
<span class="n">BeforeRenderingGbuffer</span>          <span class="c1">// G-Buffer之前</span>
<span class="n">AfterRenderingGbuffer</span>           
    <span class="err">↓</span>
<span class="n">BeforeRenderingOpaques</span>          <span class="c1">// 不透明物体之前</span>
<span class="n">AfterRenderingOpaques</span>           <span class="c1">// 不透明物体之后</span>
    <span class="err">↓</span>
<span class="n">BeforeRenderingSkybox</span>           <span class="c1">// 天空盒之前</span>
<span class="n">AfterRenderingSkybox</span>            
    <span class="err">↓</span>
<span class="n">BeforeRenderingTransparents</span>     <span class="c1">// 透明物体之前</span>
<span class="n">AfterRenderingTransparents</span>      <span class="c1">// 透明物体之后 ← 你现在用的</span>
    <span class="err">↓</span>
<span class="n">BeforeRenderingPostProcessing</span>   <span class="c1">// 后处理之前</span>
<span class="n">AfterRenderingPostProcessing</span>    <span class="c1">// 后处理之后</span>
    <span class="err">↓</span>
<span class="n">AfterRendering</span>                  <span class="c1">// 最晚</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="addrenderpasses">AddRenderPasses()</h2> <h3 id="传参">传参</h3> <p><strong>每帧把参数写入 material，设渲染队列</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403000337.png" alt=""/></p> <h1 id="pass-类">Pass 类</h1> <h2 id="初始化">初始化</h2> <p><strong>设置参数初始化</strong> <img src="/assets/img/TAMonth04/Pasted image 20260402221634.png" alt=""/></p> <h2 id="execute">Execute()</h2> <p><strong>指定RT和Pass，因为RT不能同时读取和写入，所以要先拷贝</strong> <img src="/assets/img/TAMonth04/Pasted image 20260402223608.png" alt=""/></p> <h1 id="c完整代码">C#完整代码</h1> <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</pre></td><td class="rouge-code"><pre><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.Rendering</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.Rendering.Universal</span><span class="p">;</span>

<span class="c1">// ========== Feature 类：负责创建和注册 Pass ==========</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ScreenSpaceOutline</span> <span class="p">:</span> <span class="n">ScriptableRendererFeature</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Shader</span> <span class="n">outlineShader</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">0.5f</span><span class="p">,</span> <span class="m">5f</span><span class="p">)]</span> <span class="k">public</span> <span class="kt">float</span> <span class="n">thickness</span> <span class="p">=</span> <span class="m">1f</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">0f</span><span class="p">,</span> <span class="m">10f</span><span class="p">)]</span>   <span class="k">public</span> <span class="kt">float</span> <span class="n">_DepthThreshold</span> <span class="p">=</span> <span class="m">0.5f</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">0f</span><span class="p">,</span> <span class="m">50f</span><span class="p">)]</span>   <span class="k">public</span> <span class="kt">float</span> <span class="n">_DepthSensitivity</span> <span class="p">=</span> <span class="m">10f</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">0f</span><span class="p">,</span> <span class="m">20.0f</span><span class="p">)]</span>   <span class="k">public</span> <span class="kt">float</span> <span class="n">_NormalThreshold</span> <span class="p">=</span> <span class="m">0.5f</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">Color</span> <span class="n">outlineColor</span> <span class="p">=</span> <span class="n">Color</span><span class="p">.</span><span class="n">black</span><span class="p">;</span>

    <span class="k">private</span> <span class="n">Material</span> <span class="n">outlineMaterial</span><span class="p">;</span>
    <span class="k">private</span> <span class="n">ScreenSpaceOutlinePass</span> <span class="n">m_Pass</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Create</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">outlineShader</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span> <span class="c1">// shader 没赋值就跳过，不崩</span>
        <span class="n">outlineMaterial</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Material</span><span class="p">(</span><span class="n">outlineShader</span><span class="p">);</span>
        <span class="n">m_Pass</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ScreenSpaceOutlinePass</span><span class="p">(</span><span class="n">outlineMaterial</span><span class="p">);</span>
        <span class="c1">//控制 Pass 插入渲染队列的时机</span>
        <span class="n">m_Pass</span><span class="p">.</span><span class="n">renderPassEvent</span> <span class="p">=</span> <span class="n">RenderPassEvent</span><span class="p">.</span><span class="n">AfterRenderingTransparents</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">AddRenderPasses</span><span class="p">(</span><span class="n">ScriptableRenderer</span> <span class="n">renderer</span><span class="p">,</span> <span class="k">ref</span> <span class="n">RenderingData</span> <span class="n">renderingData</span><span class="p">)</span>
    <span class="p">{</span>   
        <span class="k">if</span> <span class="p">(</span><span class="n">m_Pass</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
        <span class="c1">// 每帧把参数写入 material</span>
        <span class="n">outlineMaterial</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"_OutlineThickness"</span><span class="p">,</span> <span class="n">thickness</span><span class="p">);</span>
        <span class="n">outlineMaterial</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"_DepthThreshold"</span><span class="p">,</span> <span class="n">_DepthThreshold</span><span class="p">);</span>
        <span class="n">outlineMaterial</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"_DepthSensitivity"</span><span class="p">,</span> <span class="n">_DepthSensitivity</span><span class="p">);</span>
        <span class="n">outlineMaterial</span><span class="p">.</span><span class="nf">SetFloat</span><span class="p">(</span><span class="s">"_NormalThreshold"</span><span class="p">,</span> <span class="n">_NormalThreshold</span><span class="p">);</span>
        <span class="n">outlineMaterial</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="s">"_OutlineColor"</span><span class="p">,</span> <span class="n">outlineColor</span><span class="p">);</span>
        <span class="c1">//分配RT</span>
        <span class="n">m_Pass</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">renderer</span><span class="p">.</span><span class="n">cameraColorTarget</span><span class="p">);</span>
        <span class="c1">//加入渲染队列</span>
        <span class="n">renderer</span><span class="p">.</span><span class="nf">EnqueuePass</span><span class="p">(</span><span class="n">m_Pass</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// ========== Pass 类：负责实际渲染指令 ==========</span>
<span class="k">internal</span> <span class="k">class</span> <span class="nc">ScreenSpaceOutlinePass</span> <span class="p">:</span> <span class="n">ScriptableRenderPass</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="n">Material</span> <span class="n">material</span><span class="p">;</span>
    <span class="k">private</span> <span class="n">RenderTargetIdentifier</span> <span class="n">cameraColorTarget</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ScreenSpaceOutlinePass</span><span class="p">(</span><span class="n">Material</span> <span class="n">mat</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">material</span> <span class="p">=</span> <span class="n">mat</span><span class="p">;</span>
        
        <span class="c1">//开启法线</span>
        <span class="c1">//this.requiresIntermediateTexture = true;</span>
        <span class="c1">// 告诉渲染管线此 Pass 需要颜色信息</span>
        <span class="nf">ConfigureInput</span><span class="p">(</span><span class="n">ScriptableRenderPassInput</span><span class="p">.</span><span class="n">Color</span><span class="p">);</span>
        <span class="nf">ConfigureInput</span><span class="p">(</span><span class="n">ScriptableRenderPassInput</span><span class="p">.</span><span class="n">Normal</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Setup</span><span class="p">(</span><span class="n">RenderTargetIdentifier</span> <span class="n">colorTarget</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">cameraColorTarget</span> <span class="p">=</span> <span class="n">colorTarget</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Execute</span><span class="p">(</span><span class="n">ScriptableRenderContext</span> <span class="n">context</span><span class="p">,</span> <span class="k">ref</span> <span class="n">RenderingData</span> <span class="n">renderingData</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">CommandBuffer</span> <span class="n">cmd</span> <span class="p">=</span> <span class="n">CommandBufferPool</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">"ScreenSpaceOutlinePass"</span><span class="p">);</span>

        <span class="n">RenderTextureDescriptor</span> <span class="n">desc</span> <span class="p">=</span> <span class="n">renderingData</span><span class="p">.</span><span class="n">cameraData</span><span class="p">.</span><span class="n">cameraTargetDescriptor</span><span class="p">;</span>
        <span class="c1">//申请不带深度的RT</span>
        <span class="n">desc</span><span class="p">.</span><span class="n">depthBufferBits</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>
        <span class="c1">//申请临时RT</span>
        <span class="n">cmd</span><span class="p">.</span><span class="nf">GetTemporaryRT</span><span class="p">(</span><span class="n">Shader</span><span class="p">.</span><span class="nf">PropertyToID</span><span class="p">(</span><span class="s">"_TempRT"</span><span class="p">),</span> <span class="n">desc</span><span class="p">);</span>
        <span class="c1">//拷贝A-&gt;b</span>
        <span class="n">cmd</span><span class="p">.</span><span class="nf">Blit</span><span class="p">(</span><span class="n">cameraColorTarget</span><span class="p">,</span> <span class="n">Shader</span><span class="p">.</span><span class="nf">PropertyToID</span><span class="p">(</span><span class="s">"_TempRT"</span><span class="p">));</span>

        <span class="c1">// 把场景颜色显式绑定到 Pass的_MainTex</span>
        <span class="n">cmd</span><span class="p">.</span><span class="nf">SetGlobalTexture</span><span class="p">(</span><span class="s">"_MainTex"</span><span class="p">,</span> <span class="n">Shader</span><span class="p">.</span><span class="nf">PropertyToID</span><span class="p">(</span><span class="s">"_TempRT"</span><span class="p">));</span>

        <span class="c1">//Pass处理_TempRT后在拷贝回去</span>
        <span class="n">cmd</span><span class="p">.</span><span class="nf">Blit</span><span class="p">(</span><span class="n">Shader</span><span class="p">.</span><span class="nf">PropertyToID</span><span class="p">(</span><span class="s">"_TempRT"</span><span class="p">),</span> <span class="n">cameraColorTarget</span><span class="p">,</span> <span class="n">material</span><span class="p">);</span>

        <span class="c1">//释放内存</span>
        <span class="n">cmd</span><span class="p">.</span><span class="nf">ReleaseTemporaryRT</span><span class="p">(</span><span class="n">Shader</span><span class="p">.</span><span class="nf">PropertyToID</span><span class="p">(</span><span class="s">"_TempRT"</span><span class="p">));</span>
        
        <span class="c1">//执行</span>
        <span class="n">context</span><span class="p">.</span><span class="nf">ExecuteCommandBuffer</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
        <span class="c1">//释放内存</span>
        <span class="n">CommandBufferPool</span><span class="p">.</span><span class="nf">Release</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="描边pass">描边Pass</h1> <p><strong>这里使用Sobel算子来计算，开销还是比较大的，可以换成其它算子</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403000428.png" alt=""/></p> <h2 id="场景深度描边">场景深度描边</h2> <p><strong>正常情况下，如果阈值固定，深度计算出的描边会有瑕疵，过小平面会有问题</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403000556.png" alt=""/> <strong>过大，近处描边则会缺失</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403000907.png" alt=""/> <strong>这里使用深度进行适当放大阈值，可以较好解决这个问题</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403000922.png" alt=""/></p> <h2 id="场景法线描边">场景法线描边</h2> <p><strong>至于细节方面使用场景法线描边进行混合，阈值小的话还是比较乱的</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403001030.png" alt=""/></p> <h2 id="pass完整代码">Pass完整代码</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"Hidden/ScreenSpaceOutline"</span>
<span class="p">{</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderType"</span> <span class="o">=</span> <span class="s">"Opaque"</span> <span class="s">"RenderPipeline"</span> <span class="o">=</span> <span class="s">"UniversalPipeline"</span> <span class="p">}</span>
        <span class="n">Cull</span> <span class="n">Off</span> <span class="n">ZWrite</span> <span class="n">Off</span> <span class="n">ZTest</span> <span class="n">Always</span>

        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">HLSLPROGRAM</span>
            <span class="cp">#pragma vertex vert
</span>            <span class="cp">#pragma fragment frag
</span>            <span class="cp">#include</span> <span class="cpf">"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"</span><span class="cp">
</span>            <span class="cp">#include</span> <span class="cpf">"Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"</span><span class="cp">
</span>            <span class="cp">#include</span> <span class="cpf">"Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"</span><span class="cp">
</span>
            <span class="k">struct</span> <span class="n">Attributes</span>
            <span class="p">{</span>
                <span class="n">float4</span> <span class="n">positionOS</span> <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
            <span class="p">};</span>

            <span class="k">struct</span> <span class="n">Varyings</span>
            <span class="p">{</span>
                <span class="n">float4</span> <span class="n">positionHCS</span> <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">uv</span> <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
            <span class="p">};</span>

            <span class="n">TEXTURE2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">);</span>
            <span class="n">SAMPLER</span><span class="p">(</span><span class="n">sampler_MainTex</span><span class="p">);</span>

            <span class="c1">// 描边参数</span>
            <span class="kt">float</span> <span class="n">_OutlineThickness</span><span class="p">;</span>     <span class="c1">// 采样偏移距离（像素单位）</span>
            <span class="kt">float</span> <span class="n">_DepthThreshold</span><span class="p">;</span>       <span class="c1">// 深度边缘判定阈值</span>
            <span class="kt">float</span> <span class="n">_DepthSensitivity</span><span class="p">;</span>     <span class="c1">// 深度灵敏度</span>
            <span class="kt">float</span> <span class="n">_NormalThreshold</span><span class="p">;</span>      <span class="c1">// 法线边缘判定阈值</span>
            <span class="n">float4</span> <span class="n">_OutlineColor</span><span class="p">;</span>        <span class="c1">// 描边颜色</span>

            <span class="n">Varyings</span> <span class="n">vert</span><span class="p">(</span><span class="n">Attributes</span> <span class="n">input</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">Varyings</span> <span class="n">output</span> <span class="o">=</span> <span class="p">(</span><span class="n">Varyings</span><span class="p">)</span><span class="mi">0</span><span class="p">;</span>
                <span class="n">output</span><span class="p">.</span><span class="n">positionHCS</span> <span class="o">=</span> <span class="n">TransformObjectToHClip</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">positionOS</span><span class="p">);</span>
                <span class="n">output</span><span class="p">.</span><span class="n">uv</span> <span class="o">=</span> <span class="n">input</span><span class="p">.</span><span class="n">uv</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">output</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="c1">// 采样深度并线性化</span>
            <span class="kt">float</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="kt">float</span> <span class="n">rawDepth</span> <span class="o">=</span> <span class="n">SampleSceneDepth</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">LinearEyeDepth</span><span class="p">(</span><span class="n">rawDepth</span><span class="p">,</span> <span class="n">_ZBufferParams</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="c1">//采样场景法线</span>
            <span class="n">float3</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="k">return</span> <span class="n">SampleSceneNormals</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="n">half4</span> <span class="n">frag</span><span class="p">(</span><span class="n">Varyings</span> <span class="n">input</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_Target</span>
            <span class="p">{</span>
                <span class="n">float2</span> <span class="n">texelSize</span> <span class="o">=</span> <span class="n">_OutlineThickness</span> <span class="o">/</span> <span class="n">_ScreenParams</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
                
                <span class="c1">//默认深度</span>
                <span class="kt">float</span> <span class="n">dBias</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span><span class="p">);</span>
                <span class="c1">// 随着深度增加，允许的阈值应当变大，防止远景产生过多线段</span>
                <span class="kt">float</span> <span class="n">depthThreshold</span> <span class="o">=</span> <span class="n">_DepthThreshold</span> <span class="o">*</span> <span class="n">dBias</span> <span class="o">*</span> <span class="n">_DepthSensitivity</span><span class="p">;</span>

                <span class="c1">// Sobel 算子采样 8 邻域深度</span>
                <span class="kt">float</span> <span class="n">d0</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d1</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d2</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d3</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span>  <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d4</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span>  <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d5</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span>  <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d6</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">0</span><span class="p">,</span>  <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">d7</span> <span class="o">=</span> <span class="n">SampleDepth</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span>  <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>

                <span class="c1">// Sobel X 和 Y 方向梯度</span>
                <span class="kt">float</span> <span class="n">sobelX</span> <span class="o">=</span> <span class="o">-</span><span class="n">d0</span> <span class="o">-</span> <span class="mi">2</span><span class="o">*</span><span class="n">d3</span> <span class="o">-</span> <span class="n">d5</span> <span class="o">+</span> <span class="n">d2</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">d4</span> <span class="o">+</span> <span class="n">d7</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">sobelY</span> <span class="o">=</span> <span class="o">-</span><span class="n">d0</span> <span class="o">-</span> <span class="mi">2</span><span class="o">*</span><span class="n">d1</span> <span class="o">-</span> <span class="n">d2</span> <span class="o">+</span> <span class="n">d5</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">d6</span> <span class="o">+</span> <span class="n">d7</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">depthEdge</span> <span class="o">=</span> <span class="n">sqrt</span><span class="p">(</span><span class="n">sobelX</span> <span class="o">*</span> <span class="n">sobelX</span> <span class="o">+</span> <span class="n">sobelY</span> <span class="o">*</span> <span class="n">sobelY</span><span class="p">);</span>


                <span class="c1">// 法线Sobel采样8邻域</span>
                <span class="n">float3</span> <span class="n">n0</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n1</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">0</span><span class="p">,</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n2</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n3</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n4</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n5</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n6</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">n7</span> <span class="o">=</span> <span class="n">SampleNormal</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">uv</span> <span class="o">+</span> <span class="n">float2</span><span class="p">(</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">texelSize</span><span class="p">);</span>

                <span class="n">float3</span> <span class="n">normalSobelX</span> <span class="o">=</span> <span class="o">-</span><span class="n">n0</span> <span class="o">-</span> <span class="mi">2</span><span class="o">*</span><span class="n">n3</span> <span class="o">-</span> <span class="n">n5</span> <span class="o">+</span> <span class="n">n2</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">n4</span> <span class="o">+</span> <span class="n">n7</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">normalSobelY</span> <span class="o">=</span> <span class="o">-</span><span class="n">n0</span> <span class="o">-</span> <span class="mi">2</span><span class="o">*</span><span class="n">n1</span> <span class="o">-</span> <span class="n">n2</span> <span class="o">+</span> <span class="n">n5</span> <span class="o">+</span> <span class="mi">2</span><span class="o">*</span><span class="n">n6</span> <span class="o">+</span> <span class="n">n7</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">normalEdge</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">normalSobelX</span><span class="p">)</span> <span class="o">+</span> <span class="n">length</span><span class="p">(</span><span class="n">normalSobelY</span><span class="p">);</span>

                <span class="c1">// 深度和法线边缘合并</span>
                <span class="kt">float</span> <span class="n">edge</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="n">step</span><span class="p">(</span><span class="n">depthThreshold</span><span class="p">,</span> <span class="n">depthEdge</span><span class="p">),</span> <span class="n">step</span><span class="p">(</span><span class="n">_NormalThreshold</span><span class="p">,</span> <span class="n">normalEdge</span><span class="p">));</span>

                <span class="c1">//float edge = step(_DepthThreshold, depthEdge);</span>
                <span class="c1">// 采样原始画面颜色</span>
                <span class="n">half4</span> <span class="n">sceneColor</span> <span class="o">=</span> <span class="n">SAMPLE_TEXTURE2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">sampler_MainTex</span><span class="p">,</span> <span class="n">input</span><span class="p">.</span><span class="n">uv</span><span class="p">);</span>

                <span class="c1">// 超过阈值的地方叠加描边颜色</span>
                <span class="kt">float</span> <span class="n">outline</span> <span class="o">=</span> <span class="n">edge</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">lerp</span><span class="p">(</span><span class="n">sceneColor</span><span class="p">,</span> <span class="n">_OutlineColor</span><span class="p">,</span> <span class="n">outline</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="n">ENDHLSL</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="使用方法">使用方法</h1> <p><strong>找到URP设置指定上去就行</strong> <img src="/assets/img/TAMonth04/Pasted image 20260403001322.png" alt=""/></p>]]></content><author><name></name></author><category term="TAMonth04"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[URP - RendererFeature ：ScreenSpaceOutline]]></summary></entry><entry><title type="html">平滑法线处理 - 八面体映射</title><link href="https://xueqingzhe.github.io/blog/2026/%E5%B9%B3%E6%BB%91%E6%B3%95%E7%BA%BF%E5%A4%84%E7%90%86-%E5%85%AB%E9%9D%A2%E4%BD%93%E6%98%A0%E5%B0%84/" rel="alternate" type="text/html" title="平滑法线处理 - 八面体映射"/><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/%E5%B9%B3%E6%BB%91%E6%B3%95%E7%BA%BF%E5%A4%84%E7%90%86%20-%20%E5%85%AB%E9%9D%A2%E4%BD%93%E6%98%A0%E5%B0%84</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/%E5%B9%B3%E6%BB%91%E6%B3%95%E7%BA%BF%E5%A4%84%E7%90%86-%E5%85%AB%E9%9D%A2%E4%BD%93%E6%98%A0%E5%B0%84/"><![CDATA[<p>游戏角色（尤其是NPR/卡通渲染）有个经典问题：硬表面的 <strong>split normal</strong>（分裂法线）在描边和阴影计算时会出现接缝。解决方案是额外烘培一套”平滑法线”——但模型的UV1已经用来存贴图坐标了，怎么传进Shader？</p> <h1 id="范数">范数</h1> <p><strong>在处理八面体之前需要先了解范数</strong></p> <h2 id="简单解释">简单解释</h2> <p>简单来说，它是“<strong>长度</strong>”或“<strong>距离</strong>”这一直觉概念在多维向量空间（甚至函数空间）中的一般化定义。 如果你把一个向量想象成空间中的一个点，那么范数就是描述这个点<strong>离原点有多远</strong>，或者这个<strong>向量有多大</strong>的一个数值。</p> <h2 id="范数的类型">范数的类型</h2> <p><strong>这里只介绍两种相关的范数</strong> <strong>L1范数</strong>（曼哈顿距离）： <strong><em>L1范数可以理解为出租车距离，也就是它的距离是由出租车行驶的每段距离相加，也就是各个分量之和</em></strong></p> \[|v| = |x| + |y| + |z|\] <p><strong>L2范数</strong>（欧几里得长度）： <strong><em>L2范数则是接触最多的，比如求两点之间的直线距离，直角三角形求斜边长度</em></strong></p> \[|v| = \sqrt{(x² + y² + z²)}\] <p><strong>在二维中图像表示如下图</strong> <img src="/assets/img/TAMonth04/Pasted image 20260314195935.png" alt=""/> <img src="/assets/img/TAMonth04/Pasted image 20260314133155.png" alt=""/></p> <h1 id="八面体映射原理">八面体映射原理</h1> <h2 id="三维与二维">三维与二维</h2> <p><strong>从二维图到三维，就是加上Z轴，二维图像其实就是八面体的顶视图，这其实就引出一个问题，八面体是上下对称重叠的，那么如果将下半部分，也就是z&lt;0的部分展开，就可以变成一个平面数据，也就可以变相将三维数据存入二维了</strong> <img src="/assets/img/TAMonth04/Pasted image 20260314195819.png" alt=""/></p> <h2 id="三维映射原理">三维映射原理</h2> <p><strong>对于模型的法线向量来说，归一化后它的范围就是一个球范围，那么如果要映射到八面体上，就可以让X和Y分量除以L1范数</strong> <img src="/assets/img/TAMonth04/Pasted image 20260314201825.png" alt=""/></p> <p><strong>对于Z分量，则是另有用途，前面说过八面体上下是重叠，这里就需要使用Z分量进行处理，如果是在下方，那么X分量和Y分量则需要反转或者说折叠</strong> <img src="/assets/img/TAMonth04/Pasted image 20260314201506.png" alt=""/></p> <h3 id="两种理解方法">两种理解方法</h3> <blockquote> <p>从公式入手推导</p> </blockquote> <p><strong>折叠其实就是相对于边界对称，这里则需要先计算点和边界的距离了，也就是点与直线的距离</strong></p> \[d = \frac{|Ax_0 + By_0 + C|}{\sqrt{A^2 + B^2}}\] <p>这里会有四条边界 第一象限（x&gt;0, y&gt;0）： x + y = 1 第二象限（x&lt;0, y&gt;0）： -x + y = 1 第三象限（x&lt;0, y&lt;0）： -x - y = 1 第四象限（x&gt;0, y&lt;0）： x - y = 1 那么原坐标沿着对称线法线方向移动2d，就可以得到对称的点坐标了，因为范围是0~1，从一象限来看，边界法线就是$(1,1)$，归一化后就是$(\frac{1}{\sqrt{2}},\frac{1}{\sqrt{2}})$,对于$\sqrt{A^2 + B^2}$实际上就是边界的长度，也就是$\sqrt{2}$， 假设原坐标为(a,b),那么对称的坐标就是$a-2d *\frac{1}{\sqrt{2}}$,化简之后，会得到变换后的坐标X分量是$(1-b)$ Y分量则是$(1-a)$，因为四条象限的法向量有正负之分，那么最终结果会取决于法向量的个分量的正负，但是这里的法向量正负正好符合象限X,Y的正负，也就是最终表达式是\(x = (1-|b|) * Sign(x)\)\(y = (1-|a|) * Sign(y)\) <img src="/assets/img/TAMonth04/Pasted image 20260314204827.png|897" alt=""/></p> <blockquote> <p>从边界限制条件入手</p> </blockquote> <p>这个约束来自<strong>折叠是镜像</strong>——边界上的点折叠前后位置不变。 取边界上任意一点 $(a,b)$，代入折叠公式后应该得到原点： 要让边界点不动，需要：</p> \[new_x = a\] \[new_y = b\] <table> <tbody> <tr> <td>但边界上点满足 $</td> <td>a</td> <td>+</td> <td>b</td> <td>= 1$，所以可以得到 $$a = (1 -</td> <td>b</td> <td>) * sign(a)$$</td> </tr> </tbody> </table> \[b = 1 - |a| * sign(b)\] <table> <tbody> <tr> <td>折叠后的点必须满足 $</td> <td>new_x</td> <td>+</td> <td>new_y</td> <td>&gt; 1$，即在菱形外侧。验证通过</td> </tr> </tbody> </table> \[|new_x| + |new_y|\] \[= (1 - |b|) + (1 - |a|)\] \[= 2 - (|a| + |b|)\] <h1 id="平滑法线">平滑法线</h1> <p><strong>可以理解为对法线进行权重影响，这个权重来自于角度，一个顶点的法线，由周边的所有三角形法线和经过角度加权之后得到</strong> <img src="/assets/img/TAMonth04/Pasted image 20260314230833.png" alt=""/></p> <h1 id="平滑法线烘焙到uv">平滑法线烘焙到UV</h1> <p><strong>blender脚本</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
</pre></td><td class="rouge-code"><pre><span class="n">import</span> <span class="n">bpy</span>  
<span class="n">from</span> <span class="n">mathutils</span> <span class="n">import</span> <span class="o">*</span>
<span class="n">from</span> <span class="n">math</span> <span class="n">import</span> <span class="o">*</span>
<span class="n">import</span> <span class="n">numpy</span> <span class="n">as</span> <span class="n">np</span>

<span class="cp">#创建空字典列表list
</span><span class="n">dict</span> <span class="o">=</span> <span class="p">{}</span>
<span class="cp">#获取模型Mesh
</span><span class="n">mesh</span> <span class="o">=</span> <span class="n">bpy</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">meshes</span><span class="p">[</span><span class="err">'</span><span class="n">MESHNAME</span><span class="err">'</span><span class="p">]</span>

<span class="cp">#calc_tangents：
#在网格中计算每个顶点的切线（tangent）、双切线（bitangent）和法线（normal），以便后续的方向相关计算。
</span><span class="n">mesh</span><span class="p">.</span><span class="n">calc_tangents</span><span class="p">(</span><span class="n">uvmap</span> <span class="o">=</span> <span class="err">'</span><span class="n">UVMap</span><span class="err">'</span><span class="p">)</span>

<span class="cp">#计算两个向量之间的夹角
# a·b = |a||b|cosθ
# θ = arccos(a·b / (|a||b|))
# 这里返回的弧度值  θ * (π/180) ≈ xxx弧度
</span><span class="n">def</span> <span class="n">included_angle</span><span class="p">(</span><span class="n">v0</span><span class="p">,</span> <span class="n">v1</span><span class="p">)</span><span class="o">:</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">arccos</span><span class="p">(</span><span class="n">v0</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">v1</span><span class="p">)</span><span class="o">/</span><span class="p">(</span><span class="n">v0</span><span class="p">.</span><span class="n">length</span> <span class="o">*</span> <span class="n">v1</span><span class="p">.</span><span class="n">length</span><span class="p">))</span>

<span class="cp"># 3维降为2维
</span><span class="n">def</span> <span class="n">unitVectorToOct</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">:</span>
    <span class="cp"># 步骤1：计算L1范数（曼哈顿距离）
</span>    <span class="n">d</span> <span class="o">=</span> <span class="n">abs</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">+</span> <span class="n">abs</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">y</span><span class="p">)</span> <span class="o">+</span> <span class="n">abs</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">z</span><span class="p">)</span>
    <span class="cp"># 步骤2：通过除以L1范数进行投影到八面体表面
</span>    <span class="cp"># o 是八面体的坐标
</span>    <span class="n">o</span> <span class="o">=</span> <span class="n">Vector</span><span class="p">((</span><span class="n">v</span><span class="p">.</span><span class="n">x</span> <span class="o">/</span> <span class="n">d</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">y</span> <span class="o">/</span> <span class="n">d</span><span class="p">))</span>
    <span class="cp"># 步骤3：如果z是负数，需要进行特殊处理以保持连续性
</span>    <span class="k">if</span> <span class="n">v</span><span class="p">.</span><span class="n">z</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">:</span>
        <span class="cp"># 使用折叠技术处理八面体的下半部分
</span>        <span class="n">o</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">abs</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">y</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="k">if</span> <span class="n">o</span><span class="p">.</span><span class="n">x</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">o</span><span class="p">.</span><span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">abs</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">x</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="k">if</span> <span class="n">o</span><span class="p">.</span><span class="n">y</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">o</span>

<span class="cp">#.co是coordinate的缩写，表示顶点的坐标。
# 取出模型的每一个顶点坐标，然后清空置空列表，后续存入新的坐标数据
</span><span class="k">for</span> <span class="n">vertex</span> <span class="n">in</span> <span class="n">mesh</span><span class="p">.</span><span class="n">vertices</span><span class="o">:</span>
    <span class="cp"># 初始化 "&lt;Vector (1.0, 2.0, 3.0)&gt;": []
</span>    <span class="n">dict</span><span class="p">[</span><span class="n">str</span><span class="p">(</span><span class="n">vertex</span><span class="p">.</span><span class="n">co</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[]</span>

<span class="cp"># 获取模型Mesh的每一个面
# l0,l1,l2分别表示模型Mesh的每一个面中的每一个顶点数据
# l0,l1,l2这样的写法是Blender的写法，表示loop0,loop1,loop2的循环体
# mesh.loops 存储了多边形顶点的循环信息
# poly.loop_start 表示多边形顶点的循环开始索引
</span><span class="k">for</span> <span class="n">poly</span> <span class="n">in</span> <span class="n">mesh</span><span class="p">.</span><span class="n">polygons</span><span class="o">:</span>
    <span class="cp">#获取模型Mesh的每一个三角面中的每一个顶点数据
</span>    <span class="n">l0</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">loops</span><span class="p">[</span><span class="n">poly</span><span class="p">.</span><span class="n">loop_start</span><span class="p">]</span> 
    <span class="n">l1</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">loops</span><span class="p">[</span><span class="n">poly</span><span class="p">.</span><span class="n">loop_start</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
    <span class="n">l2</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">loops</span><span class="p">[</span><span class="n">poly</span><span class="p">.</span><span class="n">loop_start</span> <span class="o">+</span> <span class="mi">2</span><span class="p">]</span>
    
    <span class="cp">#获取模型Mesh的每一个面中的每一个顶点数据
</span>    <span class="cp">## 顶点的主要属性：
</span>    <span class="cp">#vertex.co           顶点的3D坐标 (Vector类型，包含x,y,z)
</span>    <span class="cp">#vertex.normal       顶点的法线方向
</span>    <span class="cp">#vertex.index        顶点在mesh.vertices数组中的索引号
</span>    <span class="cp">#vertex.groups       顶点组信息（用于骨骼绑定等）
</span>    <span class="n">v0</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">[</span><span class="n">l0</span><span class="p">.</span><span class="n">vertex_index</span><span class="p">]</span>
    <span class="n">v1</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">[</span><span class="n">l1</span><span class="p">.</span><span class="n">vertex_index</span><span class="p">]</span>
    <span class="n">v2</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">[</span><span class="n">l2</span><span class="p">.</span><span class="n">vertex_index</span><span class="p">]</span>

    <span class="cp">#计算向量，三角形两条边向量
</span>    <span class="n">vec0</span> <span class="o">=</span> <span class="n">v1</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v0</span><span class="p">.</span><span class="n">co</span>
    <span class="n">vec1</span> <span class="o">=</span> <span class="n">v2</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v0</span><span class="p">.</span><span class="n">co</span>
    
    <span class="cp">#计算向量叉积，三角形两条边向量叉乘，得到法线
</span>    <span class="n">n</span> <span class="o">=</span> <span class="n">vec0</span><span class="p">.</span><span class="n">cross</span><span class="p">(</span><span class="n">vec1</span><span class="p">)</span>
    <span class="n">n</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="n">normalized</span><span class="p">()</span>

    <span class="cp">#v0.co 是一个 Vector 类型，包含 (x,y,z) 坐标值
</span>    <span class="cp">#Python的字典要求键（key）必须是"可哈希的"（hashable）
</span>    <span class="cp">#Vector 类型不能直接用作字典的键
</span>    <span class="cp">#这里将顶点坐标转换为字符串，便于后续使用
</span>    <span class="cp">#k0,k1,k2是字典的Key
</span>    <span class="n">k0</span> <span class="o">=</span> <span class="n">str</span><span class="p">(</span><span class="n">v0</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>     <span class="err">#</span> <span class="err">将顶点</span><span class="mi">0</span><span class="err">的坐标转换为字符串</span>
    <span class="n">k1</span> <span class="o">=</span> <span class="n">str</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>     <span class="err">#</span> <span class="err">将顶点</span><span class="mi">1</span><span class="err">的坐标转换为字符串</span>
    <span class="n">k2</span> <span class="o">=</span> <span class="n">str</span><span class="p">(</span><span class="n">v2</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>     <span class="err">#</span> <span class="err">将顶点</span><span class="mi">2</span><span class="err">的坐标转换为字符串</span>
    
    <span class="cp"># 计算三角形三个顶点的权重
</span>    <span class="k">if</span> <span class="n">k0</span> <span class="n">in</span> <span class="n">dict</span><span class="o">:</span>
        <span class="cp">#计算顶点0的权重，w实际上是两向量夹角角度
</span>        <span class="n">w</span> <span class="o">=</span> <span class="n">included_angle</span><span class="p">(</span><span class="n">v2</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v0</span><span class="p">.</span><span class="n">co</span><span class="p">,</span> <span class="n">v1</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v0</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>
        <span class="cp"># 添加数据
</span>        <span class="n">dict</span><span class="p">[</span><span class="n">k0</span><span class="p">].</span><span class="n">append</span><span class="p">({</span><span class="s">"n"</span> <span class="o">:</span> <span class="n">n</span><span class="p">,</span> <span class="s">"w"</span> <span class="o">:</span> <span class="n">w</span><span class="p">})</span>
        
    <span class="k">if</span> <span class="n">k1</span> <span class="n">in</span> <span class="n">dict</span><span class="o">:</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">included_angle</span><span class="p">(</span><span class="n">v0</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v1</span><span class="p">.</span><span class="n">co</span><span class="p">,</span> <span class="n">v2</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v1</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>
        <span class="n">dict</span><span class="p">[</span><span class="n">k1</span><span class="p">].</span><span class="n">append</span><span class="p">({</span><span class="s">"n"</span> <span class="o">:</span> <span class="n">n</span><span class="p">,</span> <span class="s">"w"</span> <span class="o">:</span> <span class="n">w</span><span class="p">})</span>
        
    <span class="k">if</span> <span class="n">k2</span> <span class="n">in</span> <span class="n">dict</span><span class="o">:</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">included_angle</span><span class="p">(</span><span class="n">v1</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v2</span><span class="p">.</span><span class="n">co</span><span class="p">,</span> <span class="n">v0</span><span class="p">.</span><span class="n">co</span> <span class="o">-</span> <span class="n">v2</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>
        <span class="n">dict</span><span class="p">[</span><span class="n">k2</span><span class="p">].</span><span class="n">append</span><span class="p">({</span><span class="s">"n"</span> <span class="o">:</span> <span class="n">n</span><span class="p">,</span> <span class="s">"w"</span> <span class="o">:</span> <span class="n">w</span><span class="p">})</span>
        
<span class="k">for</span> <span class="n">poly</span> <span class="n">in</span> <span class="n">mesh</span><span class="p">.</span><span class="n">polygons</span><span class="o">:</span>
    <span class="cp"># 获取每一个三角面数据 
</span>    <span class="cp">#range(poly.loop_start, poly.loop_start + 3)遍历3个点
</span>    <span class="k">for</span> <span class="n">loop_index</span> <span class="n">in</span> <span class="n">range</span><span class="p">(</span><span class="n">poly</span><span class="p">.</span><span class="n">loop_start</span><span class="p">,</span> <span class="n">poly</span><span class="p">.</span><span class="n">loop_start</span> <span class="o">+</span> <span class="mi">3</span><span class="p">)</span><span class="o">:</span>
        <span class="n">l</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">loops</span><span class="p">[</span><span class="n">loop_index</span><span class="p">]</span>
        <span class="n">vertex_index</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">vertex_index</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">vertices</span><span class="p">[</span><span class="n">vertex_index</span><span class="p">]</span>
        <span class="cp">#smoothNormal初始化
</span>        <span class="n">smoothNormal</span> <span class="o">=</span> <span class="n">Vector</span><span class="p">((</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">))</span>
        <span class="n">weightSum</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">k</span> <span class="o">=</span> <span class="n">str</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">co</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">k</span> <span class="n">in</span> <span class="n">dict</span><span class="o">:</span>
            <span class="n">a</span> <span class="o">=</span> <span class="n">dict</span><span class="p">[</span><span class="n">k</span><span class="p">]</span>
            <span class="k">for</span> <span class="n">d</span> <span class="n">in</span> <span class="n">a</span><span class="o">:</span>
                <span class="n">n</span> <span class="o">=</span> <span class="n">d</span><span class="p">[</span><span class="sc">'n'</span><span class="p">]</span>
                <span class="n">w</span> <span class="o">=</span> <span class="n">d</span><span class="p">[</span><span class="sc">'w'</span><span class="p">]</span>
                <span class="cp">#加权平均法线的计算公式：
</span>                <span class="cp">#例如：smoothNormal = (n1 * w1 + n2 * w2 + n3 * w3) / (w1 + w2 + w3)
</span>                <span class="n">smoothNormal</span> <span class="o">+=</span> <span class="n">n</span> <span class="o">*</span> <span class="n">w</span>
                <span class="n">weightSum</span> <span class="o">+=</span> <span class="n">w</span>
        <span class="k">if</span> <span class="n">smoothNormal</span><span class="p">.</span><span class="n">length</span> <span class="o">!=</span> <span class="mi">0</span><span class="o">:</span>
            <span class="n">smoothNormal</span> <span class="o">/=</span> <span class="n">weightSum</span>
            <span class="n">smoothNormal</span> <span class="o">=</span> <span class="n">smoothNormal</span><span class="p">.</span><span class="n">normalized</span><span class="p">()</span>
        <span class="k">else</span><span class="o">:</span>
            <span class="cp"># 如果计算出的平滑法线无效（长度为0），
</span>            <span class="cp"># 使用原始顶点法线作为后备方案
</span>            <span class="n">smoothNormal</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">normal</span>
        
        <span class="n">normal</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">normal</span>           <span class="err">#</span> <span class="err">获取顶点的原始法线</span>
        <span class="n">tangent</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">tangent</span>         <span class="err">#</span> <span class="err">获取顶点的原始切线</span>
        <span class="n">bitangent</span> <span class="o">=</span> <span class="n">l</span><span class="p">.</span><span class="n">bitangent</span>     <span class="err">#</span> <span class="err">获取顶点的原始副切线</span>

        <span class="cp"># 计算投影
</span>        <span class="cp"># 使用原始的切线空间基底，计算平滑法线在这个空间中的表示
</span>        <span class="n">x</span> <span class="o">=</span> <span class="n">tangent</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">smoothNormal</span><span class="p">)</span>       <span class="err">#</span> <span class="err">平滑法线在原始切线方向上的投影</span>
        <span class="n">y</span> <span class="o">=</span> <span class="n">bitangent</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">smoothNormal</span><span class="p">)</span>     <span class="err">#</span> <span class="err">平滑法线在原始副切线方向上的投影</span>
        <span class="n">z</span> <span class="o">=</span> <span class="n">normal</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">smoothNormal</span><span class="p">)</span>        <span class="err">#</span> <span class="err">平滑法线在原始法线方向上的投影</span>
        
        <span class="cp">#UV数组的大小总是等于loops的数量
</span>        <span class="n">uv1</span> <span class="o">=</span> <span class="n">mesh</span><span class="p">.</span><span class="n">uv_layers</span><span class="p">[</span><span class="err">'</span><span class="n">UVMap</span><span class="p">.</span><span class="mo">001</span><span class="err">'</span><span class="p">].</span><span class="n">uv</span><span class="p">[</span><span class="n">loop_index</span><span class="p">]</span>
        
        <span class="n">uv1</span><span class="p">.</span><span class="n">vector</span> <span class="o">=</span> <span class="n">unitVectorToOct</span><span class="p">(</span><span class="n">Vector</span><span class="p">((</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">z</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><summary type="html"><![CDATA[平滑法线处理 - 八面体映射]]></summary></entry><entry><title type="html">Lv.3 Unity主线：一个简单的PBRShader</title><link href="https://xueqingzhe.github.io/blog/2026/Lv.3-Unity%E4%B8%BB%E7%BA%BF-%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84PBRShader/" rel="alternate" type="text/html" title="Lv.3 Unity主线：一个简单的PBRShader"/><published>2026-01-31T00:00:00+00:00</published><updated>2026-01-31T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/Lv.3%20Unity%E4%B8%BB%E7%BA%BF%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84PBRShader</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/Lv.3-Unity%E4%B8%BB%E7%BA%BF-%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84PBRShader/"><![CDATA[<h1 id="使用资产">使用资产</h1> <p>通过网盘分享的文件：PBR测试内容 链接: https://pan.baidu.com/s/1MpDbI4TXPY2lKaz2uZC3_w?pwd=2psd 提取码: 2psd</p> <h1 id="完整代码">完整代码</h1> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"Unlit/PBRShader"</span>
<span class="p">{</span>
    <span class="n">properties</span>
    <span class="p">{</span>
        <span class="n">_MainTex</span><span class="p">(</span><span class="s">"MainTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>

        <span class="n">_NormalMap</span><span class="p">(</span><span class="s">"NormalMap"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"bump"</span><span class="p">{}</span>
        <span class="n">_NormalMapScale</span><span class="p">(</span><span class="s">"NormalMapScale"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">5</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>

        <span class="n">_RoughnessTex</span><span class="p">(</span><span class="s">"RoughnessTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span><span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>
        <span class="n">_MetallicTex</span><span class="p">(</span><span class="s">"MetallicTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span><span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>
        <span class="n">_HeightTex</span><span class="p">(</span><span class="s">"HeightTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span><span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>
        <span class="n">_AOTex</span><span class="p">(</span><span class="s">"AOTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span><span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>
        <span class="n">_CubeMap</span><span class="p">(</span><span class="s">"CubeMap"</span><span class="p">,</span> <span class="n">cube</span><span class="p">)</span><span class="o">=</span> <span class="s">"white"</span><span class="p">{}</span>

        <span class="n">_SpecularColor</span><span class="p">(</span><span class="s">"SpecularColor"</span><span class="p">,</span><span class="n">color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">_SpecularIntensity</span><span class="p">(</span><span class="s">"SpecularIntensity"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">50</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="n">_SpecularShininess</span><span class="p">(</span><span class="s">"SpecularShininess"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">10</span><span class="p">))</span> <span class="o">=</span> <span class="mi">2</span>

        <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">Roughness</span> <span class="n">Adjustment</span><span class="p">)]</span>
        <span class="p">[</span><span class="n">Space</span><span class="p">]</span>
        <span class="n">_RoughnessIntensity</span> <span class="p">(</span><span class="s">"RoughnessIntensity"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="p">[</span><span class="n">Space</span><span class="p">]</span>
        <span class="n">_RoughnessBias</span> <span class="p">(</span><span class="s">"Bias"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="p">[</span><span class="n">Space</span><span class="p">]</span>
        <span class="n">_RoughnessRemapMin</span> <span class="p">(</span><span class="s">"Remap Min"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">_RoughnessRemapMax</span> <span class="p">(</span><span class="s">"Remap Max"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">ParallaxMapping</span> <span class="n">Basic</span><span class="p">)]</span>
        <span class="n">_ParallaxScale</span> <span class="p">(</span><span class="s">"Parallax Scale 未使用"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">1</span>
        <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">ParallaxOcclusionMapping</span><span class="p">)]</span>
        <span class="n">_HeightScale</span> <span class="p">(</span><span class="s">"Height Scale"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">2</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mo">05</span>
        <span class="n">_MinLayers</span><span class="p">(</span><span class="s">"Parallax Min Layers"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">128</span><span class="p">))</span> <span class="o">=</span> <span class="mi">10</span>
        <span class="n">_MaxLayers</span><span class="p">(</span><span class="s">"Parallax Max Layers"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">128</span><span class="p">))</span> <span class="o">=</span> <span class="mi">50</span>
        <span class="n">_HeightReferencePlane</span> <span class="p">(</span><span class="s">"Height Reference Plane"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span>  <span class="c1">// 解决凹凸问题</span>
        <span class="c1">//_POMFadeDistance ("POM Near Fade Distance", Range(0, 10)) = 2.0  </span>
        <span class="n">_ShadowSoftness</span><span class="p">(</span><span class="s">"POM Shadow Strength"</span> <span class="p">,</span><span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span>
        <span class="n">_ShadowSteps</span><span class="p">(</span><span class="s">"POM Shadow Steps"</span> <span class="p">,</span><span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">16</span><span class="p">))</span> <span class="o">=</span> <span class="mi">8</span>

        <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">EnvBRDFLUT</span><span class="p">)]</span>
        <span class="p">[</span><span class="n">Toggle</span><span class="p">]</span><span class="n">_UseCustomLUT</span><span class="p">(</span><span class="s">"Custom BRDF LUT"</span><span class="p">,</span> <span class="kt">float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span>
        <span class="n">_CustomBRDFLUT</span><span class="p">(</span><span class="s">"Custom BRDF LUT"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>

    <span class="p">}</span>

    <span class="n">Subshader</span>
    <span class="p">{</span>
        <span class="n">Tags</span>
        <span class="p">{</span>
            <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span>
            <span class="s">"Queue"</span><span class="o">=</span><span class="s">"Geometry"</span>
        <span class="p">}</span>

        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">Name</span><span class="s">"FORWARD"</span>
            <span class="n">Tags</span><span class="p">{</span><span class="s">"LightMode"</span><span class="o">=</span><span class="s">"ForwardBase"</span><span class="p">}</span>

            <span class="n">CGPROGRAM</span>
            <span class="cp">#pragma vertex vert
</span>            <span class="cp">#pragma fragment frag
</span>            <span class="cp">#pragma multi_compile_fwdbase
</span>
            <span class="cp">#include</span> <span class="cpf">"UnityCG.cginc"</span><span class="cp">
</span>            <span class="cp">#include</span> <span class="cpf">"Lighting.cginc"</span><span class="cp">
</span>            <span class="cp">#include</span> <span class="cpf">"AutoLight.cginc"</span><span class="cp">
</span>             
            <span class="k">struct</span> <span class="n">appdata</span>
            <span class="p">{</span>
                <span class="n">float4</span> <span class="n">vertex</span>   <span class="o">:</span> <span class="n">POSITION</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">normal</span>   <span class="o">:</span> <span class="n">NORMAL</span><span class="p">;</span>
                <span class="n">float4</span> <span class="n">tangent</span>  <span class="o">:</span> <span class="n">TANGENT</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">uv</span>       <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
                
            <span class="p">};</span>

            <span class="k">struct</span> <span class="n">v2f</span>
            <span class="p">{</span>
                <span class="n">float4</span> <span class="n">pos</span>          <span class="o">:</span> <span class="n">SV_POSITION</span><span class="p">;</span>
                <span class="n">float4</span> <span class="n">positionWS</span>   <span class="o">:</span> <span class="n">TEXCOORD0</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">normalWS</span>     <span class="o">:</span> <span class="n">TEXCOORD1</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">tangentWS</span>    <span class="o">:</span> <span class="n">TEXCOORD2</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">bitangentWS</span>  <span class="o">:</span> <span class="n">TEXCOORD3</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">texcoord</span>     <span class="o">:</span> <span class="n">TEXCOORD4</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">viewDirTS</span>    <span class="o">:</span> <span class="n">TEXCOORD5</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">lightDirTS</span>   <span class="o">:</span> <span class="n">TEXCOORD6</span><span class="p">;</span>
                <span class="n">UNITY_SHADOW_COORDS</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>
            <span class="p">};</span>

            <span class="n">sampler2D</span> <span class="n">_MainTex</span><span class="p">;</span>
            <span class="n">half4</span> <span class="n">_MainTex_ST</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_NormalMap</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_RoughnessTex</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_MetallicTex</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_HeightTex</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_AOTex</span><span class="p">;</span>
            <span class="n">samplerCUBE</span> <span class="n">_CubeMap</span><span class="p">;</span>
            <span class="n">float4</span> <span class="n">_CubeMap_HDR</span><span class="p">;</span> <span class="c1">//此参数声明后，Unity会自动配置</span>
            <span class="n">half</span> <span class="n">_NormalMapScale</span><span class="p">;</span>
            <span class="n">half4</span> <span class="n">_SpecularColor</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_SpecularIntensity</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_SpecularShininess</span><span class="p">;</span>
           

            <span class="n">half</span> <span class="n">_RoughnessIntensity</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_RoughnessBias</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_RoughnessRemapMin</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_RoughnessRemapMax</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_RoughnessBoost</span><span class="p">;</span>

            <span class="n">half</span> <span class="n">_ParallaxScale</span><span class="p">;</span>

            <span class="n">half</span> <span class="n">_HeightScale</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_MinLayers</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_MaxLayers</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_HeightReferencePlane</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_POMFadeDistance</span><span class="p">;</span>

            <span class="n">half</span> <span class="n">_ShadowSoftness</span><span class="p">;</span>
            <span class="n">half</span> <span class="n">_ShadowSteps</span><span class="p">;</span>

            <span class="n">half</span> <span class="n">_UseCustomLUT</span><span class="p">;</span>
            <span class="n">sampler2D</span> <span class="n">_CustomBRDFLUT</span><span class="p">;</span>

            <span class="n">float3</span> <span class="n">ParallaxOcclusionMapping</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">,</span> <span class="n">float3</span> <span class="n">viewDirTS</span><span class="p">,</span> <span class="n">float3</span> <span class="n">lightDirTS</span><span class="p">)</span>
            <span class="p">{</span>
                
                <span class="c1">// === 保存原始UV导数 ===</span>
                <span class="n">float2</span> <span class="n">dx</span> <span class="o">=</span> <span class="n">ddx</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
                <span class="n">float2</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">ddy</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
                
                <span class="c1">// 这个系数能保证在极低角度时视差平滑消失</span>
                <span class="c1">//float angleFade = saturate(viewDirTS.z * 5.0);</span>
                <span class="kt">float</span> <span class="n">angleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">pow</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">));</span>
                <span class="c1">//float angleFade = smoothstep(0.15, 0.4, viewDirTS.z);</span>

                <span class="c1">// 截断淡出，效果较差</span>
                <span class="c1">// float2 rawOffset = viewDirTS.xy / max(viewDirTS.z, 0.0001) * _HeightScale;</span>
                <span class="c1">// float maxLen = _HeightScale * 2.0; </span>
                <span class="c1">// float currentLen = length(rawOffset);</span>
                <span class="c1">// float2 maxParallaxOffset = rawOffset * (min(currentLen, maxLen) / max(currentLen, 0.001));</span>

                <span class="c1">// === 1. 动态调整采样层数 ===</span>
                <span class="kt">float</span> <span class="n">numLayers</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_MaxLayers</span><span class="p">,</span> <span class="n">_MinLayers</span><span class="p">,</span> <span class="n">abs</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">));</span>
                <span class="c1">// === 2. 计算步进参数 ===</span>
                <span class="kt">float</span> <span class="n">layerStepDistance</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span>              <span class="c1">// 步进距离</span>
                <span class="n">float2</span> <span class="n">maxParallaxOffset</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span> <span class="o">*</span> <span class="n">angleFade</span><span class="p">;</span> <span class="c1">//最大步进距离</span>
                <span class="n">float2</span> <span class="n">deltaUV</span> <span class="o">=</span> <span class="n">maxParallaxOffset</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span> <span class="c1">//步进UV</span>
                <span class="c1">// === 3. 初始化 第一层数据===</span>
                <span class="kt">float</span> <span class="n">currentLayerDistance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>         <span class="c1">//起始层基准</span>
                <span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">uv</span> <span class="o">+</span> <span class="n">maxParallaxOffset</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">_HeightReferencePlane</span><span class="p">);</span>   <span class="c1">// 当前UV坐标</span>
                <span class="c1">//float currentHeight = 1 - tex2D(_HeightTex, currentUV).r; // 初始表面高度</span>
                <span class="kt">float</span> <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>

                <span class="c1">// 缓存上一步的数据，避免循环结束后重复采样</span>
                <span class="kt">float</span> <span class="n">prevHeight</span> <span class="o">=</span> <span class="n">currentHeight</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">prevLayerDistance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>

                <span class="c1">//[unroll(50)] // 展开循环优化（数字要≥_MaxLayers）</span>
                <span class="p">[</span><span class="n">loop</span><span class="p">]</span> <span class="c1">//标记循环</span>
                <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numLayers</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="c1">// 1.检查是否当前位置数值是否大于当前高度值</span>
                    <span class="k">if</span><span class="p">(</span><span class="n">currentLayerDistance</span> <span class="o">&gt;=</span> <span class="n">currentHeight</span><span class="p">)</span>
                        <span class="k">break</span><span class="p">;</span>
                    
                    <span class="c1">// 2.记录旧状态</span>
                    <span class="n">prevHeight</span> <span class="o">=</span> <span class="n">currentHeight</span><span class="p">;</span>
                    <span class="n">prevLayerDistance</span> <span class="o">=</span> <span class="n">currentLayerDistance</span><span class="p">;</span>

                    <span class="c1">// 3.步进操作</span>
                    <span class="c1">// 沿视线方向步进，下一层的数据，这里虽然命名是current，但是数据已经是下一层的了</span>
                    <span class="n">currentUV</span> <span class="o">-=</span> <span class="n">deltaUV</span><span class="p">;</span>                               <span class="c1">//UV步进1层</span>
                    <span class="n">currentLayerDistance</span> <span class="o">+=</span>  <span class="n">layerStepDistance</span><span class="p">;</span>         <span class="c1">//当前位置步进1层 </span>

                    <span class="c1">// 4.采样新高度</span>
                    <span class="c1">//currentHeight = 1 - tex2Dlod(_HeightTex, float4(currentUV, 0, 0)).r; //深度值随UV步进1层</span>
                    <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="c1">// 步进操作前，也就是当前UV</span>
                <span class="n">float2</span> <span class="n">prevUV</span> <span class="o">=</span> <span class="n">currentUV</span> <span class="o">+</span> <span class="n">deltaUV</span><span class="p">;</span>

                <span class="c1">//当前步进距离对高度值影响              </span>
                <span class="kt">float</span> <span class="n">beforeDistance</span> <span class="o">=</span> <span class="n">prevHeight</span> <span class="o">-</span> <span class="n">prevLayerDistance</span><span class="p">;</span>
                 
                <span class="c1">//下一层，总步进距离对高度值影响</span>
                <span class="kt">float</span> <span class="n">afterDistance</span> <span class="o">=</span> <span class="n">currentHeight</span> <span class="o">-</span> <span class="n">currentLayerDistance</span><span class="p">;</span>  
    
                <span class="c1">// 插值权重，两个端点之间的线性插值</span>
                <span class="kt">float</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">afterDistance</span> <span class="o">/</span> <span class="p">(</span><span class="n">afterDistance</span> <span class="o">-</span> <span class="n">beforeDistance</span><span class="p">);</span>

                <span class="c1">// 最终UV = 加权平均</span>
                <span class="c1">// 对每一次步进的UV插值</span>
                <span class="n">float2</span> <span class="n">finalUV</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">currentUV</span><span class="p">,</span> <span class="n">prevUV</span><span class="p">,</span> <span class="n">weight</span><span class="p">);</span>
                <span class="c1">//float finalHeight = lerp(currentLayerDistance, prevLayerDistance, weight);</span>

                <span class="c1">// === 自阴影循环 (Soft Shadows) ===</span>
                <span class="kt">float</span> <span class="n">shadow</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                
                <span class="c1">//float lightAngleFade = smoothstep(0.05, 0.25, lightDirTS.z);</span>
                <span class="kt">float</span> <span class="n">lightAngleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">pow</span><span class="p">(</span><span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">));</span>
                
                <span class="k">if</span><span class="p">(</span><span class="n">_ShadowSoftness</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">angleFade</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span> <span class="o">&amp;&amp;</span> <span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="kt">float</span> <span class="n">hitDepth</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">finalUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                    
                    <span class="k">if</span><span class="p">(</span><span class="n">hitDepth</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="c1">// 计算完整的UV偏移方向（从交点到光源方向在UV上的投影）</span>
                        <span class="n">float2</span> <span class="n">lightDirUV</span> <span class="o">=</span> <span class="n">lightDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span><span class="p">;</span>
                        
                        <span class="c1">// 限制最大追踪距离，防止低角度时偏移过大</span>
                        <span class="kt">float</span> <span class="n">maxOffset</span> <span class="o">=</span> <span class="n">_HeightScale</span> <span class="o">*</span> <span class="mi">2</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                        <span class="kt">float</span> <span class="n">offsetLen</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">lightDirUV</span><span class="p">);</span>
                        <span class="k">if</span><span class="p">(</span><span class="n">offsetLen</span> <span class="o">&gt;</span> <span class="n">maxOffset</span><span class="p">)</span>
                        <span class="p">{</span>
                            <span class="n">lightDirUV</span> <span class="o">=</span> <span class="n">lightDirUV</span> <span class="o">/</span> <span class="n">offsetLen</span> <span class="o">*</span> <span class="n">maxOffset</span><span class="p">;</span>
                        <span class="p">}</span>
                        
                        <span class="c1">// 按深度比例的总UV偏移</span>
                        <span class="n">float2</span> <span class="n">totalUV</span> <span class="o">=</span> <span class="n">lightDirUV</span> <span class="o">*</span> <span class="n">hitDepth</span><span class="p">;</span>
                        
                        <span class="c1">// 均匀分配到每一步</span>
                        <span class="n">float2</span> <span class="n">stepUV</span> <span class="o">=</span> <span class="n">totalUV</span> <span class="o">/</span> <span class="n">_ShadowSteps</span><span class="p">;</span>
                        <span class="kt">float</span> <span class="n">stepDepth</span> <span class="o">=</span> <span class="n">hitDepth</span> <span class="o">/</span> <span class="n">_ShadowSteps</span><span class="p">;</span>
                        
                        <span class="kt">float</span> <span class="n">currentDepth</span> <span class="o">=</span> <span class="n">hitDepth</span><span class="p">;</span>
                        <span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">finalUV</span><span class="p">;</span>
                        
                        <span class="p">[</span><span class="n">loop</span><span class="p">]</span>
                        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">_ShadowSteps</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span><span class="p">)</span>
                        <span class="p">{</span>
                            <span class="n">currentDepth</span> <span class="o">-=</span> <span class="n">stepDepth</span><span class="p">;</span>
                            <span class="n">currentUV</span> <span class="o">+=</span> <span class="n">stepUV</span><span class="p">;</span>
                            
                            <span class="k">if</span><span class="p">(</span><span class="n">currentDepth</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span>
                                <span class="k">break</span><span class="p">;</span>
                            
                            <span class="kt">float</span> <span class="n">terrainDepth</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                            
                            <span class="k">if</span><span class="p">(</span><span class="n">terrainDepth</span> <span class="o">&lt;</span> <span class="n">currentDepth</span><span class="p">)</span>
                            <span class="p">{</span>
                                <span class="kt">float</span> <span class="n">occlusion</span> <span class="o">=</span> <span class="n">currentDepth</span> <span class="o">-</span> <span class="n">terrainDepth</span><span class="p">;</span>
                                <span class="n">shadow</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">shadow</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">saturate</span><span class="p">(</span><span class="n">occlusion</span> <span class="o">*</span> <span class="n">_ShadowSoftness</span><span class="p">));</span>
                                
                                <span class="k">if</span><span class="p">(</span><span class="n">shadow</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span><span class="p">)</span>
                                <span class="p">{</span>
                                    <span class="n">shadow</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                                    <span class="k">break</span><span class="p">;</span>
                                <span class="p">}</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="c1">// 根据淡出系数混合阴影</span>
                <span class="n">shadow</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="n">shadow</span><span class="p">,</span> <span class="n">angleFade</span> <span class="o">*</span> <span class="n">lightAngleFade</span><span class="p">);</span>
                
                <span class="c1">// 根据淡出系数混合阴影（让阴影也随视角淡出）</span>
                <span class="c1">//shadow = lerp(1.0, shadow, angleFade);</span>

                <span class="c1">//return finalUV;</span>
                <span class="k">return</span> <span class="n">float3</span><span class="p">(</span><span class="n">finalUV</span><span class="p">,</span><span class="n">shadow</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="n">float2</span> <span class="n">EnvBRDFApprox</span><span class="p">(</span><span class="kt">float</span> <span class="n">roughness</span><span class="p">,</span> <span class="kt">float</span> <span class="n">NoV</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="c1">// Epic Games通过拟合大量数据得出的魔法系数</span>
                <span class="k">const</span> <span class="n">float4</span> <span class="n">c0</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mo">0275</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mi">572</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">022</span><span class="p">);</span>
                <span class="k">const</span> <span class="n">float4</span> <span class="n">c1</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">0425</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">);</span>

                <span class="n">float4</span> <span class="n">r</span> <span class="o">=</span> <span class="n">roughness</span> <span class="o">*</span> <span class="n">c0</span> <span class="o">+</span> <span class="n">c1</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">a004</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">x</span> <span class="o">*</span> <span class="n">r</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">exp2</span><span class="p">(</span><span class="o">-</span><span class="mi">9</span><span class="p">.</span><span class="mi">28</span> <span class="o">*</span> <span class="n">NoV</span><span class="p">))</span> <span class="o">*</span> <span class="n">r</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">r</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">AB</span> <span class="o">=</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">)</span> <span class="o">*</span> <span class="n">a004</span> <span class="o">+</span> <span class="n">r</span><span class="p">.</span><span class="n">zw</span><span class="p">;</span>

                <span class="k">return</span> <span class="n">AB</span><span class="p">;</span>  <span class="c1">// AB.x = scale, AB.y = bias</span>
            <span class="p">}</span>

            <span class="n">v2f</span> <span class="n">vert</span><span class="p">(</span><span class="n">appdata</span> <span class="n">v</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">v2f</span> <span class="n">o</span><span class="p">;</span>
                <span class="n">o</span><span class="p">.</span><span class="n">pos</span> <span class="o">=</span> <span class="n">UnityObjectToClipPos</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>
                <span class="n">o</span><span class="p">.</span><span class="n">positionWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">vertex</span><span class="p">);</span>

                <span class="n">o</span><span class="p">.</span><span class="n">normalWS</span> <span class="o">=</span> <span class="n">UnityObjectToWorldNormal</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">normal</span><span class="p">);</span>
                <span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span> <span class="o">=</span> <span class="n">UnityObjectToWorldDir</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">tangent</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
                <span class="c1">//* v.tangent.w处理平台差异</span>
                <span class="n">o</span><span class="p">.</span><span class="n">bitangentWS</span> <span class="o">=</span> <span class="n">cross</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">normalWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span><span class="p">)</span> <span class="o">*</span> <span class="n">v</span><span class="p">.</span><span class="n">tangent</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
                <span class="n">o</span><span class="p">.</span><span class="n">texcoord</span> <span class="o">=</span> <span class="n">TRANSFORM_TEX</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span><span class="n">_MainTex</span><span class="p">);</span>

                <span class="c1">//ParallaxMapping_Basic start</span>
                <span class="n">float3x3</span> <span class="n">TBN</span> <span class="o">=</span> <span class="n">float3x3</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">bitangentWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">normalWS</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">worldViewDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span> <span class="o">-</span> <span class="n">o</span><span class="p">.</span><span class="n">positionWS</span><span class="p">);</span>       
                <span class="n">o</span><span class="p">.</span><span class="n">viewDirTS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">TBN</span><span class="p">,</span> <span class="n">worldViewDir</span><span class="p">);</span> 

                <span class="n">half3</span> <span class="n">lightWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
                <span class="n">o</span><span class="p">.</span><span class="n">lightDirTS</span><span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">TBN</span><span class="p">,</span> <span class="n">lightWS</span><span class="p">);</span> 

                <span class="c1">//ParallaxMapping_Basic end</span>
                <span class="n">TRANSFER_SHADOW</span><span class="p">(</span><span class="n">o</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">o</span><span class="p">;</span>
            <span class="p">}</span>
            

            <span class="n">float4</span> <span class="n">frag</span><span class="p">(</span><span class="n">v2f</span> <span class="n">i</span><span class="p">)</span> <span class="o">:</span> <span class="n">SV_Target</span>
            <span class="p">{</span>
                <span class="c1">//vectors</span>
                <span class="n">float3</span> <span class="n">positionWS</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">positionWS</span><span class="p">;</span> 
                <span class="n">float3</span> <span class="n">normalWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">normalWS</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">tangentWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">tangentWS</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">bitangentWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">bitangentWS</span><span class="p">);</span>
                <span class="n">float3x3</span> <span class="n">TBN</span> <span class="o">=</span> <span class="n">float3x3</span><span class="p">(</span><span class="n">tangentWS</span><span class="p">,</span> <span class="n">bitangentWS</span><span class="p">,</span> <span class="n">normalWS</span><span class="p">);</span>
                <span class="n">half3</span> <span class="n">lightWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">viewDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span> <span class="o">-</span> <span class="n">positionWS</span><span class="p">);</span>

                <span class="n">half3</span> <span class="n">lightDirTS</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">lightDirTS</span><span class="p">;</span> 
                <span class="c1">//************tex*************</span>
                <span class="c1">//ParallaxMapping_Basic start</span>
                <span class="c1">//float height = 1.0 - tex2D(_HeightTex, i.texcoord).r;</span>
                <span class="c1">//float2 offset = i.viewDirTS.xy / i.viewDirTS.z * height * _ParallaxScale * 0.01;</span>
                <span class="c1">//float2 texcoord = i.texcoord - offset;</span>
                <span class="c1">//ParallaxMapping_Basic end</span>

                <span class="n">float3</span> <span class="n">POMData</span> <span class="o">=</span> <span class="n">ParallaxOcclusionMapping</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">texcoord</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">viewDirTS</span><span class="p">,</span>  <span class="n">lightDirTS</span><span class="p">);</span>
                <span class="n">float2</span> <span class="n">texcoord</span> <span class="o">=</span> <span class="n">POMData</span><span class="p">.</span><span class="n">xy</span><span class="p">;</span>
                <span class="kt">float</span>  <span class="n">POMShadow</span> <span class="o">=</span> <span class="n">POMData</span><span class="p">.</span><span class="n">z</span><span class="p">;</span>

                <span class="n">float3</span> <span class="n">lightColor</span> <span class="o">=</span> <span class="n">_LightColor0</span><span class="p">.</span><span class="n">xyz</span><span class="p">;</span>
                <span class="n">float4</span> <span class="n">baseCol</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">normalMapData</span> <span class="o">=</span> <span class="n">UnpackNormal</span><span class="p">(</span><span class="n">tex2D</span><span class="p">(</span><span class="n">_NormalMap</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">)).</span><span class="n">xyz</span><span class="p">;</span>
                <span class="n">normalMapData</span><span class="p">.</span><span class="n">xy</span> <span class="o">*=</span> <span class="n">_NormalMapScale</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">metallic</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MetallicTex</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">).</span><span class="n">b</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">roughness</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_RoughnessTex</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">).</span><span class="n">r</span> <span class="o">+</span> <span class="n">_RoughnessBoost</span><span class="p">;</span>

                <span class="c1">//halfLambet</span>
                <span class="n">float3</span> <span class="n">pixelNormal</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">mul</span><span class="p">(</span><span class="n">normalMapData</span><span class="p">,</span> <span class="n">TBN</span><span class="p">));</span>
                <span class="kt">float</span> <span class="n">nol</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dot</span><span class="p">(</span><span class="n">pixelNormal</span><span class="p">,</span> <span class="n">lightWS</span><span class="p">));</span>
                <span class="kt">float</span> <span class="n">halfLambet</span> <span class="o">=</span> <span class="n">nol</span><span class="o">*</span><span class="mi">0</span><span class="p">.</span><span class="mi">5</span> <span class="o">+</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">diffuseColor</span> <span class="o">=</span> <span class="n">baseCol</span> <span class="o">*</span> <span class="n">nol</span> <span class="o">*</span> <span class="n">lightColor</span><span class="p">;</span>
                <span class="c1">//Specular</span>
                <span class="c1">//phong</span>
                <span class="n">half3</span> <span class="n">reflectDir</span> <span class="o">=</span> <span class="n">reflect</span><span class="p">(</span><span class="o">-</span><span class="n">lightWS</span><span class="p">,</span> <span class="n">pixelNormal</span><span class="p">);</span>
                <span class="n">half</span> <span class="n">phongItem</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dot</span><span class="p">(</span><span class="n">reflectDir</span><span class="p">,</span> <span class="n">viewDir</span><span class="p">));</span>
               
                <span class="c1">//Blinn-phong</span>
                <span class="n">half3</span> <span class="n">halfDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">lightWS</span> <span class="o">+</span> <span class="n">viewDir</span><span class="p">);</span>
                <span class="n">half</span> <span class="n">blinnPhongItem</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dot</span><span class="p">(</span><span class="n">pixelNormal</span><span class="p">,</span> <span class="n">halfDir</span><span class="p">));</span>       

               
              
                <span class="c1">//整体漫反射剔除金属的的漫反射(错误写法，能量不守恒)</span>
                <span class="c1">//float3 pbrDiffuseColor = lerp(0.96 * diffuseColor, 0, metallic);</span>
                
                <span class="n">roughness</span> <span class="o">=</span> <span class="n">roughness</span> <span class="o">*</span> <span class="n">_RoughnessIntensity</span> <span class="o">+</span> <span class="n">_RoughnessBias</span><span class="p">;</span>
                <span class="n">roughness</span> <span class="o">=</span> <span class="n">clamp</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">_RoughnessRemapMin</span><span class="p">,</span> <span class="n">_RoughnessRemapMax</span><span class="p">);</span>

                <span class="c1">//GGXSpecular</span>
                <span class="n">float3</span> <span class="n">f0</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">float3</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">),</span> <span class="n">baseCol</span><span class="p">,</span> <span class="n">metallic</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">roughness2</span> <span class="o">=</span> <span class="n">roughness</span> <span class="o">*</span> <span class="n">roughness</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">d</span> <span class="o">=</span> <span class="n">roughness2</span><span class="o">*</span><span class="n">roughness2</span><span class="o">/</span><span class="p">(</span><span class="mi">3</span><span class="p">.</span><span class="mi">14159</span><span class="o">*</span><span class="p">(</span><span class="n">blinnPhongItem</span> <span class="o">*</span> <span class="n">blinnPhongItem</span> <span class="o">*</span> <span class="p">(</span><span class="n">roughness2</span> <span class="o">*</span> <span class="n">roughness2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="n">blinnPhongItem</span> <span class="o">*</span> <span class="n">blinnPhongItem</span> <span class="o">*</span> <span class="p">(</span><span class="n">roughness2</span> <span class="o">*</span> <span class="n">roughness2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>

                <span class="kt">float</span> <span class="n">k</span> <span class="o">=</span> <span class="p">(</span><span class="n">roughness</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">roughness</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">nov</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dot</span><span class="p">(</span><span class="n">pixelNormal</span><span class="p">,</span> <span class="n">viewDir</span><span class="p">));</span>
                <span class="kt">float</span> <span class="n">gv</span> <span class="o">=</span> <span class="n">nov</span> <span class="o">/</span> <span class="p">(</span><span class="n">nov</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">k</span><span class="p">)</span> <span class="o">+</span> <span class="n">k</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">gl</span> <span class="o">=</span> <span class="n">nol</span> <span class="o">/</span> <span class="p">(</span><span class="n">nol</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">k</span><span class="p">)</span> <span class="o">+</span> <span class="n">k</span><span class="p">);</span>
                <span class="kt">float</span> <span class="n">g</span> <span class="o">=</span> <span class="n">gv</span> <span class="o">*</span> <span class="n">gl</span><span class="p">;</span> 
                <span class="n">float3</span> <span class="n">f</span> <span class="o">=</span> <span class="n">f0</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">f0</span><span class="p">)</span> <span class="o">*</span> <span class="n">pow</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,(</span><span class="n">dot</span><span class="p">(</span><span class="n">viewDir</span><span class="p">,</span> <span class="n">halfDir</span><span class="p">))),</span> <span class="mi">5</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">KD_GGX</span> <span class="o">=</span>  <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">f</span><span class="p">)</span><span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">metallic</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">ggxBRDF</span> <span class="o">=</span> <span class="n">d</span> <span class="o">*</span> <span class="n">f</span> <span class="o">*</span> <span class="n">g</span> <span class="o">/</span> <span class="p">(</span><span class="mi">4</span> <span class="o">*</span> <span class="n">nol</span> <span class="o">*</span> <span class="n">nov</span>  <span class="o">+</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">pbrSpecularColor</span> <span class="o">=</span> <span class="n">ggxBRDF</span> <span class="o">*</span> <span class="n">_SpecularColor</span> <span class="o">*</span> <span class="n">lightColor</span> <span class="o">*</span> <span class="n">nol</span><span class="p">;</span>
                <span class="n">float3</span> <span class="n">pbrDiffuseColor</span> <span class="o">=</span>  <span class="n">baseCol</span> <span class="o">/</span><span class="mi">3</span><span class="p">.</span><span class="mi">14159</span> <span class="o">*</span> <span class="n">KD_GGX</span> <span class="o">*</span> <span class="n">nol</span> <span class="o">*</span> <span class="n">lightColor</span><span class="p">;</span>


                <span class="c1">//******环境光处理**********</span>

                <span class="cm">/******************漫反射环境光*********************/</span>
                <span class="c1">//EnvironmentColor</span>
                <span class="c1">//1.环境光（Unity 自动处理）</span>
                <span class="n">fixed3</span> <span class="n">envColor</span> <span class="o">=</span> <span class="n">UNITY_LIGHTMODEL_AMBIENT</span><span class="p">.</span><span class="n">rgb</span> <span class="o">*</span> <span class="n">baseCol</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
                <span class="c1">//               ^^^^^^^^^^^^^^^^^^^^^^^ Unity 内置的环境光颜色</span>
                <span class="c1">//2.SH环境光（支持 Skybox/Gradient/Color）</span>
                <span class="n">fixed3</span> <span class="n">envColorSH</span> <span class="o">=</span> <span class="n">ShadeSH9</span><span class="p">(</span><span class="n">half4</span><span class="p">(</span><span class="n">normalWS</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">))</span> <span class="o">*</span> <span class="n">baseCol</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
                <span class="c1">//               ^^^^^^^ Unity 的球谐函数，根据法线方向采样环境光</span>
                <span class="cm">/******************镜面反射环境光*********************/</span>
                <span class="c1">//1.采样反射探针（Reflection Probe）</span>
                <span class="n">half4</span> <span class="n">skyData</span> <span class="o">=</span> <span class="n">UNITY_SAMPLE_TEXCUBE</span><span class="p">(</span><span class="n">unity_SpecCube0</span><span class="p">,</span> <span class="n">reflectDir</span><span class="p">);</span>
                <span class="n">half3</span> <span class="n">skyColor</span> <span class="o">=</span> <span class="n">DecodeHDR</span><span class="p">(</span><span class="n">skyData</span><span class="p">,</span> <span class="n">unity_SpecCube0_HDR</span><span class="p">);</span>
                    
                <span class="c1">// 计算反射方向</span>
                <span class="n">float3</span> <span class="n">reflviewDir</span> <span class="o">=</span> <span class="n">reflect</span><span class="p">(</span><span class="o">-</span><span class="n">viewDir</span><span class="p">,</span> <span class="n">normalize</span><span class="p">(</span><span class="n">pixelNormal</span><span class="p">));</span>
               
                <span class="c1">//环境光漫反射</span>
                <span class="n">float3</span> <span class="n">f_indirect</span> <span class="o">=</span> <span class="n">f0</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">f0</span><span class="p">)</span> <span class="o">*</span> <span class="n">pow</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,(</span><span class="n">dot</span><span class="p">(</span><span class="n">viewDir</span><span class="p">,</span> <span class="n">pixelNormal</span><span class="p">))),</span> <span class="mi">5</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">kD_indirect</span>  <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">f_indirect</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">metallic</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">indirectDiffuse</span>  <span class="o">=</span> <span class="n">kD_indirect</span> <span class="o">*</span> <span class="n">envColorSH</span><span class="p">;</span>
                
                <span class="c1">//环境光镜面漫反射</span>
                <span class="kt">float</span> <span class="n">mipLevel</span> <span class="o">=</span> <span class="n">roughness</span><span class="o">*</span> <span class="mi">7</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                <span class="n">float4</span> <span class="n">envSpecularRaw</span> <span class="o">=</span> <span class="n">texCUBElod</span><span class="p">(</span><span class="n">_CubeMap</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">reflviewDir</span><span class="p">,</span> <span class="n">mipLevel</span><span class="p">));</span>
                <span class="n">half3</span> <span class="n">envSpecular</span> <span class="o">=</span> <span class="n">DecodeHDR</span><span class="p">(</span><span class="n">envSpecularRaw</span><span class="p">,</span> <span class="n">_CubeMap_HDR</span><span class="p">);</span> 

                <span class="c1">//1.拟合BRDF(未使用)</span>
                <span class="n">float2</span> <span class="n">envBRDF</span> <span class="o">=</span> <span class="n">EnvBRDFApprox</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">);</span>
                <span class="n">float3</span> <span class="n">envSpecularEasy</span> <span class="o">=</span> <span class="n">envSpecular</span> <span class="o">*</span> <span class="n">f_indirect</span><span class="p">;</span>
                <span class="c1">//2.采样BRDFLUT</span>
                <span class="n">float2</span> <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">unity_NHxRoughness</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
                <span class="n">float2</span> <span class="n">CustombrdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_CustomBRDFLUT</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
                
                <span class="k">if</span><span class="p">(</span><span class="n">_UseCustomLUT</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">CustombrdfLUT</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="n">envSpecular</span> <span class="o">=</span>  <span class="n">envSpecular</span> <span class="o">*</span> <span class="p">(</span><span class="n">f0</span> <span class="o">*</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
                
                <span class="c1">//AO</span>
                <span class="kt">float</span> <span class="n">AO</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_AOTex</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                <span class="c1">//float fakeAO = saturate(dot(normalWS, float3(0,1,0))) * 0.6 + 0.4;</span>



                <span class="c1">//Shadow</span>
                <span class="kt">float</span> <span class="n">shadow</span> <span class="o">=</span> <span class="n">SHADOW_ATTENUATION</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
 
                <span class="c1">//output</span>
                <span class="n">float3</span> <span class="n">finalColor</span> <span class="o">=</span> <span class="p">(</span><span class="n">pbrDiffuseColor</span> <span class="o">+</span> <span class="n">pbrSpecularColor</span><span class="p">)</span> <span class="o">*</span> <span class="n">shadow</span> <span class="o">+</span> <span class="p">(</span><span class="n">indirectDiffuse</span> <span class="o">+</span> <span class="n">envSpecular</span> <span class="p">)</span><span class="o">*</span> <span class="n">AO</span> <span class="o">*</span> <span class="n">POMShadow</span> <span class="p">;</span>


                <span class="k">return</span> <span class="n">float4</span><span class="p">(</span><span class="n">finalColor</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> 
            <span class="p">}</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
        
    <span class="p">}</span><span class="n">FallBack</span><span class="s">"Diffuse"</span>

<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="测试大致效果">测试大致效果</h1> <p><img src="/assets/img/TAMonth02/Pasted image 20260131201627.png" alt=""/></p> <p><img src="/assets/img/TAMonth02/Pasted image 20260131201844.png" alt=""/></p>]]></content><author><name></name></author><category term="TAMonth02"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[Lv.3 Unity主线：一个简单的PBRShader]]></summary></entry><entry><title type="html">理论支线：PBR - 基于图像的照明( image based lighting-IBL)</title><link href="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-PBR-%E5%9F%BA%E4%BA%8E%E5%9B%BE%E5%83%8F%E7%9A%84%E7%85%A7%E6%98%8E(-image-based-lighting-IBL)/" rel="alternate" type="text/html" title="理论支线：PBR - 基于图像的照明( image based lighting-IBL)"/><published>2026-01-24T00:00:00+00:00</published><updated>2026-01-24T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF%EF%BC%9APBR%20-%20%E5%9F%BA%E4%BA%8E%E5%9B%BE%E5%83%8F%E7%9A%84%E7%85%A7%E6%98%8E(%20image%20based%20lighting-IBL)</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-PBR-%E5%9F%BA%E4%BA%8E%E5%9B%BE%E5%83%8F%E7%9A%84%E7%85%A7%E6%98%8E(-image-based-lighting-IBL)/"><![CDATA[<h1 id="介绍">介绍</h1> <p>这通常通过作立方体贴图环境贴图（取自现实世界或从三维场景生成）实现，使我们能直接将其用于光照方程：将每个立方体贴图像素视为光发射体。这样我们就能有效捕捉环境的全局光照和整体感觉，让物体在环境中有更好的归属感。</p> <h2 id="ibl渲染方程方程">IBL渲染方程方程</h2> \[L_o(p,\omega_o) = \int\limits_{\Omega}(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i\] <p>这里可以拆分成两个部分</p> \[L_o(p,\omega_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i + \int\limits_{\Omega} (k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i\] <p><strong>这里其实看公式可以知道这就是漫反射 + 镜面反射，这个内容和直接光是类似的</strong> <strong>那么环境光漫反射其实就是SH(球谐光照)，镜面反射则是源自于环境反射的HDR图，这里主要是讲镜面反射</strong></p> <h2 id="采样">采样</h2> <p><strong>通常采用是texCUBElod，因为需要将粗糙度和mipLevel相关联。</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kt">float</span> <span class="n">mipLevel</span> <span class="o">=</span> <span class="n">roughness</span><span class="o">*</span> <span class="mi">7</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">envSpecularRaw</span> <span class="o">=</span> <span class="n">texCUBElod</span><span class="p">(</span><span class="n">_CubeMap</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">reflviewDir</span><span class="p">,</span> <span class="n">mipLevel</span><span class="p">));</span>
<span class="n">half3</span> <span class="n">envSpecular</span> <span class="o">=</span> <span class="n">DecodeHDR</span><span class="p">(</span><span class="n">envSpecularRaw</span><span class="p">,</span> <span class="n">_CubeMap_HDR</span><span class="p">);</span> 
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="与ggx的关系和使用方法">与GGX的关系和使用方法</h1> <h2 id="直接光照的镜面反射ggx">直接光照的镜面反射(GGX)</h2> \[f_{DirectSpecular}^{GGX}(l, v) = \frac{D(h) \cdot F(v, h) \cdot G(l, v, h)}{4 \cdot (n \cdot l) \cdot (n \cdot v)} \cdot (n \cdot l) \cdot L_{light}\] <p>其中:</p> \[F(v, h) = F_0 + (1 - F_0)(1 - v \cdot h)^5\] <h2 id="environment-brdf">Environment BRDF</h2> <p><strong>对于直接光，是只需要处理单一光源，所以F和G项都是实时计算的，但是对于环境光镜面反射来说，光线是四面八方传来，这意味着实时计算需要计算无数光线方向，这样肯定是不行的，所以环境光需要一个更好的处理办法，那就是Environment BRDF</strong></p> <p>它是一张2D查找表(LUT)，描述镜面反射能量随观察角度的衰减曲线，其实就是控制衰减形状和距离程度 想象一张图片:</p> <ul> <li><strong>横轴(X)</strong>: <code class="language-plaintext highlighter-rouge">NoV</code> (法线和视角夹角余弦值,范围0→1)</li> <li><strong>纵轴(Y)</strong>: <code class="language-plaintext highlighter-rouge">roughness</code> (粗糙度,范围0→1)</li> <li><strong>像素值</strong>: <code class="language-plaintext highlighter-rouge">RG两通道</code> 存储 <code class="language-plaintext highlighter-rouge">(scale, bias)</code> 两个数值</li> </ul> <h2 id="环境光照的镜面反射ibl">环境光照的镜面反射(IBL)</h2> <h3 id="简化只处理菲涅尔项">简化只处理菲涅尔项</h3> <p><strong>能用，几乎没有衰减也就是比较明亮，卡通渲染应该比较合适</strong></p> \[f_{IBLSpecular} = \underbrace{L_{prefiltered}(r, \text{roughness})}_{\text{D和G已经在这里了!}} \cdot F(n,v)\] <p>其中: <strong>F项发生了变化，因为环境光是四周辐射的而非根据视线方向，所以它的因子变成了法线</strong></p> \[F(n, v) = F_0 + (1 - F_0)(1 - n \cdot v)^5\] <h3 id="使用近似拟合方法">使用近似拟合方法</h3> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">float2</span> <span class="nf">EnvBRDFApprox</span><span class="p">(</span><span class="kt">float</span> <span class="n">roughness</span><span class="p">,</span> <span class="kt">float</span> <span class="n">NoV</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Epic Games通过拟合大量数据得出的魔法系数</span>
    <span class="k">const</span> <span class="n">float4</span> <span class="n">c0</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mo">0275</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mi">572</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">022</span><span class="p">);</span>
    <span class="k">const</span> <span class="n">float4</span> <span class="n">c1</span> <span class="o">=</span> <span class="n">float4</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">0425</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">);</span>
    <span class="n">float4</span> <span class="n">r</span> <span class="o">=</span> <span class="n">roughness</span> <span class="o">*</span> <span class="n">c0</span> <span class="o">+</span> <span class="n">c1</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">a004</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">x</span> <span class="o">*</span> <span class="n">r</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">exp2</span><span class="p">(</span><span class="o">-</span><span class="mi">9</span><span class="p">.</span><span class="mi">28</span> <span class="o">*</span> <span class="n">NoV</span><span class="p">))</span> <span class="o">*</span> <span class="n">r</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">r</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
    <span class="n">float2</span> <span class="n">AB</span> <span class="o">=</span> <span class="n">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mo">04</span><span class="p">)</span> <span class="o">*</span> <span class="n">a004</span> <span class="o">+</span> <span class="n">r</span><span class="p">.</span><span class="n">zw</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">AB</span><span class="p">;</span>  <span class="c1">// AB.x = scale, AB.y = bias</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong>使用近似后和仅处理理菲涅尔项的对比</strong> <img src="/assets/img/TAMonth02/Pasted image 20260127204725.png" alt=""/></p> <h3 id="使用unity默认lut">使用Unity默认LUT</h3> <p><strong>IBL贴图过滤采样中处理D和G项，但G项并不完整或者没有。所以更好的方案是在额外计算F和G积分项,也就是使用LUT图</strong> Epic Games的聪明解决方案:拆成两部分</p> \[\int L_i \cdot BRDF \approx \underbrace{\int L_i \cdot D}_{\text{第1部分:预过滤环境贴图}} \times \underbrace{\int \frac{FG}{4(n \cdot v)} }_{\text{第2部分:Environment BRDF}}\] <p><strong>关键思想</strong>:</p> <ul> <li><strong>第1部分</strong>只和环境光、粗糙度有关 → 提前烘焙成Cubemap的不同mip层</li> <li><strong>第2部分</strong>只和材质参数(roughness, NoV)有关 → 提前计算成一张2D表</li> </ul> <p><strong>采样Unity默认LUT</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="err"> </span><span class="n">float2</span> <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">unity_NHxRoughness</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
<span class="err"> </span><span class="n">envSpecular</span> <span class="o">=</span> <span class="err"> </span><span class="n">envSpecular</span> <span class="o">*</span> <span class="p">(</span><span class="n">f0</span> <span class="o">*</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="使用自定义lut">使用自定义LUT</h3> <p>图片：https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/</p> <p><img src="/assets/img/TAMonth02/ibl_brdf_lut.png" alt=""/></p> <p><strong>这里使用了一个_UseCustomLUT作为开关</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">EnvBRDFLUT</span><span class="p">)]</span>
<span class="p">[</span><span class="n">Toggle</span><span class="p">]</span><span class="n">_UseCustomLUT</span><span class="p">(</span><span class="s">"Custom BRDF LUT"</span><span class="p">,</span> <span class="kt">float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span>
<span class="n">_CustomBRDFLUT</span><span class="p">(</span><span class="s">"Custom BRDF LUT"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
<span class="o">----------------------------------------</span>
<span class="c1">//采样BRDFLUT</span>
<span class="n">float2</span> <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">unity_NHxRoughness</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">CustombrdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_CustomBRDFLUT</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
                
<span class="k">if</span><span class="p">(</span><span class="n">_UseCustomLUT</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">CustombrdfLUT</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">envSpecular</span> <span class="o">=</span>  <span class="n">envSpecular</span> <span class="o">*</span> <span class="p">(</span><span class="n">f0</span> <span class="o">*</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="对比">对比</h2> <table> <thead> <tr> <th>项目</th> <th>直接光(GGX)</th> <th>环境光(IBL)</th> </tr> </thead> <tbody> <tr> <td><strong>完整公式</strong></td> <td>$\frac{DFG}{4(n \cdot l)(n \cdot v)} \cdot (n \cdot l)$</td> <td>$L_{prefiltered}(r) \cdot F$</td> </tr> <tr> <td><strong>D项</strong></td> <td>D(h) 实时计算</td> <td>预计算到贴图Mip里</td> </tr> <tr> <td><strong>F项</strong></td> <td>$F_0 + (1-F_0)(1-v \cdot h)^5$</td> <td>$F_0 + (1-F_0)(1-n \cdot v)^5$</td> </tr> <tr> <td><strong>G项</strong></td> <td>G(l,v,h)实时计算</td> <td>预计算到贴图里</td> </tr> <tr> <td> </td> <td> </td> <td> </td> </tr> </tbody> </table> <table> <thead> <tr> <th>项目</th> <th>直接光照(GGX)</th> <th>环境光照(IBL)</th> </tr> </thead> <tbody> <tr> <td><strong>F公式</strong></td> <td>$F_0 + (1-F_0)(1-v \cdot h)^5$</td> <td>$F_0 + (1-F_0)(1-n \cdot v)^5$</td> </tr> <tr> <td><strong>输入角度</strong></td> <td><code class="language-plaintext highlighter-rouge">VoH</code></td> <td><code class="language-plaintext highlighter-rouge">NoV</code></td> </tr> <tr> <td><strong>物理含义</strong></td> <td>光线在微表面上的菲涅尔</td> <td>宏观表面的菲涅尔</td> </tr> <tr> <td><strong>计算时机</strong></td> <td>每个光源都要算</td> <td>算一次就够</td> </tr> </tbody> </table> <ul> <li>环境光来自<strong>各个方向</strong>,没有单一的L</li> <li>近似认为反射方向R周围的锥形区域贡献最大</li> <li>这个锥形区域的中心轴 ≈ 法线N</li> <li>所以用<code class="language-plaintext highlighter-rouge">NoV</code>(法线到视角)</li> </ul> <h1 id="代码参考">代码参考</h1> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="c1">//环境光漫反射</span>
<span class="n">float3</span> <span class="n">f_indirect</span> <span class="o">=</span> <span class="n">f0</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">f0</span><span class="p">)</span> <span class="o">*</span> <span class="nf">pow</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,(</span><span class="n">dot</span><span class="p">(</span><span class="n">viewDir</span><span class="p">,</span> <span class="n">pixelNormal</span><span class="p">))),</span> <span class="mi">5</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">kD_indirect</span>  <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">f_indirect</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">metallic</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">indirectDiffuse</span>  <span class="o">=</span> <span class="n">kD_indirect</span> <span class="o">*</span> <span class="n">envColorSH</span><span class="p">;</span>
            
<span class="c1">//环境光镜面漫反射</span>
<span class="kt">float</span> <span class="n">mipLevel</span> <span class="o">=</span> <span class="n">roughness</span><span class="o">*</span> <span class="mi">7</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
<span class="n">float4</span> <span class="n">envSpecularRaw</span> <span class="o">=</span> <span class="n">texCUBElod</span><span class="p">(</span><span class="n">_CubeMap</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">reflviewDir</span><span class="p">,</span> <span class="n">mipLevel</span><span class="p">));</span>
<span class="n">half3</span> <span class="n">envSpecular</span> <span class="o">=</span> <span class="n">DecodeHDR</span><span class="p">(</span><span class="n">envSpecularRaw</span><span class="p">,</span> <span class="n">_CubeMap_HDR</span><span class="p">);</span> 

<span class="c1">//1.拟合BRDF(未使用)</span>
<span class="n">float2</span> <span class="n">envBRDF</span> <span class="o">=</span> <span class="n">EnvBRDFApprox</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">envSpecularEasy</span> <span class="o">=</span> <span class="n">envSpecular</span> <span class="o">*</span> <span class="n">f_indirect</span><span class="p">;</span>
<span class="c1">//2.采样BRDFLUT</span>
<span class="n">float2</span> <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">unity_NHxRoughness</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">CustombrdfLUT</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_CustomBRDFLUT</span><span class="p">,</span> <span class="n">float2</span><span class="p">(</span><span class="n">roughness</span><span class="p">,</span> <span class="n">nov</span><span class="p">)).</span><span class="n">rg</span><span class="p">;</span>
            
<span class="k">if</span><span class="p">(</span><span class="n">_UseCustomLUT</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">brdfLUT</span> <span class="o">=</span> <span class="n">CustombrdfLUT</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">envSpecular</span> <span class="o">=</span>  <span class="n">envSpecular</span> <span class="o">*</span> <span class="p">(</span><span class="n">f0</span> <span class="o">*</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">brdfLUT</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
            
<span class="c1">//AO</span>
<span class="kt">float</span> <span class="n">AO</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_AOTex</span><span class="p">,</span> <span class="n">texcoord</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong>最终环境光的混合需要和AO相乘但和直接光的阴影不相干，有些教程里面将AO直接和baseColor相乘这其实是错误的。</strong> <strong>混合输出方式参考</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">//output</span>
<span class="n">float3</span> <span class="n">finalColor</span> <span class="o">=</span> <span class="p">(</span><span class="n">pbrDiffuseColor</span> <span class="o">+</span> <span class="n">pbrSpecularColor</span><span class="p">)</span> <span class="o">*</span> <span class="n">shadow</span> <span class="o">+</span> <span class="p">(</span><span class="n">indirectDiffuse</span> <span class="o">+</span> <span class="n">envSpecular</span> <span class="p">)</span><span class="o">*</span> <span class="n">AO</span> <span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><category term="TAMonth02"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[理论支线：PBR - 基于图像的照明( image based lighting-IBL)]]></summary></entry><entry><title type="html">理论支线：直接光漫反射与GGX高光的混合问题</title><link href="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-%E7%9B%B4%E6%8E%A5%E5%85%89%E6%BC%AB%E5%8F%8D%E5%B0%84%E4%B8%8EGGX%E9%AB%98%E5%85%89%E7%9A%84%E6%B7%B7%E5%90%88%E9%97%AE%E9%A2%98/" rel="alternate" type="text/html" title="理论支线：直接光漫反射与GGX高光的混合问题"/><published>2026-01-24T00:00:00+00:00</published><updated>2026-01-24T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF%EF%BC%9A%E7%9B%B4%E6%8E%A5%E5%85%89%E6%BC%AB%E5%8F%8D%E5%B0%84%E4%B8%8EGGX%E9%AB%98%E5%85%89%E7%9A%84%E6%B7%B7%E5%90%88%E9%97%AE%E9%A2%98</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-%E7%9B%B4%E6%8E%A5%E5%85%89%E6%BC%AB%E5%8F%8D%E5%B0%84%E4%B8%8EGGX%E9%AB%98%E5%85%89%E7%9A%84%E6%B7%B7%E5%90%88%E9%97%AE%E9%A2%98/"><![CDATA[<h1 id="渲染方程">渲染方程</h1> <p><strong>直接光分为漫反射和镜面反射两部分，混合的话他们分别对应一个系数</strong></p> \[L_o(p,\omega_o) = \int\limits_{\Omega}(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i\] <p><strong>这里需要说明：Kd = (1 - F) × (1 - metallic)</strong> <strong>而Ks = F (菲涅尔项)，它实际上已经包含在GGX计算公式中了，所以无需再处理</strong></p> <h1 id="简化">简化</h1> <p><strong><em>这段代码提前剔除了金属部分漫反射，会导致能量不守恒，但是如果卡通渲染实际上没什么问题，可以不用求KD,就是说实话不处理pbrDiffuseColor也没问题，直接加上GGX就行，加这个总感觉是多余，0.04的差距肉眼也看不出来。</em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">//整体漫反射剔除金属的的漫反射</span>
<span class="n">float3</span> <span class="n">pbrDiffuseColor</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">96</span> <span class="o">*</span> <span class="n">diffuseColor</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">metallic</span><span class="p">);</span>
<span class="c1">//非金属</span>
<span class="n">float3</span> <span class="n">f0</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="p">,</span> <span class="n">diffuseColor</span><span class="p">,</span> <span class="n">metallic</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="参考代码">参考代码</h1> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">float3</span> <span class="n">KD_GGX</span> <span class="o">=</span> <span class="err"> </span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">f</span><span class="p">)</span><span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">metallic</span><span class="p">);</span>
<span class="c1">//======================</span>
<span class="n">float3</span> <span class="n">pbrSpecularColor</span> <span class="o">=</span> <span class="n">ggxBRDF</span> <span class="o">*</span> <span class="n">_SpecularColor</span> <span class="o">*</span> <span class="n">lightColor</span> <span class="o">*</span> <span class="n">nol</span><span class="p">;</span>
<span class="n">float3</span> <span class="n">pbrDiffuseColor</span> <span class="o">=</span>  <span class="n">baseCol</span> <span class="o">/</span><span class="mi">3</span><span class="p">.</span><span class="mi">14159</span> <span class="o">*</span> <span class="n">KD_GGX</span> <span class="o">*</span> <span class="n">nol</span> <span class="o">*</span> <span class="n">lightColor</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong>最终混和则需要和阴影计算</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">float3</span> <span class="n">finalColor</span> <span class="o">=</span> <span class="p">(</span><span class="n">pbrDiffuseColor</span> <span class="o">+</span> <span class="n">pbrSpecularColor</span><span class="p">)</span> <span class="o">*</span> <span class="n">shadow</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><category term="TAMonth02"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[理论支线：直接光漫反射与GGX高光的混合问题]]></summary></entry><entry><title type="html">Lv.3 Unity主线：视差(Parallax Mapping)</title><link href="https://xueqingzhe.github.io/blog/2026/Lv.3-Unity%E4%B8%BB%E7%BA%BF-%E8%A7%86%E5%B7%AE(Parallax-Mapping)/" rel="alternate" type="text/html" title="Lv.3 Unity主线：视差(Parallax Mapping)"/><published>2026-01-23T00:00:00+00:00</published><updated>2026-01-23T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/Lv.3%20Unity%E4%B8%BB%E7%BA%BF%EF%BC%9A%E8%A7%86%E5%B7%AE(Parallax%20Mapping)</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/Lv.3-Unity%E4%B8%BB%E7%BA%BF-%E8%A7%86%E5%B7%AE(Parallax-Mapping)/"><![CDATA[<h1 id="核心公式">核心公式</h1> <p><strong><em>视差原理其实就根据视线方向来影响UV采样，那么这就需要将视线方向转换到切线空间，也就是乘上TBN矩阵就行</em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">offsetUV</span> <span class="o">=</span> <span class="n">uv</span> <span class="o">-</span> <span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span> <span class="o">*</span> <span class="n">scale</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h1 id="parallaxmapping_basic">ParallaxMapping_Basic</h1> <h2 id="代码">代码</h2> <p><strong>简单的视差处理，性能较好但是在高差过高会出现明显断层，所以只能支持非常小的视差_ParallaxScale的值在0~5左右，乘上0.01，也就是几厘米的视差</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="n">_ParallaxScale</span> <span class="p">(</span><span class="s">"Parallax Scale"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">1</span>
<span class="c1">//=======================================================</span>
<span class="err"> </span><span class="n">half</span> <span class="n">_ParallaxScale</span><span class="p">;</span>
<span class="err"> </span><span class="c1">//================顶点vert处理输出viewDirTS==============</span>
<span class="n">float3x3</span> <span class="n">TBN</span> <span class="o">=</span> <span class="n">float3x3</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">bitangentWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">normalWS</span><span class="p">);</span>
<span class="n">float3</span> <span class="n">worldViewDir</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceCameraPos</span> <span class="o">-</span> <span class="n">o</span><span class="p">.</span><span class="n">positionWS</span><span class="p">);</span>  
<span class="n">o</span><span class="p">.</span><span class="n">viewDirTS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">TBN</span><span class="p">,</span> <span class="n">worldViewDir</span><span class="p">);</span>   
<span class="c1">//===================片元frag中处理UV===================== </span>
<span class="c1">//高度图可能需要1-处理</span>
<span class="c1">//float height = 1.0 - tex2D(_HeightTex, i.texcoord).r;</span>
<span class="kt">float</span> <span class="n">height</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">i</span><span class="p">.</span><span class="n">texcoord</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">i</span><span class="p">.</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">*</span> <span class="n">height</span> <span class="o">*</span> <span class="n">_ParallaxScale</span> <span class="o">*</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span><span class="p">;</span>
<span class="n">float2</span> <span class="n">texcoord</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="n">texcoord</span> <span class="o">-</span> <span class="n">offset</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="效果">效果</h2> <p><strong>效果大致如下</strong> <img src="/assets/img/TAMonth02/ParallaxMapping_Basic.gif" alt=""/></p> <h1 id="parallax-occlusion-mapping-pom">Parallax Occlusion Mapping (POM)</h1> <h2 id="基础版">基础版</h2> <h3 id="代码-1">代码</h3> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
</pre></td><td class="rouge-code"><pre><span class="n">float2</span> <span class="nf">ParallaxOcclusionMapping</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">,</span> <span class="n">float3</span> <span class="n">viewDirTS</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// === 1. 动态调整采样层数 ===</span>
    <span class="kt">float</span> <span class="n">numLayers</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_MaxLayers</span><span class="p">,</span> <span class="n">_MinLayers</span><span class="p">,</span> <span class="n">abs</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">));</span>
    <span class="c1">// === 2. 计算步进参数 ===</span>
    <span class="kt">float</span> <span class="n">layerStepDistance</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span>              <span class="c1">// 步进距离</span>
    <span class="n">float2</span> <span class="n">deltaUV</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">*</span> <span class="n">_HeightScale</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span> <span class="c1">//步进UV</span>
    <span class="c1">// === 3. 初始化 第一层数据===</span>
    <span class="kt">float</span> <span class="n">currentLayerDistance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>  <span class="c1">// 当前射线深度</span>
    <span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">uv</span><span class="p">;</span>           <span class="c1">// 当前UV坐标</span>
    <span class="kt">float</span> <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">).</span><span class="n">r</span><span class="p">;</span> <span class="c1">// 初始表面高度</span>
               
    <span class="p">[</span><span class="n">unroll</span><span class="p">(</span><span class="mi">50</span><span class="p">)]</span> <span class="c1">// 展开循环优化（数字要≥_MaxLayers）</span>
    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numLayers</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// 检查是否当前位置数值是否大于当前高度值</span>
        <span class="k">if</span><span class="p">(</span><span class="n">currentLayerDistance</span> <span class="o">&gt;=</span> <span class="n">currentHeight</span><span class="p">)</span>
            <span class="k">break</span><span class="p">;</span>
                    
        <span class="c1">// ===  步进操作  ===</span>
        <span class="c1">// 沿视线方向步进，下一层的数据，这里虽然命名是current，但是数据已经是下一层的了</span>
        <span class="n">currentUV</span> <span class="o">-=</span> <span class="n">deltaUV</span><span class="p">;</span>                               <span class="c1">//UV步进1层</span>
        <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">).</span><span class="n">r</span><span class="p">;</span> <span class="c1">//深度值随UV步进1层</span>
        <span class="n">currentLayerDistance</span> <span class="o">+=</span>  <span class="n">layerStepDistance</span><span class="p">;</span>         <span class="c1">//当前位置步进1层 </span>
    <span class="p">}</span>
    <span class="c1">// 步进操作前，也就是当前UV</span>
    <span class="n">float2</span> <span class="n">prevUV</span> <span class="o">=</span> <span class="n">currentUV</span> <span class="o">+</span> <span class="n">deltaUV</span><span class="p">;</span>

    <span class="c1">//当前步进距离对高度值影响</span>
    <span class="c1">//1 - tex2D(_HeightTex, prevUV).r 当前采样高度图数据</span>
    <span class="c1">//currentLayerDistance - layerStepDistance  当前层的总步进距离</span>
    <span class="kt">float</span> <span class="n">beforeDistance</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">prevUV</span><span class="p">).</span><span class="n">r</span> <span class="o">-</span> <span class="p">(</span><span class="n">currentLayerDistance</span> <span class="o">-</span> <span class="n">layerStepDistance</span><span class="p">);</span> 

    <span class="c1">//下一层，总步进距离对高度值影响</span>
    <span class="kt">float</span> <span class="n">afterDistance</span> <span class="o">=</span> <span class="n">currentHeight</span> <span class="o">-</span> <span class="n">currentLayerDistance</span><span class="p">;</span>  
    
    <span class="c1">// 插值权重，两个端点之间的线性插值</span>
    <span class="kt">float</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">afterDistance</span> <span class="o">/</span> <span class="p">(</span><span class="n">afterDistance</span> <span class="o">-</span> <span class="n">beforeDistance</span><span class="p">);</span>
    <span class="c1">// 最终UV = 加权平均</span>
    <span class="c1">// 对每一次步进的UV插值</span>
    <span class="n">float2</span> <span class="n">finalUV</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">currentUV</span><span class="p">,</span> <span class="n">prevUV</span><span class="p">,</span> <span class="n">weight</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">finalUV</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="效果-1">效果</h3> <p><img src="/assets/img/TAMonth02/ParallaxOcclusionMapping_Basic.gif" alt=""/></p> <h2 id="需要调整的问题优化">需要调整的问题(优化)</h2> <h3 id="视差凹凸基准平面设置">视差凹凸基准平面设置</h3> <p><strong>当前视差的凹凸只能向下凹陷，也就是基准面是0，如果需要上下同时凹凸，那么需要调整基准面，其实就是对起始层UV进行调整</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">//最大步进距离</span>
<span class="n">float2</span> <span class="n">maxParallaxOffset</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span><span class="p">;</span> 
<span class="c1">//======================</span>
<span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">uv</span> <span class="o">+</span> <span class="n">maxParallaxOffset</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">_HeightReferencePlane</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="锯齿和纹理模糊">锯齿和纹理模糊</h3> <blockquote> <p>GPU 如何选择 Mipmap 级别?</p> </blockquote> <p><strong>正常情况使用tex2D来采样</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">fixed4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">tex2D</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">uv</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <ul> <li><strong>自动计算导数</strong>：GPU 自动执行 <code class="language-plaintext highlighter-rouge">ddx(uv)</code> 和 <code class="language-plaintext highlighter-rouge">ddy(uv)</code></li> <li><strong>自动选择 mipmap</strong>：根据导数大小自动选 mip level</li> <li><strong>问题</strong>：因为相当于模型进行上下凹凸，POM 后导数不准确</li> </ul> <p><strong>那么影响就是导数不准确，这会导致mipmap可能会过大，导致纹理模糊和锯齿</strong></p> <p><strong>解决办法有两种：</strong> <strong>1.使用tex2Dlod来强制指定mipLevel，也就是跳过ddx(uv)，ddy(uv)，但是固定lod效果会差一些。如果你离物体很远，由于没有 Mipmap 过滤，画面会产生极强的闪烁和噪点</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">fixed4</span> <span class="n">color</span> <span class="o">=</span> <span class="n">tex2Dlod</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="n">uv</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">mipLevel</span><span class="p">));</span>
<span class="c1">//tex2Dlod，float4数据第三位通常为0，代表是纹理数组索引</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong>2.使用tex2Dgrad，手动指定导数，也就是排除凹凸对影响</strong> <strong>在某些平台（特别是移动端 GPU），<code class="language-plaintext highlighter-rouge">ddx()/ddy()</code> 只能对插值器（varying）计算导数，不能对函数内部的局部变量计算，也就是需要再循环外部进行dxdy的计算，不要在循环内部计算</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="n">float2</span> <span class="n">dx</span> <span class="o">=</span> <span class="n">ddx</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
<span class="n">float2</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">ddy</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
<span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_MainTex</span><span class="p">,</span> <span class="n">UV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">);</span>
<span class="c1">//=========也就是在循环中使用tex2Dgrad==========</span>
<span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="视角畸变">视角畸变</h3> <p><strong>当视角接近平行或者和平面非常近时会发生畸变</strong> <img src="/assets/img/TAMonth02/Pasted image 20260123000850.png" alt=""/> <strong>1.最简单的解决办法是对viewDirTS.z进行限制调整，也就是+0.42</strong> <code class="language-plaintext highlighter-rouge">+ 0.42</code> 这个数值，它其实起源于早期游戏开发（如 CryEngine 时代）的一种经验算法。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">//修改前</span>
<span class="n">float2</span> <span class="n">maxParallaxOffset</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span><span class="p">;</span> <span class="c1">//最大步进距离</span>
<span class="c1">//修改后</span>
<span class="n">float2</span> <span class="n">maxParallaxOffset</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">+</span> <span class="mi">0</span><span class="p">.</span><span class="mi">42</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span><span class="p">;</span> <span class="c1">//最大步进距离</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong>修改后则会好一些，但是这会牺牲一点视差强度</strong> <img src="/assets/img/TAMonth02/Pasted image 20260123000939.png" alt=""/> <strong>2.第二种方法使用一个更好的淡出系数</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">//倍数线性淡出，省性能</span>
<span class="kt">float</span> <span class="n">angleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">*</span> <span class="mi">5</span><span class="p">.</span><span class="mi">0</span><span class="p">);</span>
<span class="c1">//指数淡出，更自然</span>
<span class="kt">float</span> <span class="n">angleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">pow</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">));</span>
<span class="c1">//smoothstep可尝试，效果不是很理想</span>
<span class="kt">float</span> <span class="n">angleFade</span> <span class="o">=</span> <span class="n">smoothstep</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">15</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">4</span><span class="p">,</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p>当视角低于 <strong>11°</strong> (1/5) 时，视差开始线性向零收缩。 <strong>指数淡出系数</strong> <img src="/assets/img/TAMonth02/Pasted image 20260123192633.png" alt=""/></p> <h3 id="自阴影">自阴影</h3> <p><strong>因为要做到模型凹凸，自然会有遮挡，所以需要处理自阴影</strong> <img src="/assets/img/TAMonth02/Pasted image 20260123233852.png" alt=""/> 原理和视线的步进分层一样，只不过换成了灯光向量了，需要转换到切线空间</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">half3</span> <span class="n">lightWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">_WorldSpaceLightPos0</span><span class="p">.</span><span class="n">xyz</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">lightDirTS</span><span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">TBN</span><span class="p">,</span> <span class="n">lightWS</span><span class="p">);</span> 
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="优化版完整函数代码">优化版完整函数代码</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
</pre></td><td class="rouge-code"><pre><span class="n">float3</span> <span class="nf">ParallaxOcclusionMapping</span><span class="p">(</span><span class="n">float2</span> <span class="n">uv</span><span class="p">,</span> <span class="n">float3</span> <span class="n">viewDirTS</span><span class="p">,</span> <span class="n">float3</span> <span class="n">lightDirTS</span><span class="p">)</span>
            <span class="p">{</span>
                
                <span class="c1">// === 保存原始UV导数 ===</span>
                <span class="n">float2</span> <span class="n">dx</span> <span class="o">=</span> <span class="n">ddx</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
                <span class="n">float2</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">ddy</span><span class="p">(</span><span class="n">uv</span><span class="p">);</span>
                
                <span class="c1">// 这个系数能保证在极低角度时视差平滑消失</span>
                <span class="c1">//float angleFade = saturate(viewDirTS.z * 5.0);</span>
                <span class="kt">float</span> <span class="n">angleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">pow</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">));</span>
                <span class="c1">//float angleFade = smoothstep(0.15, 0.4, viewDirTS.z);</span>

                <span class="c1">// 截断淡出，效果较差</span>
                <span class="c1">// float2 rawOffset = viewDirTS.xy / max(viewDirTS.z, 0.0001) * _HeightScale;</span>
                <span class="c1">// float maxLen = _HeightScale * 2.0; </span>
                <span class="c1">// float currentLen = length(rawOffset);</span>
                <span class="c1">// float2 maxParallaxOffset = rawOffset * (min(currentLen, maxLen) / max(currentLen, 0.001));</span>

                <span class="c1">// === 1. 动态调整采样层数 ===</span>
                <span class="kt">float</span> <span class="n">numLayers</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">_MaxLayers</span><span class="p">,</span> <span class="n">_MinLayers</span><span class="p">,</span> <span class="n">abs</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">));</span>
                <span class="c1">// === 2. 计算步进参数 ===</span>
                <span class="kt">float</span> <span class="n">layerStepDistance</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span>              <span class="c1">// 步进距离</span>
                <span class="n">float2</span> <span class="n">maxParallaxOffset</span> <span class="o">=</span> <span class="n">viewDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">viewDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span> <span class="o">*</span> <span class="n">angleFade</span><span class="p">;</span> <span class="c1">//最大步进距离</span>
                <span class="n">float2</span> <span class="n">deltaUV</span> <span class="o">=</span> <span class="n">maxParallaxOffset</span> <span class="o">/</span> <span class="n">numLayers</span><span class="p">;</span> <span class="c1">//步进UV</span>
                <span class="c1">// === 3. 初始化 第一层数据===</span>
                <span class="kt">float</span> <span class="n">currentLayerDistance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>         <span class="c1">//起始层基准</span>
                <span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">uv</span> <span class="o">+</span> <span class="n">maxParallaxOffset</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">_HeightReferencePlane</span><span class="p">);</span>   <span class="c1">// 当前UV坐标</span>
                <span class="c1">//float currentHeight = 1 - tex2D(_HeightTex, currentUV).r; // 初始表面高度</span>
                <span class="kt">float</span> <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>

                <span class="c1">// 缓存上一步的数据，避免循环结束后重复采样</span>
                <span class="kt">float</span> <span class="n">prevHeight</span> <span class="o">=</span> <span class="n">currentHeight</span><span class="p">;</span>
                <span class="kt">float</span> <span class="n">prevLayerDistance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>

                <span class="c1">//[unroll(50)] // 展开循环优化（数字要≥_MaxLayers）</span>
                <span class="p">[</span><span class="n">loop</span><span class="p">]</span> <span class="c1">//标记循环</span>
                <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numLayers</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="c1">// 1.检查是否当前位置数值是否大于当前高度值</span>
                    <span class="k">if</span><span class="p">(</span><span class="n">currentLayerDistance</span> <span class="o">&gt;=</span> <span class="n">currentHeight</span><span class="p">)</span>
                        <span class="k">break</span><span class="p">;</span>
                    
                    <span class="c1">// 2.记录旧状态</span>
                    <span class="n">prevHeight</span> <span class="o">=</span> <span class="n">currentHeight</span><span class="p">;</span>
                    <span class="n">prevLayerDistance</span> <span class="o">=</span> <span class="n">currentLayerDistance</span><span class="p">;</span>

                    <span class="c1">// 3.步进操作</span>
                    <span class="c1">// 沿视线方向步进，下一层的数据，这里虽然命名是current，但是数据已经是下一层的了</span>
                    <span class="n">currentUV</span> <span class="o">-=</span> <span class="n">deltaUV</span><span class="p">;</span>                               <span class="c1">//UV步进1层</span>
                    <span class="n">currentLayerDistance</span> <span class="o">+=</span>  <span class="n">layerStepDistance</span><span class="p">;</span>         <span class="c1">//当前位置步进1层 </span>

                    <span class="c1">// 4.采样新高度</span>
                    <span class="c1">//currentHeight = 1 - tex2Dlod(_HeightTex, float4(currentUV, 0, 0)).r; //深度值随UV步进1层</span>
                    <span class="n">currentHeight</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="c1">// 步进操作前，也就是当前UV</span>
                <span class="n">float2</span> <span class="n">prevUV</span> <span class="o">=</span> <span class="n">currentUV</span> <span class="o">+</span> <span class="n">deltaUV</span><span class="p">;</span>

                <span class="c1">//当前步进距离对高度值影响              </span>
                <span class="kt">float</span> <span class="n">beforeDistance</span> <span class="o">=</span> <span class="n">prevHeight</span> <span class="o">-</span> <span class="n">prevLayerDistance</span><span class="p">;</span>
                 
                <span class="c1">//下一层，总步进距离对高度值影响</span>
                <span class="kt">float</span> <span class="n">afterDistance</span> <span class="o">=</span> <span class="n">currentHeight</span> <span class="o">-</span> <span class="n">currentLayerDistance</span><span class="p">;</span>  
    
                <span class="c1">// 插值权重，两个端点之间的线性插值</span>
                <span class="kt">float</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">afterDistance</span> <span class="o">/</span> <span class="p">(</span><span class="n">afterDistance</span> <span class="o">-</span> <span class="n">beforeDistance</span><span class="p">);</span>

                <span class="c1">// 最终UV = 加权平均</span>
                <span class="c1">// 对每一次步进的UV插值</span>
                <span class="n">float2</span> <span class="n">finalUV</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">currentUV</span><span class="p">,</span> <span class="n">prevUV</span><span class="p">,</span> <span class="n">weight</span><span class="p">);</span>
                <span class="c1">//float finalHeight = lerp(currentLayerDistance, prevLayerDistance, weight);</span>

                <span class="c1">// === 自阴影循环 (Soft Shadows) ===</span>
                <span class="kt">float</span> <span class="n">shadow</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                
                <span class="c1">//float lightAngleFade = smoothstep(0.05, 0.25, lightDirTS.z);</span>
                <span class="kt">float</span> <span class="n">lightAngleFade</span> <span class="o">=</span> <span class="n">saturate</span><span class="p">(</span><span class="n">pow</span><span class="p">(</span><span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">));</span>
                
                <span class="k">if</span><span class="p">(</span><span class="n">_ShadowSoftness</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">angleFade</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span> <span class="o">&amp;&amp;</span> <span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="kt">float</span> <span class="n">hitDepth</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">finalUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                    
                    <span class="k">if</span><span class="p">(</span><span class="n">hitDepth</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="c1">// 计算完整的UV偏移方向（从交点到光源方向在UV上的投影）</span>
                        <span class="n">float2</span> <span class="n">lightDirUV</span> <span class="o">=</span> <span class="n">lightDirTS</span><span class="p">.</span><span class="n">xy</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">lightDirTS</span><span class="p">.</span><span class="n">z</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">)</span> <span class="o">*</span> <span class="n">_HeightScale</span><span class="p">;</span>
                        
                        <span class="c1">// 限制最大追踪距离，防止低角度时偏移过大</span>
                        <span class="kt">float</span> <span class="n">maxOffset</span> <span class="o">=</span> <span class="n">_HeightScale</span> <span class="o">*</span> <span class="mi">2</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                        <span class="kt">float</span> <span class="n">offsetLen</span> <span class="o">=</span> <span class="n">length</span><span class="p">(</span><span class="n">lightDirUV</span><span class="p">);</span>
                        <span class="k">if</span><span class="p">(</span><span class="n">offsetLen</span> <span class="o">&gt;</span> <span class="n">maxOffset</span><span class="p">)</span>
                        <span class="p">{</span>
                            <span class="n">lightDirUV</span> <span class="o">=</span> <span class="n">lightDirUV</span> <span class="o">/</span> <span class="n">offsetLen</span> <span class="o">*</span> <span class="n">maxOffset</span><span class="p">;</span>
                        <span class="p">}</span>
                        
                        <span class="c1">// 按深度比例的总UV偏移</span>
                        <span class="n">float2</span> <span class="n">totalUV</span> <span class="o">=</span> <span class="n">lightDirUV</span> <span class="o">*</span> <span class="n">hitDepth</span><span class="p">;</span>
                        
                        <span class="c1">// 均匀分配到每一步</span>
                        <span class="n">float2</span> <span class="n">stepUV</span> <span class="o">=</span> <span class="n">totalUV</span> <span class="o">/</span> <span class="n">_ShadowSteps</span><span class="p">;</span>
                        <span class="kt">float</span> <span class="n">stepDepth</span> <span class="o">=</span> <span class="n">hitDepth</span> <span class="o">/</span> <span class="n">_ShadowSteps</span><span class="p">;</span>
                        
                        <span class="kt">float</span> <span class="n">currentDepth</span> <span class="o">=</span> <span class="n">hitDepth</span><span class="p">;</span>
                        <span class="n">float2</span> <span class="n">currentUV</span> <span class="o">=</span> <span class="n">finalUV</span><span class="p">;</span>
                        
                        <span class="p">[</span><span class="n">loop</span><span class="p">]</span>
                        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">_ShadowSteps</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span><span class="p">)</span>
                        <span class="p">{</span>
                            <span class="n">currentDepth</span> <span class="o">-=</span> <span class="n">stepDepth</span><span class="p">;</span>
                            <span class="n">currentUV</span> <span class="o">+=</span> <span class="n">stepUV</span><span class="p">;</span>
                            
                            <span class="k">if</span><span class="p">(</span><span class="n">currentDepth</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span>
                                <span class="k">break</span><span class="p">;</span>
                            
                            <span class="kt">float</span> <span class="n">terrainDepth</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">tex2Dgrad</span><span class="p">(</span><span class="n">_HeightTex</span><span class="p">,</span> <span class="n">currentUV</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">).</span><span class="n">r</span><span class="p">;</span>
                            
                            <span class="k">if</span><span class="p">(</span><span class="n">terrainDepth</span> <span class="o">&lt;</span> <span class="n">currentDepth</span><span class="p">)</span>
                            <span class="p">{</span>
                                <span class="kt">float</span> <span class="n">occlusion</span> <span class="o">=</span> <span class="n">currentDepth</span> <span class="o">-</span> <span class="n">terrainDepth</span><span class="p">;</span>
                                <span class="n">shadow</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">shadow</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">saturate</span><span class="p">(</span><span class="n">occlusion</span> <span class="o">*</span> <span class="n">_ShadowSoftness</span><span class="p">));</span>
                                
                                <span class="k">if</span><span class="p">(</span><span class="n">shadow</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span><span class="p">)</span>
                                <span class="p">{</span>
                                    <span class="n">shadow</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
                                    <span class="k">break</span><span class="p">;</span>
                                <span class="p">}</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="c1">// 根据淡出系数混合阴影</span>
                <span class="n">shadow</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="n">shadow</span><span class="p">,</span> <span class="n">angleFade</span> <span class="o">*</span> <span class="n">lightAngleFade</span><span class="p">);</span>
                
                <span class="c1">// 根据淡出系数混合阴影（让阴影也随视角淡出）</span>
                <span class="c1">//shadow = lerp(1.0, shadow, angleFade);</span>

                <span class="c1">//return finalUV;</span>
                <span class="k">return</span> <span class="n">float3</span><span class="p">(</span><span class="n">finalUV</span><span class="p">,</span><span class="n">shadow</span><span class="p">);</span>
            <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="大致效果">大致效果</h2> <p><img src="/assets/img/TAMonth02/ParallaxOcclusionMapping_v2.gif" alt=""/></p>]]></content><author><name></name></author><category term="TAMonth02"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[Lv.3 Unity主线：视差(Parallax Mapping)]]></summary></entry><entry><title type="html">UnityTips：参数面板后面的{}和属性标签</title><link href="https://xueqingzhe.github.io/blog/2026/UnityTips-%E5%8F%82%E6%95%B0%E9%9D%A2%E6%9D%BF%E5%90%8E%E9%9D%A2%E7%9A%84-%E5%92%8C%E5%B1%9E%E6%80%A7%E6%A0%87%E7%AD%BE/" rel="alternate" type="text/html" title="UnityTips：参数面板后面的{}和属性标签"/><published>2026-01-04T00:00:00+00:00</published><updated>2026-01-04T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/UnityTips%EF%BC%9A%E5%8F%82%E6%95%B0%E9%9D%A2%E6%9D%BF%E5%90%8E%E9%9D%A2%E7%9A%84%7B%7D%E5%92%8C%E5%B1%9E%E6%80%A7%E6%A0%87%E7%AD%BE</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/UnityTips-%E5%8F%82%E6%95%B0%E9%9D%A2%E6%9D%BF%E5%90%8E%E9%9D%A2%E7%9A%84-%E5%92%8C%E5%B1%9E%E6%80%A7%E6%A0%87%E7%AD%BE/"><![CDATA[<p><strong><em>在声明变量的时候，后面默认参数会加一个{}，这其实是以前的属性标签的旧语法，现在已经废弃了，但为了向旧版本兼容所有留下来了，保持空白就行。不写也能编译过的</em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre> <span class="n">Properties</span>
    <span class="p">{</span>
        <span class="n">_MainTex</span> <span class="p">(</span><span class="s">"MainTex"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
    <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="属性标签">属性标签</h2> <p><strong>以下是 Unity ShaderLab 中常用的属性标签（Property Attributes），按功能分类：</strong></p> <h2 id="-纹理相关">📦 纹理相关</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[MainTexture]</code></td> <td>标记为主纹理，Material.mainTexture 会自动引用</td> <td><code class="language-plaintext highlighter-rouge">[MainTexture] _BaseMap</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[MainColor]</code></td> <td>标记为主颜色，Material.color 会自动引用</td> <td><code class="language-plaintext highlighter-rouge">[MainColor] _BaseColor</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[NoScaleOffset]</code></td> <td>隐藏纹理的缩放和偏移控件</td> <td><code class="language-plaintext highlighter-rouge">[NoScaleOffset] _MainTex</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Normal]</code></td> <td>标记为法线贴图</td> <td><code class="language-plaintext highlighter-rouge">[Normal] _BumpMap</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[PerRendererData]</code></td> <td>纹理数据来自每个渲染器（如 SpriteRenderer）</td> <td><code class="language-plaintext highlighter-rouge">[PerRendererData] _MainTex</code></td> </tr> </tbody> </table> <h2 id="-颜色相关">🎨 颜色相关</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[HDR]</code></td> <td>高动态范围颜色（显示拾色器）</td> <td><code class="language-plaintext highlighter-rouge">[HDR] _EmissionColor</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Gamma]</code></td> <td>标记颜色/浮点数为 Gamma 空间（sRGB）</td> <td><code class="language-plaintext highlighter-rouge">[Gamma] _Color</code></td> </tr> </tbody> </table> <h2 id="-数值范围与类型">🔢 数值范围与类型</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[IntRange]</code></td> <td>整数范围滑块</td> <td><code class="language-plaintext highlighter-rouge">[IntRange] _Quality</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[PowerSlider]</code></td> <td>指数曲线滑块</td> <td><code class="language-plaintext highlighter-rouge">[PowerSlider(3.0)] _Smoothness</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Enum]</code></td> <td>自定义枚举下拉菜单</td> <td><code class="language-plaintext highlighter-rouge">[Enum(Off,0,On,1)] _Mode</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[KeywordEnum]</code></td> <td>为每个选项生成 Shader Keyword</td> <td><code class="language-plaintext highlighter-rouge">[KeywordEnum(None,Add,Multiply)] _Blend</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Toggle]</code></td> <td>开关（生成 <code class="language-plaintext highlighter-rouge">property name_ON</code> keyword）</td> <td><code class="language-plaintext highlighter-rouge">[Toggle] _USE_FEATURE</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[ToggleUI]</code></td> <td>开关（不生成 keyword，仅 UI）</td> <td><code class="language-plaintext highlighter-rouge">[ToggleUI] _Advanced</code></td> </tr> </tbody> </table> <h2 id="-ui-布局">📐 UI 布局</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Space]</code></td> <td>添加垂直间距</td> <td><code class="language-plaintext highlighter-rouge">[Space]</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Space(20)]</code></td> <td>添加指定高度的间距</td> <td><code class="language-plaintext highlighter-rouge">[Space(20)]</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Header]</code></td> <td>添加分组标题</td> <td><code class="language-plaintext highlighter-rouge">[Header(Main Settings)]</code></td> </tr> </tbody> </table> <h2 id="️-高级控制">⚙️ 高级控制</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[HideInInspector]</code></td> <td>在 Inspector 中隐藏</td> <td><code class="language-plaintext highlighter-rouge">[HideInInspector] _Internal</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[NonModifiableTextureData]</code></td> <td>纹理不可修改（只读）</td> <td><code class="language-plaintext highlighter-rouge">[NonModifiableTextureData] _LUT</code></td> </tr> </tbody> </table> <h2 id="-向量分量控制">📊 向量分量控制</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Vector2]</code></td> <td>限制为 2 分量向量</td> <td><code class="language-plaintext highlighter-rouge">[Vector2] _Tiling</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Vector3]</code></td> <td>限制为 3 分量向量</td> <td><code class="language-plaintext highlighter-rouge">[Vector3] _Position</code></td> </tr> </tbody> </table> <h2 id="-特殊用途">🔄 特殊用途</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>适用版本</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Curve]</code></td> <td>曲线编辑器（用于 float 属性）</td> <td>Unity 2020+</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Gradient]</code></td> <td>渐变编辑器（需要自定义绘制）</td> <td>需自定义</td> </tr> </tbody> </table> <hr/> <h2 id="-综合使用示例以下是-unity-shaderlab-中常用的属性标签property-attributes按功能分类">📝 综合使用示例以下是 Unity ShaderLab 中常用的属性标签（Property Attributes），按功能分类：</h2> <h2 id="-纹理相关-1">📦 纹理相关</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[MainTexture]</code></td> <td>标记为主纹理，Material.mainTexture 会自动引用</td> <td><code class="language-plaintext highlighter-rouge">[MainTexture] _BaseMap</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[MainColor]</code></td> <td>标记为主颜色，Material.color 会自动引用</td> <td><code class="language-plaintext highlighter-rouge">[MainColor] _BaseColor</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[NoScaleOffset]</code></td> <td>隐藏纹理的缩放和偏移控件</td> <td><code class="language-plaintext highlighter-rouge">[NoScaleOffset] _MainTex</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Normal]</code></td> <td>标记为法线贴图</td> <td><code class="language-plaintext highlighter-rouge">[Normal] _BumpMap</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[PerRendererData]</code></td> <td>纹理数据来自每个渲染器（如 SpriteRenderer）</td> <td><code class="language-plaintext highlighter-rouge">[PerRendererData] _MainTex</code></td> </tr> </tbody> </table> <h2 id="-颜色相关-1">🎨 颜色相关</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[HDR]</code></td> <td>高动态范围颜色（显示拾色器）</td> <td><code class="language-plaintext highlighter-rouge">[HDR] _EmissionColor</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Gamma]</code></td> <td>标记颜色/浮点数为 Gamma 空间（sRGB）</td> <td><code class="language-plaintext highlighter-rouge">[Gamma] _Color</code></td> </tr> </tbody> </table> <h2 id="-数值范围与类型-1">🔢 数值范围与类型</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[IntRange]</code></td> <td>整数范围滑块</td> <td><code class="language-plaintext highlighter-rouge">[IntRange] _Quality</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[PowerSlider]</code></td> <td>指数曲线滑块</td> <td><code class="language-plaintext highlighter-rouge">[PowerSlider(3.0)] _Smoothness</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Enum]</code></td> <td>自定义枚举下拉菜单</td> <td><code class="language-plaintext highlighter-rouge">[Enum(Off,0,On,1)] _Mode</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[KeywordEnum]</code></td> <td>为每个选项生成 Shader Keyword</td> <td><code class="language-plaintext highlighter-rouge">[KeywordEnum(None,Add,Multiply)] _Blend</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Toggle]</code></td> <td>开关（生成 <code class="language-plaintext highlighter-rouge">property name_ON</code> keyword）</td> <td><code class="language-plaintext highlighter-rouge">[Toggle] _USE_FEATURE</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[ToggleUI]</code></td> <td>开关（不生成 keyword，仅 UI）</td> <td><code class="language-plaintext highlighter-rouge">[ToggleUI] _Advanced</code></td> </tr> </tbody> </table> <h2 id="-ui-布局-1">📐 UI 布局</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Space]</code></td> <td>添加垂直间距</td> <td><code class="language-plaintext highlighter-rouge">[Space]</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Space(20)]</code></td> <td>添加指定高度的间距</td> <td><code class="language-plaintext highlighter-rouge">[Space(20)]</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Header]</code></td> <td>添加分组标题</td> <td><code class="language-plaintext highlighter-rouge">[Header(Main Settings)]</code></td> </tr> </tbody> </table> <h2 id="️-高级控制-1">⚙️ 高级控制</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[HideInInspector]</code></td> <td>在 Inspector 中隐藏</td> <td><code class="language-plaintext highlighter-rouge">[HideInInspector] _Internal</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[NonModifiableTextureData]</code></td> <td>纹理不可修改（只读）</td> <td><code class="language-plaintext highlighter-rouge">[NonModifiableTextureData] _LUT</code></td> </tr> </tbody> </table> <h2 id="-向量分量控制-1">📊 向量分量控制</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>示例</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Vector2]</code></td> <td>限制为 2 分量向量</td> <td><code class="language-plaintext highlighter-rouge">[Vector2] _Tiling</code></td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Vector3]</code></td> <td>限制为 3 分量向量</td> <td><code class="language-plaintext highlighter-rouge">[Vector3] _Position</code></td> </tr> </tbody> </table> <h2 id="-特殊用途-1">🔄 特殊用途</h2> <table> <thead> <tr> <th>标签</th> <th>说明</th> <th>适用版本</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">[Curve]</code></td> <td>曲线编辑器（用于 float 属性）</td> <td>Unity 2020+</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">[Gradient]</code></td> <td>渐变编辑器（需要自定义绘制）</td> <td>需自定义</td> </tr> </tbody> </table> <h2 id="-综合使用示例">📝 综合使用示例</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
</pre></td><td class="rouge-code"><pre><span class="n">Properties</span> <span class="p">{</span>
    <span class="c1">// ========== 标题和间距 ==========</span>
    <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">Main</span> <span class="n">Settings</span><span class="p">)]</span>
    <span class="p">[</span><span class="n">Space</span><span class="p">(</span><span class="mi">10</span><span class="p">)]</span>
    
    <span class="c1">// 主纹理和颜色（会被 Material.mainTexture/color 引用）</span>
    <span class="p">[</span><span class="n">MainTexture</span><span class="p">]</span> <span class="p">[</span><span class="n">NoScaleOffset</span><span class="p">]</span> <span class="n">_BaseMap</span> <span class="p">(</span><span class="s">"Albedo"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
    <span class="p">[</span><span class="n">MainColor</span><span class="p">]</span> <span class="n">_BaseColor</span> <span class="p">(</span><span class="s">"Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
    
    <span class="p">[</span><span class="n">Space</span><span class="p">]</span>
    <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">Normal</span> <span class="n">and</span> <span class="n">Height</span><span class="p">)]</span>
    
    <span class="c1">// 法线贴图</span>
    <span class="p">[</span><span class="n">Normal</span><span class="p">]</span> <span class="n">_BumpMap</span> <span class="p">(</span><span class="s">"Normal Map"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"bump"</span> <span class="p">{}</span>
    
    <span class="c1">// 开关控制</span>
    <span class="p">[</span><span class="n">Toggle</span><span class="p">(</span><span class="n">_PARALLAXMAP</span><span class="p">)]</span> <span class="n">_UseParallax</span> <span class="p">(</span><span class="s">"Use Parallax"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="p">[</span><span class="n">Space</span><span class="p">(</span><span class="mi">15</span><span class="p">)]</span>
    <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">Effects</span><span class="p">)]</span>
    
    <span class="c1">// HDR 颜色</span>
    <span class="p">[</span><span class="n">HDR</span><span class="p">]</span> <span class="n">_EmissionColor</span> <span class="p">(</span><span class="s">"Emission"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
    
    <span class="c1">// 指数滑块</span>
    <span class="p">[</span><span class="n">PowerSlider</span><span class="p">(</span><span class="mi">3</span><span class="p">.</span><span class="mi">0</span><span class="p">)]</span> <span class="n">_Smoothness</span> <span class="p">(</span><span class="s">"Smoothness"</span><span class="p">,</span> <span class="n">Range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">))</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span>
    
    <span class="c1">// 枚举下拉</span>
    <span class="p">[</span><span class="n">Enum</span><span class="p">(</span><span class="n">UnityEngine</span><span class="p">.</span><span class="n">Rendering</span><span class="p">.</span><span class="n">BlendMode</span><span class="p">)]</span> <span class="n">_SrcBlend</span> <span class="p">(</span><span class="s">"Src Blend"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="p">[</span><span class="n">Enum</span><span class="p">(</span><span class="n">UnityEngine</span><span class="p">.</span><span class="n">Rendering</span><span class="p">.</span><span class="n">BlendMode</span><span class="p">)]</span> <span class="n">_DstBlend</span> <span class="p">(</span><span class="s">"Dst Blend"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="c1">// 关键字枚举（会生成多个 Shader Variant）</span>
    <span class="p">[</span><span class="n">KeywordEnum</span><span class="p">(</span><span class="n">None</span><span class="p">,</span> <span class="n">Add</span><span class="p">,</span> <span class="n">Multiply</span><span class="p">,</span> <span class="n">Screen</span><span class="p">)]</span> <span class="n">_BlendMode</span> <span class="p">(</span><span class="s">"Blend Mode"</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="p">[</span><span class="n">Space</span><span class="p">]</span>
    <span class="p">[</span><span class="n">Header</span><span class="p">(</span><span class="n">Advanced</span><span class="p">)]</span>
    
    <span class="c1">// 内部变量（不显示）</span>
    <span class="p">[</span><span class="n">HideInInspector</span><span class="p">]</span> <span class="n">_Internal</span> <span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="n">Float</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="c1">// 每渲染器数据</span>
    <span class="p">[</span><span class="n">PerRendererData</span><span class="p">]</span> <span class="n">_RendererColor</span> <span class="p">(</span><span class="s">"Renderer Color"</span><span class="p">,</span> <span class="n">Color</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><category term="TAMonth01"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[UnityTips：参数面板后面的{}和属性标签]]></summary></entry><entry><title type="html">UnityTips：多SubShder</title><link href="https://xueqingzhe.github.io/blog/2026/UnityTips-%E5%A4%9ASubShder/" rel="alternate" type="text/html" title="UnityTips：多SubShder"/><published>2026-01-04T00:00:00+00:00</published><updated>2026-01-04T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/UnityTips%EF%BC%9A%E5%A4%9ASubShder</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/UnityTips-%E5%A4%9ASubShder/"><![CDATA[<h2 id="shader结构层级">Shader结构层级</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"ShaderName"</span>                    <span class="c1">// ← 最外层：Shader</span>
<span class="p">{</span>
    <span class="n">Properties</span> <span class="p">{</span> <span class="p">}</span>                      <span class="c1">// ← 属性定义（全局）</span>
    
    <span class="n">SubShader</span>                           <span class="c1">// ← 第一个SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="p">}</span>                        <span class="c1">// SubShader Tags</span>
        <span class="n">LOD</span> <span class="mi">200</span>
        
        <span class="n">Pass</span>                            <span class="c1">// ← Pass 1</span>
        <span class="p">{</span>
            <span class="n">Tags</span> <span class="p">{</span> <span class="p">}</span>                    <span class="c1">// Pass Tags</span>
            <span class="c1">// 渲染状态</span>
            <span class="n">CGPROGRAM</span>
            <span class="c1">// Shader代码</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
        
        <span class="n">Pass</span>                            <span class="c1">// ← Pass 2</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="n">SubShader</span>                           <span class="c1">// ← 第二个SubShader（Fallback）</span>
    <span class="p">{</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="n">FallBack</span> <span class="s">"Diffuse"</span>                  <span class="c1">// ← 最终Fallback</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="为什么要多个subshader">为什么要多个SubShader？</h2> <h3 id="原因1不同硬件支持">原因1：不同硬件支持</h3> <p>不同的GPU有不同的能力，多个SubShader可以针对不同硬件：</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"Custom/MultiPlatform"</span>
<span class="p">{</span>
    <span class="n">Properties</span>
    <span class="p">{</span>
        <span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Texture"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
    <span class="p">}</span>
    
    <span class="c1">// SubShader 1: 高端设备（PC、主机）</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="p">}</span>
        <span class="n">LOD</span> <span class="mi">300</span>  <span class="c1">// Level of Detail 300</span>
        
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="cp">#pragma vertex vert
</span>            <span class="cp">#pragma fragment frag
</span>            <span class="cp">#pragma target 5.0  // Shader Model 5.0（高端）
</span>            
            <span class="c1">// 使用复杂特性：</span>
            <span class="c1">// - 计算着色器</span>
            <span class="c1">// - 曲面细分</span>
            <span class="c1">// - 几何着色器</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// SubShader 2: 中端设备（移动高端）</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="p">}</span>
        <span class="n">LOD</span> <span class="mi">200</span>
        
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="cp">#pragma vertex vert
</span>            <span class="cp">#pragma fragment frag
</span>            <span class="cp">#pragma target 3.0  // Shader Model 3.0（中端）
</span>            
            <span class="c1">// 较简单的效果</span>
            <span class="c1">// - 基础PBR</span>
            <span class="c1">// - 简化光照</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// SubShader 3: 低端设备（旧移动设备）</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderType"</span><span class="o">=</span><span class="s">"Opaque"</span> <span class="p">}</span>
        <span class="n">LOD</span> <span class="mi">100</span>
        
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="cp">#pragma vertex vert
</span>            <span class="cp">#pragma fragment frag
</span>            <span class="cp">#pragma target 2.0  // Shader Model 2.0（低端）
</span>            
            <span class="c1">// 最简单的效果</span>
            <span class="c1">// - 顶点光照</span>
            <span class="c1">// - 无阴影</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="n">FallBack</span> <span class="s">"Mobile/Diffuse"</span>  <span class="c1">// 如果所有SubShader都不支持</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="原因2不同渲染管线">原因2：不同渲染管线</h3> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"Custom/MultiPipeline"</span>
<span class="p">{</span>
    <span class="n">Properties</span> <span class="p">{</span> <span class="p">}</span>
    
    <span class="c1">// SubShader 1: URP（通用渲染管线）</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderPipeline"</span><span class="o">=</span><span class="s">"UniversalPipeline"</span> <span class="p">}</span>
        
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">Tags</span> <span class="p">{</span> <span class="s">"LightMode"</span><span class="o">=</span><span class="s">"UniversalForward"</span> <span class="p">}</span>
            <span class="n">HLSLPROGRAM</span>
            <span class="cp">#include</span> <span class="cpf">"Packages/com.unity.render-pipelines.universal/..."</span><span class="cp">
</span>            <span class="n">ENDHLSL</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// SubShader 2: HDRP（高清渲染管线）</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Tags</span> <span class="p">{</span> <span class="s">"RenderPipeline"</span><span class="o">=</span><span class="s">"HDRenderPipeline"</span> <span class="p">}</span>
        
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">Tags</span> <span class="p">{</span> <span class="s">"LightMode"</span><span class="o">=</span><span class="s">"Forward"</span> <span class="p">}</span>
            <span class="n">HLSLPROGRAM</span>
            <span class="cp">#include</span> <span class="cpf">"Packages/com.unity.render-pipelines.high-definition/..."</span><span class="cp">
</span>            <span class="n">ENDHLSL</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// SubShader 3: Built-in管线</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">Tags</span> <span class="p">{</span> <span class="s">"LightMode"</span><span class="o">=</span><span class="s">"ForwardBase"</span> <span class="p">}</span>
            <span class="n">CGPROGRAM</span>
            <span class="cp">#include</span> <span class="cpf">"UnityCG.cginc"</span><span class="cp">
</span>            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h3 id="原因3质量分级">原因3：质量分级</h3> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
</pre></td><td class="rouge-code"><pre><span class="n">Shader</span> <span class="s">"Custom/QualityLevels"</span>
<span class="p">{</span>
    <span class="n">Properties</span>
    <span class="p">{</span>
        <span class="n">_MainTex</span> <span class="p">(</span><span class="s">"Texture"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"white"</span> <span class="p">{}</span>
        <span class="n">_NormalMap</span> <span class="p">(</span><span class="s">"Normal Map"</span><span class="p">,</span> <span class="mi">2</span><span class="n">D</span><span class="p">)</span> <span class="o">=</span> <span class="s">"bump"</span> <span class="p">{}</span>
    <span class="p">}</span>
    
    <span class="c1">// 超高质量：法线贴图 + 视差贴图 + 细节贴图</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">LOD</span> <span class="mi">400</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="c1">// 完整PBR + 所有贴图</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// 高质量：法线贴图 + PBR</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">LOD</span> <span class="mi">300</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="c1">// PBR + 法线贴图</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// 中质量：简化光照</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">LOD</span> <span class="mi">200</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="c1">// Blinn-Phong + 法线贴图</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// 低质量：基础漫反射</span>
    <span class="n">SubShader</span>
    <span class="p">{</span>
        <span class="n">LOD</span> <span class="mi">100</span>
        <span class="n">Pass</span>
        <span class="p">{</span>
            <span class="n">CGPROGRAM</span>
            <span class="c1">// Lambert漫反射</span>
            <span class="n">ENDCG</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><category term="TAMonth01"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[UnityTips：多SubShder]]></summary></entry><entry><title type="html">UnityTips：法线的空间变化</title><link href="https://xueqingzhe.github.io/blog/2026/UnityTips-%E6%B3%95%E7%BA%BF%E7%9A%84%E7%A9%BA%E9%97%B4%E5%8F%98%E5%8C%96/" rel="alternate" type="text/html" title="UnityTips：法线的空间变化"/><published>2026-01-04T00:00:00+00:00</published><updated>2026-01-04T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/UnityTips%EF%BC%9A%E6%B3%95%E7%BA%BF%E7%9A%84%E7%A9%BA%E9%97%B4%E5%8F%98%E5%8C%96</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/UnityTips-%E6%B3%95%E7%BA%BF%E7%9A%84%E7%A9%BA%E9%97%B4%E5%8F%98%E5%8C%96/"><![CDATA[<h1 id="法线空间转换">法线空间转换</h1> <p><strong><em>通常法线空间转换，使用下面两种方法</em></strong></p> <h2 id="1使用内置函数">1使用内置函数</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">o</span><span class="p">.</span><span class="n">normalWS</span> <span class="o">=</span> <span class="n">unity_ObjectToWorldNormal</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">normalOS</span><span class="p">);</span>
<span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span> <span class="o">=</span> <span class="n">UnityObjectToWorldDir</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">tangentOS</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div> <h2 id="2使用矩阵乘法">2使用矩阵乘法</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">o</span><span class="p">.</span><span class="n">normalWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">float4</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">normalOS</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">),</span> <span class="n">unity_WorldToObject</span><span class="p">).</span><span class="n">xyz</span><span class="p">;</span>
<span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">tangentOS</span><span class="p">.</span><span class="n">xyz</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)).</span><span class="n">xyz</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong><em>D:\Unity\2022.3.62f3c1\Editor\Data\CGIncludes\UnityCG.cginc。在源文件中Unity的转换方法是这样的。</em></strong> <img src="/assets/img/TAMonth01/Pasted image 20251205170810.png" alt=""/></p> <h1 id="关于法线的矩阵乘法顺序">关于法线的矩阵乘法顺序</h1> <p><strong><em>对于法线和切线转换，调用顺序是不一样的，这是为什么呢</em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">o</span><span class="p">.</span><span class="n">normalWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">float4</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">normalOS</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">),</span> <span class="n">unity_WorldToObject</span><span class="p">).</span><span class="n">xyz</span><span class="p">;</span>
<span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">tangentOS</span><span class="p">.</span><span class="n">xyz</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">)).</span><span class="n">xyz</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong><em>通常来说，以切线为准，切线直接使用unity_ObjectToWorld转换就可以了</em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span> <span class="o">=</span> <span class="n">mul</span><span class="p">(</span><span class="n">unity_ObjectToWorld</span><span class="p">,</span> <span class="n">float4</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">tangentOS</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">));</span>
</pre></td></tr></tbody></table></code></pre></div></div> <p><strong><em>法线转换是不能直接这样做的，因为在非均匀缩放的时候法线会跟着变。这里需要推一下</em></strong> <img src="/assets/img/TAMonth01/tempFileForShare_20251205-181701(1).jpg" alt=""/></p> <h2 id="副切线副法线">副切线（副法线）</h2> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">o</span><span class="p">.</span><span class="n">binormalWS</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">cross</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">normalWS</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">tangentWS</span><span class="p">))</span> <span class="o">*</span> <span class="n">v</span><span class="p">.</span><span class="n">tangent</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
<span class="c1">//* v.tangent.w是为了处理不同平台副法线翻转问题</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name></name></author><category term="TAMonth01"/><category term="unity"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[UnityTips：法线的空间变化]]></summary></entry><entry><title type="html">理论支线：Gamma Correction 伽马校正</title><link href="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-Gamma-Correction-%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3/" rel="alternate" type="text/html" title="理论支线：Gamma Correction 伽马校正"/><published>2026-01-03T00:00:00+00:00</published><updated>2026-01-03T00:00:00+00:00</updated><id>https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF%EF%BC%9AGamma%20Correction%20%20%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3</id><content type="html" xml:base="https://xueqingzhe.github.io/blog/2026/%E7%90%86%E8%AE%BA%E6%94%AF%E7%BA%BF-Gamma-Correction-%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3/"><![CDATA[<h1 id="gamma-correction">Gamma Correction</h1> <p><strong>Gamma Correction 伽马校正，其实有两种意思，需要根据上下文判断</strong> 为了避免混淆，图形学领域倾向于使用：</p> <ul> <li><strong>Gamma encoding（伽马编码）</strong>：pow(color, 1/2.2) - 存储前做的</li> <li><strong>Gamma decoding（伽马解码）</strong>：pow(color, 2.2) - 显示器做的</li> <li><strong>Display gamma</strong>：显示器的2.2伽马特性</li> </ul> <p>一旦我们计算出场景的最终像素颜色，就必须将它们显示在显示器上。在数字成像的早期时代，大多数显示器都是阴极射线管（CRT）显示器。这些显示器具有物理特性：输入电压增加两倍并不意味着亮度翻倍。将输入电压加倍后，亮度等于显示器伽马值的指数关系约为 2.2。这（巧合地）也与人类测量亮度的方式非常相似，因为亮度也以类似的（反幂）关系显示。为了更好地理解这一切意味着什么，请看看以下图片 <img src="/assets/img/TAMonth01/Pasted image 20251231181450.png" alt=""/> 上线看起来是<strong>人眼正确</strong>的亮度比例，将亮度加倍（比如从0.1到0.2）确实看起来亮度翻了一倍，且差异很稳定。然而，当我们谈论光的物理亮度时，比如离开光源的光子数量，底部的刻度实际上显示的是正确的亮度。在最低光度下，将亮度加倍会恢复正确的物理亮度，但由于<strong>我们的眼睛对亮度的感知不同（更易受暗色变化影响）</strong>，看起来很奇怪。 <img src="/assets/img/TAMonth01/Pasted image 20251231182349.png" alt=""/> <strong>这里最下方是显示器输出亮度，中间虚线则是我们处理颜色的也就是线性颜色。如果将线性颜色增大2倍，那么显示器亮度会暴增4倍因为是pow(color,2.2)。这就会导致问题，为了抵消这个物理影响，所以我们需要进行反伽马或者叫Gamma encoding（伽马编码），也就是pow(color,1/2.2)这个操作</strong> <img src="/assets/img/TAMonth01/Pasted image 20251231183332.png" alt=""/></p> <h1 id="srgb-textures">sRGB textures </h1> <p>由于显示器在应用伽马后显示颜色，每当你在电脑上画画、编辑或绘画时，你是在根据显示器上看到的颜色来选择颜是在 sRGB 空间，也就是人眼感知空间，但是渲染器系统读取之后再渲染，会导致进行两次伽马pow(color,2.2)。当我们基于显示器上看到的图像创建图像时，我们实际上是伽马校正了图像的颜色值，使其在显示器上看起来正确。因为我们在渲染器中再次进行伽马校正，图像最终会变得过于明亮。 <img src="/assets/img/TAMonth01/Pasted image 20251231190820.png" alt=""/> <strong>正常的图像创作流程：</strong></p> <ol> <li>美术在显示器上看到的图像（已经被显示器做了 gamma 2.2 解码）</li> <li>美术保存图像时，图像编辑软件会做 gamma 1/2.2 编码在输出</li> <li>保存的图像文件（如 PNG、JPG）<strong>已经是 gamma 编码过的 sRGB 数据</strong></li> </ol> <p>那么给到渲染器读取的是已经做 gamma 1/2.2 编码的图像数据，然后输出时又会做 gamma 1/2.2 编码，那么最终图像就会过曝。因此通常需要对图像进行预处理。 比如UE中就会对纹理进行预处理，对图像标记SRGB，先进行gamma 2.2 解码在进行使用，一般来说只针对<strong>颜色贴图</strong>会有这样操作，如果是<strong>参与计算的灰度数据图</strong>则不需要SRGB，因为肉眼不需要看到这个灰度，只是参与运算 <img src="/assets/img/TAMonth01/Pasted image 20251231191208.png" alt=""/></p> <h1 id="对光照衰减的影响">对光照衰减的影响</h1> <h2 id="平方反比定律真实物理">平方反比定律（真实物理）</h2> <p>现实世界中，光照遵循：</p> \[I = \frac{I_0}{d^2}\] <p>其中：</p> <ul> <li>$I$ = 衰减后的光照强度</li> <li>$I_0$ = 光源初始强度</li> <li>$d$ = 距离光源的距离 也就是 <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="kt">float</span> <span class="n">distanceSqr</span> <span class="o">=</span> <span class="n">distance</span> <span class="o">*</span> <span class="n">distance</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">physicalAttenuation</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">/</span> <span class="n">max</span><span class="p">(</span><span class="n">distanceSqr</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span> <span class="o">*</span> <span class="mi">0</span><span class="p">.</span><span class="mo">01</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div> </div> <h2 id="伽马空间下的衰减补偿调整">伽马空间下的衰减补偿调整</h2> <p><img src="/assets/img/TAMonth01/Pasted image 20260103174926.png" alt=""/> <strong>对于如果直接在Gamma Space下工作，直接使用物理衰减会导致过暗，因为Gamma Space相当于我们最终看到的效果，但是如果是渲染器渲染到屏幕最后还是会进行伽马矫正一次，那么渲染图像就会偏暗</strong> <strong>正确的物理衰减是</strong></p> </li> </ul> \[I = \frac{1.0}{d^2}\] <p><strong>伽马矫正就导致成了</strong></p> \[I = (\frac{1.0}{d^{2}})^{2.2}\] <p><strong>为了补偿，可以使用不正确的线性衰减</strong></p> \[I = \frac{1.0}{d}\] <p><strong>这样矫正后结果是</strong></p> \[I = (\frac{1.0}{d})^{2.2}\approx\frac{1.0}{d^2}\] <p><strong>这和正确的物理衰减会更加接近</strong></p> <p><strong>对于伽马空间的补偿方式其实可以忽略，因为现在游戏引擎都是使用线性空间工作，而不是伽马空间</strong></p> <h2 id="线性空间处理">线性空间处理</h2> <p><strong>如果是在线性空间下工作，可以直接使用符合物理正确的衰减公式，不需要管其它，渲染到屏幕上最终会自动进行伽马矫正</strong></p>]]></content><author><name></name></author><category term="TAMonth01"/><category term="shader"/><category term="rendering"/><summary type="html"><![CDATA[理论支线：Gamma Correction 伽马校正]]></summary></entry></feed>