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 必须判断「墙后面的人腿那一截不该画出来」。