目录
[1. 功能支持差异(最核心)](#1. 功能支持差异(最核心))
[2. 语法 / 语义兼容差异](#2. 语法 / 语义兼容差异)
[3. 性能差异](#3. 性能差异)
[in 输入参数(只读)](#in 输入参数(只读))
Shader编码工具
创建和使用Shader

实际上无论是创建Standard Surface Shader还是Image Effect Shader,都可以把它编写成想要的Shader。不同之处在于:Unity会在不同类型的Shader里预先插入一部分代码,从而便于最开始的编写。模板只是 "快捷方式",不是限制。
Unity Shader 的两大核心用途:
- 「物体渲染」:绑定到材质,作用于 3D/2D 物体(如模型、Sprite),核心是处理顶点 / 像素的空间、颜色、光照等,对应 Standard Surface Shader 等模板;
- 「图像处理」:绑定到脚本(如 OnRenderImage),作用于整个屏幕 / 渲染纹理,核心是采样屏幕纹理并修改像素颜色(如模糊、调色、描边),对应 Image Effect Shader 模板。
Shader的编写方式
1)顶点-片段着色器
- 本质 :最底层、最灵活的编写方式,直接编写顶点着色器(Vert)和片元(片段)着色器(Frag),完全掌控渲染管线的每一步(顶点变换、像素计算、渲染状态)。
- 核心特点 :
- 手动写
Pass、渲染状态、语义绑定、矩阵变换(比如UnityObjectToClipPos); - 代码量最大,但可控性极强,能实现任何自定义效果(比如屏幕特效、自定义光照、特殊渲染逻辑);
- 需理解渲染管线底层逻辑(如顶点变换、光栅化、深度测试)。
- 手动写
- 适用场景:自定义特殊效果(如溶解、描边、屏幕后处理)、性能优化(精准控制 Pass 数量)、跨平台兼容性调试。
2)表面着色器
- 本质 :Unity 封装的高层级写法,只需定义 "表面属性"(如漫反射颜色、高光、法线)和 "光照模型",Unity 自动生成多个 Pass、处理光照计算和渲染状态。
- 核心特点 :
- 无需手动写 Pass,只需实现
surf函数定义表面属性,Unity 自动适配前向 / 延迟渲染; - 代码简洁,专注效果逻辑而非管线细节,适合快速实现光照相关效果;
- 灵活性稍差,自动生成的 Pass 可能有冗余(影响性能),复杂自定义效果难实现。
- 无需手动写 Pass,只需实现
- 适用场景:常规物体渲染(如 PBR 材质、漫反射 / 高光光照)、快速原型开发、无需极致性能的场景。
3)固定函数着色器
- 本质:最古老的写法,基于 "固定功能管线"(非可编程管线),无需写着色器代码,仅通过 ShaderLab 指令配置纹理、颜色、光照参数。
- 核心特点 :
- 无可编程逻辑,仅靠
Color、Texture、Lighting等指令配置; - 兼容性极强(支持极老的显卡 / 移动端),但功能极度受限(无法实现复杂效果);
- Unity 已逐步淘汰,仅用于兼容超低端设备。
- 无可编程逻辑,仅靠
- 适用场景:仅兼容古董级设备(几乎不用),新手无需深入学习。
CG语法基础
在Unity Shader 中,ShaderLab语言起到组织代码结构,负责渲染状态配置的作用,而真正实现渲染效果的部分是 CG,更主流的是 HLSL(CG 是 HLSL 的子集,Unity 已逐步转向 HLSL)编写的。
ShaderLab:Unity Shader 的 "骨架"(结构 + 渲染规则)
ShaderLab 是 Unity 自定义的声明式语言,核心作用分两层,不只是 "组织结构":
- 第一层:组织代码结构 定义 Shader 的整体框架,比如
Properties(材质面板参数)、SubShader(渲染方案)、Pass(渲染通道)、Fallback(降级方案)等,把不同功能的代码块按渲染管线的规则组织起来。 - 第二层:配置渲染状态 这是容易忽略的关键 ------ShaderLab 还负责设置渲染管线的 "规则",比如是否开启深度测试(
ZTest)、混合模式(Blend)、背面剔除(Cull)、深度写入(ZWrite)等,这些直接影响渲染效果的底层规则,而非单纯的 "结构"。
为什么说 "CG 逐步被 HLSL 替代"
你可能会在老教程里看到大量 CG 代码,但 Unity 近年的文档 / 示例都优先用 HLSL,原因是:
- CG 已停止维护,HLSL 是微软持续更新的标准,兼容更多平台(比如移动端、主机);
- Unity 的 URP/HDRP 管线完全基于 HLSL,
HLSLPROGRAM块是官方推荐写法(和CGPROGRAM用法一致,只是标识不同); - 语法上几乎无差异:比如
float4、tex2D、UnityObjectToClipPos等核心语法,CG 和 HLSL 完全通用,新手无需刻意区分,只需知道 "HLSL 是现在的主流"。
1.编译指令
CG程序片段通过指令嵌入在Pass中,夹在指令CGPROGRAM和ENDCG之间,通常看起来是这样的:

在CG程序片段之前,通常需要先使用#pragma声明编译指令
| 指令 | 作用 | 示例 & 说明 |
|---|---|---|
#pragma vertex 函数名 |
指定顶点着色器的入口函数 | #pragma vertex vert → 告诉编译器:顶点着色器的入口是名为vert的函数(函数名可自定义,比如vshader) |
#pragma fragment 函数名 |
指定片元(片段)着色器的入口函数 | #pragma fragment frag → 片元着色器入口是frag函数,必须和代码中函数名一致 |
#pragma surface 函数名 光照模型 |
表面着色器****专属:指定surf函数和光照模型 |
#pragma surface surf Lambert → 表面着色器入口是surf函数,使用 Lambert 漫反射光照模型;常见光照模型:Lambert(漫反射)、BlinnPhong(高光)、Standard(PBR) |
常用扩展指令(提升灵活性)
| 指令 | 作用 | 适用场景 |
|---|---|---|
#pragma multi_compile 关键词1 关键词2 ... |
多版本编译:生成多个着色器变体,运行时按关键词切换 | 比如开关特效:#pragma multi_compile _ _USE_FOG → 生成 "启用雾效 / 禁用雾效" 两个版本,代码中用#ifdef _USE_FOG判断;Unity 内置常用:#pragma multi_compile_fog(雾效)、#pragma multi_compile_shadowcaster(阴影投射) |
#pragma shader_feature 关键词1 关键词2 ... |
按需编译:和multi_compile类似,但只编译用到的变体(减少包体) |
替代multi_compile,适合材质面板可选的功能(比如是否启用高光):#pragma shader_feature _ _SPECULAR_ON |
#pragma target 版本号 |
指定着色器目标模型(Shader Model),启用对应特性 | #pragma target 3.0 → 启用 SM3.0 特性(如纹理数组、复杂数学函数);新手默认用 3.0,移动端可选 2.0(兼容性更好) |
#pragma only_renderers 渲染器 |
只编译指定渲染器的版本 | 适配多平台:#pragma only_renderers opengl d3d11 vulkan → 仅编译 OpenGL、DX11、Vulkan 版本 |
#pragma exclude_renderers 渲染器 |
排除指定渲染器的版本 | 避免适配老平台:#pragma exclude_renderers gles1 → 排除 GLES1.0(极老移动端) |
进阶优化指令(性能 / 包体优化)
| 指令 | 作用 | 优化效果 |
|---|---|---|
#pragma skip_variants 关键词 |
跳过指定变体编译,减少包体 | #pragma skip_variants SHADOWS_SOFT → 跳过软阴影变体,适合不需要软阴影的场景 |
#pragma optimize on/off |
开启 / 关闭编译器优化 | #pragma optimize on(默认)→ 编译器自动优化代码(如删除无用变量);调试时可设off,方便查错 |
#pragma debug |
启用调试模式,保留更多调试信息 | 调试着色器时使用:#pragma debug → 编译器不删除调试信息,配合 RenderDoc 等工具排查问题 |
#pragma glsl |
强制按 GLSL 语法编译(适配移动端 OpenGL) | 跨平台适配:#pragma glsl → 让 CG 代码兼容 GLSL 语法(比如纹理采样函数适配) |
关键注意事项
- 指令顺序 :编译指令必须写在
CGPROGRAM/HLSLPROGRAM之后、其他代码(变量 / 函数)之前; - 表面着色器 vs 顶点 - 片元着色器 :
#pragma surface仅用于表面着色器,顶点 - 片元着色器用vertex/fragment; - 内置宏简化 :Unity 提供很多内置
multi_compile宏,比如:#pragma multi_compile_instancing(GPU 实例化)#pragma multi_compile_lightpass(多光源)- 直接用内置宏比手写关键词更省心。
1)编译目标等级
当编写完Shader程序之后,其中的CG代码可以被编译到不同的Shader Models(简称SM)中,为了能够使用更高级的GPU功能,需要对应使用更高等级的编译目标。
=>
写的同一段 CG/HLSL 代码,Unity 编译器可以根据「Shader Model(SM)版本」的要求,生成适配不同 GPU 功能等级的编译结果------ 就像同一篇文章能翻译成小学版、中学版、大学版,分别适配不同知识水平的读者,核心内容不变,但能调用的 "高级表达"(GPU 功能)不同。
不同版本的 Shader Model(SM)对应 GPU 支持的功能等级,想要用高级 GPU 特性(比如复杂计算、纹理数组),必须把 CG/HLSL 代码编译到足够高版本的 SM 上,否则要么编译失败,要么高级功能无法生效。
Shader Model 是什么?
Shader Model(着色器模型)可以理解为:
GPU 厂商(NVIDIA/AMD/ 高通)制定的「着色器代码功能规范」------ 不同版本的 SM 定义了 GPU 能支持的语法、函数、资源限制(比如纹理数量、数学精度、循环次数)。
2)渲染平台
Unity 中常见的渲染平台(按场景分类)
| 设备场景 | 主流渲染平台(图形 API) | 关键特点 |
|---|---|---|
| Windows PC | Direct3D 11/12(D3D11/12)、OpenGL | D3D 是 Windows 原生 API,支持高版本 SM(5.0+),可使用光线追踪、计算着色器等高级功能 |
| macOS/Linux | OpenGL、Metal(macOS) | Metal 是苹果原生高性能 API,OpenGL 逐步被淘汰 |
| 移动端(安卓) | OpenGL ES 2.0/3.0、Vulkan | OpenGL ES 2.0 对应 SM2.0(低端机),3.0 对应 SM3.0;Vulkan 是新一代 API,性能更高 |
| 移动端(iOS) | Metal | 苹果强制要求,仅支持 Metal,对应 SM3.0+ |
| 主机(PS/Xbox) | Vulkan、D3D12、专属 API | 支持最高级 SM 版本,可充分利用硬件性能 |
| WebGL | WebGL 1.0/2.0 | WebGL 1.0≈OpenGL ES 2.0,2.0≈OpenGL ES 3.0,功能受限(无计算着色器) |
渲染平台对 Shader 的核心影响(新手必知)
写的 CG/HLSL 代码能否正常运行、性能如何,完全依赖于目标渲染平台的支持:
1. 功能支持差异(最核心)
- 比如「光线追踪」仅支持 D3D12/Vulkan/Metal(高版本 API),WebGL/OpenGL ES 2.0 完全不支持;
- 「纹理数组」在 OpenGL ES 3.0(安卓)/Metal(iOS)上可用,但 OpenGL ES 2.0(低端安卓机)不可用;
- 解决方式:通过
#pragma target指定 SM 版本(如 SM3.0 适配 OpenGL ES 3.0),或用编译指令屏蔽平台不支持的功能。2. 语法 / 语义兼容差异
- 比如
SV_POSITION在 D3D 中是标准语义,但在老版本 OpenGL 中需兼容POSITION;- CG/HLSL 中的部分函数(如
tex2Dlod)在不同平台的行为略有差异,Unity 会通过内置宏(如UNITY_SAMPLE_TEX2D)自动适配。3. 性能差异
- 同一 Shader 在 Vulkan/Metal 上的运行效率远高于 OpenGL ES 2.0;
- 移动端平台对 Shader 的 "指令数、纹理采样数" 限制更严格(比如 OpenGL ES 2.0 最多支持 8 个纹理采样)。
2.着色器函数
顶点-片段着色器主要是通过顶点函数和片元函数来实现的。
在 CG / HLSL(Unity Shader) 中,in、out、inout是参数修饰符 ,用来明确数据的流向(输入 / 输出),不是 C# 那种"引用"概念。
实际传递方式通常是 按值
HLSL / CG 没有"按引用传参"的概念(不像 C# / C++)
in 输入参数(只读)
在函数体内只能读取,不能修改(❗ 前提 :是 HLSL / CG / ShaderLab 中的 in, 不是 C# 的 in(那是只读引用))
含义:
-
这个参数是 输入
-
函数内部 不能修改
-
外部值不受影响
void Foo(in float3 v)
{
// v = float3(1,0,0); // × 编译错误
float3 n = v; // √ 只读
}
out输出参数(只写)
特点:
-
使用前不需要初始化
-
函数内部 必须赋值
-
外部变量会被覆盖
为什么 CG / HLSL 需要这些关键字?
GPU 编程的特点:
没有"返回值复杂对象"的习惯
更偏向 显式数据流
一个函数常要输出 多个结果
void Func(in a, out b, inout c)
"显式数据流"
在 Shader / GPU 编程 里,其实是一个设计思想,不是某个语法。
显式数据流 = 数据的"从哪里来、到哪里去"是明确写出来的,而不是靠返回值、隐式状态或全局变量。
意味着:
用 参数(
in / out / inout)用 结构体
用 明确的语义(SV_Position、TEXCOORD0 等)
来告诉 GPU:每一步数据怎么流动。
Shader 中的"显式数据流"长什么样?
示例 1:用参数明确输入输出
void ComputeLighting( in float3 normal, in float3 lightDir, out float3 color)
{
color = max(dot(normal, lightDir), 0);
}
这里的数据流是:
示例 2:用结构体(最 Shader 的方式)
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
};
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS);
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
return output;
}
数据流是完全显式的:
为什么 Shader 特别强调"显式数据流"?
GPU 是大规模并行
成千上万个线程
没有"全局状态安全区"
隐式数据 = 不可预测
GPU 没有"自动内存管理"
没有 GC
没有栈展开
一切靠寄存器 / buffer
必须明确谁拥有数据
1)无返回值的函数
无返回值的顾名思义就是函数不会返回任何变量,而是通过out关键词将变量输出
语法结构:
void name(in 参数,out 参数)
{
//函数体
}
in:输入参数,语法为:in+数据类型+名称,一个函数可以有多个输入
out:输出参数,语法为:out+数据类型+名称,一个函数可以有多个输出
// 顶点着色器函数:void表示无返回值,通过out参数输出裁剪空间坐标
void vert(
float4 vertex : POSITION, // 输入:模型空间顶点坐标(带POSITION语义)
out float4 position : SV_POSITION // 输出:裁剪空间顶点坐标(带SV_POSITION语义) )
{
// 核心逻辑:把模型空间顶点坐标转换为裁剪空间坐标
position = UnityObjectToClipPos(vertex);
}
函数作用:
顶点着色器函数 (命名为
vert是约定俗成),作用是:接收模型空间的顶点坐标,将其转换为 GPU 能识别的「裁剪空间坐标」,并通过out参数输出 ------ 这是 3D 物体渲染的必经步骤(把模型顶点 "摆到屏幕上")。
2)有返回值的函数
有返回值的函数不再使用out关键词输出参数,而是会在最后通过return关键词返回一个变量
type name(in 参数)
{
//函数体
return 返回值;
}
顶点函数和片元函数中支持的数据类型
fixed,fixed2,fixed3,fixed4
低精度浮点值,使用11位精度进行存储,数值区间为[-2.0,2.0],用于存储颜色,标准化后的向量
half,half2,half3,half4
中精度浮点值,使用316位精度进行存储,数值区间为[-60000,60000]
float,float2,float3,float4
高精度浮点值,使用32位精度进行存储,用于存储顶点坐标、未标准化的向量、纹理坐标等
struct
结构体,可以将多个变量整体进行打包
3.语义

一个冒号,跟着一个全为大写的关键词,这到底有什么作用呢?
这就是语义!!!
函数的输入参数和输出参数都需要填充一个语义来表示它们要传递的数据信息。
语义可以执行大量繁琐的操作,事用户能够避免直接与GPU底层进行交流
1)顶点着色器的输入语义
在顶点着色器中,顶点数据是以输入参数的方式传递给顶点函数的,每一个输入的参数都需要填充一个语义,用于表示所传递的数据。
------>(大白话解释为)
顶点着色器要用到的模型数据(比如顶点坐标、UV、法线),得通过函数参数传进来;但光传参数还不够,必须****给每个参数贴个「标签(语义)」,告诉程序 "这个参数里装的是模型的哪类数据"------ 不然程序根本分不清哪个参数是坐标、哪个是 UV。
本质是告诉显卡:"把模型的某类原始数据(比如顶点坐标、法线、UV)赋值给顶点着色器的某个变量"。输入语义必须对应模型的实际数据,否则顶点着色器会取不到正确值(甚至取到随机值)。
| 输入语义 | 数据类型 | 核心用途 | |
|---|---|---|---|
POSITION(或 POSITION0) |
float4/float3 |
接收模型空间顶点坐标(顶点着色器最核心的输入,必用) | |
NORMAL |
float3 |
接收模型空间法线向量(用于光照计算、法线贴图) | |
TEXCOORD0 |
float2/float4 |
接收第一套 UV 纹理坐标(主 UV,用于采样主纹理) | |
TEXCOORD1 |
float2/float4 |
接收第二套 UV 纹理坐标(烘焙光照、法线贴图 UV、动画 UV) | |
COLOR(或 COLOR0) |
fixed4/float4 |
接收顶点颜色(模型烘焙的顶点色、顶点动画颜色) |
当顶点信息包含的元素少于顶点着色器输入所需要的元素时,缺少的部分会被0补充,而w分量会被1补充。
2)顶点着色器输出和片段着色器输入语义
顶点着色器最重要的一项任务就是需要输出顶点在裁切空间的坐标,这样GPU就可以知道顶点在屏幕上的栅格化位置以及深度值。
在顶点函数中,这个输出参数需要使用float4类型的SV_POSITION语义进行填充。
顶点着色器产生的输出值将会在三角形遍历阶段经过插值计算,最终作为像素值输入到片元着色器。
顶点着色器的输出即为片元着色器的输入。
片元着色器会自动获取顶点着色器输出的裁切空间顶点坐标,所以片元函数输入的SV_POSITION可以省略。
需要注意的是,与顶点函数的输入语义不同,**TEXCOORDn不再特指模型的UV坐标,COLORn也不再特指顶点颜色。**它们的使用范围更广,可以用于声明任何符合要求的数据,所以在使用过程中不要被语义的名称欺骗了。
核心意思是顶点着色器的输入语义和片元着色器的输入语义,其名称的"历史含义"在今天已不再被严格遵循。
1. 顶点着色器输入:名字是固定的,含义是固定的
在顶点着色器中,像
POSITION、NORMAL、TEXCOORD0、COLOR0这些语义,名字和含义是强绑定的。
POSITION传入的就必须是顶点位置。
TEXCOORD0传入的默认就是第一套纹理坐标(UV)。
COLOR0传入的默认就是顶点颜色。这是由模型数据(如Mesh)决定的,比较严格。
2. 顶点到片元的传递:语义名称变成"通道标签"
当顶点着色器处理完数据,需要传给片元着色器时,情况就变了。此时,
TEXCOORD0、COLOR0这类名字不再是"含义" ,而更像是**"数据通道的编号或容器"**。
历史(DirectX早期) :
TEXCOORD就是放纹理坐标,COLOR就是放颜色值。硬件为它们设计了特定用途的寄存器。现在(现代GPU) :这些语义名称与硬件寄存器解耦了,它们只是一个"标签",用来标识一个数据变量,告诉GPU"请把这个变量的值,从顶点着色器传到片元着色器的对应通道里"。
3. 你的问题:什么意思?(重点来了)
这意味着,在从顶点着色器输出,到片元着色器输入 这个环节,你可以用 TEXCOORD1这个"通道"来传递任何你想传递的数据,而不仅仅是UV坐标。比如:
你可以用
TEXCOORD1来传递一个计算好的世界空间法线。你可以用
TEXCOORD2来传递一个自定义的、用于特效的参数。同理,
COLOR1也可以用来传递一个强度值,而不是颜色。总结一下:
不要被名字骗了 :
TEXCOORDn不一定是UV,COLORn不一定是颜色。它们在现代Shader中主要是**"数据通道"**的作用。灵活性 :这给了Shader编写者极大的灵活性。只要顶点着色器输出和片元着色器输入用相同的语义标签 (如
TEXCOORD2),就能确保数据被正确传递,至于这个数据具体代表什么,由程序员自己定义。
3)片段着色器输出语义
片段着色器通常只会输出一个fixed4类型的颜色信息,输出的值会存储到渲染目标(Render Target)中,输出参数使用SV_TARGET语义进行填充
=>
片段着色器的核心任务是为屏幕上的每个像素计算最终颜色,这个颜色会通过SV_TARGET语义标记的输出变量,写入到显卡的「渲染目标」(比如屏幕缓冲区、纹理)中。
片段着色器:
核心工作就是计算这个像素最终该显示什么颜色
fixed4 类型:
fixed是着色器中的一种精度类型(比float精度低、性能高),常用于颜色值(范围 0~1);
fixed4代表 4 个分量的固定精度值,对应颜色的RGBA渲染目标:
可以把它理解成「显卡里的一块画布」------ 这块画布可以是最终显示在屏幕上的缓冲区(默认渲染目标),也可以是一张纹理(离屏渲染时用,比如做后处理、镜面反射)。片段着色器计算出的颜色,最终都会「画」到这块画布上。
SV_TARGET 语义:
作用是明确告诉显卡:作用是指定该颜色输出要写入到哪个渲染目标(画布)
4.在CG中调用属性变量
CG代码块中如何调用Properties代码块中开放出来的属性呢?
1)CG中声明属性变量
Shader通过Properties代码块声明开放出来的属性,如果想要在Shader程序中访问这些属性,则需要在CG代码块中再次进行声明
typename;
**type为变量的类型,**name为属性变量的名称
|------------------|--------------------------------------------------------------------|
| 开放属性的类型 | CG中属性变量的类型 |
| Float,Range | 浮点和范围类型的属性,根据精度可以使用float,half或fixed声明 |
| Color,Vector | 颜色和向量类的属性,可以使用float4,half4或fixed4声明,其中颜色使用低精度的fixed4声明可以减少性能消耗 |
| 2D | 2D纹理贴图属性,使用sampler2D 声明 |
| Cube | 立方体贴图属性,使用samplerCube 声明 |
| 3D | 3D纹理贴图属性,使用sampler3D声明 |
2)在Shader中使用颜色

它的意思是:创建一个显示纯色的、不发光(Unlit)的着色器。
顶点着色器
输入 :
vertex是模型的顶点位置(在模型本地空间)。输出 :
position是顶点在屏幕裁剪空间的位置,这是GPU光栅化所必需的。功能 :
UnityObjectToClipPos是一个Unity内置函数,它完成了模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间的矩阵变换。这个函数是这个顶点着色器做的唯一工作。片元着色器
输出 :
color是最终要写入屏幕的颜色,语义SV_TARGET表示输出到渲染目标。功能 :直接将材质的
_MainColor颜色值输出给每一个像素。
3)在Shader中使用贴图
虽然纹理贴图 在Properties代码块中被定义之后,还需要再CG代码块中再次声明。但是与其他属性不同的是,CG还需要额外声明一个变量用于存储贴图的其他信息。
在使用贴图的时候经常会用到平铺(Tiling)和偏移(Offset)属性,额外声明的变量就是为了存储这些信息。
声明一个纹理变量的Tiling和Offset的语法结构如下:
float4 {TextureName} _ST;
1)TextureName:纹理属性的名称
2)ST:Scale和Transform的首字母,表示UV的缩放和平移
在CG所声明的变量为float4类型 ,其中x和y分量分别为Tiling的X值和Y值 ,z和w分量分别为Offset的X值和Y值。
纹理坐标的计算公式为:
texcoord=uv·{TextureName}·xy+{TextureName}·zw
主要特别注意的是,在计算纹理坐标的时候,一定要先乘以平铺值再加上偏移值

在顶点函数中,添加了一个名称为uv的输入参数用于获取顶点的UV数据之后,又添加了一个名称为texcoord的输出参数用于保存在顶点函数中计算出的纹理坐标
按照公式,把顶点的UV乘以平铺值然后加上偏移值得到了纹理坐标texcoord,然后输出到片段着色器中。
在片段着色器中添加了一个名称为texcoord的输入参数用于接收从顶点着色器传递过来的纹理变量。然后调用tex2D()函数,使用纹理坐标texcoord对纹理_MainTex进行采样,最后将采样结果乘上_MainColor进行输出。
通常情况下,纹理资源都需要按照这种流程进行使用,除非能够确定某个纹理资源永远不会用到Tiling和Offset,则可以省略对该纹理资源ST变量的声明,同时不再计算其纹理坐标。
void vert(in float4 vertex:POSITION,in float2 uv:TEXCOORD0,
out float4 position:SV_POSITION,out float2 texcoord:TEXCOORD0)
{
position=UnityObjectToClipPos(vertex);
texcoord=uv;
}
4)在Shader中使用立方体贴图
立方体贴图它是由前后左右上下六个方向组成的立方体盒子,也可以在unity中使用全景图转换得到,通常被用来作为环境反射:
立方体贴图的采样所使用的函数为:
texCUBE(Cube,r);
函数中的Cube表示立方体贴图,r表示视线方向在物体表面上的反射方向。

Cube可以直接在CG中声明这个属性变量,然后直接获取,但r是如何得到的呢?
反射向量的计算公式:r=2·[(-v)·n]n+v
视线方向v;表面法线n
Unity 自定义表面反射 Shader,核心作用是为模型实现「基础纹理着色 + 立方体贴图(Cubemap)环境反射」效果。
这个 Shader 的核心目标是:
- 给模型贴基础纹理(_MainTex)并叠加主色调(_MainColor);
- 基于模型表面的法线,计算环境反射方向,从立方体贴图(_Cubemap)中采样反射颜色;
- 通过反射强度(_Reflection)控制「基础纹理色」和「环境反射色」的混合比例,最终输出模型的最终颜色。
添加Cubemap属性和反射强度
_Cubemap("Cubemap",Cube)=""{}
_Reflection("Reflection",Range(0,1))=0
声明Cubemap和反射属性变量
samplerCUBE _Cubemap;
fixed _Reflection;
在顶点函数的输入参数中,添加了模型的法线向量normal,输出参数中添加了世界空间顶点坐标worldPos和世界空间法线向量worldNormal
输入:模型空间原始数据 输出:传递给片元着色器的数据
void vert (in float4 vertex:POSITION,in float3 normal:NORMAL,in float4 uv:TEXCOORD0,
out float4 position:SV_POSITION,out float4 worldPos:TEXCOORD0,out float3 worldNormal:TEXCOORD1,out float2 texcoord:TEXCOORD2)
{
// 1. 顶点坐标:模型空间 → 裁剪空间(屏幕空间)
position =UnityObjectToClipPos(vertex);
/*
为什么这么写? - UnityObjectToClipPos是Unity内置宏,等价于 mul(UNITY_MATRIX_MVP, vertex) - 作用:把模型空间的顶点转换为裁剪空间坐标,这是GPU能渲染的最终顶点位置,必须计算! - 如果不做这一步,模型无法显示在屏幕上。
*/
// 2. 顶点坐标:模型空间 → 世界空间****worldPos=mul(unity_ObjectToWorld,vertex);/*
为什么这么写? - unity_ObjectToWorld是Unity内置矩阵(模型→世界空间) - 作用:把顶点从模型自身的局部空间,转换到整个场景的世界空间 - 片元着色器需要世界空间的顶点坐标来计算「摄像机到顶点的方向」。
*/
**// 3. 法线向量:模型空间 → 世界空间(关键!法线转换和顶点不同)**worldNormal=mul(normal,(float3x3)unity_WorldToObject);
worldNormal=normalize(worldNormal);/*
为什么这么写?
① 法线不能直接用unity_ObjectToWorld转换!
- 顶点是「位置向量」,用模型→世界矩阵(unity_ObjectToWorld);
- 法线是「方向向量」,需要用世界→模型矩阵的转置逆矩阵((float3x3)unity_WorldToObject等价于这个逆矩阵); - 原因:如果模型有非均匀缩放(比如拉伸),直接用顶点矩阵会让法线方向错误,必须用逆矩阵修正。
② normalize(worldNormal):
- 矩阵乘法和插值会让法线长度偏离1,归一化后才能保证法线是单位向量(计算点积/反射时必须)。
③ 作用:得到世界空间的法线,供片元着色器计算反射向量。
*/
**// 4. UV坐标:处理缩放和平移(适配_MainTex_ST属性)**texcoord=uv*_MainTex_ST.xy+_MainTex_ST.zw;
/*
为什么这么写?
- _MainTex_ST是Unity内置的纹理缩放/平移参数(ST=Scale/Translate);
- 作用:实现纹理的缩放(xy)和平移(zw),在Inspector里调整纹理的Tiling/Offset时,这里会生效;
*/
}
为什么要转换到世界空间?
片元着色器需要计算反射向量,而反射向量的计算依赖:
- 世界空间的顶点位置(worldPos):计算摄像机到顶点的方向;
- 世界空间的法线(worldNormal):计算反射方向;如果直接用模型空间的数据,反射效果会随模型的位置 / 旋转 / 缩放而错误,世界空间是 "全局统一的坐标系",能保证反射计算的正确性。
片元着色器(frag):像素颜色计算(核心)
void frag(in float4 position:SV_POSITION,in float4 worldPos :TEXCOORD0,in float3 worldNormal:TEXCOORD1,in float2 texcoord:TEXCOORD2,
out fixed4 color :SV_Target)
{// 1:计算基础纹理颜色(纹理采样 + 主色调叠加)
fixed4 main = tex2D(_MainTex, texcoord) * _MainColor;
/*
- tex2D(_MainTex, texcoord):根据UV坐标采样2D纹理的颜色; - 乘以_MainColor:让纹理色叠加自定义主色调,灵活调整整体颜色。
*/
// 2:计算世界空间的视角方向(摄像机→顶点)
float3 viewDir = worldPos.xyz - _WorldSpaceCameraPos;
viewDir = normalize(viewDir);
/*
- _WorldSpaceCameraPos:Unity内置变量,摄像机的世界坐标;
- viewDir = 顶点世界坐标 - 摄像机世界坐标:得到从摄像机指向顶点的方向;
- normalize:归一化为单位向量(后续点积计算需要)。
*/
// 3:计算反射方向(核心!基于法线的反射公式)
float3 refDir = 2 * dot(-viewDir, worldNormal) * worldNormal + viewDir;
refDir = normalize(refDir);
/*
作用:计算视角方向相对于表面法线的反射方向,这个方向用来采样立方体贴图。
简化写法:可以直接用Unity内置函数 reflect(-viewDir, worldNormal),效果完全一样。
*/
// 4:采样立方体贴图(获取反射颜色)
fixed4 reflection = texCUBE(_Cubemap, refDir);
/*
texCUBE:立方体贴图的采样函数,传入反射方向refDir,返回该方向的环境颜色;
作用:模拟模型表面反射的环境色(比如金属球反射天空盒)。
*/
// 5:混合基础色和反射色(最终颜色)
color = lerp(main, reflection, _Reflection);
/*
lerp(a, b, t):线性插值函数,t∈[0,1]; - t=_Reflection:0时只显示基础纹理色,1时只显示反射色,0~1之间混合两者;
作用:通过反射强度灵活控制反射效果的强弱。
*/
}
5.结构体
由于函数有多个输入和输出,为了使代码编写方便,并且看起来美观,引入了一个新的数据类型------结构体
结构体允许存储多个不同类型的变量,并将多个变量包装成为一个整体进行输入或输出。
语法如下:
struct Type
{
//变量 1;
//变量 2;
//变量 n;
}
结构体内包含的变量仍然需要定义数据类型和名称,然后填充对应的语义。
最后通过[结构体名称].[变量名称]的语法访问,例如:v.vertex表示访问名称为v的结构体内的vertex变量

顶点着色器的位置、作用,以及前后衔接的阶段?
新手记住 "输入是模型原始顶点**,输出是裁剪空间顶点,夹在'输入装配'和'光栅化'之间"** 即可

比如你渲染一个立方体(8 个顶点):
- 输入装配阶段:读取立方体的 8 个顶点坐标、6 组 UV、8 个法线,打包成顶点数据;
- 顶点着色器阶段:对 8 个顶点逐个处理,把模型空间坐标转成裁剪空间坐标,同时把 UV / 法线传递下去;
- 光栅化阶段:把立方体的 6 个面(由顶点组成)转换成屏幕上的像素(比如 1000 个片元),并对 UV / 颜色做插值;
- 片元着色器阶段:对 1000 个片元逐个计算颜色(比如采样纹理)。
新手关键总结(避坑)
- 顶点着色器的 "边界" :只处理 "顶点级数据" ,不碰像素;输入是模型原始顶点(
POSITION语义),输出是裁剪空间顶点(SV_POSITION语义);- 和片元着色器的核心区别 :
- 顶点着色器:逐顶点执行,计算量小(顶点数远少于像素数),负责 "定位顶点位置";
- 片元着色器:逐像素执行,计算量大,负责 "计算像素颜色";
- 为什么顶点着色器必须输出 SV_POSITION :光栅化阶段依赖这个坐标生成片元,没有的话整个渲染管线会中断,物体直接看不见。
总结
- 顶点着色器位于渲染管线的输入装配之后、光栅化之前,是可编程管线的第一个核心阶段;
- 核心任务是 "变换顶点坐标到裁剪空间"+"传递顶点数据",逐顶点执行;
- 它的输出是光栅化的输入,最终决定了片元的位置和插值数据,是连接模型数据和像素渲染的关键环节。
简单记:顶点着色器的工作是**"给每个顶点找好屏幕上的位置,并把附加数据打包好",交给光栅化转成像素,再由片元着色器上色。**


