Unity Shader 顶点色:利用模型顶点颜色传递渲染数据

从原理到实战------用 RGBA 四通道携带植被冷暖变化、地形混合权重等丰富信息,在 URP Shader 中高效解包使用。

什么是顶点色

在三维模型的每个顶点上,除了记录位置、法线、UV 坐标之外,还可以存储一组 RGBA 颜色值 ,这就是顶点色(Vertex Color) 。 它随模型本身传输到 GPU,无需额外贴图采样,零带宽开销地为 Shader 提供逐顶点的自定义数据。

顶点色并不局限于表示"颜色"。在游戏开发与实时渲染中,它更多作为一种数据通道使用: 植被摆动遮罩、地形混合权重、顶点 AO、冷暖光照偏移......凡是能够在建模阶段"烘焙"进去的数值,都可以塞进这四个通道。

💡

顶点色与贴图的本质区别:顶点色的精度取决于模型面数(低多边形下梯度较粗),贴图精度则由分辨率决定。两者互补:高频细节用贴图,低频空间分布用顶点色。

在 URP Shader 中读取顶点色

Unity URP 使用 HLSL,通过语义 COLORCOLOR0 将顶点色从 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 之间无声的数据通道。

相关推荐
不绝19120 小时前
导航系统/NavMeshAgent组件
unity
mxwin1 天前
Unity Shader 屏幕空间 UVScreen Space UV 完全指南
unity·游戏引擎·uv
LF男男1 天前
TouchManager
unity·c#
mxwin1 天前
Unity Shader 径向模糊与径向 UV 变形速度感 · 冲击波效果完全指南
unity·游戏引擎·shader·uv
weixin_423995001 天前
unity 微信开发小游戏,网络资源获取数据
unity·游戏引擎
Yasin Chen1 天前
Unity TMP_SDF 分析(五)片元着色器
unity·游戏引擎·着色器
mxwin1 天前
Unity Shader Texture Bombing用随机旋转与偏移的多次采样,打破大地形纹理的
unity·游戏引擎
代数狂人1 天前
《深入浅出Godot 4与C# 3D游戏开发》第二章:编辑器导航
3d·编辑器·游戏引擎·godot
zcc8580797621 天前
Unity MVVM UniTask + 轻量级 ReactiveProperty
unity
zcc8580797621 天前
Unity 自动生成UI绑定+MVVM 架构模板
unity