Unity UGUI 程序化网格 05:四个顶点颜色如何形成渐变
颜色归属
第 4 篇已经让四边形位置完全由 RectTransform 决定。本文不增加顶点,也不修改索引,只为每个顶点填写独立颜色。完成后,读者应能区分组件级颜色与顶点颜色,按固定顶点顺序配置四角,并用 Mesh 中的 colors32 验证数据。
本文输入合同
v0 左下 = RGBA(23,52,89,255)
v1 左上 = RGBA(36,123,160,255)
v2 右上 = RGBA(144,224,239,255)
v3 右下 = RGBA(255,183,3,255)
indices = 0,1,2, 2,3,0
这里沿用一条长期合同:顶点编号具有稳定含义。若某一篇临时把 v1 改成右下,当前颜色会落到错误角,下一篇 UV 也会跟着旋转。因此系列始终沿用左下、左上、右上、右下,而不是把它们描述成“随便四个点”。
从这一章开始,顶点不再只是一个位置。UIVertex 是一次渲染提交中的完整顶点记录,位置、颜色以及下一篇的 UV 都附着在同一编号上。索引引用的是这条完整记录,而不是只引用坐标。理解这一点后,渐变接缝、纹理翻转和圆形颜色分布才可以沿着同一套数据关系排查。
四角配色

场景中的 GradientQuadGraphic 尺寸为 420×240,Pivot 为中心。左侧从深蓝过渡到青色,右上为浅青,右下为黄色。画面本身只能证明“有颜色”,不能证明四个角写入了哪一组值,因此本文同时保留顶点颜色表与 EditMode 断言。
复现场景时,创建一个空 UI 对象并挂载组件,先保持默认四组颜色,再把 RectTransform 设为固定尺寸。不要同时指定额外纹理或自定义材质,因为它们会把本文唯一变量从“顶点颜色”扩展为多条渲染输入。若固定画面与颜色表不符,先检查四角编号与 Alpha,再检查材质;不要先修改三角形索引,因为现有拓扑已经由第 3、4 章验证。
Inspector 固定值
Bottom Left = #173459FF
Top Left = #247BA0FF
Top Right = #90E0EFFF
Bottom Right = #FFB703FF
RectTransform = 420 x 240
Pivot = (0.5, 0.5)
颜色状态由 GradientQuadGraphic 的四个序列化字段拥有,OnPopulateMesh 只读取当前快照并写入 UIVertex.color。GPU 如何对三角形内部做逐像素插值不属于组件状态;组件只负责保证两个三角形共享同一组 v0 与 v2,避免共享对角线出现两份互不一致的端点颜色。
颜色插值
一个三角形的三个顶点分别带有位置和颜色。片元位于三角形内部时,渲染管线会根据它相对三个顶点的位置混合颜色。入门阶段无需计算每个像素,但必须理解两个边界:顶点上的颜色值是我们明确写入的;内部颜色是这些值的插值结果,不是 C# 循环生成的像素数组。
triangle A = (v0, v1, v2)
triangle B = (v2, v3, v0)
shared edge = v0 <-> v2
shared endpoints:
v0 color is one stored value
v2 color is one stored value
因此两个三角形在共享边上使用相同端点颜色。
若复制出另一组 v0/v2 并写入不同颜色,共享边可能出现接缝。

Color32 的四个通道都是 0..255。前三个通道表示红、绿、蓝,第四个通道表示透明度。透明度为 0 时,即使 RGB 非零,默认透明混合下也可能完全不可见。这里四个顶点的 A 都是 255,先把“颜色是否写对”和“透明混合是否可见”分开验证。
本文选择 Color32,是因为固定证据直接以四个整数通道记录,读者可以逐项核对 JSON,而无需讨论浮点显示格式。这不是要求所有 UI 颜色都必须存为字节;如果业务需要更高精度或高动态范围,数据类型应由真实材质合同决定。系列当前只需要四个可复算的颜色端点,所以不额外建立渐变资源、颜色关键帧或插值曲线。
两个三角形内部各自做线性插值,四角渐变并不等于某个独立的二维双线性函数。当前对角线固定为 v0-v2,所以颜色分布也受这条三角形划分影响。对入门组件而言,这个结果稳定且可解释;若未来视觉要求指定中心颜色或完全控制二维渐变形状,触发条件是新增顶点或更换材质,而不是继续向四个角字段堆叠参数。
顶点色实现
完整实现继续读取 GetPixelAdjustedRect(),继续提交四个顶点和六个索引。新增的四个字段直接对应四个顶点,不增加颜色数组,也不设计通用渐变配置;对于只有四个角的系列组件,显式字段更容易在 Inspector 与代码之间核对。
// Assets/OnPopulateMeshBook/Chapters/Chapter05_VertexColor/
// Runtime/GradientQuadGraphic.cs · GradientQuadGraphic
using UnityEngine;
using UnityEngine.UI;
namespace OnPopulateMeshBook.Chapter05
{
public sealed class GradientQuadGraphic : Graphic
{
[SerializeField] private Color32 bottomLeft =
new Color32(23, 52, 89, 255);
[SerializeField] private Color32 topLeft =
new Color32(36, 123, 160, 255);
[SerializeField] private Color32 topRight =
new Color32(144, 224, 239, 255);
[SerializeField] private Color32 bottomRight =
new Color32(255, 183, 3, 255);
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
var rect = GetPixelAdjustedRect();
if (rect.width <= 0f || rect.height <= 0f) return;
AddVertex(vh,
new Vector2(rect.xMin, rect.yMin), bottomLeft);
AddVertex(vh,
new Vector2(rect.xMin, rect.yMax), topLeft);
AddVertex(vh,
new Vector2(rect.xMax, rect.yMax), topRight);
AddVertex(vh,
new Vector2(rect.xMax, rect.yMin), bottomRight);
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
private static void AddVertex(
VertexHelper vh, Vector2 position, Color32 color)
{
var vertex = UIVertex.simpleVert;
vertex.position = position;
vertex.color = color;
vh.AddVert(vertex);
}
}
}
逐步实现有三步。先复制第 4 篇的位置与索引规则;再给 AddVertex 增加 Color32 color 参数并写入 vertex.color;最后增加四个序列化字段,按顶点顺序传入。不能只增加字段而继续写 vertex.color = this.color,因为那样四次调用仍使用同一个组件颜色。
UIVertex.simpleVert 提供一份可用的初始顶点值,随后代码明确覆盖本文关心的位置与颜色。每次循环复用同一个局部变量没有问题,因为 AddVert 接收的是当前顶点值;但把同一个颜色变量传四次就会得到四份相同数据。排查时应读取 Mesh 的 colors32,不要根据字段名推断传参已经正确。
在 Inspector 中逐角修改颜色时,建议一次只改一个角为醒目的不透明色。看到对应角变化后再恢复固定值,并继续下一角。这一操作链验证“序列化字段 → OnPopulateMesh 参数 → 顶点编号 → 画面角落”。若四个字段一起随机修改,只能证明组件有反应,不能证明左下字段确实拥有左下顶点。
本文保持两个不变量:位置仍由 RectTransform 单独拥有;一个角只存在一个共享顶点。颜色改变需要重建顶点数据,但当前快照没有额外的运行时 setter,因此由 Inspector 序列化变化触发 UGUI 刷新即可。若后续增加代码接口,接口所有者应在值真正变化时调用 SetVerticesDirty,而不是每帧无条件重建。
这里刻意没有让 GradientQuadGraphic 同时维护组件统一色与四角色的乘法规则。两套颜色状态如果没有明确优先级,运行时控制器写统一色、编辑器配置四角色后,读者很难判断最终顶点值。本文选择四角字段作为唯一颜色事实源;第 6 篇为了隔离 UV,会使用继承的统一 Graphic.color。两个章节是独立快照,不应把其中一个的颜色所有权偷偷解释成另一个。
快照验证
测试直接读取生成 Mesh 的 colors32。正常样本先断言颜色数组长度为 4,再比较对角顶点,防止实现把四次调用退化成同一颜色。退化样本把高度设为零,确认组件在写颜色之前返回空网格。固定 JSON 进一步记录四个精确 RGBA,补足“不同”断言没有覆盖的具体值。
// Assets/OnPopulateMeshBook/Chapters/Chapter05_VertexColor/
// Tests/Editor/GradientQuadGraphicTests.cs
using NUnit.Framework;
using OnPopulateMeshBook.Chapter05;
using OnPopulateMeshBook.TestSupport;
using UnityEngine;
public sealed class GradientQuadGraphicTests
{
[Test]
public void Populate_AssignsFourDistinctVertexColors()
{
var graphic =
MeshTestSupport.CreateGraphic<GradientQuadGraphic>(
new Vector2(120f, 80f));
var mesh = MeshTestSupport.Build(graphic);
Assert.That(mesh.colors32, Has.Length.EqualTo(4));
Assert.That(mesh.colors32[0],
Is.Not.EqualTo(mesh.colors32[2]));
Assert.That(mesh.colors32[1],
Is.Not.EqualTo(mesh.colors32[3]));
MeshTestSupport.Destroy(graphic, mesh);
}
[Test]
public void Populate_ZeroHeightProducesEmptyMesh()
{
var graphic =
MeshTestSupport.CreateGraphic<GradientQuadGraphic>(
new Vector2(120f, 0f));
var mesh = MeshTestSupport.Build(graphic);
Assert.That(mesh.vertexCount, Is.Zero);
MeshTestSupport.Destroy(graphic, mesh);
}
}
{
"fixedInput": "four vertex colors",
"component": "GradientQuadGraphic",
"vertices": [
[-210.0, -120.0, 0.0],
[-210.0, 120.0, 0.0],
[210.0, 120.0, 0.0],
[210.0, -120.0, 0.0]
],
"colors": [
[23, 52, 89, 255],
[36, 123, 160, 255],
[144, 224, 239, 255],
[255, 183, 3, 255]
],
"indices": [0, 1, 2, 2, 3, 0]
}

运行命令与第 4 篇使用同一批处理入口。系列测试共同执行的好处是,本文增加颜色后,第 1–4 章的旧快照也必须继续通过。
"$UNITY_EDITOR" \
-batchmode -nographics -quit \
-projectPath "$PROJECT_ROOT/.tmp/unity-onpopulate-mesh-lab" \
-runTests -testPlatform EditMode \
-testResults "$ARTIFACTS/editmode-results.xml" \
-logFile "$ARTIFACTS/editmode.log"
test-run result=Passed
total=19 passed=19 failed=0 skipped=0
duration=0.0400379s
GradientQuadGraphicTests.Populate_AssignsFourDistinctVertexColors
result=Passed
duration=0.002048s
GradientQuadGraphicTests.Populate_ZeroHeightProducesEmptyMesh
result=Passed
duration=0.000140s
process_exit_code=0
测试结果不等于像素级色彩校准。本文没有采集显示器颜色,也没有声称某个中心像素必须等于特定 RGB;现有证据只证明 Mesh 持有四个预期顶点、四组固定颜色和六个合法索引。颜色空间、材质和后处理改变时,需要另建像素读取实验,不能从当前顶点表外推。
这也是“代码 → 数据 → 画面”的证据顺序。代码说明每个字段应进入哪个顶点,JSON 说明固定场景实际生成了哪些通道值,测试说明以后若四角退化成同色会失败,结果图供读者核对整体方向。任何一层单独存在都不够:只看代码可能遗漏序列化场景值,只看图无法复算通道,只看测试又不知道实际默认配色。
对角线问题
把 AddVertex 中的参数忽略,四个角全部写入 bottomLeft,网格仍然是合法矩形,却会退化成纯色。位置测试与索引测试都不会发现它,颜色测试会发现两组对角值相同。
// 失败边界:参数 color 被错误地忽略
private void AddVertex(
VertexHelper vh, Vector2 position, Color32 color)
{
var vertex = UIVertex.simpleVert;
vertex.position = position;
vertex.color = bottomLeft;
vh.AddVert(vertex);
}
failure input:
four configured corner colors
wrong colors:
v0=(23,52,89,255)
v1=(23,52,89,255)
v2=(23,52,89,255)
v3=(23,52,89,255)
failed condition:
colors32[0] != colors32[2]
colors32[1] != colors32[3]
recovery:
write the method parameter into UIVertex.color
另一个常见错误是把四个 Alpha 都设为 0,然后误判 OnPopulateMesh 没有执行。排查顺序应先读取 Mesh:如果顶点数、索引数与 colors32 都存在,几何链已经完成,问题位于颜色或材质;若顶点数为零,才回到生成入口。让同一层同时负责几何、颜色和可见性诊断,只会掩盖真正失败边界。
零高度与透明度为零必须区分。零高度在几何生成前被归一化为空网格,Mesh 不含顶点;透明度为零仍生成四个顶点、六个索引,只是颜色通道使结果不可见。两个输入都可能表现为“画面上没有矩形”,但 Mesh 数据完全不同,因此恢复路径也不同:前者检查布局尺寸,后者检查颜色 Alpha。
共享边错误则更隐蔽:若为了“每个三角形写起来简单”而创建六个顶点,再给复制出的 v0 或 v2 配置不同颜色,两个三角形仍各自合法,却在对角线上失去连续性。恢复方式不是增加抗锯齿,而是恢复共享顶点或确保复制端点属性完全一致。当前需求没有独立端点属性,因此四顶点方案是更小、也更容易验证的选择。
纹理之前
共享 v0 与 v2 让两个三角形沿对角线使用同一份端点颜色。只有确实需要同位置保存不同属性时才复制顶点;当前四角渐变没有这项需求。
本文的容量边界就是四个角和一条固定对角线。它适合面板底色、简单能量区和运行时调试可视化,不承担多关键帧色带、径向渐变或颜色动画系统。只有产品明确要求额外颜色控制点,并且四角插值已经无法表达固定画面时,才进入更多顶点或材质方案;在此之前增加通用渐变框架只会让四个可核对字段变成一组难以追踪的配置。
下一篇在同一顶点记录中增加 uv0,说明纹理从哪里取样;RectTransform 边界、四角编号和固定索引继续保持不变。