杰瑞科技汇

Unity Shader教程从哪学起?

Unity Shader 完整学习指南

第一部分:思想准备与基础概念

在开始写代码之前,理解一些核心概念至关重要。

Unity Shader教程从哪学起?-图1
(图片来源网络,侵删)

什么是 Shader(着色器)?

Shader 是一段运行在 GPU 上的小程序,它的核心任务是告诉 GPU 如何计算和渲染屏幕上的每一个像素。

想象一下:你的 3D 模型只是一个线框和顶点的集合,Shader 就像是给这个模型穿上“衣服”,并定义了这件衣服的颜色、质感、光泽、透明度等所有外观属性。

GPU 与 CPU 的分工

  • CPU (中央处理器):负责游戏逻辑、物理计算、资源管理等“重量级”任务,它准备好所有需要渲染的数据(如模型、摄像机、灯光信息),然后下达“渲染”命令。
  • GPU (图形处理器):拥有成千上万个小型核心,擅长进行大规模的并行计算,当 CPU 下达渲染命令后,GPU 会启动成千上万个 Shader 实例,同时处理屏幕上成千上万个像素点的颜色计算。

核心思想:把所有与图形渲染相关的计算都交给 GPU,让 CPU 专注于游戏逻辑,这样游戏才能流畅运行。

Shader 的渲染流水线

Shader 在渲染过程中主要参与以下几个阶段(以最常用的 Forward Rendering 为例):

Unity Shader教程从哪学起?-图2
(图片来源网络,侵删)
  1. 应用阶段:CPU 收集所有需要渲染的物体,准备好数据,并调用 Draw Call 告诉 GPU 开始渲染。
  2. 几何阶段:GPU 接收到数据后,进行顶点处理(如模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间 -> 屏幕空间)。
  3. 光栅化阶段:确定哪些图元(三角形)覆盖了屏幕上的哪些像素。
  4. 片段着色阶段这是 Shader 大显身手的地方,GPU 对每一个覆盖到的像素(此时称为“片段”或 Fragment)运行你的片段着色器代码,计算它的最终颜色。
  5. 合并阶段:将计算出的颜色写入帧缓冲区,并处理透明度测试、深度测试等。

你写的 Shader,主要就是在控制第 4 阶段。


第二部分:Unity Shader 的三种形式

Unity 提供了创建 Shader 的三种方式,它们代表了不同的抽象层次。

固定功能管线

这是最古老、最简单的方式,你不需要编写代码,而是通过设置材质面板上的各种参数(如颜色、高光颜色、高光强度等)来告诉 Unity 如何渲染,Unity 内部会使用一套预设好的 Shader 代码。

  • 优点:极其简单,无需编程知识。
  • 缺点:功能极其有限,无法实现复杂效果。
  • 适用场景:初学者快速入门,或对效果要求极简单的项目。

Surface Shader (表面着色器)

这是 Unity 为了简化 Shader 开发而提供的一种高级抽象,你只需要关注光照模型和表面属性(如颜色、法线、平滑度等),Unity 会自动帮你处理顶点光照、阴影、光照贴图等复杂逻辑。

Unity Shader教程从哪学起?-图3
(图片来源网络,侵删)
  • 优点:开发速度快,能轻松实现复杂的光照效果,自动处理很多底层细节。
  • 缺点:不够灵活,生成的 Shader 代码较多,性能开销比自定义 Shader 大。
  • 适用场景:需要快速实现符合物理标准的材质,如皮肤、金属、布料等。

Vertex & Fragment Shader (顶点/片段着色器)

这是最底层、最灵活、最强大的方式,你需要自己编写 GLSL/HLSL 代码来控制顶点变换和像素颜色计算,Unity 提供了 CGPROGRAMHLSLPROGRAM 两种编写方式,推荐使用后者(更现代,跨平台支持更好)。

  • 优点:完全控制渲染流程,性能最高,可以实现任何你能想象到的视觉效果。
  • 缺点:学习曲线最陡峭,需要深入理解图形学原理,代码量较大。
  • 适用场景:实现特效(如火焰、水流、卡通渲染)、后处理效果、自定义渲染管线等。

本教程将主要聚焦于第三种 Vertex & Fragment Shader,因为它代表了 Shader 编程的核心能力。


第三部分:从零开始编写你的第一个 Vertex & Fragment Shader

我们将创建一个最简单的 Shader:它只会将物体显示为一种纯色。

步骤 1:创建 Shader 文件

在 Project 窗口中右键 -> Create -> Shader -> Standard Surface Shader,然后双击打开,删掉所有内容,我们从头开始写。

步骤 2:Shader 文件的基本结构

一个 Shader 文件通常包含以下几个部分:

Shader "MyUnlitShader" // 1. Shader 名称,在材质面板中显示
{
    Properties // 2. 属性面板,显示在材质的 Inspector 窗口中
    {
        _MainTex ("Texture", 2D) = "white" {} // 示例属性
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader // 3. 子着色器,一个 Shader 可以包含多个,供不同平台选择
    {
        Tags { "RenderType"="Opaque" } // 标签,用于告诉渲染器如何处理这个物体
        Pass // 4. 通道,一次完整的渲染过程
        {
            // 这里是具体的渲染代码
        }
    }
}

步骤 3:编写一个最简单的 Pass

我们来实现一个“无光照”的纯色 Shader。

Shader "MyUnlit/01 Simple Color"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100 // Level of Detail,LOD 值越低,Shader 越简单
        Pass
        {
            // --- HLSL 编程开始 ---
            HLSLPROGRAM
            #pragma vertex vert // 告诉编译器,顶点着色函数名为 "vert"
            #pragma fragment frag // 告诉编译器,片段着色函数名为 "frag"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // 引入核心库
            // --- CBUFFER (常量缓冲区) ---
            // 用于将 CPU 的数据高效传递给 GPU
            CBUFFER_START(UnityPerMaterial)
                float4 _MainColor;
            CBUFFER_END
            // --- 顶点着色器输入结构体 ---
            struct Attributes
            {
                float4 positionOS   : POSITION; // 模型空间下的顶点位置
            };
            // --- 顶点着色器输出/片段着色器输入结构体 ---
            struct Varyings
            {
                float4 positionHCS  : SV_POSITION; // 裁剪空间下的顶点位置 (必须叫 SV_POSITION)
            };
            // --- 顶点着色器函数 ---
            // 输入: v (顶点数据)
            // 输出: o (处理后的顶点数据)
            Varyings vert (Attributes v)
            {
                Varyings o;
                o.positionHCS = TransformObjectToHClip(v.positionOS.xyz); // 将顶点从模型空间转换到裁剪空间
                return o;
            }
            // --- 片段着色器函数 ---
            // 输入: i (从顶点着色器传递过来的数据)
            // 输出: float4 (最终像素的 RGBA 颜色)
            float4 frag (Varyings i) : SV_Target
            {
                return _MainColor; // 直接返回我们定义的颜色
            }
            // --- HLSL 编程结束 ---
            ENDHLSL
        }
    }
}

代码解释:

  • #pragma vertex frag:这是指令,告诉 Unity 哪个函数是顶点着色器,哪个是片段着色器。
  • struct Attributes:定义了从模型传递给顶点着色器的数据。POSITION 是一个语义,告诉 GPU 这个变量是顶点位置。
  • struct Varyings:定义了从顶点着色器传递给片段着色器的数据。SV_POSITION 是一个系统值语义,表示这个变量是最终在屏幕上的位置。
  • vert 函数:对每个顶点执行一次,最重要的工作是将顶点位置从模型空间转换到裁剪空间(屏幕空间的前身)。TransformObjectToHClip 是 URP 提供的便捷函数。
  • frag 函数:对每个覆盖的像素执行一次,这里我们直接返回 _MainColor,所以整个物体都会是这个颜色。
  • SV_Target:这是一个系统值语义,表示函数的返回值是最终渲染到屏幕的颜色。

步骤 4:使用你的 Shader

  1. 创建一个材质。
  2. 在材质的 Inspector 窗口中,点击 Shader 下拉菜单,选择 "MyUnlit/01 Simple Color"。
  3. 你会发现材质面板上出现了 "Main Color" 属性,可以在这里调整颜色。
  4. 将材质拖拽到一个 3D 物体上,你就能看到效果了!

第四部分:进阶学习路径

掌握了基础后,你可以沿着以下路径深入:

路径 1:添加纹理

这是最常见的需求,我们需要在 Shader 中使用一张纹理图片。

  1. 在 Properties 中添加纹理属性

    _MainTex ("Main Texture", 2D) = "white" {}
  2. 在 CBUFFER 中声明纹理

    TEXTURE2D(_MainTex); // 声明纹理
    SAMPLER(sampler_MainTex); // 声明采样器
  3. 在片段着色器中采样纹理

    float4 frag (Varyings i) : SV_Target
    {
        // UV坐标通常由顶点着色器传递过来
        // float2 uv = i.uv; // 假设你的 Varyings 结构里有 uv
        float4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
        return texColor * _MainColor;
    }

    注意:UV 坐标也需要从 Attributes 结构体中传递进来。

路径 2:理解光照

要让物体看起来更真实,我们需要实现光照模型,最简单的是 Lambert 漫反射模型

  1. 获取光照信息:Unity 的渲染管线(如 URP)会自动将光照信息(如主方向光的颜色、方向)传递给 Shader,你需要定义一个 Light 结构体来接收它。

  2. 在片段着色器中计算光照

    // 在 frag 函数内部
    Light mainLight = GetMainLight(); // 获取主光源信息
    float3 normalDir = normalize(i.normalWS); // 获取世界空间下的法线
    float NdotL = saturate(dot(normalDir, mainLight.direction)); // 计算法线与光照方向的点积
    float3 diffuse = mainLight.color.rgb * NdotL; // 计算漫反射颜色
    float4 finalColor = float4(diffuse * _MainColor.rgb, 1.0);
    return finalColor;

    这需要你的 Varyings 结构体中包含世界空间下的法线 i.normalWS

路径 3:实现卡通渲染

卡通渲染是一个经典案例,它通过将光照强度分级来实现。

  1. 修改光照计算:将 NdotL 的结果进行离散化。
    float NdotL = saturate(dot(normalDir, mainLight.direction));
    float toon = step(0.5, NdotL); // NdotL > 0.5,结果为 1,否则为 0
    // 或者更平滑的梯度
    // float toon = smoothstep(0.4, 0.5, NdotL);
    float3 diffuse = mainLight.color.rgb * toon;

路径 4:学习后处理效果

后处理效果是在渲染完所有 3D 物体后,对整个屏幕图像进行处理,灰度、泛光、颜色分级等。

  1. 创建一个 Post-Processing Stack:在 URP 中,你需要创建一个 Post-process Volume
  2. 编写一个全屏 Pass:你的 Shader 的 Pass 需要渲染一个覆盖整个屏幕的四边形。
  3. 采样屏幕颜色:在片段着色器中,使用 SAMPLE_TEXTURE2D 采样 _CameraColorTexture(这个纹理是上一帧渲染好的屏幕图像)。
  4. 修改颜色:对采样到的颜色进行数学运算,如 float gray = dot(color.rgb, float3(0.299, 0.587, 0.114));

第五部分:推荐资源

理论学习 + 大量实践是掌握 Shader 的唯一途径。

书籍

  1. 《Unity Shader 完整开发指南》:国内经典,由毛星云所著,内容非常全面,适合入门和进阶。
  2. 《GPU Gems》系列:图形学圣经,汇集了 NVIDIA 工程师的各种高级图形技巧,可以当参考书查阅。

在线教程与网站

  1. Catlike Coding强烈推荐! 这可能是全世界最好的 Unity Shader 入门教程,它从零开始,用极富启发性的方式带你一步步理解图形学原理和 Shader 编程,网站地址:catlikecoding.com/unity/tutorials/
  2. The Book of Shaders:专注于 GLSL 的教程,但图形学原理是通用的,非常适合理解 Shader 的数学基础,网站地址:thebookofshaders.com/
  3. B站/YouTube:搜索 "Unity Shader Tutorial",有大量视频教程,可以关注一些优秀的 UP 主,如 Sebastian Lague(他的算法和图形学视频非常惊艳)、Catlike Coding(有视频版)等。
  4. Unity Learn:Unity 官方学习平台,有关于 Shader 的官方课程。

工具

  1. Amplify Shader Editor:一个节点式的 Shader 编辑器,通过拖拽节点就能生成 Shader 代码,对于不擅长写代码的人来说是神器,也可以用它来学习 Shader 的结构。
  2. Shader Forge:另一个流行的节点式编辑器。

社区

  • Unity 官方论坛
  • Reddit (r/Unity3D, r/Unity2D)
  • 知乎、CSDN 等技术社区

总结与建议

  1. 不要怕:Shader 的数学和概念看起来很吓人,但只要分解开,一步步来,你会发现它是有逻辑可循的。
  2. 从模仿开始:找一些你喜欢的 Shader 效果,尝试去读懂别人的代码,然后模仿它实现出来。
  3. 多写多调试:Shader 编程离不开调试,学会使用 #pragma debugDebug 函数(在 URP 中)来输出颜色信息,或者使用 RenderTexture 来观察中间结果。
  4. 打好数学基础:向量、矩阵、三角函数是 Shader 的语言,不需要精通所有推导,但要理解它们的几何意义和在图形学中的应用。

祝你学习顺利,早日创作出令人惊艳的视觉效果!

分享:
扫描分享到社交APP
上一篇
下一篇