杰瑞科技汇

unity shader 教程

Unity Shader 完整学习指南

目录

  1. 第一部分:基础入门

    unity shader 教程-图1
    (图片来源网络,侵删)
    • 什么是 Shader?
    • Unity 中的 Shader 资源
    • ShaderLab 基础结构
    • 第一个 Shader:创建一个纯色材质
    • 理解 Properties、SubShader、Pass
    • Unity 的渲染管线简介
  2. 第二部分:核心概念 - CG/HLSL 着色器语言

    • 什么是 CG/HLSL?
    • 顶点着色器 与 片元着色器
    • 关键数据结构:v2f (vertex to fragment)
    • 内置变量与函数
    • Unity 中的坐标空间
  3. 第三部分:实用 Shader 编写

    • 基础光照模型:Lambert 反射
    • 纹理:如何使用贴图
    • 法线贴图:增加表面细节
    • 透明度:透明与半透明效果
    • 卡通渲染:简单的 Cel-Shader 效果
  4. 第四部分:高级主题

    • Surface Shader:简化光照模型的编写
    • Shader Variant & Keywords:实现可配置的 Shader
    • 自定义光照模型:编写自己的光照计算
    • 后处理效果:在场景渲染后进行修改
    • GPU Instancing:提升性能
  5. 第五部分:学习资源与最佳实践

    unity shader 教程-图2
    (图片来源网络,侵删)
    • 推荐书籍与教程
    • 官方文档与社区
    • Shader 开发工具
    • 性能优化技巧

第一部分:基础入门

什么是 Shader?

Shader(着色器)是一段运行在 GPU 上的小程序,它告诉 GPU 如何渲染一个像素,它决定了物体的最终颜色、高光、阴影等视觉表现。

  • 顶点着色器:负责处理每个顶点的位置、法线、UV 等信息,它决定了顶点在屏幕上的最终位置。
  • 片元着色器:负责处理每个像素(或称片元)的颜色,它决定了屏幕上每个像素应该显示什么颜色。

Unity 中的 Shader 资源

在 Unity 中,Shader 资源本身并不直接执行渲染,它更像是一个“蓝图”或“配方”,定义了渲染所需的 Pass(通道)和属性,Unity 会根据这个蓝图,生成真正在 GPU 上运行的代码。

ShaderLab 基础结构

ShaderLab 是 Unity 用来编写 Shader 的一种专用语言,它封装了底层的 CG/HLSL 代码,并提供了一些 Unity 特有的功能,如渲染队列、渲染状态设置等。

一个基本的 Shader 结构如下:

unity shader 教程-图3
(图片来源网络,侵删)
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,它只输出一个固定的颜色。

  1. 在 Project 窗口中,右键 -> Create -> Shader -> Standard Surface Shader,将其命名为 MyUnlitShader
  2. 打开 MyUnlitShader,删除所有 Surface Shader 相关的代码,保留最基础的 ShaderLab 结构。
  3. 我们将使用一个 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 坐标来确定每个像素对应纹理上的哪个位置。

步骤:

  1. Properties 中声明一个 2D 纹理:_MainTex ("Main Texture", 2D) = "white" {}
  2. 在 CG 代码中声明对应的变量:sampler2D _MainTex;
  3. v2f 结构中添加 UV 传递:float2 uv : TEXCOORD0;
  4. 在顶点着色器中传递 UV:o.uv = v.uv;
  5. 在片元着色器中采样纹理:fixed4 texColor = tex2D(_MainTex, i.uv);
  6. 将纹理颜色与光照结果相乘。

法线贴图

法线贴图是一种特殊的纹理,它不存储颜色,而是存储每个像素的表面法线信息,通过修改光照计算,可以在低模上表现出高模的细节。

原理:用纹理中的法线信息替换或修改模型原有的法线,然后进行光照计算。

关键:需要 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 通道实现。

步骤:

  1. Properties 中声明一个控制透明度的变量:_Alpha ("Alpha", Range(0,1)) = 1
  2. 在 SubShader 的 Tags 中指定渲染类型:Tags { "Queue"="Transparent" "RenderType"="Transparent" }
  3. 在 Pass 中设置混合模式:
    Blend SrcAlpha OneMinusSrcAlpha // 标准透明混合
    ZWrite Off // 关闭深度写入,避免透明物体互相遮挡时出错
  4. 在片元着色器中设置最终颜色的 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 文件,这对于优化(如开启/关闭特效)和灵活性非常有用。

使用方法:

  1. 在 Shader 中定义关键字:#pragma shader_feature FEATURE_KEYWORD
  2. 在材质面板上,通过 Keywords 字段来启用或禁用它。
  3. 在 CG 代码中,使用 #ifdef FEATURE_KEYWORD 来判断。

Unity 会为所有启用的关键字组合单独编译一个 Shader Variant,以避免运行时判断带来的性能开销。

自定义光照模型

如果你不满足于 Unity 提供的 LambertPhong 等光照模型,可以自己编写。

步骤:

  1. 在 Surface Shader 的 #pragma 指令中指定你的光照模型函数:#pragma surface surf MyLightingModel
  2. 编写一个光照模型函数,其签名必须符合 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;
    }

后处理效果

后处理是在整个场景渲染到屏幕后,再对最终图像进行全局处理的技术,泛光、灰度、颜色滤镜等。

实现方法:

  1. 创建一个使用 Image Effect Shader 的脚本。
  2. 创建一个特殊的 Shader,它只渲染一个覆盖全屏的四边形。
  3. 在 Shader 的片元着色器中,对输入的屏幕图像(通过 _MainTex 采样)进行处理。

GPU Instancing

GPU Instancing 允许 GPU 在一次绘制调用中渲染多个相同的物体,极大地提升了性能(如渲染大量草地、树木)。

启用方法:

  1. 在 Shader 的 Pass 中添加指令:#pragma multi_compile_instancing
  2. 对于每个需要实例化的属性,使用 UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END 来定义。

第五部分:学习资源与最佳实践

推荐资源

  • 官方文档Unity Manual - Shaders - 最权威、最全面的资料。
  • Catlike Codinghttps://catlikecoding.com/unity/tutorials/ - 强烈推荐! 从零开始,讲解极其细致,配有大量交互式示例。
  • The Book of Shadershttps://thebookofshaders.com/ - 虽然不是专门针对 Unity,但对图形学基础和 GLSL/HLSL 的讲解非常出色。
  • B站/YouTube 教程:搜索 "Unity Shader Tutorial",有很多优秀的中文和英文视频教程。

开发工具

  • Unity Shader Graph:一个节点式的 Shader 编写工具,非常适合初学者和快速原型开发,无需编写代码。
  • Amplify Shader Editor:功能强大的第三方节点式 Shader 编辑器。
  • RenderDoc:一个图形调试工具,可以让你实时查看渲染的每一帧,检查纹理、着色器输入输出等,是排查 Shader Bug 的利器。

最佳实践

  1. 从简单开始:先实现一个纯色 Shader,再逐步添加纹理、光照等。
  2. 理解原理:不要只复制粘贴代码,理解每个函数、每个变量的作用,尤其是坐标空间的转换。
  3. 善用官方示例:Unity 自带了很多示例 Shader(在 Assets > Import Package > Effects 中),仔细研究它们。
  4. 性能优化
    • 尽量减少计算量,尤其是在片元着色器中。
    • 合理使用 Shader Keywords 和 Variants。
    • 避免在片元着色器中进行分支判断(if/else),可以使用 steplerp 等数学函数代替。
    • 对不需要高精度的变量使用低精度类型(如 half, fixed)。
  5. 版本控制:Shader 文件是文本文件,非常适合使用 Git 进行版本管理。

希望这份指南能为你打开 Unity Shader 的大门,Shader 编程是一门艺术,也是一门科学,需要大量的练习和耐心,祝你学习愉快!

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