iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

把 Shader 想成「會被大量重複執行的小程式」,跑在你的顯示卡(GPU)上。
在 Unity 裡,我們用一種叫 ShaderLab 的外層語法包住 HLSL 程式,告訴引擎「這個材質要怎麼畫」。今天的目標是:看得懂 + 跑得起 + 改得動


0. 兩分鐘大地圖:Unity 裡的 Shader 長什麼樣?

Shader "你的名字/路徑"
{
  Properties {    //← 在 Inspector 露出的旋鈕(顏色、貼圖、數值…)
    _BaseColor ("Base Color", Color) = (1,1,1,1)
    _BaseMap   ("Base Map", 2D) = "white" {}
  }

  SubShader {      // ← 真正的繪製規則(built-in可以有多個)
    Tags { "RenderPipeline"="UniversalRenderPipeline"  // URP
           "RenderType"="Opaque" "Queue"="Geometry" }

    Pass {          // ← 一次繪製的階段(Forward、Shadow 等)
      Name "ForwardUnlit"
      Tags{ "LightMode"="UniversalForward" }

      HLSLPROGRAM  // ← 這裡面就是 HLSL(像 C 語言)!
        // #pragma vertex / fragment 指定進入點
      ENDHLSL
    }
  }
}
  • Properties:給美術/你在 Inspector 操作用的參數。
  • SubShader/Pass:告訴引擎這個材質用什麼「管線與通道」來畫。
  • HLSLPROGRAM … ENDHLSL:你寫的 GPU 小程式(頂點 VS / 像素 FS)。

1. 你的第一支 Unity Unlit(純色)Shader

我們先做最小可運行範例:把模型用一個顏色畫出來。

步驟

  1. Project 視窗 > 右鍵 > Create > Shader > URP > Unlit Shader(或「Empty Shader」自己貼下方內容)。
  2. Create > Material,選這個 Shader;把材質拖到場景中的 Cube。

cube
shader

代碼

Shader "Unlit/PBR"
{
    Properties
  {
    _BaseColor ("Base Color", Color) = (1,1,1,1)
  }

  SubShader
  {
    Tags { "RenderType"="Opaque" }
    LOD 100

    Pass
    {
        
      CGPROGRAM            

      // built-in 內建的函式庫
      #include "UnityCG.cginc"        
      #include "UnityLightingCommon.cginc"      

      #pragma vertex   vert
      #pragma fragment frag
      #pragma target   3.0
          

      // 1) Inspector 旋鈕對應到「可被 SRP Batcher 使用」的常數緩衝
      float4 _BaseColor;      

      // 2) VS 輸入/輸出:位置一定要有,其他想傳就加
      struct appdata {
        float4 positionOS : POSITION;   // 物件空間位置
      };
      struct v2f {
        float4 positionHCS : SV_POSITION; // 齊次裁剪空間(畫到螢幕要用)
      };

      // 3) 頂點著色器:把物件位置轉到螢幕(裁剪)空間
      v2f vert (appdata v)
      {
        v2f o;
        o.positionHCS = UnityObjectToClipPos(v.positionOS);
        return o;
      }

      // 4) 像素著色器:輸出顏色
      half4 frag (v2f i) : SV_Target
      {
        return _BaseColor;
      }
    ENDCG
    }
  }
}

你剛學到:

  • UnityObjectToClipPos 幫你做 P*V*M 變換(Day 2 的內容)。
  • 這是一支 Unlit:不算光,只畫顏色。
    (範例,將base color設成紅色)
    shading

2. 讓它吃貼圖(Hello Texture)

把顏色換成從貼圖取樣,並保留一個顏色做調整(乘上去)。

新增texture的欄位

  Properties
  {
    _MainTex ("Texture", 2D) = "white" {}
  }

宣告變數

      // 1) Inspector 旋鈕對應到的常數緩衝      
       float4 _BaseColor;
       sampler2D _MainTex;
       //ST = Scale / Translation
       //xy=縮放(tile)  zw=偏移(offset)
       float4 _MainTex_ST;       
  • _MainTex_ST : 可選,若沒有要縮放圖片即可省略。
修改資料格式
     // 2) VS 輸入/輸出:位置一定要有,其他想傳就加
      struct appdata {
        float4 positionOS : POSITION;   // 物件空間位置
        //第一紋理座標
        float2 texcoord :TEXCOORD0;
      };
      struct v2f {
        float4 positionHCS : SV_POSITION; // 齊次裁剪空間(畫到螢幕要用)
        float2 uv:TEXCOORD2; // 存在 TEXCOORD2
      };

計算/套用uv

// 3) 頂點著色器:把物件位置轉到螢幕(裁剪)空間
      v2f vert (appdata v)
      {
        v2f o;
        o.positionHCS = UnityObjectToClipPos(v.positionOS);
        //計算UV
        //o.uv.xy=v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
        o.uv=TRANSFORM_TEX(v.texcoord,_MainTex) ;
        return o;
      }

      // 4) 像素著色器:輸出顏色
      half4 frag (v2f i) : SV_Target
      {
        //模型貼圖顏色
        fixed3 albedo=tex2D(_MainTex ,i.uv).rgb ;   
        return half4(albedo,1);
      }
  • tex2D 會依據uv對圖片做採樣。

albedo

小提醒(很重要):

  • 這張 BaseMap 在 Inspector 通常是 sRGB 貼圖,Unity 會幫你轉成線性做運算,最後再轉回螢幕色。
  • 如果你塞進去的是 法線貼圖,記得要把 Texture Type 設為 Normal map(不做 sRGB 矯正)。

3. 讓它有基本打光(Lambert 漫反射)

現在加上「一盞方向光」,用 Lambert公式(簡化)拿到主光。
https://chart.googleapis.com/chart?cht=tx&chl=L%3Dl%5Ccdot%20n

修改資料格式儲存world資訊(與太陽相同空間)

// 2) VS 輸入/輸出:位置一定要有,其他想傳就加
      struct appdata {
        float4 positionOS : POSITION;   // 物件空間位置
        float3 normal :NORMAL;         // 法線
        //第一紋理座標
        float2 texcoord :TEXCOORD0;
      };
      struct v2f {
        float4 positionHCS : SV_POSITION; // 齊次裁剪空間(畫到螢幕要用)
        float3 worldNormal:TEXCOORD0;   // 存在 TEXCOORD0
        float3 worldPos:TEXCOORD1; // 存在 TEXCOORD1
        float2 uv:TEXCOORD2; // 存在 TEXCOORD2
      };

修改shading

// 3) 頂點著色器:把物件位置轉到螢幕(裁剪)空間
      v2f vert (appdata v)
      {
        v2f o;
        o.positionHCS = UnityObjectToClipPos(v.positionOS);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.worldPos=mul(unity_ObjectToWorld,v.positionOS).xyz;
        //計算UV
        //o.uv.xy=v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
        o.uv=TRANSFORM_TEX(v.texcoord,_MainTex) ;
        return o;
      }

      // 4) 像素著色器:輸出顏色
      half4 frag (v2f i) : SV_Target
      {
        //因內建函數通常沒有歸一化,計算frag時,常要用normalize進行歸一化
        fixed3 worldNormal=normalize(i.worldNormal);
        fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos)); 

        float Lambertian=dot(worldNormal,worldLightDir);
        Lambertian=saturate(Lambertian); //限制在0~1之間
        //模型貼圖顏色
        fixed3 albedo=tex2D(_MainTex ,i.uv).rgb ;   
        return half4(albedo * Lambertian,1);
      }

成果: (可以動一下太陽光的角度,觀察陰影變化)
lambert

你剛學到:

  • UnityWorldSpaceLightDir 能拿到給定point與主方向光的方向。
  • 法線要轉到 世界空間 再和光做點積。
  • 光照不對時,常見是 方向符號法線沒正規化

4. 常見開關:透明、雙面、Z 寫入、混色

有時你會做玻璃/特效圖層,需要調繪製狀態:

// 透明
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

// 雙面(雙面渲染注意法線方向與陰影)
Cull Off

可以看到因ZWrite Off導致深度錯誤,且blend讓他能做顏色疊加。
blending

透明會讓 Early-Z 失效,排序與效能都更敏感。初學先用不透明材質,等熟悉再玩透明。


5. Inspector 與程式如何溝通?(Properties ↔ CBUFFER/資源)

  • Properties 定義的 _BaseColor_BaseMap 要在 HLSL 也有對應:
  • 貼圖類 → TEXTURE2D / SAMPLER 宏配對。
  • 名稱要一致(包括大小寫)。

6. 坑

  1. 法線怪、光斑亂

    • 解:用 TransformObjectToWorldNormal;若有 Normal Map,要 TBN 轉空間並再 normalize
  2. 透明順序錯

    • 解:透明物件應排在 Queue Transparent,並以由遠到近繪製;或使用加權混合等技巧。
  3. 顏色灰/偏色

    • 解:BaseMap 要走 sRGB;法線/Metallic/Roughness/AO 走 線性;Project 設定建議 Linear 色彩空間
  4. 材質看不到

    • 解:Render Queue 太後面被別人蓋掉、ZWrite/Depth 設錯、或模型的 UV/法線壞掉。

7. 五分鐘小實驗:你會立刻有感

  1. 做個純色 Unlit:改 _BaseColor,確認你可以控制畫面。
  2. 換成貼圖:把 _MainTex 換圖,看取樣有沒有生效;縮放模型遠近看看是否有 Mipmap 抖動。
  3. 切換 Cull Back/Off:看雙面與單面差別(背面剔除會省很多像素工作)。
  4. Queue: Geometry ↔ Transparent:體會 ZWrite/Blend 對繪製順序與效果的影響。

8. 我需要 Shader Graph 嗎?

  • 想快速拼效果、不熟 GLSL/HLSL → Shader Graph 很棒
  • 要做特殊 Pass / 自訂幾何流程 / 最佳化 → 最終多半會回到 GLSL/HLSL
  • 建議:兩者都會一點。Graph 幫你探索,GLSL/HLSL 幫你突破。


9. 一句話總結

Unity Shader = ShaderLab(外層) + GLSL/HLSL(內核)
先把 Unlit → Texture → Lambert 三步走完,你就完成 Shader 入門的 80%。
接下來你可以往 法線貼圖、PBR、陰影、後處理、Compute 延伸——但無論做什麼,記得「先讓最小範例能跑、能改、能量測」。


明日預告(Day 7)

我們要比較 Phong / Blinn-Phong / Cook-Torrance:用同一個模型切換三種光照,讓你看懂為什麼 PBR(Cook-Torrance)在真實感上更勝一籌,以及「何時用傳統、何時用 PBR」。


上一篇
Day 5|PBR(Physically Based Rendering)原理與實作
系列文
渲染與GPU編程7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言