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 之间无声的数据通道。

相关推荐
星夜泊客4 小时前
Unity 排行榜 UI 优化:从全量生成到滚动复用
ui·unity·性能优化·游戏引擎
CDN3604 小时前
游戏盾导致 Unity/UE 引擎崩溃?内存占用、SO 库冲突深度排查
游戏·unity·游戏引擎
心前阳光4 小时前
Unity之Luban使用流程
unity·游戏引擎
mxwin5 小时前
Unity URP 下的 GPU Instancing减少 DrawCall 的关键技术
unity·游戏引擎·shader
小贺儿开发5 小时前
Unity3D LED点阵屏幕模拟
http·unity·浏览器·网络通信·led·互动·点阵屏
RReality6 小时前
【Unity Shader】 溶解效果实战教程
unity·游戏引擎
mxwin6 小时前
Unity URP SRP Batcher 完全指南 URP/HDRP 下的核心批处理机制,大幅降低 CPU 开销
unity·游戏引擎·shader·单一职责原则
小清兔6 小时前
unity中的音频相关_笔记
笔记·unity·音视频
RPGMZ6 小时前
RPGMZ游戏引擎 宠物战斗游戏基础功能实现
javascript·游戏·游戏引擎·宠物·rpgmz·rpgmakermz·宠物战斗系统