Unity Shader 完整学习指南
目录
-
第一部分:基础入门
(图片来源网络,侵删)- 什么是 Shader?
- Unity 中的 Shader 资源
- ShaderLab 基础结构
- 第一个 Shader:创建一个纯色材质
- 理解 Properties、SubShader、Pass
- Unity 的渲染管线简介
-
第二部分:核心概念 - CG/HLSL 着色器语言
- 什么是 CG/HLSL?
- 顶点着色器 与 片元着色器
- 关键数据结构:
v2f(vertex to fragment) - 内置变量与函数
- Unity 中的坐标空间
-
第三部分:实用 Shader 编写
- 基础光照模型:Lambert 反射
- 纹理:如何使用贴图
- 法线贴图:增加表面细节
- 透明度:透明与半透明效果
- 卡通渲染:简单的 Cel-Shader 效果
-
第四部分:高级主题
- Surface Shader:简化光照模型的编写
- Shader Variant & Keywords:实现可配置的 Shader
- 自定义光照模型:编写自己的光照计算
- 后处理效果:在场景渲染后进行修改
- GPU Instancing:提升性能
-
第五部分:学习资源与最佳实践
(图片来源网络,侵删)- 推荐书籍与教程
- 官方文档与社区
- Shader 开发工具
- 性能优化技巧
第一部分:基础入门
什么是 Shader?
Shader(着色器)是一段运行在 GPU 上的小程序,它告诉 GPU 如何渲染一个像素,它决定了物体的最终颜色、高光、阴影等视觉表现。
- 顶点着色器:负责处理每个顶点的位置、法线、UV 等信息,它决定了顶点在屏幕上的最终位置。
- 片元着色器:负责处理每个像素(或称片元)的颜色,它决定了屏幕上每个像素应该显示什么颜色。
Unity 中的 Shader 资源
在 Unity 中,Shader 资源本身并不直接执行渲染,它更像是一个“蓝图”或“配方”,定义了渲染所需的 Pass(通道)和属性,Unity 会根据这个蓝图,生成真正在 GPU 上运行的代码。
ShaderLab 基础结构
ShaderLab 是 Unity 用来编写 Shader 的一种专用语言,它封装了底层的 CG/HLSL 代码,并提供了一些 Unity 特有的功能,如渲染队列、渲染状态设置等。
一个基本的 Shader 结构如下:

Shader "Tutorial/MyFirstShader" {
Properties {
// 在材质面板上显示的属性
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
// 定义渲染 Pass
Pass {
// 设置渲染状态
// CG/HLSL 代码放在这里
}
}
// 可以定义多个 SubShader,用于兼容不同硬件
FallBack "Diffuse"
}
第一个 Shader:创建一个纯色材质
让我们创建一个最简单的 Shader,它只输出一个固定的颜色。
- 在 Project 窗口中,右键 -> Create -> Shader -> Standard Surface Shader,将其命名为
MyUnlitShader。 - 打开
MyUnlitShader,删除所有 Surface Shader 相关的代码,保留最基础的ShaderLab结构。 - 我们将使用一个
Pass来直接输出颜色。
Shader "Tutorial/MyUnlitShader" {
Properties {
_MainColor ("Main Color", Color) = (1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
// 顶点着色器函数名
#pragma vertex vert
// 片元着色器函数名
#pragma fragment frag
// 从 Properties 声明变量,以便在 CG 代码中使用
fixed4 _MainColor;
// 定义顶点着色器的输入结构
struct appdata {
float4 vertex : POSITION;
};
// 定义从顶点着色器传递到片元着色器的结构
struct v2f {
float4 vertex : SV_POSITION;
};
// 顶点着色器
v2f vert (appdata v) {
v2f o;
// 将顶点位置从模型空间转换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
// 片元着色器
fixed4 frag (v2f i) : SV_Target {
// 直接返回主颜色
return _MainColor;
}
ENDCG
}
}
FallBack "Unlit/Texture"
}
创建一个新材质,将这个 Shader 赋给它,然后将材质拖到场景中的物体上,你就能看到物体变成了你设置的颜色。
代码解释:
#pragma vertex vert和#pragma fragment frag:告诉编译器我们的顶点和片元着色器函数分别叫什么。struct appdata:定义了从 Unity 传递给顶点着色器的原始数据,这里我们只需要顶点位置POSITION。struct v2f:定义了从顶点着色器传递给片元着色器的数据。SV_POSITION是片元着色器必需的,表示像素在屏幕上的位置。vert函数:将顶点位置通过UnityObjectToClipPos转换为裁剪空间坐标,这是渲染到屏幕前的必要步骤。frag函数:直接返回_MainColor,所以整个物体都显示这个颜色。
第二部分:核心概念 - CG/HLSL 着色器语言
什么是 CG/HLSL?
CG (C for Graphics) 是 NVIDIA 开发的一种着色器语言,而 HLSL (High-Level Shading Language) 是微软 DirectX 使用的语言,两者非常相似,在 Unity 中,我们通常使用 HLSL 语法。
顶点着色器 vs. 片元着色器
- 顶点着色器:处理顶点,它的工作是计算顶点的最终位置,并可以传递一些数据(如颜色、UV、法线)给片元着色器,处理的顶点数量远少于像素数量。
- 片元着色器:处理像素,它接收来自顶点着色器的数据,并进行插值,然后计算出每个像素的最终颜色。
关键数据结构:v2f (vertex to fragment)
v2f 是连接顶点和片元着色器的桥梁,你需要在顶点着色器中为它填充数据,然后在片元着色器中使用它,如果你想使用纹理,就需要将 UV 坐标从顶点着色器传递到片元着色器。
struct v2f {
float4 vertex : SV_POSITION; // 像素位置
float2 uv : TEXCOORD0; // UV 坐标
};
在顶点着色器中:
o.uv = v.uv;
在片元着色器中:
fixed4 col = tex2D(_MainTex, i.uv);
内置变量与函数
Unity 提供了大量内置变量和函数来简化开发,
UnityObjectToClipPos(v.vertex): 模型空间 -> 裁剪空间tex2D(sampler, uv): 采样 2D 纹理_WorldSpaceLightPos0: 世界空间的光源位置_LightColor0: 光源颜色
Unity 中的坐标空间
理解不同空间之间的转换是 Shader 编写的核心。
- Object Space (模型空间): 以物体自身中心为原点的坐标系。
- World Space (世界空间): 以场景世界原点为原点的坐标系。
- View Space (观察空间): 以摄像机为原点的坐标系。
- Clip Space (裁剪空间): 经过投影变换后的坐标系,用于光栅化。
第三部分:实用 Shader 编写
基础光照模型:Lambert 反射
Lambert 模型是最简单的漫反射光照模型,它模拟了粗糙表面的光照效果。
原理:像素颜色 = 材质颜色 × 光源颜色 × max(0, 法线与光线方向的点积)
// 在 Pass 中
fixed4 _DiffuseColor;
fixed4 _LightColor;
// 顶点着色器中计算光照方向和法线,并传递给片元着色器
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算世界空间法线
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算世界空间光线方向
o.worldLightDir = UnityWorldSpaceLightDir(v.vertex);
return o;
}
// 片元着色器中进行最终计算
fixed4 frag (v2f i) : SV_Target {
// 归一化向量
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
// 计算点积(N dot L)
fixed NdotL = saturate(dot(worldNormal, worldLightDir));
// 最终颜色 = 材质色 × 光源色 × 点积结果
fixed3 finalColor = _DiffuseColor.rgb * _LightColor.rgb * NdotL;
return fixed4(finalColor, 1.0);
}
纹理
纹理是一张图片,它被“包裹”在 3D 模型表面,通过 UV 坐标来确定每个像素对应纹理上的哪个位置。
步骤:
- 在
Properties中声明一个 2D 纹理:_MainTex ("Main Texture", 2D) = "white" {} - 在 CG 代码中声明对应的变量:
sampler2D _MainTex; - 在
v2f结构中添加 UV 传递:float2 uv : TEXCOORD0; - 在顶点着色器中传递 UV:
o.uv = v.uv; - 在片元着色器中采样纹理:
fixed4 texColor = tex2D(_MainTex, i.uv); - 将纹理颜色与光照结果相乘。
法线贴图
法线贴图是一种特殊的纹理,它不存储颜色,而是存储每个像素的表面法线信息,通过修改光照计算,可以在低模上表现出高模的细节。
原理:用纹理中的法线信息替换或修改模型原有的法线,然后进行光照计算。
关键:需要 UnpackNormal 函数来解码法线贴图(因为它通常是 DXT5 压缩格式,并存储在 RGB 通道中)。
sampler2D _NormalTex; fixed4 _NormalTex_ST; // 用于控制 UV 缩放和偏移 // 在顶点着色器中 o.uv = TRANSFORM_TEX(v.uv, _NormalTex); // 在片元着色器中 fixed3 normal = UnpackNormal(tex2D(_NormalTex, i.uv)); // 然后使用这个 'normal' 进行 NdotL 计算
透明度
透明效果通过修改像素的 Alpha 通道实现。
步骤:
- 在
Properties中声明一个控制透明度的变量:_Alpha ("Alpha", Range(0,1)) = 1 - 在 SubShader 的
Tags中指定渲染类型:Tags { "Queue"="Transparent" "RenderType"="Transparent" } - 在 Pass 中设置混合模式:
Blend SrcAlpha OneMinusSrcAlpha // 标准透明混合 ZWrite Off // 关闭深度写入,避免透明物体互相遮挡时出错
- 在片元着色器中设置最终颜色的 Alpha 值:
return fixed4(finalColor, _Alpha);
卡通渲染
卡通渲染的核心思想是:将光照计算结果离散化,而不是平滑的渐变。
方法:对 NdotL 的结果进行取整或阶梯处理。
// 在 frag 函数中 float NdotL = dot(worldNormal, worldLightDir); NdotL = saturate(NdotL); // 阶梯处理 float steps = 5.0; float stepNdotL = floor(NdotL * steps) / steps; // 或者使用 smoothstep 创建硬边 float rim = 1.0 - NdotL; float rimThreshold = 0.1; float rimIntensity = smoothstep(rimThreshold, rimThreshold + 0.1, rim); fixed3 finalColor = _DiffuseColor.rgb * _LightColor.rgb * stepNdotL + rimIntensity * _RimColor.rgb;
第四部分:高级主题
Surface Shader
Surface Shader 是 Unity 提供的一个强大工具,它极大地简化了与 Unity 内置光照模型的交互,你只需要编写 surf 函数,Unity 会自动帮你处理顶点着色器、片元着色器、光照计算等所有复杂部分。
Shader "Tutorial/SimpleSurfaceShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
fixed4 _Color;
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Shader Variant & Keywords
Shader Keywords 允许你在一个 Shader 中实现多种配置,而无需创建多个 Shader 文件,这对于优化(如开启/关闭特效)和灵活性非常有用。
使用方法:
- 在 Shader 中定义关键字:
#pragma shader_feature FEATURE_KEYWORD - 在材质面板上,通过
Keywords字段来启用或禁用它。 - 在 CG 代码中,使用
#ifdef FEATURE_KEYWORD来判断。
Unity 会为所有启用的关键字组合单独编译一个 Shader Variant,以避免运行时判断带来的性能开销。
自定义光照模型
如果你不满足于 Unity 提供的 Lambert、Phong 等光照模型,可以自己编写。
步骤:
- 在 Surface Shader 的
#pragma指令中指定你的光照模型函数:#pragma surface surf MyLightingModel - 编写一个光照模型函数,其签名必须符合 Unity 的规范:
fixed4 LightingMyLightingModel (SurfaceOutput s, fixed3 lightDir, fixed atten) { // 在这里编写你的光照计算逻辑 fixed NdotL = saturate(dot(s.Normal, lightDir)); fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten; c.a = s.Alpha; return c; }
后处理效果
后处理是在整个场景渲染到屏幕后,再对最终图像进行全局处理的技术,泛光、灰度、颜色滤镜等。
实现方法:
- 创建一个使用
Image Effect Shader的脚本。 - 创建一个特殊的 Shader,它只渲染一个覆盖全屏的四边形。
- 在 Shader 的片元着色器中,对输入的屏幕图像(通过
_MainTex采样)进行处理。
GPU Instancing
GPU Instancing 允许 GPU 在一次绘制调用中渲染多个相同的物体,极大地提升了性能(如渲染大量草地、树木)。
启用方法:
- 在 Shader 的 Pass 中添加指令:
#pragma multi_compile_instancing - 对于每个需要实例化的属性,使用
UNITY_INSTANCING_BUFFER_START和UNITY_INSTANCING_BUFFER_END来定义。
第五部分:学习资源与最佳实践
推荐资源
- 官方文档:Unity Manual - Shaders - 最权威、最全面的资料。
- Catlike Coding:https://catlikecoding.com/unity/tutorials/ - 强烈推荐! 从零开始,讲解极其细致,配有大量交互式示例。
- The Book of Shaders:https://thebookofshaders.com/ - 虽然不是专门针对 Unity,但对图形学基础和 GLSL/HLSL 的讲解非常出色。
- B站/YouTube 教程:搜索 "Unity Shader Tutorial",有很多优秀的中文和英文视频教程。
开发工具
- Unity Shader Graph:一个节点式的 Shader 编写工具,非常适合初学者和快速原型开发,无需编写代码。
- Amplify Shader Editor:功能强大的第三方节点式 Shader 编辑器。
- RenderDoc:一个图形调试工具,可以让你实时查看渲染的每一帧,检查纹理、着色器输入输出等,是排查 Shader Bug 的利器。
最佳实践
- 从简单开始:先实现一个纯色 Shader,再逐步添加纹理、光照等。
- 理解原理:不要只复制粘贴代码,理解每个函数、每个变量的作用,尤其是坐标空间的转换。
- 善用官方示例:Unity 自带了很多示例 Shader(在
Assets > Import Package > Effects中),仔细研究它们。 - 性能优化:
- 尽量减少计算量,尤其是在片元着色器中。
- 合理使用 Shader Keywords 和 Variants。
- 避免在片元着色器中进行分支判断(
if/else),可以使用step、lerp等数学函数代替。 - 对不需要高精度的变量使用低精度类型(如
half,fixed)。
- 版本控制:Shader 文件是文本文件,非常适合使用 Git 进行版本管理。
希望这份指南能为你打开 Unity Shader 的大门,Shader 编程是一门艺术,也是一门科学,需要大量的练习和耐心,祝你学习愉快!
