顶点/片元着色器:从零开始写第一个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就像学骑车,先会跑起来,再谈技巧。