从原理到实战------用 RGBA 四通道携带植被冷暖变化、地形混合权重等丰富信息,在 URP Shader 中高效解包使用。
什么是顶点色
在三维模型的每个顶点上,除了记录位置、法线、UV 坐标之外,还可以存储一组 RGBA 颜色值 ,这就是顶点色(Vertex Color) 。 它随模型本身传输到 GPU,无需额外贴图采样,零带宽开销地为 Shader 提供逐顶点的自定义数据。
顶点色并不局限于表示"颜色"。在游戏开发与实时渲染中,它更多作为一种数据通道使用: 植被摆动遮罩、地形混合权重、顶点 AO、冷暖光照偏移......凡是能够在建模阶段"烘焙"进去的数值,都可以塞进这四个通道。

💡
顶点色与贴图的本质区别:顶点色的精度取决于模型面数(低多边形下梯度较粗),贴图精度则由分辨率决定。两者互补:高频细节用贴图,低频空间分布用顶点色。
在 URP Shader 中读取顶点色
Unity URP 使用 HLSL,通过语义 COLOR 或 COLOR0 将顶点色从 CPU 侧传送到顶点着色器, 再经插值后送入片元着色器。
① 在 Attributes 结构体中声明输入
cs
// 顶点着色器输入结构体
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
float4 color : COLOR; // ← 顶点色
};
② 在 Varyings 结构体中传递到片元
cs
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 vertexColor : COLOR; // ← 插值后的顶点色
};
③ 顶点着色器中赋值
cs
Varyings vert(Attributes input)
{
Varyings output;
output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.vertexColor = input.color; // 直接传递,GPU 自动线性插值
return output;
}
ℹ️
GPU 在光栅化阶段对顶点色进行双线性插值,因此即使相邻两顶点颜色差异很大,面内像素也会得到平滑过渡------这正是它适合表达空间渐变的原因。
案例一 · 植被冷暖色变化
户外植被(草地、树冠)的观感深受光照方向 影响: 迎光面偏暖(偏黄绿 / 金),背光面偏冷(偏蓝绿)。 如果直接在贴图里画死颜色,换了光照方向就会失真; 而用顶点色的 R 通道存储一个 0→1 的冷暖权重, 在 Shader 中动态偏移颜色,可以做到随时调节且几乎无性能开销。

Shader 实现
在片元着色器中,用 vertexColor.r 在冷、暖两种色调之间做 lerp:
cs
// ── 材质属性(在 Properties 中暴露)──
float4 _WarmColor; // 迎光暖色,例如 (1.0, 0.85, 0.4, 1)
float4 _CoolColor; // 背光冷色,例如 (0.3, 0.55, 0.9, 1)
float _WarmStrength; // 冷暖混合强度 0--1
half4 frag(Varyings input) : SV_Target
{
// 1. 读取基础反照率
half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
// 2. 从顶点色 R 通道读取冷暖权重
float warmWeight = input.vertexColor.r;
// 3. 计算冷暖色调偏移量
half3 tint = lerp(_CoolColor.rgb, _WarmColor.rgb, warmWeight);
tint = lerp(1.0, tint, _WarmStrength); // 用强度系数混入白色(中性)
// 4. 叠加到基础色
baseColor.rgb *= tint;
return baseColor;
}
⚠️
注意顶点色在 Unity 中默认为 Linear 颜色空间 。如果项目使用 Gamma 空间,需要在 Shader 中手动调用 GammaToLinearSpace() 转换,否则颜色偏差会很明显。
扩展:加入风向动态偏移
可以进一步将风方向向量与顶点色冷暖权重结合: 风来向侧偏暖(受光多),背风侧偏冷,并在顶点着色器中叠加摆动偏移, 让植被同时携带动态和颜色两种效果。
cs
float3 _WindDirection; // 归一化世界空间风向
float _WindStrength;
Varyings vert(Attributes input)
{
Varyings o;
float3 worldPos = TransformObjectToWorld(input.positionOS.xyz);
// A 通道存摆动强度遮罩(根部=0,叶尖=1)
float swayMask = input.color.a;
float wave = sin(_Time.y * 2.0 + worldPos.x * 0.5);
worldPos += _WindDirection * wave * _WindStrength * swayMask;
o.positionHCS = TransformWorldToHClip(worldPos);
o.vertexColor = input.color;
o.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return o;
}
案例二 · 地形纹理混合权重
地形着色常见做法是用多张贴图(草地、泥土、岩石、雪地)按权重 叠加。 传统方案把权重存进贴图,但对于精心雕刻的地形网格来说, 直接把权重烘焙进顶点色更省带宽,且与模型结构对齐,不会因 UV 拉伸产生权重失真。
约定通道分配如下:
| 通道 | 语义 | 示例值 |
|---|---|---|
R |
草地(Grass)权重 | 平地中心 = 1.0,岩石面 = 0.0 |
G |
泥土(Dirt)权重 | 道路 / 裸地 = 1.0 |
B |
岩石(Rock)权重 | 峭壁法线朝上角度小 = 1.0 |
A |
雪地(Snow)权重 / 额外遮罩 | 高海拔顶点 = 1.0 |

Shader 实现:三层混合
cs
Shader 实现:三层混合
HLSL
TerrainLitPass.hlsl --- 片元着色器(三层混合)
// 三张地形贴图
TEXTURE2D(_GrassMap); SAMPLER(sampler_GrassMap);
TEXTURE2D(_DirtMap); SAMPLER(sampler_DirtMap);
TEXTURE2D(_RockMap); SAMPLER(sampler_RockMap);
half4 frag(Varyings i) : SV_Target
{
// 从顶点色提取混合权重(RG通道)
float wGrass = i.vertexColor.r;
float wDirt = i.vertexColor.g;
float wRock = i.vertexColor.b;
// 归一化权重,确保总和 = 1
float wTotal = wGrass + wDirt + wRock;
wGrass /= wTotal; wDirt /= wTotal; wRock /= wTotal;
// 采样三张贴图
half4 grass = SAMPLE_TEXTURE2D(_GrassMap, sampler_GrassMap, i.uv);
half4 dirt = SAMPLE_TEXTURE2D(_DirtMap, sampler_DirtMap, i.uv);
half4 rock = SAMPLE_TEXTURE2D(_RockMap, sampler_RockMap, i.uv);
// 加权混合
half4 finalColor = grass * wGrass
+ dirt * wDirt
+ rock * wRock;
return finalColor;
}
进阶:基于高度的混合(Height-based Blend)
简单的线性混合在贴图交界处会显得模糊。 可以将各层贴图的高度图(存在各自贴图的 alpha 通道)与顶点色权重相乘, 得到基于高度的锐利过渡:
cs
// 高度混合辅助函数
float4 HeightBlend(float4 a, float ha,
float4 b, float hb,
float t, float sharpness)
{
float ha2 = ha + (1.0 - t);
float hb2 = hb + t;
float m = max(ha2, hb2) - sharpness;
float wa = max(ha2 - m, 0);
float wb = max(hb2 - m, 0);
return (a * wa + b * wb) / (wa + wb);
}
// 调用示例(草/泥两层,ha/hb 来自贴图 alpha)
half4 blended = HeightBlend(grass, grass.a, dirt, dirt.a, wDirt, 0.2);
如何在 DCC 软件中烘焙顶点色
顶点色需要在建模阶段设置,下面简述各主流工具的做法:

💡
Blender 快速上手: 进入 Vertex Paint 模式 → 选择笔刷通道(R / G / B / A)→ 绘制 → 导出 FBX 时勾选 Mesh → Vertex Colors。 Unity 导入时无需额外设置,Shader 中直接用 COLOR 语义即可读取。
运行时动态修改顶点色
某些场景需要在运行时 修改顶点色,比如玩家踩踏后草地褪绿变黄、 受伤植物叶片颜色渐变等。Unity 提供了 Mesh.colors / Mesh.colors32 API:
cs
using UnityEngine;
public class VertexColorPainter : MonoBehaviour
{
private Mesh _mesh;
private Color[] _colors;
void Start()
{
// 获取可写网格副本
_mesh = GetComponent<MeshFilter>().mesh; // 注意:.mesh 返回实例副本
_colors = _mesh.colors;
if (_colors.Length == 0)
_colors = new Color[_mesh.vertexCount];
}
/// 将指定顶点的冷暖权重(R 通道)设为目标值
public void SetWarmWeight(int vertexIndex, float warmValue)
{
_colors[vertexIndex].r = Mathf.Clamp01(warmValue);
_mesh.colors = _colors; // 上传到 GPU(每帧调用有性能开销)
}
}
⚠️
每次赋值 _mesh.colors 都会将整个颜色数组重新上传到 GPU, 高面数模型慎用逐帧修改。如需大规模动态更新,改用 Compute Shader + ComputeBuffer 或 将数据改存 贴图采样,避免 CPU↔GPU 数据拷贝瓶颈。
完整 URP Shader 模板
以下是包含顶点色读取的最小可用 URP Lit Shader,可直接在项目中使用:
cs
⚠️
每次赋值 _mesh.colors 都会将整个颜色数组重新上传到 GPU, 高面数模型慎用逐帧修改。如需大规模动态更新,改用 Compute Shader + ComputeBuffer 或 将数据改存 贴图采样,避免 CPU↔GPU 数据拷贝瓶颈。
完整 URP Shader 模板
以下是包含顶点色读取的最小可用 URP Lit Shader,可直接在项目中使用:
ShaderLab / HLSL
VertexColorLit.shader
Shader "Custom/URP/VertexColorLit"
{
Properties
{
_BaseMap ("Albedo", 2D) = "white" {}
_WarmColor ("Warm Tint", Color) = (1,0.85,0.4,1)
_CoolColor ("Cool Tint", Color) = (0.3,0.55,0.9,1)
_WarmStrength("Tint Strength", Range(0,1)) = 0.4
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Opaque" }
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _WarmColor, _CoolColor;
half _WarmStrength;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
struct Varyings {
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 vertexColor : COLOR;
};
Varyings vert(Attributes i) {
Varyings o;
o.positionHCS = TransformObjectToHClip(i.positionOS.xyz);
o.uv = TRANSFORM_TEX(i.uv, _BaseMap);
o.vertexColor = i.color;
return o;
}
half4 frag(Varyings i) : SV_Target {
half4 base = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
half3 tint = lerp(_CoolColor.rgb, _WarmColor.rgb, i.vertexColor.r);
tint = lerp((half3)1, tint, _WarmStrength);
base.rgb *= tint;
return base;
}
ENDHLSL
}
}
}
性能与最佳实践
| 建议 | 说明 |
|---|---|
| 尽量在 DCC 阶段烘焙 | 避免运行时写 Mesh.colors 带来的 CPU→GPU 传输开销 |
| 精度够用即可 | 顶点色精度为 uint8(0--255),浮点误差约 0.004,对于连续渐变已足够 |
| 通道复用 | 一个顶点最多 4 个通道,合理规划避免浪费,不足时可增加 UV 通道补充 |
| 配合 LOD | 低 LOD 模型顶点少,梯度更粗;高 LOD 才有精细的颜色分布,注意 LOD 切换时的视觉连续性 |
| 移动端注意精度 | 移动端建议用 half4 而非 float4 接收顶点色,减少 ALU 压力 |
| 调试可视化 | 用 return i.vertexColor; 直接输出顶点色,方便在 Scene View 中检查烘焙结果 |
小结
顶点色是一种成本极低、用途极广的数据传递机制。 它的核心价值在于:将"空间信息"从建模阶段预烘焙,转化为 Shader 运行时可以直接读取、 不需要任何额外贴图采样的逐顶点数据。
本文覆盖了两个典型场景:
- 植被冷暖变化------用 R 通道驱动迎光/背光的色调偏移,A 通道控制摆动强度,单 Shader 实现随光照动态的植被外观。
- 地形混合权重------用 RGB 三通道分别存储草地/泥土/岩石的混合权重,配合高度混合算法实现锐利自然的材质过渡。
掌握顶点色之后,可以将同样的思路延伸到更多场景: 雨水侵湿效果的浸湿范围遮罩、布料动力学的刚度分布、 角色皮肤散射的次表面散射深度...... 每一个顶点都是你与 GPU 之间无声的数据通道。