第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做了几件事:

  1. 定义了渲染状态(剔除、深度测试、混合模式)
  2. 声明了顶点着色器vert和片元着色器frag
  3. 在vert里把顶点从模型空间转换到裁剪空间
  4. 在frag里计算简单的漫反射光照

你想想看,整个渲染流程其实就这两步:顶点处理 + 像素着色。vert处理每个顶点,frag处理每个像素。中间的光栅化阶段由GPU自动完成,你不需要操心。

记住这个核心概念:Pass里的CGPROGRAM...ENDCG块,才是真正写着色器代码的地方。前面的Properties和SubShader都是“配置”,这里才是“执行”。

1.5 本章小结

咱们今天把ShaderLab的骨架搭起来了:

  • Shader:最外层容器,定义名称
  • Properties:暴露给材质编辑器的参数接口
  • SubShader:针对不同硬件级别的渲染方案
  • Pass:真正的渲染执行单元

下一章,咱们会深入Properties,讲讲纹理采样、颜色空间、以及那些容易被忽略的细节。到时候我会分享一个我在VR项目里踩过的纹理压缩的坑,保证让你印象深刻。

今天就到这里。动手写一个你自己的ShaderLab文件吧,哪怕只是改改颜色值,也比光看强。代码这东西,写多了自然就熟了。

课后练习:创建一个新的Shader文件,添加一个纹理属性、一个颜色属性、一个滑动条属性。然后在Pass里用这些属性修改材质的最终颜色。试试看,10分钟就能搞定。


公众号:蓝海资料掘金营,微信deep3321