第1章:ShaderLab基础——ShaderLab语法、Properties属性块、SubShader与Pass
大家好,欢迎来到《Unity着色器开发:从基础到进阶》的第一章。
说实话,很多初学者一打开Shader文件就懵了。满屏的英文,花括号套花括号,完全不知道从哪下手。我当年也是这样,第一次打开Unity内置的Standard着色器,差点直接关掉编辑器。
别怕。ShaderLab其实没那么复杂。它本质上就是Unity自己定义的一套“着色器描述语言”。你不需要从零写汇编,只需要告诉Unity:我要用什么属性、跑在哪个渲染管线、用哪个Pass。
嗯,咱们今天就把这块硬骨头啃下来。
1.1 ShaderLab语法结构
先看一个最简的ShaderLab文件长什么样:
Shader "Custom/MyFirstShader"
{
Properties
{
// 属性定义在这里
}
SubShader
{
Pass
{
// 真正的着色器代码在这里
}
}
}
这个结构,说白了就是三层嵌套:
- 最外层:Shader名字,在材质面板里显示为“Custom/MyFirstShader”
- 中间层:Properties + SubShader(可以有多个SubShader)
- 最内层:Pass,一个SubShader里可以有好几个Pass
我个人习惯把Shader名字写成“分类/具体名称”的格式。比如“UI/HealthBar”、“Environment/Water”。这样在材质选择器里找起来特别快。你在项目里也建议这么搞,不然项目大了以后,几百个Shader全堆在一起,找起来想哭。
核心要点:ShaderLab是Unity的“外壳”,真正的渲染逻辑在Pass里用HLSL/CG编写。
1.2 Properties属性块
Properties是Shader和材质编辑器之间的桥梁。你在Properties里定义的变量,会直接出现在Inspector面板上。美术同学可以直接拖拽贴图、调颜色、改数值,完全不用碰代码。
看个实际例子:
Properties
{
_MainTex ("主纹理", 2D) = "white" {}
_Color ("主颜色", Color) = (1, 1, 1, 1)
_Glossiness ("光滑度", Range(0, 1)) = 0.5
_Intensity ("强度", Float) = 1.0
_Vector ("方向", Vector) = (1, 0, 0, 0)
}
这里我列出了五种最常用的属性类型:
| 属性类型 | 语法格式 | 默认值示例 | 说明 |
|---|---|---|---|
| 2D纹理 | _Name ("显示名", 2D) = "default" {} | "white" {} | 默认可以是white、black、gray、bump |
| 颜色 | _Name ("显示名", Color) = (r,g,b,a) | (1,1,1,1) | RGBA四个通道,范围0-1 |
| 范围 | _Name ("显示名", Range(min,max)) = value | 0.5 | 在Inspector显示为滑动条 |
| 浮点数 | _Name ("显示名", Float) = value | 1.0 | 直接输入数值 |
| 向量 | _Name ("显示名", Vector) = (x,y,z,w) | (1,0,0,0) | 四个分量的向量 |
我的小技巧:属性命名建议用下划线开头,比如_MainTex。这是Unity社区的约定俗成,方便在代码里一眼认出这是Shader属性,而不是普通变量。
你可能会问:为什么属性名和显示名要分开写?
原因很简单。属性名是给代码看的,必须唯一且不能有空格。显示名是给美术同学看的,可以写中文、带空格、甚至加表情符号。我曾经在一个项目里把显示名写成“主纹理✨”,美术同学特别喜欢,说找起来方便。
嗯,这里要注意一点:Properties里定义的变量,必须在SubShader或Pass里声明对应的同名变量才能使用。否则Unity会报warning,说变量未使用。
1.3 SubShader与Pass
SubShader是Shader的核心执行单元。一个Shader可以包含多个SubShader,Unity会从上到下依次检查,选择第一个能在当前硬件上运行的SubShader。
为什么要搞多个SubShader?
说白了就是为了兼容性。高端显卡支持的功能多,低端显卡可能不支持。你可以为高端显卡写一个复杂的SubShader,再为低端显卡写一个简化版。Unity会自动选择合适的那个。
我曾经在一个手游项目里吃过这个亏。当时只写了一个SubShader,用了很多高级特性。结果在几台老款安卓机上直接渲染成黑色。后来老老实实加了一个Fallback SubShader,问题才解决。
看一个多SubShader的例子:
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// ... 顶点和片元着色器代码
ENDCG
}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 100
Pass
{
// 简化版的渲染代码
}
}
这里LOD(Level of Detail)数值决定了SubShader的优先级。数值越高,表示质量越高。Unity会根据当前设备的性能等级,选择LOD值不超过设定上限的SubShader。
避坑指南:我曾经在项目里把两个SubShader的LOD写反了,结果高端设备跑的是低质量版本,低端设备反而跑高质量版本,直接卡成PPT。检查了整整一天才发现是LOD数值搞反了。所以写LOD的时候一定要仔细核对。
1.4 Pass的深入理解
Pass是真正的渲染单元。一个SubShader里可以有多个Pass,每个Pass执行一次完整的渲染流程。
多Pass有什么用?我举几个实际场景:
- 描边效果:第一个Pass渲染物体本身,第二个Pass在边缘画线
- 阴影投射:专门用一个Pass处理阴影
- 多光源渲染:Forward渲染路径下,每个光源对应一个Pass
- 后处理特效:多个Pass叠加不同的滤镜效果
看一个最简单的Pass结构:
Pass
{
Name "FORWARD"
Tags { "LightMode"="ForwardBase" }
Cull Back
ZWrite On
ZTest LEqual
Blend Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 normal = normalize(i.worldNormal);
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed ndotl = max(0, dot(normal, lightDir));
fixed4 col = _Color * ndotl;
return col;
}
ENDCG
}
这个Pass做了几件事:
- 定义了渲染状态(剔除、深度测试、混合模式)
- 声明了顶点着色器vert和片元着色器frag
- 在vert里把顶点从模型空间转换到裁剪空间
- 在frag里计算简单的漫反射光照
你想想看,整个渲染流程其实就这两步:顶点处理 + 像素着色。vert处理每个顶点,frag处理每个像素。中间的光栅化阶段由GPU自动完成,你不需要操心。
记住这个核心概念:Pass里的CGPROGRAM...ENDCG块,才是真正写着色器代码的地方。前面的Properties和SubShader都是“配置”,这里才是“执行”。
1.5 本章小结
咱们今天把ShaderLab的骨架搭起来了:
- Shader:最外层容器,定义名称
- Properties:暴露给材质编辑器的参数接口
- SubShader:针对不同硬件级别的渲染方案
- Pass:真正的渲染执行单元
下一章,咱们会深入Properties,讲讲纹理采样、颜色空间、以及那些容易被忽略的细节。到时候我会分享一个我在VR项目里踩过的纹理压缩的坑,保证让你印象深刻。
今天就到这里。动手写一个你自己的ShaderLab文件吧,哪怕只是改改颜色值,也比光看强。代码这东西,写多了自然就熟了。
课后练习:创建一个新的Shader文件,添加一个纹理属性、一个颜色属性、一个滑动条属性。然后在Pass里用这些属性修改材质的最终颜色。试试看,10分钟就能搞定。
公众号:蓝海资料掘金营,微信deep3321