Cocos Creator Shader 实现波浪进度条

波浪进度条由两部分组成:

  • WaveProgressFill.effect:顶点与片元 shader,负责绘制动态液面;
  • WaveProgressComponent.ts:加载材质、更新进度,并持续把时间传给 shader。

它不依赖节点裁切或序列帧,而是直接在 Sprite 的 UV 坐标中计算液面,因此可以适配不同形状和尺寸的进度图。

核心思路

普通的从底部填充进度条,可以理解为:

glsl 复制代码
fillY <= progress

其中 progress 范围为 0 到 1。

为了让水平液面变成波浪,只要给进度值叠加一个随横坐标和时间变化的正弦函数:

glsl 复制代码
float wave = sin(uv0.x * waveFreq + time * waveSpeed) * waveAmp;
float fillLine = clamp(progress + wave, 0.0, 1.0);

参数含义:

  • uv0.x:当前像素的横向 UV 坐标;
  • waveFreq:波浪频率,越大波峰越密;
  • time:累计时间;
  • waveSpeed:波浪移动速度;
  • waveAmp:波浪上下摆动幅度;
  • progress:当前进度;
  • fillLine:当前横向位置上的液面高度。

项目中使用:

glsl 复制代码
fillY = 1.0 - uv0.y;

这样 Sprite 底部为 0、顶部为 1,更符合"从下向上填充"的进度语义。

Effect 参数设计

Shader 中定义了这些可调参数:

参数 默认值 作用
progress 0.0 填充进度,范围 0~1
time 0.0 波浪动画时间
waveAmp 0.02 波浪振幅
waveFreq 21.0 波浪频率
waveSpeed 5.0 波浪滚动速度
edgeSoftness 0.005 液面边缘柔和程度
crestWidth 0.028 液面高光带宽度
crestStrength 0.18 高光强度
vertexWaveAmp 3.0 顶点起伏幅度

这些属性写在 Cocos Creator Effect 的 properties 中,因此可以直接在材质面板调节。

使用 smoothstep 制作柔和液面

如果直接判断:

glsl 复制代码
fillY < fillLine

液面边缘会很生硬,容易产生锯齿。

因此使用 smoothstep 生成平滑遮罩:

glsl 复制代码
float fillMask =
    (1.0 - smoothstep(
        fillLine - edgeSoftness,
        fillLine + edgeSoftness,
        fillY
    )) * fillStart;

结果是:

  • 液面下方:fillMask 接近 1;
  • 液面上方:fillMask 接近 0;
  • 液面附近:透明度平滑过渡。

最终把遮罩乘到纹理 alpha 上:

glsl 复制代码
vec4 outputColor = vec4(
    texColor.rgb,
    texColor.a * fillMask
);

这样即使 Sprite 本身带有透明区域或异形轮廓,也能保持原始形状。

给液面增加高光

仅有波浪边缘时,视觉上会比较平。可以在液面附近增加一条高光带:

glsl 复制代码
float crestMask =
    smoothstep(fillLine - crestWidth, fillLine, fillY) * fillMask;

vec3 crestColor =
    texColor.rgb + vec3(crestStrength * crestMask);

vec4 outputColor = vec4(
    mix(texColor.rgb, crestColor, crestMask),
    texColor.a * fillMask
);

这种做法不是替换纹理颜色,而是在原颜色基础上轻微增亮,因此不会破坏原有美术资源。

顶点阶段的轻微起伏

片元 shader 已经能完成波浪裁切。不过项目中还在顶点 shader 中,对液面附近的顶点做了轻微偏移:

glsl 复制代码
float surfaceBand =
    smoothstep(
        fillLine - crestWidth * 2.0,
        fillLine - edgeSoftness,
        fillY
    ) * fillMask;

float vertexOffset =
    sin(uv0.x * waveFreq + time * waveSpeed)
    * vertexWaveAmp
    * surfaceBand;

pos.y += vertexOffset;

这里的关键是 surfaceBand:它把位移限制在液面附近,避免整个填充区域一起上下抖动。

需要注意,顶点位移依赖 Sprite 网格的顶点数量。普通四边形 Sprite 的顶点较少,因此主要效果仍然由片元阶段的透明度遮罩提供;顶点位移只是额外增强液面的动态感。

TypeScript 组件如何驱动 Shader

组件挂在带有 Sprite 的节点上,负责加载材质并设置参数。

加载材质后,不应直接共用材质资源,而是创建独立副本:

ts 复制代码
const clonedMaterial = new Material();
clonedMaterial.copy(material);

this.sprite.customMaterial = clonedMaterial;

这样每个进度条都能拥有独立的 progresstime,不会出现多个进度条同步变化的问题。

更新进度时:

ts 复制代码
public setProgress(progress: number): void {
    this.currentProgress = Math.max(0, Math.min(1, progress));
    this.waveMaterial.setProperty('progress', this.currentProgress);
}

每帧更新波浪时间:

ts 复制代码
protected update(): void {
    if (!this.waveMaterial) {
        return;
    }

    const time = director.getTotalTime() * 0.001 - this.waveStartTime;
    this.waveMaterial.setProperty('time', Math.max(0, time));
}

使用时只需要:

ts 复制代码
const wave = node.getComponent(WaveProgressComponent)
    || node.addComponent(WaveProgressComponent);

wave.setup({
    bundleName: GameDef.base_common,
    materialPath: 'material/WaveProgressFill',
    progress: 0.35,
});

wave.setProgress(0.72);

业务层只负责传入 0~1 的进度值,波浪的持续运动完全交给 shader。

常见问题

进度方向反了

重点检查:

glsl 复制代码
fillY = 1.0 - uv0.y;

如果你的 UV 坐标已经是"底部为 0",则可能应直接使用:

glsl 复制代码
fillY = uv0.y;

另外,RenderTexture 采样时也要注意 UV 翻转。

多个进度条一起动

通常是多个 Sprite 共用了同一个材质实例。解决办法是加载后执行:

ts 复制代码
const clonedMaterial = new Material();
clonedMaterial.copy(material);

每个组件使用独立材质。

进度为 0 时还残留波纹

不要只写硬判断。应通过 fillStartsmoothstep 控制零进度边界,避免波浪在空状态下漏出。

波浪不动

检查:

  • 材质是否已异步加载完成;
  • 节点是否激活,组件的 update() 是否执行;
  • time 是否持续传入;
  • waveSpeed 是否太小。

可扩展方向

当前效果只有一层正弦波。还可以继续扩展:

  • 叠加两组不同频率和速度的波浪,获得更自然的液体效果;
  • 使用独立颜色制作毒液、魔法能量或海水效果;
  • 用 tween 平滑驱动 setProgress(),让数值增长也更自然;
  • 根据进度动态修改波浪振幅,例如低进度时平稳、满进度时更活跃。

总结

波浪进度条的关键,是把普通的进度线转换为 UV 空间中的动态正弦液面:

glsl 复制代码
fillLine = progress + sin(x + time) * amplitude;

再通过 smoothstep 制作柔和边缘,用原纹理 alpha 与填充遮罩相乘实现异形裁切,并由组件持续更新 progresstime

这种方案渲染逻辑集中在 shader 内,业务层只维护进度值,适合在 Cocos Creator 中复用到经验条、等级图标、体力条或各类能量填充 UI。

相关推荐
郝学胜-神的一滴3 天前
Horse3D 游戏引擎研发笔记(七):Clydesdale——从流式日志到多输出订阅
c++·qt·unity·游戏引擎·图形渲染·unreal engine·opengl
杨丰玮4184 天前
从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(五)
java·python·游戏·游戏引擎·图形渲染·动画·贴图
郝学胜-神的一滴6 天前
[简化版 GAMES 104] 现代游戏引擎 06:从Tick时序到邮局模型,拆解确定性世界的底层密码
开发语言·c++·游戏引擎·图形渲染·软件开发·opengl
烧酒同学7 天前
【C++】记录size of std::vector的巧妙坑
开发语言·c++·图形渲染
玖釉-7 天前
nvpro_core2 源码与架构解析:NVIDIA Vulkan 图形开发基础框架
c++·windows·图形渲染
SNAKEpc1213812 天前
OpenGL(十六)- 着色器统一值
c语言·c++·线性代数·算法·矩阵·图形渲染·着色器
SNAKEpc1213812 天前
OpenGL(十三)- Mip贴图
c语言·c++·图形渲染·贴图
SNAKEpc1213813 天前
OpenGL(十五)- 着色器语言GLSL
c语言·c++·线性代数·算法·矩阵·图形渲染·着色器
郝学胜-神的一滴17 天前
中级OpenGL教程 030:gl_FragCoord解锁区域渲染与深度可视化新姿势
c++·unity·游戏引擎·图形渲染·unreal engine·opengl
SNAKEpc1213818 天前
OpenGL(十一)- 变换管线
c语言·c++·算法·矩阵·图形渲染