CG語法的數據類型
// uint : 無符號整數(32位)
// int : 有符號整數(32位)
// float : 單精度浮點數(32位),通常帶后綴 f(如 1.0f)
// half : 半精度浮點數(16位),節省顯存,帶后綴 h(如 1.0h)
// fixed : 固定精度浮點數(12位左右),用于移動設備優化
// bool : 布爾值(true / false)
// string : 字符串類型,常用于屬性定義中的描述或標簽
// sampler : 通用紋理采樣器類型,實際使用時一般使用以下具體類型
// sampler1D : 一維紋理采樣器,稀有,適用于線性漸變紋理等
// sampler2D : 二維紋理采樣器,最常用,用于普通的2D貼圖
// sampler3D : 三維紋理采樣器,用于體積紋理,例如煙霧、火焰等體積效果
// samplerCUBE : 立方體紋理采樣器,用于環境映射、天空盒等立體方向貼圖
// samplerRECT : 矩形紋理采樣器,用于非標準 UV 范圍的紋理,主要用于一些特殊場景
數組:和C#中類似
一維
int a[4]={1,2,3,4}
長度 a.length
二維
int b[2][3]={{1,2,3},{4,5,6}}
長度 b.length為2
b[0].length 為3
結構體和C#基本一樣,沒有訪問修飾符,結構體聲明結束加分號,一般在函數外聲明
向量類型屬于CG語言的內置數據類型
內置的向量類型是基于基礎數據類型聲明的
向量的最大維度不超過4維
數據類型可以是任意數值類型
基本構成
數據類型2=數據類型2(n1,n2)
數據類型3=數據類型3(n1,n2,n3)
數據類型4=數據類型4(n1,n2,n3,n4)
矩陣類型屬于CG語言的內置數據類型
矩陣的最大行列不大于4,不小于1
數據類型可以是任意數值類型
關于bool值
注意:CG中向量,矩陣和數組是完全不同的,向量和矩陣是內置的數據類型,而數組則是一種數據結構,不是內置數據類型
賦值與函數
?可以表示向量和RGBa
重新洗順序
可以一直按規則取
向量聲明矩陣,可接引用
高維賦值給低維,低維以00為起點占有元素,在自己范圍內
比較表達式與c#相同,&&和||框選的條件一定會去計算
CG中取余符號只能向整數取余
CG語法中的while if? ?和C#中完全相同,少用->優化
要利用GPU并行性這一特點來替代循環
void name(in參數類型參數名,out參數類型參數名)
{}
void:以void開頭,表示沒有返回值
name:函數的名稱
in:表示是輸入參數,表示由函數外部傳遞給函數內部,內部不會修改該參數,只會使用該參數進行計算,
out:表示是輸出參數,表示由函數內部傳遞給函數的調用者,在函數內部必須對該參數值進行初始化或修改
in和out都可以省略,省略后就沒有了in和out相關的限制
type name(in 參數類型參數名)
{
}
return 返回值;type:返回值類型
return:返回指定類型的數據
頂點著色器回調函數
CG語言的語義
POSITION:把模型的頂點坐標填充到輸入的參數v當中
SV_POSITION:頂點著色器輸出的內容是裁剪空間中的頂點坐標
如果沒有這些語義來限定輸入和輸出參數的話,那么渲染器就完全不知道用戶輸入輸出的是什么,就會得到錯誤的效果
- This function is a basic vertex shader that converts a model-space vertex position (
POSITION
) into clip-space (SV_POSITION
). - It utilizes Unity's built-in transformations to handle coordinate conversion, ensuring proper rendering.
POSITION
語義 用于輸入(模型空間坐標)
👉 SV_POSITION
語義 用于輸出(裁剪空間坐標,最終用于屏幕渲染)
如果 Shader 代碼要對外界(屏幕上最終顯示的圖像)產生影響,函數的返回值必須帶有特定的語義(Semantic)
ShaderLab 常用(Semantics)
在 應用階段,模型數據通過 頂點著色器 傳遞時,Unity 支持特定的語義標簽(Semantics),用于標識不同的頂點屬性。
1. POSITION
// POSITION: 模型空間中的頂點位置 // 通常為 float4 類型
- 代表 模型空間 下的 頂點坐標,一般用于變換到其他坐標空間(如世界空間、裁剪空間)。
2. NORMAL
// NORMAL: 頂點法線 // 通常為 float3 類型
- 用于存儲 法線方向,用于 光照計算(如法線貼圖、Phong 著色等)。
3. TANGENT
// TANGENT: 頂點切線 // 通常為 float4 類型
- 主要用于 法線貼圖(Normal Mapping),結合法線計算 切線空間 的方向信息。
4. TEXCOORDn
// TEXCOORDn: 頂點的紋理坐標(UV 坐標) // 例如:TEXCOORD0, TEXCOORD1, TEXCOORD2... // 通常為 float2 或 float4 類型
- TEXCOORD0:第一組 UV 坐標(用于基礎貼圖)。
- TEXCOORD1:第二組 UV 坐標(如光照貼圖、環境貼圖等)。
- UV 坐標:用于確定紋理在模型表面的映射方式。
5. COLOR
// COLOR: 頂點顏色 // 通常為 fixed4 或 float4 類型
- 頂點的顏色信息,可以用于 漸變、頂點色混合 等特效。
語義(Semantics) 決定了 著色器如何接收和傳遞數據
兩個變量都從同一個數據來源賦值,并且沒有修改,那么它們的值就是相同的
struct v2f {float4 pos_model : POSITION; // 模型空間float4 pos_clip : SV_POSITION; // 裁剪空間
};v2f vert(float4 v : POSITION) {v2f o;o.pos_model = v; // 直接賦值,仍在模型空間o.pos_clip = UnityObjectToClipPos(v); // 變換到裁剪空間return o;
}
語義(Semantics)中的數據并不是從材質上獲取,而是?Unity 的渲染管線
不是所有語義unity都支持
大部分語義(如 POSITION
、NORMAL
、TEXCOORD
、COLOR
)的數據來自 游戲物體的 Mesh(網格),而不是直接來自材質。
有些語義的數據不是直接存儲在 Mesh 里的,而是由 Unity 的渲染管線計算并提供的,
Shader 也可以使用 材質屬性(Material Properties) 或 C# 腳本 傳遞數據
Unity 渲染管線的底層實現 中,并不是直接引用賦值,而是經過了一系列的轉換和數據傳輸。Shader 代碼中的語義(如 POSITION
、NORMAL
)接收到的數據 并不是直接從游戲對象的組件引用過來的,而是 GPU 從頂點緩沖區讀取 并 按需轉換 之后再傳遞給 Shader 的。
Unity 引擎的底層是基于 GPU 渲染管線 運行的,而游戲對象的 Mesh 數據 是存儲在 CPU 端的 Mesh 組件 中的。渲染時,這些數據不會以“引用賦值”的方式直接傳遞到 Shader,而是先經過一系列的預處理、緩存,然后被 GPU 讀取和轉換。
CPU 端:Mesh 數據存儲在 MeshFilter
和 MeshRenderer
組件
MeshFilter
:存儲 模型網格的幾何數據(頂點、法線、UV 等)。MeshRenderer
:決定 如何渲染該模型(使用哪個材質、Shader 等)
CPU → GPU:頂點數據上傳到 GPU
- Unity 不會每幀都重新從 Mesh 組件讀取數據,而是會將 Mesh 數據存入 GPU 的“頂點緩沖區(Vertex Buffer)”。
- 這些數據存儲在 GPU 內存中,供后續渲染時使用。
struct appdata {float4 vertex : POSITION; // 頂點位置float3 normal : NORMAL; // 法線float2 uv : TEXCOORD0; // UV 坐標
};
GPU 端:Shader 處理數據
- 頂點著色器(Vertex Shader)執行頂點變換:
v2f vert(appdata v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex); // 計算裁剪空間坐標o.uv = v.uv; // 傳遞 UV 坐標return o;
}
Shader 處理的是 GPU 端的數據,而不是 CPU 端的 Mesh 數據
數據在傳輸過程中會被轉換到不同的坐標空間(如 POSITION
從 模型空間 → 世界空間 → 裁剪空間)。
Unity 的 Material.SetXXX()
賦值的 Uniform 變量(如 _Color
、_MainTex
)則是 通過 Uniform Buffer
傳輸到 Shader。
應用階段(Application Stage) 指的是 CPU 端處理渲染邏輯的階段,主要負責 準備和提交渲染數據給 GPU
Unity 的渲染管線遵循 經典的 GPU 渲染流程,可以大致分為:
階段 | 處理內容 | 運行在哪 |
---|---|---|
應用階段(Application Stage) | 運行 C# 代碼、處理游戲邏輯、提交渲染數據 | CPU |
幾何階段(Geometry Stage) | 處理頂點著色器、坐標變換、裁剪等 | GPU |
光柵化階段(Rasterization Stage) | 計算像素,決定哪些像素需要渲染 | GPU |
片元著色階段(Fragment Stage) | 處理材質、光照、紋理采樣,決定最終像素顏色 | GPU |
輸出合成階段(Output Merger) | 將計算好的像素寫入幀緩沖區,最終顯示在屏幕上 | GPU |
CG的一般做法
頂點著色器當中獲取更多 模型相關信息,使用結構體 對數據進行封裝
對結構體中成員變量 加語義 來定義想要獲取的信息
GPU 渲染是 并行執行的,每個頂點、片元都是 獨立處理,所以 v2f
不可能是單例,而是 每個并行線程(Thread)都有自己的 v2f 實例。
vert()
頂點著色器,GPU 的 頂點處理階段(每個頂點運行一次),這個函數的主要任務是進行坐標變換,把頂點坐標轉換到裁剪空間(Clip Space),然后傳遞相關數據給片元著色器。
frag()
片元著色器,GPU 的 片元處理階段(每個像素運行一次)
fixed4 frag(v2f data) : SV_Target
{return fixed4(0,1,0,1); // 返回一個純綠色的像素
}
- 輸入:
v2f data
(從vert()
傳遞來的數據)- 其中包含
position
(屏幕坐標)、normal
(法線)、uv
(UV 坐標)
- 輸出:
SV_Target
:告訴 GPU 這個顏色是要渲染到屏幕上的顏色緩沖區。
如何在cG語句塊中使用shaderLab中聲明的屬性
直接在CG語句塊中,聲明 和屬性中對應類型的 同名變量即可
ShaderLab里聲明的屬性,命名,初始化
CG中的內置函數
會返回對應的數值
Unity內置封裝好的CG文件
Unity中常用的內置文件有
1.UnityCG.cginc
2.Lighting.cginc
3.UnityShaderVariables.cginc
4.HLSLSupport.cginc
等等
在CG語句塊中進行引用
通過編譯指令
#include“內置文件名.cginc
float3 worldPos= mul(_object2World,data.vertex);
封裝好的變量
如果想要了解更多的內置內容可以參閱unity官網的資料
-----------------
-----------------
渲染管線概述
排序 Sort
渲染隊列 RenderQueue
不透明隊列(RenderQueue <2500)
按攝像機距離從前到后排序
半透明隊列(RenderQueue >2500)
按攝像機距離從后到前排序
假設你有一張紋理圖是一個草地:
🖼? 圖片:一個草地圖案
📦 模型:一個地面平面
- 如果 UV 坐標為
(0, 0)
,表示這個點使用的是圖像的左下角像素的顏色。 - 如果 UV 坐標為
(0.5, 0.5)
,表示這個點使用的是圖像中心的像素顏色。 - 如果 UV 坐標超出了
[0,1]
,Unity 通常會進行平鋪(wrap)或裁切(clamp)。
在 Shader 中,UV 坐標通常配合 sampler2D
紋理采樣器使用:
sampler2D _MainTex; // 一張貼圖
float2 uv : TEXCOORD0; // 頂點傳來的 UV 坐標fixed4 col = tex2D(_MainTex, uv); // 用 UV 坐標采樣紋理顏色
👆 tex2D
就是用 UV 坐標從紋理圖中“取顏色”的操作。
檢測片元Shader的alpha值,如果為0直接舍棄
渲染過程中的每個像素(片元)在寫入顏色緩沖區之前,都會經過一個 深度測試流程(Depth Test),判斷它是不是在其他像素的前面。這個判斷和控制主要靠兩個指令:
ZWrite
(深度寫入)
- 用來控制:是否將通過深度測試的片元的深度值寫入深度緩沖區。
默認行為(ZWrite On):
- 片元通過深度測試后,它的深度值會寫入 z-buffer,刷新該位置的深度記錄。
關閉寫入(ZWrite Off):
- 片元即使通過了深度測試,它的深度值也不會寫入 z-buffer。
- 看得見的效果:顏色照樣更新,但這個像素對后續的遮擋判斷沒有“存在感”。
- 常用于:透明物體、粒子、UI 元素,因為我們不希望它影響后面的遮擋判斷。
正統流程:深度測試在 “輸出合并階段”(Output Merger)進行
這是 最標準的、保證兼容性的做法,流程是這樣:
頂點著色器 → 光柵化 → 片元著色器 → 深度測試(ZTest) → 混合 → 輸出到顏色緩沖區
在這種模型里:
- 即使一個像素最終會被深度淘汰,也要執行完片元著色器,白算一遍。
- 所以如果你 Shader 很重(比如計算復雜的光照、采樣多個紋理),但又被遮擋了,其實這些工作全都白做了,性能白浪費了。
- 在執行片元著色器之前,先用插值后的深度值做一次快速測試,如果當前像素一定會被擋住,那干脆 不執行片元著色器,直接淘汰掉。
- 這個優化可以大幅度減少片元著色器的調用次數,尤其在遮擋物很多時特別有效。
Early-Z 會受到一些行為干擾而被取消,比如:
- Shader 中寫入了
depth
(手動設置深度) - 使用了
discard
(手動丟棄像素) - 某些透明混合模式 這些都會讓驅動決定:這塊不能提前判斷,只能留到最后做深度測試。
也可能和硬件有關,
混合(Blending)機制補充
混合的本質就是:當前片元顏色(源色)與緩沖區已有顏色(目標色)進行運算,生成新的顏色寫入顏色緩沖區。
公式一般長這樣:FinalColor = SourceColor * SrcFactor + DestColor * DstFactor
名稱 | 含義 |
---|---|
SourceColor | 當前片元的輸出顏色 |
DestColor | 當前顏色緩沖區已有的顏色 |
SrcFactor | 當前顏色乘的比例因子(你可以設置) |
DstFactor | 緩沖顏色乘的比例因子(你也可以設置) |
半透明的排序問題(最核心)
為了獲得正確的半透明混合效果,必須保證從遠到近的順序進行渲染(Back to Front)。
? 為什么?
- 因為半透明是基于緩沖區已有顏色與當前片元的顏色進行“加權疊加”的。
- 如果渲染順序錯了,就會導致前面的像素被后面的覆蓋,混合結果就不正確了。
😖 問題是:
- GPU 是并行渲染的,無法保證片元(pixel)級別的排序。
- Unity 只能在物體層面(GameObject)上進行渲染順序的控制:
RenderQueue
= 3000(Transparent 隊列)material.renderQueue
可手動調節SortingGroup
/SortingLayer
可用于 2D/半2D 場景
ZWrite 通常需要關閉
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
? 為什么關閉 ZWrite?
- 如果 ZWrite 開啟了,當前像素雖然是半透明,但它會像完全不透明一樣把自己的深度值寫進 z-buffer。
- 結果就是后面的半透明像素就算更“前面”,也會被擋住(ZTest 失敗),無法混合,造成視覺錯誤。
? 特例:什么時候可以開?
- 如果你確定這個半透明物體就是最前面的,且不再參與混合,可以開。
- 比如一些特殊 UI 元素、遮罩層。
無片元級排序,GPU 無法像 CPU 那樣排序每個像素,可以出現 近處透明被遠處透明遮住
最后總結
1.CPU 應用階段
,這是運行 C#/腳本邏輯的地方,也就是 Unity 的主線程階段。
- 視錐體剔除:去掉攝像機看不到的對象
- 渲染排序:比如不透明先畫、透明后畫
- 提交 DrawCall:Unity 將所有繪制命令整理、打包,提交給 GPU
階段重點是:準備渲染信息并壓入 GPU 命令隊列,Graphics.DrawMesh()
、material.SetFloat()
等行為都發生在這里
2.頂點處理階段
float4 vertex : POSITION; ? // 模型頂點
float4 UnityObjectToClipPos(v); // MVP 變換
- 執行頂點著色器(Vertex Shader)
- 做 MVP 空間變換:將模型頂點從本地空間 → 世界空間 → 裁剪空間
- 傳出自定義數據:比如 UV 坐標、法線、Tangent、顏色等傳給片元著色器
3.光柵化操作階段
- 裁剪(Clip):去掉超出裁剪空間的三角形
- NDC:標準化設備坐標 [-1,1]
- 背面剔除:剔除背面朝向攝像機的三角形(根據繞序)
- 屏幕坐標計算
- 圖元裝配 + 光柵化:三角形 → 像素級別的“片元”(Fragment)生成
特別提示:
- 這一步之后,就可以知道每個片元的深度值了
- 所以如果 GPU 支持,就可以在這之后啟用 Early-Z(提前深度測試)
4.片元處理階段
對應于 Shader 中的 frag()
函數,是像素級別的處理。
- 光照計算(使用法線、視角等)
- 紋理采樣
- 顏色輸出計算
每個像素都執行一次片元著色器,成本高,能少就少(比如 Early-Z 的意義)
5. 輸出合并階段
- Alpha測試(是否透明剪裁)
- 模板測試(Stencil)
- 深度測試(ZTest)
- 顏色混合(Blending)
如果都通過,才寫入到:
- 顏色緩沖區(Color Buffer)
- 深度緩沖區(Depth Buffer)
- 模板緩沖區(Stencil Buffer)
階段 | GPU 是否執行 | 內容關鍵詞 | 開發者常用點 |
---|---|---|---|
應用階段 | ?(CPU 執行) | 剔除、排序、DrawCall | C# 腳本邏輯 |
頂點處理 | ? | MVP變換、自定義數據輸出 | vert() 函數 |
光柵化 | ? | 裁剪、裝配、坐標轉換 | 深度值插值、早期剔除 |
片元處理 | ? | 著色、紋理、光照 | frag() 函數 |
輸出合并 | ? | 各類測試、混合 | ZTest 、ZWrite 、Blend 、Stencil |
ShaderLab是CPU還是GPU執行
ShaderLab 里寫的代碼(尤其 Pass
中的 CG/HLSL 代碼)確實是 GPU 執行的,不是 CPU 來跑,也不是 CPU 幫你“解釋后告知 GPU 怎么渲染”,而是 這些代碼被真正編譯成 GPU 的指令集,在 GPU 上并行運行的。
GPU 不是“不能算”,而是:
- 它不適合做“通用邏輯跳轉、復雜流程控制、多線程同步”等操作。
- 它非常擅長做:
- 大量并行的浮點運算
- 簡單、重復、獨立的任務(比如每個像素的光照)
- SIMD(單指令多數據)風格的批量處理
💡 所以:
- CPU 更像是精致的多功能工匠
- GPU 更像是龐大的流水線并行工廠
ShaderLab 是 Unity 定義的一套“著色器配置語言”
- 它不是 GPU 執行的語言本身,而是描述:
- 哪個階段啟用什么 Shader
- 是否開啟混合、深度寫入、剔除等 GPU 狀態
- 使用哪個 CGPROGRAM
Pass
中的 CGPROGRAM
才是重點!
CGPROGRAM
里的代碼(如vert()
、frag()
)是寫在 HLSL/CG 語言中的。- Unity 會把這些代碼編譯成 GPU 硬件可以理解的匯編級指令(如 DXIL, SPIR-V),并上傳到 GPU。
CPU 只是“控制者”,而 GPU 是真正的“執行者”。
角色 | 工作 |
---|---|
CPU | 加載資源、設置材質、組織渲染順序、提交 DrawCall、上傳數據 |
GPU | 執行編譯后的 Shader 程序,處理頂點、像素、深度、顏色混合等操作 |
Shader 代碼不是“描述給 CPU 去跑的邏輯”,也不是“讓 CPU 發指令一步步幫 GPU 渲染”,而是:直接寫出 GPU 自己要執行的程序(在它自己的線程和執行核心里跑)
比如你寫了這樣的 Shader:
fixed4 frag(v2f i) : SV_Target {return tex2D(_MainTex, i.uv) * _Color;
}
發生的是:
- Unity 編譯 ShaderLab → 編譯出對應平臺的 GPU 匯編代碼(比如 DirectX 就是 DXIL)
- CPU 把紋理
_MainTex
、顏色_Color
上傳到 GPU - GPU 執行這段代碼,每個像素啟動一個線程,做:
- 從紋理讀取顏色(紋理采樣單元)
- 乘以你設置的
_Color
- 寫入 framebuffer
? 所以:你寫的
frag()
真正是 GPU 在執行的函數,每個像素跑一次,獨立運行。
CPU 排序 vs GPU 深度測試
CPU 確實在“物體級別”進行渲染排序(RenderQueue、Layer、Z軸順序等),但它不能精確判斷每個像素的遮擋關系。
CPU 渲染排序(Object-level)
-
是在「物體(mesh)為單位」上排序的。
-
用來保證:
-
不透明物體優先畫(提前寫入 ZBuffer)
-
半透明物體延后畫(正確混合顏色)
-
UI 渲染層順序(Sorting Layer)
-
限制是:
-
它不知道一個物體內部「前后交錯」的像素關系。
-
CPU 沒有能力分析一個物體每個三角形是否被另一個物體的部分遮擋。
-
也不可能「提前預演」每個像素的深度值。
GPU 深度測試(Pixel-level)
-
是在「每個像素(片元)」層面進行判斷。
-
它精確判斷:
-
當前渲染的片元是否比已有像素更靠前(小于 ZBuffer)
-
如果是,才寫入顏色緩沖區;否則直接丟棄。
-
必要性在于:
-
兩個物體可能互相遮擋、穿插,CPU 排序無法精確到這種程度。
-
一張墻和一個人,CPU 知道「人更靠前」,但 GPU 必須判斷「墻后面的人腿那一截不該畫出來」。