Cocos Creator自定义Shader实现UI颜色渐变:从UV坐标到Mix函数详解

📅 2026/8/8 8:27:37
Cocos Creator自定义Shader实现UI颜色渐变:从UV坐标到Mix函数详解
1. 项目概述为什么我们需要自定义颜色渐变Shader在Cocos Creator里做UI或者2D游戏我们经常会遇到一个需求给一个按钮、一个血条、或者一个背景图加上颜色渐变效果。比如一个按钮从上到下从蓝色渐变成浅蓝色或者一个能量条从左到右从红色渐变成黄色。引擎内置的Sprite组件虽然功能强大但默认只支持单色填充或纹理贴图要实现这种平滑的颜色过渡就得请出我们的“大杀器”——自定义Shader。很多刚接触Shader的朋友可能会觉得这东西高深莫测满屏的数学公式和图形学术语让人望而却步。但我想告诉你的是颜色渐变恰恰是理解Shader工作原理一个绝佳的入门案例。它不涉及复杂的光照模型、法线计算核心逻辑就是对颜色值进行简单的数学插值。通过这个项目我们不仅能实现一个实用的视觉效果更能亲手揭开Shader那层神秘的面纱理解数据是如何从CPU传递到GPU并最终变成屏幕上每一个像素颜色的。这篇文章就是为你——一位可能对Shader感到陌生甚至畏惧的Cocos开发者——准备的。我会从最基础的图形渲染管线讲起手把手带你创建一个完整的、支持水平和垂直方向渐变的Sprite Shader并解释其中每一行代码的意义。你会发现Shader并没有想象中那么难。2. 核心原理拆解从UV坐标到Mix函数在动手写代码之前我们必须先搞清楚两个核心概念UV坐标和颜色插值Mix。这是理解整个渐变效果的基石。2.1 UV坐标纹理的“身份证”每一个Sprite在渲染时其矩形区域的四个顶点都会对应纹理图片上的四个点。为了精确定位纹理上的颜色我们使用一套标准化坐标系即UV坐标系统。U轴水平方向范围从0.0到1.0。0代表纹理最左侧1代表最右侧。V轴垂直方向范围同样从0.0到1.0。0代表纹理最底部注意在OpenGL/WebGL中纹理坐标原点通常在左下角而Cocos Creator的2D系统通常将原点设在左上角以适应UI但Shader内处理时会进行适配我们通常无需关心底层差异以uv0为准即可。对于一个覆盖整个Sprite的纹理其四个顶点的UV坐标通常是(0,0),(1,0),(0,1),(1,1)。在片段着色器Fragment Shader中对于矩形内的每一个像素GPU都会根据其位置自动计算出对应的uv0值。这个uv0值就是我们实现渐变的关键。如果我们想让颜色从左到右渐变就可以用uv0.x的值从0到1去控制颜色的混合比例。2.2 颜色插值Mix函数的魔法知道了如何获取一个0到1的系数uv0.x或uv0.y后下一步就是如何将两种颜色平滑地混合起来。GLSL提供了一个非常方便的内置函数mix()。mix(a, b, t)函数接受三个参数a: 起始值可以是float, vec2, vec3, vec4等。b: 结束值类型需与a相同。t: 混合因子范围通常在[0.0, 1.0]。它的返回值是a * (1.0 - t) b * t。当t0.0时结果等于a当t1.0时结果等于b当t0.5时结果等于a和b的中间值。对于我们的颜色渐变a就是起始颜色startColor(一个vec4包含RGBA)。b就是结束颜色endColor。t就是来自UV坐标的系数例如uv0.x。所以核心代码就是一句vec4 finalColor mix(startColor, endColor, uv0.x);。这个finalColor就是当前像素根据其水平位置计算出的渐变颜色。注意这里我们直接将uv0.x一个float传给mix而mix期望t的类型与a、b相同vec4。GLSL在这种情况下会自动进行广播broadcast将uv0.x转换为vec4(uv0.x)相当于对RGBA四个通道使用同一个混合因子这正是我们需要的。2.3 与纹理颜色的结合我们的Sprite往往不是纯色块而是带有图片纹理的。因此最终的像素颜色应该是渐变颜色与纹理采样颜色的乘积或者其他混合方式乘法是最常用的叠加方式。这模拟了有色光照射在物体上的效果。 所以完整的颜色计算流程是从纹理上采样得到纹理颜色textureColor。根据UV计算渐变混合颜色gradientColor mix(startColor, endColor, factor)。将两者相乘finalColor textureColor * gradientColor。再乘以顶点颜色通常用于整体透明度或色调调整finalColor * v_color。理解了这些我们就可以开始动手构建我们的Shader了。3. 完整实现一步步创建Cocos Creator渐变Shader我们将创建一个完整的.effect资源文件。在Cocos Creator中Shader程序是以.effect文件的形式进行管理和使用的。一个.effect文件包含了渲染技术Technique、通道Pass、着色器代码CCProgram以及属性定义。3.1 创建Effect和Material资源首先在Cocos Creator编辑器的资源管理器中右键点击你想存放的文件夹选择创建 - Effect并命名为gradient.effect。这会创建一个空的Effect文件。接着同样右键选择创建 - Material命名为gradient.mtl。Material材质是Shader参数的具体实例它将Effect和具体的纹理、颜色参数绑定在一起。3.2 编写渐变Effect文件双击打开gradient.effect我们将用以下代码完全替换其内容。我会逐段进行详细解释。// Copyright (c) 2024 Your Name. 渐变Shader示例。 CCEffect %{ techniques: - passes: - vert: sprite-vs:vert # 使用内置的精灵顶点着色器 frag: sprite-fs:frag # 使用我们自定义的片段着色器 depthStencilState: depthTest: false # 2D UI通常不需要深度测试 depthWrite: false # 也不写入深度缓冲 blendState: targets: - blend: true # 启用混合用于透明效果 blendSrc: src_alpha blendDst: one_minus_src_alpha blendDstAlpha: one_minus_src_alpha rasterizerState: cullMode: none # 不剔除背面因为2D精灵是单面显示 properties: # 定义暴露给Material编辑器的属性 alphaThreshold: { value: 0.5 } startColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color } } endColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color } } }% CCProgram sprite-vs %{ // 顶点着色器直接复用Cocos内置的2D精灵着色器保证坐标变换正确 precision highp float; #include builtin/uniforms/cc-global #if USE_LOCAL #include builtin/uniforms/cc-local #endif in vec3 a_position; in vec2 a_texCoord; in vec4 a_color; out vec4 color; out vec2 uv0; vec4 vert () { vec4 pos vec4(a_position, 1); #if USE_LOCAL pos cc_matWorld * pos; #endif pos cc_matViewProj * pos; uv0 a_texCoord; color a_color; return pos; } }% CCProgram sprite-fs %{ precision highp float; #include builtin/internal/alpha-test // 引入AlphaTest宏 // 输入从顶点着色器传递来的变量 in vec4 color; in vec2 uv0; // 定义Uniform常量缓冲区用于接收从Material传入的属性 uniform Constant { vec4 startColor; vec4 endColor; }; // 如果使用了纹理声明纹理采样器 #if USE_TEXTURE #pragma builtin(local) layout(set 2, binding 11) uniform sampler2D cc_spriteTexture; #endif // 预定义宏用于控制渐变方向。我们将在Material中启用它们。 #define USE_HORIZONTAL 1 // 1启用水平渐变0禁用 #define USE_VERTICAL 0 // 1启用垂直渐变0禁用 // 注意实际项目中更优雅的做法是通过Material的defines属性动态定义这些宏。 vec4 frag () { vec4 o vec4(1, 1, 1, 1); // 初始化输出颜色为白色 // 步骤1采样纹理颜色如果使用了纹理 #if USE_TEXTURE // CCSampleWithAlphaSeparated是Cocos内置函数能正确处理预乘Alpha的纹理 o * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); #endif // 步骤2应用颜色渐变 vec4 gradientFactor vec4(1.0); // 默认渐变因子为1即不影响原色 #if USE_HORIZONTAL // 水平渐变使用uv0.x作为混合因子 gradientFactor mix(startColor, endColor, vec4(uv0.x)); #elif USE_VERTICAL // 垂直渐变使用uv0.y作为混合因子。注意UV原点。 gradientFactor mix(startColor, endColor, vec4(uv0.y)); #endif // 将渐变因子与当前颜色相乘 o * gradientFactor; // 步骤3乘以顶点颜色可用于整体透明度、染色等 o * color; // 步骤4Alpha测试如果启用 ALPHA_TEST(o); return o; } }%代码关键点解析CCEffect块定义了整个渲染通道的配置。properties里声明了startColor和endColor并指定了editor: { type: color }这会让它们在Material属性面板上显示为颜色选择器非常方便。CCProgram sprite-vs顶点着色器。我们几乎完全复用了内置的sprite-vs它负责将模型空间的顶点坐标转换到裁剪空间并将UV和顶点颜色传递给片段着色器。除非你有特殊的顶点动画需求否则不要轻易修改这部分。Uniform Constant这是一个Uniform缓冲区对象UBO。在properties中定义的startColor和endColorCocos Creator会自动将它们与这个UBO中的同名变量关联起来。这是数据从CPUMaterial传递到GPUShader的桥梁。USE_TEXTURE宏这是一个内置宏。当Material使用了纹理时Cocos Creator会在编译Shader前定义这个宏。我们的代码用#if USE_TEXTURE包裹了纹理采样逻辑使得同一个Shader既能用于纯色渐变也能用于带纹理的渐变。渐变方向控制我使用了USE_HORIZONTAL和USE_VERTICAL两个宏来控制方向。在示例代码中我写死了USE_HORIZONTAL 1。更佳实践是在Material的defines属性中动态定义这样同一个Shader资产可以复用于不同方向的物体。具体做法后面会讲。CCSampleWithAlphaSeparated这是Cocos提供的一个安全纹理采样函数尤其能正确处理Android平台上可能遇到的预乘Alpha问题建议始终使用它来代替普通的texture2D。3.3 配置Material并应用到Sprite关联Effect选中刚才创建的gradient.mtl材质球在属性检查器中找到Effect属性点击下拉框选择我们刚编写的gradient.effect。设置纹理和颜色将你的Sprite图片拖拽到cc_spriteTexture属性栏。点击startColor和endColor旁边的色块分别设置你想要的起始和结束颜色。例如startColor设为蓝色(0, 0.5, 1, 1)endColor设为白色(1,1,1,1)。动态定义宏进阶在Material属性面板的Defines区域你可以添加宏定义。点击号名称输入USE_HORIZONTAL值输入1再添加一个USE_VERTICAL值输入0。这样就明确指定了使用水平渐变。你可以创建两个Material一个定义水平一个定义垂直来复用同一个Effect。应用到Sprite在场景中创建一个Sprite节点将gradient.mtl材质球拖拽到Sprite组件的CustomMaterial属性上。如果CustomMaterial属性没有出现请确保Sprite组件的Graphics类型是Sprite。完成以上步骤后你应该立刻能在场景编辑器和预览中看到Sprite拥有了从左到右的颜色渐变效果。4. 进阶技巧与深度优化实现基础功能只是第一步。要让这个Shader更健壮、更灵活我们需要考虑更多边界情况和优化点。4.1 支持多方向与角度渐变我们之前的Shader只支持严格水平或垂直渐变。但需求可能是斜向的或者从中心向四周辐射。这需要更复杂的混合因子计算。实现斜角渐变混合因子可以基于点积运算。例如我们定义一个归一化的方向向量dir如vec2(1,1)表示右上角方向。混合因子t dot(uv, dir)。但这样t的范围可能不在[0,1]需要clamp或smoothstep函数进行平滑钳制。// 在frag函数中替换原有的渐变计算部分 vec2 dir normalize(vec2(1.0, 1.0)); // 45度角方向 float factor dot(uv0, dir); // 初步计算 factor smoothstep(0.0, length(dir), factor); // 将因子映射到0~1范围边缘更平滑 vec4 gradientFactor mix(startColor, endColor, vec4(factor)); o * gradientFactor;实现径向渐变圆形混合因子可以基于到中心点的距离。vec2 center vec2(0.5, 0.5); // UV中心 float distance length(uv0 - center); // 到中心的距离 float factor 1.0 - distance * 2.0; // 从中心(1)到边缘(0) factor clamp(factor, 0.0, 1.0); // 确保在0~1之间 vec4 gradientFactor mix(startColor, endColor, vec4(factor)); o * gradientFactor;实操心得对于复杂的渐变建议将factor的计算逻辑单独写成一个函数并通过预定义宏如USE_RADIAL来切换。同时将center、dir等参数也作为Uniform暴露给Material这样美术同学可以在编辑器里动态调整无需修改Shader代码。4.2 性能优化与最佳实践避免分支if语句GPU不喜欢条件分支。我们之前用#if是基于宏的在编译时就已经确定代码路径没有运行时开销。但如果在frag函数内部使用动态的if语句来判断方向可能会降低性能。最佳实践是通过不同的Technique或Pass来实现不同效果或者使用step、mix等函数进行数学上的“选择”而非逻辑分支。慎用Discard不要在片段着色器中轻易使用discard操作除非实现类似镂空的效果这会严重影响GPU的早期深度测试和优化。我们的Alpha测试ALPHA_TEST宏在Alpha低于阈值时内部可能调用discard因此要谨慎设置alphaThreshold值。合批Batching中断这是2D渲染中非常重要的性能指标。当两个Sprite使用不同的Material即使引用同一个Effect但参数不同时它们很可能无法被合批导致Draw Call增加。为了最大化合批尽量复用Material对于只是颜色参数不同的相同渐变效果应该使用同一个Material实例通过脚本动态修改其startColor和endColor属性。使用Uniform数组对于大量需要不同渐变的Sprite可以考虑一个更高级的方案将颜色对存储在Uniform数组中并通过每个顶点的额外属性如a_color2来索引。但这会显著增加Shader复杂度和数据准备开销需权衡利弊。WebGL 1.0兼容性我们的Shader使用了layout语法来设置纹理绑定这在WebGL 2.0和大部分现代GLES上是标准的。为了兼容WebGL 1.0例如一些较老的移动浏览器可能需要回退到使用uniform sampler2D cc_spriteTexture;的声明方式并依赖Cocos Creator的自动绑定。幸运的是Cocos Creator的#pragma builtin(local)和内置头文件通常会帮我们处理好这些兼容性问题但如果你需要支持非常古老的平台务必进行测试。4.3 常见问题与调试技巧即使代码看起来正确实际运行时也可能遇到各种“坑”。这里记录几个我踩过的以及常见的问题。问题1渐变方向反了现象颜色从左到右是从endColor变到startColor与预期相反。原因UV坐标系的原点可能因纹理导入设置、Sprite的trim模式等而不同。默认情况下uv0.x从左到右是0到1。解决检查你的startColor和endColor设置是否正确。如果确认设置无误但方向反了最简单的办法是在Shader中反转混合因子float factor 1.0 - uv0.x;。问题2渐变边缘有锯齿Aliasing现象在渐变颜色对比强烈时交界处能看到明显的阶梯状锯齿。原因这是数字图像的固有现象在颜色突变处尤其明显。解决使用smoothstep代替线性插值smoothstep(edge0, edge1, x)会在edge0和edge1之间进行平滑的Hermite插值能有效减少锯齿。例如float factor smoothstep(0.0, 1.0, uv0.x);。开启抗锯齿MSAA在项目设置中开启多采样抗锯齿这是最根本的解决方案但会消耗更多性能。增加渐变区域如果设计允许让颜色过渡的区域更宽一些。问题3Shader编译失败报错“未定义的变量”或“语法错误”排查步骤检查GLSL版本Cocos Creator的Effect系统封装了细节但如果你手写GLSL确保语法符合版本要求。我们使用的precision highp float;是兼容性较好的声明。检查宏定义作用域确保#if USE_TEXTURE和对应的#endif正确配对。一个常见的错误是在#if块内定义变量然后在块外使用它。检查Uniform块uniform Constant { ... };后面的分号不能省略。块内的变量名必须与CCEffect中properties里定义的完全一致包括大小写。使用Cocos Creator内置函数和变量如cc_matViewProj,CCSampleWithAlphaSeparated等确保你包含了正确的头文件如#include builtin/uniforms/cc-global。问题4在真机上特别是iOS效果不对或崩溃可能原因精度问题移动设备GPU尤其是iOS对精度precision非常敏感。始终在片段着色器开头声明precision highp float;。对于颜色计算mediump通常也够用但highp最保险。纹理采样问题在真机上确保纹理的Wrap Mode和Filter Mode设置正确。对于渐变Shader通常不需要重复RepeatClamp即可。使用CCSampleWithAlphaSeparated能避免很多平台差异问题。驱动或编译器差异不同厂商的GPU驱动对GLSL规范的解读可能有细微差别。尽量使用最简单、最标准的GLSL语法。复杂的表达式可以拆分成多步计算。调试技巧使用“输出调试颜色”法当效果不符合预期时可以临时修改片段着色器直接输出中间变量来观察。例如将finalColor vec4(factor, factor, factor, 1.0);如果屏幕上显示的是从黑到白的渐变说明factor计算正确问题出在颜色混合上。利用Cocos Creator的材质预览在材质面板调整颜色时预览球会实时更新这是最快的调试方式。简化测试创建一个纯白色的256x256的图片作为纹理排除纹理本身颜色带来的干扰专注于调试渐变逻辑。5. 扩展思路从渐变到更丰富的视觉效果掌握了基础的颜色渐变我们可以以此为跳板实现更多酷炫的效果。Shader的魅力就在于一点小小的改动就能带来视觉上的巨大变化。1. 多色渐变Color Ramp我们目前只有两个颜色。如何实现像彩虹一样的多色渐变思路是使用一个**一维纹理1D Texture**作为颜色查找表LUT。创建一个小尺寸如256x1的纹理从左到右填充你想要的渐变颜色序列。在Shader中将计算出的factor0到1作为U坐标去采样这个1D纹理vec4 rampColor texture(rampTexture, vec2(factor, 0.5));。用rampColor代替mix(startColor, endColor, factor)。 这样你就能通过一张图片定义任意复杂的渐变并且可以在运行时动态替换实现诸如血量变化时血条颜色从绿到黄到红的动态效果。2. 动态渐变与动画让渐变动起来只需要在每帧更新传递给Shader的Uniform值。移动渐变修改混合因子的计算。例如实现一个扫描光效果float factor sin(cc_time.x * speed uv0.x * frequency) * 0.5 0.5;。这里cc_time.x是Cocos提供的内置Uniform代表游戏运行时间。闪烁/脉冲渐变通过时间函数控制startColor或endColor的强度或色相。3. 非线性渐变我们一直用的是线性插值mix它产生均匀的颜色过渡。但有时我们需要缓入缓出的效果。使用平滑函数factor smoothstep(0.0, 1.0, uv0.x);提供平滑的起点和终点。使用幂函数factor pow(uv0.x, 2.0);会产生加速的渐变颜色在起点变化慢终点变化快。factor 1.0 - pow(1.0 - uv0.x, 2.0);则产生减速渐变。使用自定义曲线最灵活的方式是将曲线存储在一个1D纹理中用factor去采样得到一个新的、非线性的factor。这给了美术完全的控制权。4. 与遮罩Mask结合渐变效果不仅可以应用在颜色上还可以作为透明度Alpha的遮罩。将渐变因子factor直接赋值给输出颜色的Alpha通道o.a * factor;。这样你可以创建一个从左到右逐渐消失的精灵或者一个圆形的径向渐隐遮罩用于技能范围指示器等。从一个小小的颜色混合公式出发我们实际上打开了一扇通往实时图形渲染世界的大门。每一次对factor的创造性计算都可能诞生一种全新的视觉表现。当你下次再看到游戏里那些华丽的特效时不妨想想它们的底层是不是也只是一个或几个精心设计的mix函数呢动手去实验把想法变成代码把代码变成屏幕上的色彩这正是图形编程最令人着迷的地方。