ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从原理到实战:Cocos彩色旋转Shader打造动态游戏特效

从原理到实战:Cocos彩色旋转Shader打造动态游戏特效 cocos彩色旋转shader从原理到实战我把素材转圈圈、会流动的彩虹色一次讲明白“cocos彩色旋转shader”这个需求听起来像个视觉花活但在实际项目里它相当能打技能冷却转圈、加载缓冲动画、消星星消除瞬间的爆炸光环、金币拾取闪光、角色脚底的选圈特效甚至UI里一个会旋转的渐变按钮都能用它来实现。我在Cocos Creator 3.8里把这块从原理到落地完整做了一遍跑通后才发现只要弄懂“UV旋转”和“HSV色环”这两个核心点后半程几乎就是复制粘贴的事。这篇我会用完整可复现的代码把彩色旋转shader为什么能转、怎么写、怎么挂到精灵上、以及真机打包和色彩发灰这类坑一并讲透。适合刚接触shader、想在2D项目里做点动态特效的朋友也适合那些已经会写简单effect但不确定旋转到底该在顶点还是片元里做的人。1. 彩色旋转Shader是干什么的先搞清楚需求再用代码1.1 一个需求拆解从“彩色旋转”到具体要实现的效果如果只看“彩色旋转”四个字很多人第一反应是“把一张彩虹贴图转起来”。但稍微拆一下会发现它至少包含两个可分离的能力彩色颜色必须有连续性变化不能只是红黄蓝三块硬切。旋转画面随时间动起来表现为颜色流动、图案旋转或两者兼有。这两个能力分别对应两种核心技术手段。彩色需要HSV到RGB的色环映射旋转需要往UV坐标里加入时间参数。只要你把这两件事想清楚后面的代码就是套公式。我在项目里遇到的最典型场景是做一个“需要转圈表示进度”的UI图标。以前的做法是美术出一张彩虹sprite然后用Tween旋转节点或者用多个mask叠来叠去效率低且打包体积也大。用shader做的好处是一张纯白Sprite就能输出完整动态效果热更新改参数不用换图而且颜色可以按需实时调整。1.2 适合谁做、用在哪类游戏如果你正在做休闲游戏、消除类产品或者需要一个低成本、高视觉动态的方案这个shader尤其对胃口。消星星里的爆炸光环、匹配成功时的同色闪光、糖果类道具的获取弹跳效果几乎都能挂在彩色旋转shader上解决。我给一个典型的落地场景消星星类似Three Match里玩家消掉一排同色星星时消除点会爆开一个彩虹光环。用shader做的话只需要把光环图片换成普通圆形sprite挂上材质然后在点击消除的瞬间把u_speed调大、用tween缩小scale看起来就是一个从内向外旋转炸开的彩色波。类似的用法加载界面的转盘、对战开始前的倒计时光环、角色脚下的目标圈都能直接复用。这也是我在文章里反复强调“彩色旋转”的本质是“时变渐变”的原因一旦写成shader它是一个通用能力而不是某个美术单帧动画。2. 原理拆解这玩意儿为什么能转起来2.1 为什么旋转要放在片元着色器而不是顶点着色器这是很多初学者第一反应最容易踩错的地方。Cocos Creator里一个Sprite本质上是一个四边形的两个三角形顶点数就4个或6个。在顶点着色器里做旋转你只能对这4个点做位移。也就是说整张贴图作为一个矩形可以被旋转但贴图内部的每个像素并不会产生相对运动。这和你直接用Node旋转节点是一致的根本体会不到shader的威力。片元着色器则完全不同。它会对屏幕上每个被覆盖的像素片元都执行一次。对一个100x100的圆环来说每帧可能执行上万次。正因为每个像素都能独立拿到自己的UV坐标你才能在“像素级别”去改变颜色生成逻辑让图上不同位置显示不同颜色并且随时间替换变化视觉上就“活”了。打个比方顶点着色器像是把整张照片举起来转了个角度片元着色器则是在照片内部让颜料流动起来。我们想要的彩色旋转本质是后者。2.2 UV旋转矩阵绕中心旋转的正确姿势要做纹理内部的图案旋转最核心的运算是对UV坐标做旋转变换。因为UV纹理坐标的范围通常是 [0,1]但旋转的中心点一般在 (0.5, 0.5)所以你不能直接套用围绕原点的旋转矩阵。正确姿势是先平移再旋转最后平移回去把UV坐标偏移到以中心为原点centered uv - 0.5对centered坐标做二维旋转如果需要继续用原UV做采样就再加上0.5回到 [0,1] 区间二维旋转矩阵的形式是x x * cos(θ) - y * sin(θ) y x * sin(θ) y * cos(θ)这里的 θ 是弧度值。注意数学上默认这个是“逆时针”旋转。如果你想让贴图在屏幕里看起来是顺时针转一种偷懒的办法是把 θ 取负。我把它封装成GLSL代码vec2 rotateUV(vec2 uv, float angle) { vec2 centered uv - 0.5; float c cos(angle); float s sin(angle); vec2 rotated vec2( centered.x * c - centered.y * s, centered.x * s centered.y * c ); return rotated 0.5; }这里有一个容易忽略的坑Sprite的纹理贴图数据从左到右对应UV的x方向从下到上对应UV的y方向。如果你在UI里用到的贴图反转了美术导出时Y轴反了旋转方向可能看起来是反的。这类问题不是算法错了而是纹理坐标的方向约定问题排查时多留个心眼。2.3 HSV色环为什么用HSV而不是RGB做彩虹渐变彩色渐变最直接的做法是在rgb通道之间插值。比如mix(vec3(1,0,0), vec3(0,0,1), uv.x)得到红到蓝。但这样的渐变没有任何“环绕”特性你很难做到“颜色沿着圆周连续变化一圈回到红色也不突兀”。HSV颜色模型天然适合这个需求。HSV的H分量是色相是一个0到1的循环量0是红色0.33是绿色0.67是蓝色1又回到红色。只要你能把一个连续变化的量映射成H值再转回RGB就能得到一圈无缝过渡的彩虹。HSV转RGB的GLSL函数有很多版本我常用的是这个经过优化的单行式vec3 hsv2rgb(vec3 c) { vec3 rgb clamp(abs(mod(c.x * 6.0 vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0); return c.z * mix(vec3(1.0), rgb, c.y); }解释一下原理色相H是一个0到6的周期其中0、1、2、3、4、5、6分别对应六个颜色区间。上面这个函数用一些取模和clamp运算把H映射到三个颜色通道上的分段线性值。表面看起来像魔法其实就是在六个区间里做三通道的斜坡插值。Cocos Creator的GLSL里vec3和float的混合运算是逐分量的所以这个写法可以直接用。有些人会比较担心这段代码在真机上的兼容性。标准GLSL里mod和clamp都是基础函数GLES 2.0/3.0都支持这行可以放心用。2.4 两种旋转模式的区别与选择色相流动 vs UV旋转做“彩色旋转”时我发现有两条非常容易被混为一谈的路线色相流动Hue Flow不对UV做任何旋转只让色相H随着时间偏移。效果是颜色在环上“流动”像原地转动的色盘。UV旋转UV Rotation先把UV坐标旋转一个角度再用旋转后的坐标去计算色相或采样纹理。效果是整张纹理的图案都在旋转。我把它们整理成对比表维度色相流动UV旋转核心操作hue fract(hue time * speed)先做旋转矩阵再使用新的UV视觉感觉颜色沿画面流过去图案不转整个图案跟着转比如网格、光斑计算开销低无需三角函数比色相流多两次三角函数cossin适用场景纯渐变光环、加载圈、波光效果万花筒、纹理图案旋转、扫光动画实践里做“彩虹旋涡”这种效果我会优先用色相流动因为颜色变化本身已经很酷了省掉的两次三角函数在低端机、满屏特效时是实打实的性能收益。如果后续美术要求“图案必须跟着旋转”比如某个纹理里有星星图案再升级成UV旋转也不迟。其实这两种方案可以同时存在色相流动负责“彩色”UV旋转负责“旋转”。不过要小心别过度计算在移动端fragment shader不是写几行代码那么简单它是每像素执行的开销要精打细算。3. 完整实操在Cocos Creator 3.8里一步步跑通3.1 创建effect文件与工程准备Cocos Creator 3.x里shader文件的后缀是.effect新建一个命名成rainbow-rotate.effect这样的名字都行。工程建议用3.8因为这个版本对2D渲染管线和材质系统的支持最成熟后面的操作步骤也更贴近当前主流项目。effect文件里分几个关键块我先把骨架列出来再逐段解释CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true blendSrc: src_alpha blendDst: one_minus_src_alpha rasterizerState: cullMode: none properties: u_speed: { value: 0.5, editor: { displayName: Speed } } u_saturation: { value: 1.0, editor: { displayName: Saturation } } u_alpha: { value: 1.0, editor: { displayName: Alpha } } }% CCProgram vs %{ precision highp float; #include cc-global in vec3 a_position; in vec4 a_color; in vec2 a_uv; out vec4 v_color; out vec2 v_uv; void main () { v_uv a_uv; v_color a_color; gl_Position cc_matViewProj * vec4(a_position, 1.0); } }% CCProgram fs %{ // 片元着色器代码见下一节 }%有几个点是新手容易忽略的cullMode: none非常关键。它保证Sprite从背面看也不会被裁剪成消失。如果你要做双面显示或摄像机翻转这个必须设为none。blendState里设置的是半透明混合。彩虹效果本身带透明度渐变不混合的话边缘会有生硬的方块色。precision highp float;是精度声明如果不写部分Android机型会编译报错。3.2 片元着色器核心代码逐段解读这一节是整个shader的灵魂所在。下面是我在工程里验证过的完整片元着色器CCProgram fs %{ precision highp float; #include cc-global in vec4 v_color; in vec2 v_uv; uniform float u_speed; uniform float u_saturation; uniform float u_alpha; vec3 hsv2rgb (vec3 c) { vec3 rgb clamp(abs(mod(c.x * 6.0 vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0); return c.z * mix(vec3(1.0), rgb, c.y); } void main () { vec2 uv v_uv; vec2 centered uv - 0.5; float time cc_time.x; // 1. 计算当前像素相对中心的角度 float angle atan(centered.y, centered.x); // 2. 把角度映射成色相[-PI, PI] - [0, 1] float hue angle / (2.0 * 3.14159265359) 0.5; // 3. 让色相随时间旋转 hue fract(hue time * u_speed); // 4. 径向渐变让外侧和中心都透明只保留一圈亮色 float radius length(centered) * 2.0; float bright (1.0 - smoothstep(0.2, 0.5, radius)) * smoothstep(0.0, 0.2, radius); // 5. 合成颜色 vec3 color hsv2rgb(vec3(hue, u_saturation, bright)); color * v_color.rgb; float alpha bright * u_alpha * v_color.a; gl_FragColor vec4(color, alpha); } }%我重点解释几个容易踩坑的地方atan返回值的区间atan(y, x)返回[-PI, PI]。所以第一步把角度直接除以2PI后会得到[-0.5, 0.5]再加上0.5色相区间就变成[0, 1]方向是逆时针的。如果希望转起来是顺时针视觉把0.5改成-0.5或者把u_speed设成负数都行。让色相循环流动fract(hue time * u_speed)是核心。cc_time.x是shader累计运行时间单位秒u_speed控制速度。fract把结果取小数部分保证色相永远在0到1之间循环比取模更顺手。径向渐变的细节bright我用了两个smoothstep相乘。第一个1.0 - smoothstep(0.2, 0.5, radius)让圆外缘渐隐第二个smoothstep(0.0, 0.2, radius)让圆心有一个规范的小孔。这样视觉上是一个中空的光环而不是实心的色块挂在UI上不会遮挡后面内容。顶点色的乘算color * v_color.rgb和alpha * v_color.a是为了支持Sprite组件自带的Color属性。这样美术或程序可以在不新增参数的条件下直接通过调Sprite颜色来二次调色非常实用。3.3 创建Material并挂到Sprite上的完整步骤写好了effect文件接下来把它变成可以拖到场景里的材质。Cocos Creator 3.8的操作路径如下在资源管理器右键选择创建 - Material命名为RainbowRotateMat。选中该材质在属性检查器顶部点击Select按钮从弹出的effect列表里选择你导入的rainbow-rotate。材质会自动读取我们在CCEffect里声明的三个属性并把Speed/Saturation/Alpha显示在属性面板。在编辑器里就能直接拖末调节。在场景里创建一个Sprite给它的SpriteFrame指定一张纯白圆贴图建议用内置的default_sprite_splash或美术导出的白色圆形图然后把材质拖到Sprite组件的CustomMaterial插槽。保存场景运行。你会在Game视图里看到一个持续旋转的彩虹光环。这里有个细节如果用了default_sprite_splash这张默认图它往往不是圆形会有个小白边。最好换成美术做的圆形白图或用代码动态生成一张圆形纹理。我自己常用的是在项目初始化时用Canvas临时画一张圆形图缓存在资源库里方便调试。3.4 运行时用代码调节Speed等参数如果只在编辑器里调参数shader就只是个静态展示品。真正用到游戏流程里还是要能动态改变旋转速度和透明度。在Cocos Creator 3.8里可以用TypeScript这样实现import { _decorator, Component, Sprite, Material } from cc; const { ccclass, property } _decorator; ccclass(RainbowEffectCtrl) export class RainbowEffectCtrl extends Component { property(Sprite) targetSprite: Sprite null; onEnable () { // 获取当前Sprite正在使用的材质实例 const mat this.targetSprite.customMaterial; if (mat) { // 让速度变成原来的2倍 mat.setProperty(u_speed, 2.0); // 降低饱和度变成柔和色 mat.setProperty(u_saturation, 0.7); // 提亮整体 mat.setProperty(u_alpha, 1.0); } } setSpeed (v: number) { const mat this.targetSprite.customMaterial; if (mat) { mat.setProperty(u_speed, v); } } }我的习惯是在onEnable里记录一份材质引用因为运行时直接改customMaterial的属性可能影响其他同材质的Sprite。如果多个Sprite想共用同一个shader但各自不同的参数应该先从customMaterial获得一个材质实例Cocos会自动实例化再在这个实例上改参数。这是很多做UI的同学会踩的坑调了半天发现改的其实是共享材质全天下都跟着变。4. 从单场景到实战性能、打包与双面渲染的坑4.1 性能优化fillrate、混合与draw call彩色旋转shader在PC上跑得飞起放进手机就未必了。原因在于它是对每个像素计算的手机GPU的fillrate填充率是瓶颈。我实测过一个510x510的圆环素材挂在全屏UI上在低端安卓机上帧率从60掉到45问题就出在这类半透明逐像素运算的组合上。优化可以从三个方向入手减少屏幕覆盖面积。如果只是个小光环素材别做得太大shader是撑满Sprite的Sprite多大计算量就有多大。减少无用透明区域。给Sprite的贴图做透明裁剪越透明越早跳过计算越好。Cocos的alpha test可能在管线里帮你省略部分像素填充。避免在片段着色器里做循环和纹理采样。我这个效果只用到了atan、fract、smoothstep已经算轻量级了。如果你加一个for循环遍历8个方向的采样每像素开销会成倍上升务必谨慎。如果你观察后觉得“明明不卡但GPU消耗很高”别急着优化先用Cocos Creator自带的Profiler看一下DrawCall和填晶率。多数情况下彩色旋转shader的瓶颈不是drawcall而是过度的透明混合指令。4.2 打包APK前要检查的三件事工程里的shader在编辑器里正常、模拟器上正常但打包APK到真机上却出现粉红色或直接不渲染这个问题我遇到太多次了。排查顺序一般是这样第一查GLES版本兼容性。Cocos Creator 3.8默认用GLES 3.0如果项目开启了兼容低端机可能走到GLES 2.0。GLES 2.0不支持部分较新的GLSL语法。我的建议是项目里统一用最基础的写法避免使用textureLod、sampleCubeLod这类GLES 3.0才有的函数。第二查精度声明。片元着色器里precision highp float;可能在一些老机型上不被fragment stage支持。如果真机报错改成precision mediump float;往往能解决。代价是颜色精度略低但彩色旋转这种大梯度渐变视觉上几乎看不出区别。第三查自定义uniform有没有被优化掉。有时候编译器发现某个uniform没用上就会把它剔除导致你在代码里setProperty没反应。排查方式是先给u_speed一个明显较大的值如果shader里确实在用它材质面板上拖一下就能看到速度变化。如果没反应多半是uniform名拼写不一致。4.3 双面材质与混合排序问题Cocos Creator的Sprite默认只显示正面摄像机一旦绕X轴旋转超过一定角度背面的Sprite会消失。想让它双面可见不是在shader里贴两个面而是把rasterizerState的cullMode设成none即关闭背向面裁剪。这一点在Cocos里比Unity直观很多。Unity里处理双面材质要调Cull Off还要考虑法线翻转Cocos的2D Sprite不依赖法线做光照。我把cullMode: none放到shader里一个effect解决免去美术反复建材质的麻烦。混合排序则是另一个隐蔽的坑。彩色旋转shader默认是半透明的透明物体在Cocos渲染管线的排序规则是按节点树的深度来定的。如果UI里有多个半透明特效互相交叉会看到穿帮的分层。解决方案有两种调整节点的SiblingIndex让后绘制的叠上面。把shader的混合改成不透明加透明裁剪alpha test但这样会失去半透明渐隐效果。多数情况下我选第一个因为半透明的渐隐效果是这shader的卖点不值得为排序牺牲视觉。5. 扩展玩法一个shader怎么变成三个特效5.1 变体一UV旋转做万花筒如果你想不只是颜色在转而是图案本身跟着转就把旋转矩阵写进片元里然后用旋转后的UV再次计算颜色或采样贴图。关键代码是float theta time * u_speed; vec2 centered v_uv - 0.5; float c cos(theta); float s sin(theta); vec2 rotated vec2( centered.x * c - centered.y * s, centered.x * s centered.y * c ); vec2 rotatedUV rotated 0.5; // 用rotatedUV继续做颜色计算或纹理采样 float angle atan(rotatedUV.y - 0.5, rotatedUV.x - 0.5);我把这版用于制作万花筒素材把旋转UV和色相映射组合再叠加一个sin(angle * 频段 time)就能做出像镜头光晕一样的流线纹理。如果你想要“星空旋转”的傻瓜感也可以用rotatedUV去采样一张带星星的贴图星星图案会跟着一起转。5.2 变体二闪烁星星道具彩色旋转shader不仅能做环形光晕还能做“闪烁变色”。我做过一个金币道具需要每隔一秒换一种颜色并带闪光。实现方式很简单把u_speed调慢再把径向渐变里的小孔去掉让整个圆面都填充颜色。再用一个脉冲值float pulse 0.5 0.5 * sin(time * 6.0); color hsv2rgb(vec3(hue, u_saturation, pulse));视觉上就是道具在彩虹色里闪烁。消星星这类游戏里每个可消除的星星如果挂上这个shader再用代码为每个星星暴露不同的u_speed可以做出“每颗星星独自变色”的效果比纯美术动画省力得多。5.3 变体三角色光环与受击特效如果你要做2D角色脚下的目标圈或者受击时的冲击波这个shader也能用。常见的做法是把Sprite铺到角色脚下shader仍然用彩色旋转但把半径范围的bright调窄让光环细成一圈线。再配合Tween在0.2秒内把节点的scale从0放大到1.2同时降低u_alpha就能模拟一个向外扩散的魔法阵。有一次我在做16方向行走动画的角色时就是给它脚下加了一个浅淡的彩色旋转光环。当时担心shader会和动画播放互相干扰测试后发现完全不影响动画是Sprite动画组件在跑shader只是渲染层叠加效果两者各走各的线程。所以做这类叠加特效时别怕shader会“覆盖”动画它只是替Sprite换了一套颜色计算逻辑而已。5.4 用Tween控制u_speed做出爆开效果最后分享一个非常实用的组合技shader参数配合Tween做动态节奏。Cocos Creator的tween可以对节点的scale做补间但没法直接补间材质参数。我的做法是在组件里写一个回调tween(this.node) .to(0.15, { scale: new Vec3(1.4, 1.4, 1) }) .call(() { this.effectCtrl.setSpeed(8.0); }) .to(0.3, { scale: new Vec3(0.8, 0.8, 1) }) .call(() { this.effectCtrl.setSpeed(2.0); }) .to(0.2, { scale: new Vec3(0.0, 0.0, 1) }) .start();这样在消除特效的瞬间速度陡然提高光环像炸开一样快速旋转然后随scale缩小消失。实际体验比纯Tween缩放更有冲击力因为旋转产生的视觉惯性增加了“能量爆开”的感知。6. 常见问题与排查技巧实录6.1 高频问题速查表结合我在多个项目里和社区里看到的高频问题整理成表格问题现象直接原因解决方式挂上材质后画面全黑/全白CUllMode裁剪掉了背面或片段没正常输出检查rasterizerState的cullMode是否none检查gl_FragColor是否有有效值颜色静态不变一段时间后才突然跳变忘记乘time或用错cc_time.x确认uniform里有没有#include cc-global以及是否用了cc_time.x真机打包后shader花屏或粉色GLES版本或高精度不支持尝试precision mediump float;避免高版本GLSL函数多个Sprite共用一个材质互相影响修改了共享材质实例运行时使用customMaterial得到的实例化材质隔离开单独调参彩虹圆边出现方块状锯齿径向渐变的smoothstep衰减不够扩大smoothstep的过渡范围从(0.2,0.5)改为(0.1,0.6)旋转方向不对坐标轴的UV方向或矩阵符号把hue偏移从 time改成- time或把u_speed设负6.2 三个我自己踩过的坑第一个坑也是最容易忽略的直接在材质面板拖speed发现没变化。排查半天才发现是自己把uniform名写成了u_SPEED而材质面板读的是u_speed大小写不一致。GLSL对大小写敏感以后在effect里声明一次就要在属性面板和代码里统一用这一份命名。第二个坑真机上颜色偏淡偏灰。编辑器里看还挺好的一到手机上就发白。多半和项目颜色空间有关。Cocos Creator 3.x有些项目默认走线性空间shader里直接输出sRGB颜色会经过管线转换导致色偏。解决方式是在材质属性里调低u_saturation补偿或者让美术把基础贴图先压暗一点其实并没有银色子弹。第三个坑给Sprite挂了这个shader后UI界面里原本的Mask组件失效。这是因为我的shader使用了自定义混合绕过了部分默认渲染状态。后来我改成cullMode: none加常规alpha混合才恢复正常。如果你在UI里大量使用Mask建议先在目标UI节点上做一次真机验证再批量套用。7. 最后分享一个小经验把shader控制在“刚好能用”的复杂度我个人在做了多次这类彩色旋转shader后最大的体会是在移动端做shader不是越复杂越酷而是“刚好够用”最稳妥。彩色旋转效果的核心就是UV、角度、色相三个量把它们的变换关系理清楚后续其实是在这几个维度上做加减法。如果你想让效果更丰富优先考虑叠加外部参数而不是堆代码。比如给u_speed暴露成可配置属性让策划在编辑器里拖把u_saturation和u_alpha暴露出来让特效可以随时调风格。shader保持简洁的好处是出问题好排查真机兼容性好美术后期改起来也快。这篇从原理到工程实现的整个流程建议你新建一个空场景把effect和材质按步骤走一遍。跑通一次之后你就会发现“彩色旋转shader”其实已经变成了一个你自己的通用工具消星星、加载图标、技能光环随手就能拖进项目里用。
返回列表