Cocos 彩色旋转 Shader 实战:UV 旋转、HSV 色相与性能优化
发布时间:2026/9/30 7:40:06来源:尧图网络
如果你玩过一些游戏加载页或者角色释放技能时的特效一定见过那种彩色光带绕着一个中心点不断旋转的画面——红橙黄绿青蓝紫依次切换像是把彩虹拧成了一根麻花。这种效果在 Cocos 里做其实有好几条路硬编码贴图序列帧、引擎自带的 Tween 旋转节点、或者直接上自定义 Shader。我自己的结论是贴图序列帧最笨重Tween 旋转只有形没有色最值得学习的方案是在 Cocos 里写一个彩色旋转 Shader一条代码同时搞定旋转、换色、流动还能让美术直接调参数。这篇东西不是贴一段代码就完事我想把整个思考过程、数学原理、Cocos Creator 3.x 和 2.x 的具体落地差异、真机上的性能坑都讲清楚。无论你是想给加载界面加个动态背景还是给角色技能做光环特效或者单纯想入门 Cocos Shader这篇文章都够用。1. 先拆解需求“彩色旋转”到底包含了哪几层1.1 一个看似简单但很容易做丑的小效果我第一次接到这个需求是在做一个消除类游戏的开场界面策划想要一个“彩色旋转光效”当背景装饰。当时第一反应是找美术出一张彩虹螺旋贴图然后用 Cocos 的node.rotation接口每帧转一下。结果有两个问题一是贴图是静止的只是整体旋转看不出“色彩流动”二是贴图稍微放大一点边缘就有锯齿观感特别廉价。后来换了一种思路把彩虹做成一条从中心向外扩散的色带色带本身绕中心旋转同时色相还在不断变化。这其实就是“彩色旋转 Shader”的完整含义它不是简单的图片旋转而是颜色空间里的动态旋转 屏幕 UV 空间的旋转两个旋转叠加在一起视觉上才会又花又爽。1.2 拆开看UV 旋转、时间驱动、颜色映射三个要素一旦想清楚了目标效果拆起来就很简单。任何一个“彩色旋转”效果本质上由三件事组成UV 旋转把采样坐标或者说颜色生成坐标绕某个中心点旋转。旋转的幅度由引擎时间驱动这样画面才会动。颜色映射旋转后的坐标不能直接作为 UV 去采样图片而是要映射成一个颜色值。最理想的做法是把坐标转换到 HSV 色环上让不同角度对应不同的色相。随时间流动除了旋转本身色相也可以随时间做偏移这样同样一个色环每帧看到的颜色都在变动态感更强。这三个要素不是必须捆绑的但如果你想要效果足够“魔性”三者缺一不可。很多新手写 Shader 时只做了第一件事旋转是转了但颜色根本没变化看起来就是一个静态贴图在转效果大打折扣。1.3 为什么不用引擎自带的材质或动画系统Cocos Creator 的编辑器确实提供了内置材质和 Tween 动画系统比如把一张彩虹贴图的rotation属性打关键帧确实能转。但它的上限很低你没法让多个颜色带朝向不同方向旋转没法让色相在旋转过程中自动渐变也没法通过一个参数从“彩色旋转”平滑过渡到“原图呈现”。Shader 最大的优势在于所有变化都是数学函数算出来的不需要额外资源也不需要关键帧动画。同一个材质可以挂给任意 Sprite、任意粒子纹理甚至挂在 UI 上只需要微调几个 uniform 参数就能适配不同场景。所以我坚持用自定义 Effect 文件来做这也是 Cocos 里做特效的正确姿势。2. 核心数学部分UV 旋转矩阵、时间驱动和 HSV 颜色映射2.1 给 Shader 一个“心跳”统一使用 cc_timeCocos Creator 内置了一个全局时间变量cc_time在 3.x 里它是vec4类型cc_time.x是从引擎启动到当前的秒数cc_time.y是增量时间cc_time.z是总时间的 sin 值包装cc_time.w是另一个包装值。我们最常用的就是cc_time.x它天然就是不断增长的浮点数适合乘系数当角度。要特别说明一点不要在 Shader 里自己写sin(uTime)或者float time 0.016这种逻辑。片元着色器是并行执行的所有像素需要拿到同一个时间值自己维护一个“每秒加点”的变量在 Shader 里是不可行的。让引擎把时间传进来我们只用它来驱动角度变化这是 Cocos Shader 的基本共识。2.2 在 UV 坐标系里做旋转然后映射到色环UV 坐标通常就是(0,0)到(1,1)的矩形区域Tonemapping 之前你先要判断一个很关键的问题你希望旋转中心在哪里。对于一张加载页背景中心大概率是(0.5, 0.5)。所以第一步是把 uv 平移一下让中心对齐到坐标原点也就是uv - center。然后就是二维旋转矩阵。绕原点的逆时针旋转可以写成x x * cos(angle) - y * sin(angle) y x * sin(angle) y * cos(angle)这个矩阵用在 Shader 里就是一次mat2乘法。旋转完之后这个坐标已经带着旋转信息了。接下来别急着拿它去采样贴图先看它的极坐标atan(y, x)能算出当前点相对于中心的角度length(vec2(x, y))能算出它离中心多远。角度映射到色相就是整个效果的灵魂。把弧度换算到0~1的范围然后丢给 HSV 转 RGB 的函数不同角度就会呈现不同颜色。想让颜色旋转只需要让角度加上一个cc_time.x的倍数想让色相流动只需要在 HSV 的 H 通道上再加一个时间偏移。2.3 HSV 比 RGB 更适合做彩虹色写 Shader 的人都有个经验想要彩虹色一定要用 HSV不要直接硬编码 RGB 分段。用 RGB 做渐变中间过渡全是脏色而且颜色不连续。HSV 的思路是色相 H 从 0 转到 1饱和度 S 固定明度 V 固定直接转成 RGB出来的就是从红到紫的完整光谱。相当于你什么都不用管H 通道就是彩虹。HSV 转 RGB 的 GLSL 函数网上有很多版本我习惯用这一个短小精悍效率也不错vec3 hsv2rgb(vec3 c) { vec4 K vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p abs(fract(c.xxx K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); }到这里“彩色旋转”的数学骨架就完整了旋转矩阵负责让坐标转起来极坐标的角度负责给 H 通道提供输入HSV 转 RGB 负责把色相变成真正的颜色。剩下的都是工程落地问题。3. 在 Cocos Creator 中落地完整 Effect 代码与挂载流程3.1 3.x 版本的 Effect 文件可以直接抄Cocos Creator 3.x 里自定义 Shader 的载体是.effect文件。文件里面同时包含顶点着色器、片元着色器和属性定义。下面这个是我常用的彩色旋转材质直接在资源管理器里新建 Effect 文件把代码粘进去就能用// colorRotate.effect CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true rasterizerState: cullMode: none properties: rotationSpeed: { value: 0.6 } cycleSpeed: { value: 0.8 } saturation: { value: 0.9 } brightness: { value: 1.0 } center: { value: [0.5, 0.5] } mixFactor: { value: 0.4 } }% CCProgram vs %{ precision highp float; #include cc-global #include cc-local in vec3 a_position; in vec2 a_uv0; out vec2 v_uv0; void main () { vec4 pos vec4(a_position, 1.0); v_uv0 a_uv0; gl_Position cc_matViewProj * pos; } }% CCProgram fs %{ precision highp float; #include cc-global #include alpha-test #include texture in vec2 v_uv0; out vec4 fragColor; uniform Params { float rotationSpeed; float cycleSpeed; float saturation; float brightness; vec2 center; float mixFactor; }; vec3 hsv2rgb(vec3 c) { vec4 K vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p abs(fract(c.xxx K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } void main () { vec2 uv v_uv0 - center; float angle cc_time.x * rotationSpeed; float cosA cos(angle); float sinA sin(angle); uv mat2(cosA, -sinA, sinA, cosA) * uv; float theta atan(uv.y, uv.x) / (2.0 * 3.14159265) 0.5; float radius length(uv); float hue fract(theta cc_time.x * cycleSpeed); vec3 rainbow hsv2rgb(vec3(hue, saturation, brightness)); vec4 texColor texture(texture, v_uv0); vec3 finalColor mix(texColor.rgb, rainbow, mixFactor); fragColor vec4(finalColor, texColor.a); ALPHA_TEST(texColor.a); } }%这段代码里有一个细节值得注意我用mixFactor把原始贴图颜色和彩虹色做了混合。这样做是故意的因为实际项目中很少真的需要把纹理彻底丢掉更多时候是让彩色旋转叠加在原图上既能看到原图内容又有动态效果。如果你想做成完全纯色的彩虹光环把mixFactor改成1.0就行。3.2 2.x 版本的主要差异如果你还在用 Cocos Creator 2.4.x 或更早版本思路是一样的但语法有几个差异2.x 的 Effect 文件是.effect结尾结构类似但 uniform 不要包在uniform Params {}块里直接在片元着色器顶层声明uniform float rotationSpeed;这种单变量即可。properties 里的名字要和 fs 里声明的 uniform 名字一致2.x 对名称的匹配要求更严格。cc_time在 2.x 里也是引擎全局变量可以直接用但要确保在#include cc-global之后。2.x 的片元着色器大概长这样uniform float rotationSpeed; uniform float cycleSpeed; uniform float saturation; uniform float brightness; uniform vec2 center; uniform float mixFactor;其余数学逻辑完全一致。说实话如果新项目建议直接上 3.x官方对 Effect 的支持和维护更积极2.x 的项目如果没有历史包袱迁移过来统一用 3.x 风格更省心。3.3 从 Effect 到材质到 Sprite 的挂载步骤光有一个.effect文件还不行你得先创建材质再把材质赋给精灵。操作流程很简单在资源管理器里右键一个 2D 精灵然后新建一个 Material 资源。在 Material 资源上把 Effect 属性选成你刚创建的colorRotate。如果用的是纯色或者彩虹不需要纹理清掉纹理槽如果要叠加原图把 SpriteFrame 拖进去。把材质拖到场景里任意 Sprite 组件的CustomMaterial槽位中。在编辑器右上角就能看到 rotationSpeed、cycleSpeed、saturation 这些参数拖动就能实时预览。这里有一个很常见的误解有人把材质拖到 Sprite 的SpriteMaterial或者直接改了Canvas的全局材质结果效果没出来。实际上 2D 渲染下你要找的是 Sprite 组件的CustomMaterial只有这个槽位才会走自定义 Effect否则引擎会强行用内置的builtin-sprite材质把你的 Effect 覆盖掉。3.4 代码里的 uniform 命名坑Cocos Creator 对 uniform 的命名和类型容忍度比较低。比如你在 properties 里写的value: 0.6在 fs 里如果声明成了float rotationSpeed名字不一致会导致编译报错或者参数不生效。最典型的就是center属性类型是vec2你在 properties 里要写value: [0.5, 0.5]少一个数都不行。另外Params这个 uniform 块名在 3.x 里写法很严格一定不要漏掉分号。很多新手把uniform Params {}写成了uniform Params;结果整个 effect 编译失败这个错误极其常见。4. 从“能看”到“好用”参数化技巧和扩展玩法4.1 把参数交给美术比写死效果好得多第一个版本我习惯把所有参数都写死在 Shader 里后来被打击了不同场景需求完全不同加载页希望旋转慢一点、色环疏一点技能特效希望转得快、鲜艳一点UI 过场又希望透明度低、别太抢眼。与其每次改代码不如把参数全部抽出来。现在的 properties 里实际上给了你一套完整的参数卡片参数含义推荐范围rotationSpeed整体旋转速度0.2 ~ 2.0cycleSpeed色相流动速度0.2 ~ 1.5saturation饱和度0.6 ~ 1.0brightness明度0.5 ~ 1.2center旋转中心默认 0.5, 0.5mixFactor彩虹与原图混合度0 ~ 1用编辑器微调这些参数比一遍遍改代码重新构建有效率得多。重点是让美术在编辑模式下直接拖他们能实时看到变化这样合版本的时候就不用老来烦你。4.2 玩出差异化扭曲、波浪、局部彩色旋转基础版足够应付大多数场景但如果你想要更有辨识度的效果可以在这个骨架上做加法。波浪扭曲在计算 theta 之前给 uv 加一个正弦扰动比如uv.x sin(uv.y * 10.0 cc_time.x) * 0.05彩色带就会扭得像一条蛇一样。径向渐变还原本来的色相映射把hue fract(theta time)改成hue fract(radius * flowSpeed - time)彩虹就会从中心向外圈扩散类似发射流光的效果。局部区域在 Shader 里判断一下 uv 是否在某个圆环范围内然后只在圆环内部显示彩色旋转外部透出原图。这样可以用作角色脚下的光环而不是全屏特效。这些玩法不需要改整个 Shader 的结构只是在坐标变换和颜色映射之间多塞几行数学运算而已。我自己最喜欢的组合是“波浪扭曲 径向扩散 低饱和度”用它做了一个粒子纹理整个粒子系统放出一种软绵绵的彩色烟雾效果比静态粒子好太多。4.3 混合模式的思路不是只能叠在 Sprite 上如果你把这个 Effect 挂到粒子纹理上那么 blendState 就格外重要。当前代码里 blend 是 true配合默认的 SrcAlpha 和 OneMinusSrcAlpha适合大多数半透明材质。如果你想要发光的感觉可以把 blend 改成加法混合彩虹叠加后会高亮特别适合技能爆发瞬间的冲击特效。但要注意加法混合会让黑色区域也变亮如果你的彩虹色饱和度拉满叠加后很容易过曝。实际操作中建议把 brightness 控制在 1.0 左右别超过 1.3不然真机上颜色会非常刺眼。5. 真机性能观察与踩坑记录5.1 片元着色器里的三角函数不是免费的我知道很多教程让你放心大胆地写sin、cos、atan但真实移动端项目里我们的 Fragment Shader 是逐像素执行的哪怕一张 256x256 的贴图全屏铺开也会有几十万个像素同时调用这个函数。每个像素多算一次atan和两次三角函数粒子和 UI 叠加在一起帧率下降很快。优化手段分两种。如果旋转速度不需要特别精细可以把角度计算里的一部分挪到顶点着色器里去比如旋转矩阵只和角度有关角度和时间相关这部分可以在顶点阶段算好传下去。另一种方案是预计算一张颜色查找表把色相映射结果存成一张 256x1 的纹理在片元里采样它替代hsv2rgb的数学计算。我实际测过后者在低端机上收益明显。5.2 cc_time 的精度与 Android 兼容性Cocos Creator 3.x 在 Android 真机上cc_time.x是高精度浮点数理论上没问题。但如果你在某个机型上发现旋转速度忽快忽慢或者颜色跳动不连续很可能是项目里某个地方修改了全局时间缩放。比如你用了director.getDeltaTime()做了慢动作但 Shader 里直接用的是cc_time.x这俩是不同的时间体系会明显不一致。解决办法是如果你希望旋转速度受游戏暂停或时间缩放控制就不要直接用cc_time.x而是在代码里把时间值通过 uniform 传进来由逻辑层统一控制。对于 UI 装饰类特效直接用cc_time.x倒是没问题因为不会暂停更新。5.3 我踩过的一个大坑UV 原点方向搞反了很多 2D 美术同事习惯把图片左上角当(0,0)但 OpenGL 系渲染里纹理坐标的(0,0)在左下角。我第一版写的中心旋转在编辑器看着是对的一到真机上就发现旋转中心偏了半个屏幕就是因为我在心里默认了左上角原点。调试这个问题的经验是准备一张带明显对角颜色的测试图左上角红色、右下角蓝色然后看旋转效果。如果发现中心点对应的是图片的右上角而不是正中心把center参数的 vec2 的两个分量反过来试一下多半就是原点方向的问题。5.4 与动画状态机和 UI 一起用时的注意事项彩色旋转 Shader 很适合做角色状态切换时的底光也适合挂给 UI 面板做动态边框。但如果挂到 UI 上有几点要特别注意UI 节点本身就带合批系统如果给大量 UI 节点挂不同参数的同一材质会导致 DrawCall 暴涨因为合批要求材质参数一致。我的做法是同一界面里最多两个节点使用这个材质其余背景效果用序列帧或粒子替代。另外如果你在动画状态机里切换多个精灵帧最好保证所有帧的纹理尺寸一致否则 UV 映射会拉伸变形。我在技能系统里就是专门准备了一张 512x512 的背景纹理把所有状态切换都放在这一张图上最终效果稳定DrawCall 也低。最后分享一个小技巧给这个 Shader 的mixFactor接上一个从 0 到 1 的动画曲线彩色旋转就会从原图慢慢浮现出来非常适合做“魔化”“觉醒”这类变身演出。你不需要额外做任何特效资源只要一个 Tween 控制参数即可这也是 Shader 参数化最迷人的地方——一个材质可以泛生出无数种玩法。
网站建设高端定制企业官网