news 2026/10/14 8:27:17

CocosCreator Shader实战:从零实现卡牌流光效果与调参技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CocosCreator Shader实战:从零实现卡牌流光效果与调参技巧

最近在调卡牌UI时,策划提了个需求:卡片上时不时有一道高光扫过,看起来要“高级一点”。我最初用的是Tween移动一张预烘焙好的白色渐变条,结果换个角度就要重新烘焙,透明度还容易穿帮。后来决定直接用CocosCreator的Shader来做,于是就有了这个系列的第二篇学习笔记:流光效果。

第一篇文章已经把effect文件的基本结构过了一遍,这篇会从“流光效果为什么能用Shader实现”讲起,从最简单的单条光带开始,逐步加入旋转、宽度控制、多流光、遮罩等扩展,最后把我在实际项目中踩过的坑也一并记录下来。如果你刚接触CocosCreator的Shader,这篇应该能帮你一次跑通。

1. 流光效果的本质:别再用序列帧和Tween硬撑了

1.1 从“移动贴图”到“逐像素计算”

流光效果在游戏UI里太常见了:按钮扫光、卡牌闪光、技能图标高光、列表刷新光晕。大多数人第一反应是做一张带渐变的白色光条图片,然后用Tween来回移动它,或者直接做序列帧动画。

这套做法有个天生的问题:光是二维的,光条按照某个角度扫过,为了适配不同角度的流光,得准备多张不同方向的贴图;如果UI布局变化,光条位置、宽窄又要重新调。更麻烦的是,光条图片一旦遇到不规则形状的图标,你还得做遮罩,不然高光会跑出图形边界。

Shader的做法完全不同:不移动任何贴图,而是在片元着色器里对每个像素计算“当前位置是否处于高光带内”,再决定这个像素该亮多少。你只需要一个普通的纹理贴图,高光带的宽度、角度、速度、颜色全部可以用参数控制。从性能和资源管理角度看,Shader方案都要干净得多。

1.2 光带背后的数学直觉

先抛开Shader,想象有一根长条的亮线经过一张图片。在某个瞬间,图片上离亮线中心越近的像素越亮,离得越远越暗。如果把图片的横坐标看作位置,光带扫描就是“亮线中心随时间改变位置”。

用数学表达就是:

  • 位置坐标x,范围可以归一化到[0, 1]
  • 亮线中心c(t)随时间移动,例如c(t) = fract(t * speed)
  • 每个像素到亮线中心的距离d = abs(x - c(t))
  • 亮度intensity = 1 - smoothstep(0, width, d)

这就是一个最简单的“扫描线”。再配合UV坐标的旋转,就能实现任意角度的流光。没错,Shader里的流光并不神秘,本质上就是一张“距离衰减图”在动。

1.3 为什么放在片元着色器里最划算

流光需要在每个像素上计算,所以放在Fragment Shader(片元着色器)里最合适。顶点着色器只处理少数几个顶点,给不了“像素级高光”的精细度。GPU本身就是大规模并行做这种简单数学计算的,一个128×128的图标,一帧只需要一万多次浮点运算,完全可以忽略不计。

在CocosCreator中,一个控件往往只有四个顶点,剩下的都是片元着色器的工作。因此只要不是大量复杂函数嵌套,做几十个流光控件毫无压力。

2. CCEffect基础:先写一个能跑的最小Shader

2.1 一个最简流光effect文件

下面这个文件可以直接放到CocosCreator工程里使用。它基于常见的CocosCreator 2.x/3.x effect语法,以CCEffect开头,包含一个technique和一个pass。

CCEffect %{ techniques: - passes: - vert: vs frag: fs properties: - texture: { value: white, type: sampler2D } - u_speed: { value: 0.5, editor: { tooltip: "流光速度" } } - u_width: { value: 0.1, editor: { tooltip: "流光宽度" } } - u_angle: { value: 0.0, editor: { tooltip: "流光角度(弧度)" } } - u_flowColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color, tooltip: "流光颜色" } } - u_flowStrength: { value: 1.0, editor: { tooltip: "流光强度" } } %} 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() { gl_Position = cc_matViewProj * cc_matWorld * vec4(a_position, 1.0); v_uv0 = a_uv0; } }% CCProgram fs %{ precision highp float; #include <cc-global> in vec2 v_uv0; uniform sampler2D texture; uniform float u_speed; uniform float u_width; uniform float u_angle; uniform vec4 u_flowColor; uniform float u_flowStrength; void main() { vec4 texColor = texture2D(texture, v_uv0); // 把UV中心从(0,0)移到(0.5,0.5),方便旋转 vec2 uv = v_uv0 - vec2(0.5, 0.5); // 按角度旋转UV float c = cos(u_angle); float s = sin(u_angle); vec2 ruv = vec2(uv.x * c - uv.y * s, uv.x * s + uv.y * c); // 流光中心随时间在[-0.5, 0.5]之间循环 float bandCenter = fract(cc_time.x * u_speed) - 0.5; // 当前像素到流光中心的距离 float d = abs(ruv.x - bandCenter); // 距离越近越亮,用smoothstep生成柔和的边缘 float light = 1.0 - smoothstep(0.0, u_width, d); // 混合流光颜色 vec4 finalColor = texColor; finalColor.rgb += u_flowColor.rgb * u_flowStrength * light * texColor.a; gl_FragColor = finalColor; } }%

这段代码虽然短,但已经把流光的核心逻辑全包含了。接下来拆开讲每一部分。

2.2 关键字段说明:properties、uniform、varying

properties里定义了可在材质面板上调整的参数。texture对应Sprite的贴图,u_speed控制速度,u_width控制光带宽度,u_angle控制扫过角度,u_flowColor控制颜色,u_flowStrength控制额外强度。这些属性最终会生成对应的uniform变量,在CocosCreator运行时自动从材质面板取值。

CCProgram vs和CCProgram fs分别是顶点着色器和片元着色器。顶点着色器只做了一件事:把顶点从模型空间转换到裁剪空间,并把UV传给片元着色器。v_uv0就是UV坐标,范围通常从左上角(0,0)到右下角(1,1)。

片元着色器里我直接用了cc_time.x,这是CocosCreator的内置统一变量,单位是秒。之所以用cc_time.x而不是自己传一个float u_time,是因为省去每帧从脚本更新uniform的麻烦。#include <cc-global>能确保cc_time可用。

2.3 为什么要乘以texColor.a

我把流光加到了颜色上,但乘了一个texColor.a。原因很简单:如果不乘alpha,透明部分也会被加上高光,UI上会出现一圈明显的光框,非常难看。乘上alpha之后,透明区域的流光被过滤掉,只有实际像素存在的地方才有光效。

这段代码里的混合模式用的是默认的透明混合,也就是源颜色乘srcAlpha,再加上目标颜色乘oneMinusSrcAlpha,所以最终透明度仍由texColor.a决定,不会出现“流光把半透明图变实”的问题。

3. 参数怎么调:旋转、宽度、速度背后的几何关系

3.1 中心化与旋转:为什么是vec2(0.5, 0.5)

经常看到Shader里写uv - vec2(0.5),很多人不理解。UI纹理的UV原点在左上角,范围0到1。如果直接拿这个UV去算角度,光带的“旋转中心”会在左上角,而不是图片中心。减去0.5后,图片中心变成了(0,0),这样旋转才是以图片中心为轴。

旋转公式长这样:

ruv.x = uv.x * cos(angle) - uv.y * sin(angle) ruv.y = uv.x * sin(angle) + uv.y * cos(angle)

这个公式来源于二维旋转矩阵。为了省去矩阵写法,我直接展开了。当angle = 0时,ruv.x = uv.x,光带就是水平扫过;当angle = π/2时,ruv.x = -uv.y,相当于垂直扫过。你完全可以用角度转弧度去调,比如45度就是0.785。

3.2 光带宽度与平滑边缘

float light = 1.0 - smoothstep(0.0, u_width, d);

smoothstep会在0和u_width之间做一个Hermite插值,把硬直的边缘变成平滑过渡。u_width设成0.05时是很窄的一条光;设成0.2时就是一大片泛光。建议在编辑器里从0.1开始调,差不多相当于图片宽度10%的样子。

为什么不直接写if(d < width) light=1.0?因为硬边缘在移动时会产生明显的锯齿或不自然的切边。用smoothstep后,光带边缘的亮度是连续渐变的,视觉上柔和很多。如果嫌半透明边缘不够锐利,可以再套一层pow或者用smoothstep的三参形式强化曲线。

3.3 用fract实现循环无缝扫过

bandCenter = fract(cc_time.x * u_speed) - 0.5;

fract取小数部分,让bandCenter永远在[0,1)之间循环,减去0.5后变成[-0.5,0.5)。也就是说,流光中心从图片最左边扫到最右边,然后瞬间回到左边。这个跳变肉眼看不见,因为回到左边时,上一轮的d值已经大于u_width,光带完全消失,所以看起来就是无缝循环。

如果想要“从右往左扫”,只需要把speed设成负数。cc_time.x * (-0.5)会让bandCenter递减,等效于反向扫描。

3.4 让流光只在指定范围出现一次

有时候光带不需要循环,比如一个新手引导箭头,只希望光从左边扫到右边,然后停下来。可以把cc_time.x * u_speed用clamp包住:

float progress = clamp(cc_time.x * u_speed, 0.0, 1.0); float bandCenter = progress - 0.5;

这样光带到右边后就不动了。如果你想让它扫到右边后淡出,还可以配合mix或者乘以一个fade系数。我一般把这个逻辑封装成第二个effect,留作特定任务用。

4. 在CocosCreator里把流光挂到精灵上

4.1 创建材质面板

先在工程Assets目录下准备好前面那个effect文件,命名为flow-light.effect。然后在编辑器中右键Assets,选择“创建 -> Material”,给它起名FlowLightMat。在Material的属性检查器里,把Effect选成flow-light。

CocosCreator会自动读取CCEffect里的properties,并生成对应的参数输入框。你会看到:

  • texture:拖入你要加流光的精灵原图
  • u_speed:默认0.5
  • u_width:默认0.1
  • u_angle:默认0
  • u_flowColor:默认白色
  • u_flowStrength:默认1

把材质保存后,拖到任意Sprite节点的CustomMaterial槽位(2.x叫CustomMaterial,3.x叫customMaterial,注意勾选Enable)。如果要整棵树的所有Sprite都使用同一个材质,直接拖到父节点的属性上即可。

4.2 用编辑器调参:一个实时预览的工作流

我强烈建议调参时不要靠猜,把场景编辑器的“动画编辑器”或者Game视图保持在运行状态,手动改材质面板里的参数,每改一个数字都立刻能看效果。

调试顺序一般是这样:

  • 先把u_speed设成0,确认光带静止在某个位置
  • 观察光带位置,调整u_angle让光带方向符合预期
  • 调整u_width到合适的视觉宽度
  • 最后把u_speed恢复到0.3~1.0范围,确认速度合适
  • 调整u_flowStrength控制整体亮度

我的习惯是把u_speed一开始设在0,这样更容易看清当前光带宽度是否均匀。如果你发现光带宽度在图片两端有差异,多半是UV中心或角度问题。

4.3 多Sprite复用时的注意事项

如果多个Sprite使用同一个材质,它们的流光完全同步,因为所有像素读的cc_time.x是一样的。如果你希望不同卡片上的流光有先后错开,就需要为每张卡片设置一个u_offset属性,或者用节点脚本给每个材质实例设置独立的uniform。

方法很简单:在properties里增加一个u_offset:

- u_offset: { value: 0.0, editor: { tooltip: "流光相位偏移" } }

然后在fs里改成:

float bandCenter = fract(cc_time.x * u_speed + u_offset) - 0.5;

运行时可以通过material.setProperty('u_offset', randomValue)给每个节点设置不同的偏移值。这样就能做出“不同卡片轮流被光扫过”的效果。

5. 实际项目中的踩坑记录:从“能跑”到“能上线”

5.1 非正方形贴图导致的光带宽度不均匀

这是新手最容易踩的坑。UV坐标是归一化的,长宽的纹理在UV空间里都是0~1,但像素密度不同。一个512×128的横幅,横向UV长度和纵向UV长度在像素上差4倍。如果你用旋转后的uv.x作为光带位置,光带宽度在水平扫过和垂直扫过时实际像素宽度完全不同。

解决思路有两个:

  • 第一个是给effect传入一个宽高比u_aspect,在Shader里把UV的x或y方向缩放后再计算
  • 第二个是直接使用textureSize这类函数获取纹理像素尺寸(CocosCreator 3.x的高版本支持,2.x不一定)

以传u_aspect为例,在fs里这样处理:

uniform float u_aspect; // 图片像素宽 / 图片像素高 vec2 uv = (v_uv0 - vec2(0.5, 0.5)) * vec2(u_aspect, 1.0);

这样UV在x方向上被等比放大,光带宽度在视觉上更接近“像素均匀”。在编辑器里给材质设置一个合适的u_aspect即可。运行时也可以从Sprite的node.getComponent(UITransform).contentSize拿到宽高,动态设置。

5.2 合图时的流光中心偏移

如果你的图标放在图集(SpriteFrame合图)里,v_uv0不是整个纹理的0~1,而是图集中某个子区域的局部UV。此时再从v_uv0 - 0.5去计算中心,得到的是图片区域局部坐标还是纹理坐标的半个?严格说不再准确,流光中心会偏心。

CocosCreator内置的2D渲染管线和自定义Shader在合图时会给v_uv0传入原始的局部UV,它已经经过图集UV偏移处理,通常指向的是子图内部的UV范围。比如一个精灵图,它的v_uv0可能从(0.2,0.3)到(0.4,0.5)。这时v_uv0 - 0.5就不是子图中心了。

要完美适配,就需要在CPU端把SpriteFrame的uv偏移量传进去,或者把图片去掉图集单独使用。我的做法是:流光材质只给独立纹理的Sprite用,图集里的图标尽量用第二套纯Shader方案,或者直接让美术把要流光的图从图集里分出来。对于卡牌这种高频UI,单独图片的加载成本完全可以接受。

5.3 真机上出现锯齿或闪线

移动端GPU精度有限,highp在某些旧设备上会退化成mediump,导致极窄流光带边缘出现闪烁。解决方案:

  • 把u_width的最小值限制在0.02以上,不要用极细的光带
  • 如果追求极细的闪光,可以改用exp(-pow(d / width, 2))这样的高斯曲线,抗锯齿效果更好
  • 必要时在真机上用precision mediump float测试,观察表现

另外,如果cc_time.x特别大(比如挂了很久的游戏),frac的浮点精度会下降。解决办法是手动做时间模1处理:把cc_time.x * u_speed先做mod(x, 3600.0)再传入,或者每帧在脚本里把时间参数做一个wrap。

5.4 性能开销:真的可以忽略吗

如果只是几十个Sprite用同一个材质,性能没有任何压力。但如果是一屏几百个卡牌同时流光,每个都执行smoothstep和fract,虽然有GPU并行,但片元数量会变得巨大。这时候可以考虑:

  • 把流光做成一个“遮罩层”叠加在UI上,而不是让每张卡都跑Shader
  • 用顶点着色器做粗略的光带位置处理,片元只做插值
  • 限制流光只能在卡牌进入屏幕时启用,离开屏幕则自动关闭材质

我在某项目里做了200张列表卡片同时流光的压力测试,在低端Android上帧率从60掉到50左右,后来把流光的u_width从0.05改成0.08,并降低整体分辨率叠加量,最终恢复流畅。流光看似只是加法,但高强度批量使用仍然要谨慎。

6. 进阶扩展:从一道光到花式流光

6.1 多重流光叠加

单个光带太单调,那就叠两个。把流光计算封装成一个函数,再按不同角度、速度、宽度分别调用,最后把结果累加。

在fs里这样写:

float flowMask(vec2 uv, float angle, float speed, float width) { float c = cos(angle); float s = sin(angle); vec2 ruv = vec2(uv.x * c - uv.y * s, uv.x * s + uv.y * c); float center = fract(cc_time.x * speed) - 0.5; float d = abs(ruv.x - center); return 1.0 - smoothstep(0.0, width, d); } void main() { vec4 texColor = texture2D(texture, v_uv0); vec2 uv = v_uv0 - vec2(0.5, 0.5); float m1 = flowMask(uv, 0.0, 0.4, 0.08); float m2 = flowMask(uv, 0.785, -0.6, 0.06); vec3 flowColor = u_flowColor.rgb * (m1 + m2) * u_flowStrength; vec4 finalColor = texColor; finalColor.rgb += flowColor * texColor.a; gl_FragColor = finalColor; }

注意叠加后的总强度可能超过1,导致颜色过曝。如果过曝,用m1 + m2后再乘以0.5,或者用min(m1 + m2, 1.0)限制。美术如果不想过曝,可以保留高光过曝感,看具体需求。

6.2 用第二张纹理做局部遮罩

很多流光希望只在图标的某块区域出现,比如图标上有一个金属边框,流光只沿边框走。这时可以用一张遮罩贴图,在fs里额外采样:

uniform sampler2D maskTex; float mask = texture2D(maskTex, v_uv0).r; finalColor.rgb += flowColor * texColor.a * mask;

这个mask可以是一个黑白图,白色区域允许流光通过,黑色区域完全阻挡。这样做的好处是,即使原图是不规则形状,高光也不会跑到透明区或不想照亮的部分。代价是多一次纹理采样,影响很小。

6.3 彩色流光:让RGB三通道分别偏移

默认流光只有一个颜色。做彩色流光时,可以让RGB三个通道各自独立延迟:

float mR = flowMask(uv, u_angle, u_speed, u_width); float mG = flowMask(uv, u_angle, u_speed * 1.1, u_width * 0.9); float mB = flowMask(uv, u_angle, u_speed * 1.2, u_width * 0.8); vec3 color = vec3(mR, mG, mB); finalColor.rgb += color * texColor.a * u_flowStrength;

由于RGB三色速度不同,流光会呈现出类似彩虹的拖尾,非常适合科技感UI。如果只想保持一个色调但带渐变,可以直接把u_flowColor替换成一个从左上到右下的渐变向量,利用UV坐标插值:

vec3 gradientColor = mix(vec3(1.0, 1.0, 1.0), u_flowColor.rgb, uv.y + 0.5);

6.4 结合进度/技能冷却的流光

如果你在做一个进度条或者技能图标冷却,可以让流光始终停留在进度值的边缘。原理很简单:把流光中心从时间函数改成u_progress:

float bandCenter = u_progress - 0.5; float d = abs(ruv.x - bandCenter); float light = 1.0 - smoothstep(0.0, u_width, d);

接着在脚本里每帧更新u_progress,流光就会跟着进度值走。这个技巧在卡牌冷却、雷达扫描、技能CD上非常实用。

写在最后

流光效果是Shader入门里回馈感最强的一类效果。它没有太多复杂的数学,只需要理解UV、时间、距离衰减这三个概念,就可以做出很不错的UI反馈。我在项目里做卡牌高光时,从Tween方案换成Shader方案后,不仅减少了一张光条贴图,还让角度的调整变成纯参数操作,策划自己就能调。

如果你正在学CocosCreator的Shader,建议先亲手把这段代码跑起来,然后改改speed、width、angle,观察光带的变化,再慢慢加多流光、遮罩、渐变。等你可以不看参考就写出不同方向的多重流光时,离真正理解Shader也就不远了。下一篇如果继续写这个系列,我会聊聊边缘发光或者溶解效果。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:26:43

AnyPS5:PS5画面串流与输入重定向的跨设备流转方案

1. 从“AnyPS5”这个名字说起&#xff1a;它到底想解决什么问题 第一次看到“AnyPS5”这个标题&#xff0c;我脑子里蹦出来的第一个念头是&#xff1a;这大概率不是一个官方项目&#xff0c;而是一个带着强烈个人色彩的命名。为什么这么说&#xff1f;因为“Any”这个前缀在技术…

作者头像 李华
网站建设 2026/10/14 8:26:08

主流大模型API聚合平台横评:七牛云AI、硅基流动、百炼、千帆、火山方舟对比

把多家大模型厂商的推理能力统一为标准接口的云端中间层,就是 API 聚合平台:一个 API Key 加一个 base URL,就能在模型之间自由切换。它的三大优势很直接——接口统一(多数兼容 OpenAI 格式,代码只改一行)、模型灵活(降低单一厂商依赖)、成本可控(部分小模型永久免费)。本文横向…

作者头像 李华
网站建设 2026/10/14 8:26:02

litellm大模型网关实战:一套配置接入所有模型,终结API碎片化

最近有个朋友跑来问我&#xff1a;项目要接好几家大模型的接口&#xff0c;业务代码快被各家 SDK 的差异搞疯了&#xff0c;有没有什么干净的办法。我第一反应就是&#xff1a;先把 litellm 装上再说。这不是广告&#xff0c;是我自己在大模型应用里被 API 碎片化狠狠折腾过之后…

作者头像 李华
网站建设 2026/10/14 8:25:57

AI视频预演:7种三维约束技术让生成可控化

1. 为什么“AI视频抽卡”正在毁掉你的创作节奏 最近两周&#xff0c;我连续帮三个做短视频的团队重做了分镜流程。他们共同的问题不是脚本不行、演员不稳&#xff0c;也不是设备不够——而是反复卡在同一个环节&#xff1a;用AI生成视频时&#xff0c;像在玩概率游戏。有人形容…

作者头像 李华
网站建设 2026/10/14 8:25:29

8089张野生动物YOLO+VOC双格式开箱即用数据集

简介&#xff1a;目标检测是计算机视觉的基础任务&#xff0c;其性能高度依赖高质量、格式规范、跨框架兼容的标注数据。YOLO与VOC作为工业界最主流的两种标注格式&#xff0c;常因编码混乱、坐标越界、类别不一致、文件名不匹配等问题导致训练失败或指标异常。本文聚焦野生动物…

作者头像 李华
网站建设 2026/10/14 8:25:12

实测:模型输出裹了代码围栏,解析器能救回来吗

版权与内容来源声明 本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容&#xff0c;均在附表 A 中标注来源&#xff1b;引用官方原文保持原样&#xff0c;不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准&#xff0c;标注「待验证」的部…

作者头像 李华