顶点/片元着色器:从零开始写第一个Unlit Shader

大家好,我是老张。今天咱们聊聊着色器里最核心的两个角色——顶点着色器和片元着色器。很多新手一上来就被各种术语吓住了,其实没那么玄乎。说白了,它们就是GPU流水线上的两个关键工序。

顶点着色器:模型的"骨架工"

顶点着色器是干啥的?我习惯这么跟新人解释:它负责处理模型上的每一个顶点。你想想看,一个3D模型是由成千上万个顶点组成的,每个顶点都有位置、法线、UV坐标这些信息。顶点着色器就是对这些信息进行"加工"。

具体来说,它做三件事:

  • 坐标变换:把顶点从模型空间转换到裁剪空间。这是最核心的工作,没有这一步,你的模型就显示不出来。
  • 数据传递:把顶点相关的数据(比如UV、法线)传递给片元着色器。
  • 逐顶点计算:做一些光照、位移等计算,这些计算在顶点级别完成,效率更高。

重要概念:顶点着色器是"逐顶点"执行的。一个模型有1000个顶点,它就执行1000次。所以这里写的代码要尽量高效,别做太重的计算。

我在项目中遇到过一个问题:有个角色模型的头发飘动效果,一开始我在片元着色器里做顶点偏移计算,结果性能直接崩了。后来改成在顶点着色器里处理,帧率瞬间从30飙到60。嗯,这就是选对处理阶段的威力。

片元着色器:像素的"化妆师"

片元着色器,有些人也叫它像素着色器。它负责决定屏幕上每个像素最终显示什么颜色。顶点着色器处理完顶点后,GPU会把这些顶点组装成三角形,然后光栅化成一个个像素点,每个像素点都会调用一次片元着色器。

它的主要工作:

  • 颜色计算:根据UV坐标采样纹理,或者计算光照,最终输出一个颜色值。
  • 逐像素效果:法线贴图、阴影、边缘光这些精细效果,都在这里完成。
  • 透明度处理:控制像素的Alpha值,实现半透明效果。

个人经验:片元着色器的执行次数远多于顶点着色器。一个1080p的屏幕有200多万个像素,每个像素都要跑一次片元着色器。所以,能放在顶点着色器里算的,就别放到片元里来。

编写第一个Unlit Shader

好,理论说完了,咱们直接上手写代码。我带你写一个最简单的Unlit Shader——不光照,只显示纯色或者纹理。

先看完整代码:

Shader "Custom/MyFirstUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv) * _Color;
                return col;
            }
            ENDCG
        }
    }
}

代码看着有点长?别慌,我拆开给你讲。

逐行解读:结构体部分

appdata 是顶点着色器的输入结构。它告诉GPU:我需要每个顶点的位置(POSITION)和UV坐标(TEXCOORD0)。

v2f 是顶点着色器传给片元着色器的数据结构。注意看,vertex 用了 SV_POSITION 语义,这是告诉GPU:这个值已经是裁剪空间坐标了,直接拿去用。

曾经踩过的坑:我刚开始写Shader时,经常忘记给顶点位置加 SV_POSITION 语义,结果模型显示不出来,找了半天才发现是语义写错了。记住,顶点位置必须用 SV_POSITION,这是硬性规定。

顶点着色器函数

vert 函数里只有两行代码:

  • UnityObjectToClipPos(v.vertex):把模型空间的顶点坐标转换到裁剪空间。这是Unity封装好的函数,省去了手动乘矩阵的麻烦。
  • TRANSFORM_TEX(v.uv, _MainTex):处理UV的平铺和偏移。如果你在材质面板里设置了Tiling和Offset,这个函数会自动应用。

片元着色器函数

frag 函数更简单:用UV坐标采样纹理,再乘上颜色值,返回最终颜色。tex2D 是纹理采样函数,它会根据UV坐标从纹理中取出对应的颜色。

小技巧:如果你只想显示纯色,可以把 _MainTex 的默认值设为 "white",这样不贴纹理时就是白色,再乘上 _Color 就能得到任意纯色。

属性面板的奥秘

Properties 块里定义了两个属性:_MainTex_Color。这些属性会出现在Unity材质面板上,方便你调整。注意命名规范:属性名以下划线开头,对应的Cg变量名也要一致。

属性名 类型 默认值 说明
_MainTex 2D "white" 主纹理,默认白色
_Color Color (1,1,1,1) 颜色叠加,默认白色

运行起来看看

把这段代码复制到Unity里,新建一个Shader文件,再新建一个材质球,把Shader拖上去,然后赋给一个Cube。你会看到一个带纹理的立方体。调整 _Color 的颜色值,物体会跟着变色。

为什么会这样?因为片元着色器里做了 tex2D(_MainTex, i.uv) * _Color,纹理颜色和属性颜色相乘,所以改变 _Color 就能影响最终输出。

核心要点:这个Shader虽然简单,但包含了顶点/片元着色器的完整流程。你以后写的所有复杂效果,都是在这个框架上扩展的。吃透它,后面的路就好走了。

好了,第一个Unlit Shader就写完了。是不是比想象中简单?下一章咱们会深入聊聊语义绑定和数据类型,到时候你会对这段代码有更深的理解。记住,写Shader就像学骑车,先会跑起来,再谈技巧。