Cocos Creator Shader 入门 ⒆ —— UV 扰动动画

💡 本系列文章收录于个人专栏 ShaderMyHead

💡 本文案例可以在 Github 上进行演示

在游戏视觉表现中,UV 扰动是一种常见却强大的渲染技巧,它通过对纹理坐标进行动态采样与扭曲,模拟出水面波动、热浪蒸腾、空间扭曲等实时视觉效果。

无论是战斗技能释放时引发的空间变形,还是环境中流动的液态表面,UV 扰动都能显著增强画面的动态质感与沉浸感,让视觉叙事更富表现力。


水面波动


空间变形

本文将介绍如何在 Cocos Creator 着色器中实现 UV 扰动动画。

一、噪声纹理扰动

我们先来实现了一个基于噪声纹理的 UV 扰动动画,该方案非常适合用于模拟水面波动、热浪扭曲、技能特效等视觉效果。

其实现的核心是 ------ 通过采样一张噪声纹理(见下图),利用其 R 和 G 通道分别控制 UV 在 X 和 Y 方向上的偏移量,再结合时间和强度参数,构造出一个动态的扭曲效果。

留意这张纹理图在 Y 方向上是可平铺的,而且它不是纯黑白色阶的图片(而是带有一点淡蓝色),这意味着其 R 和 G 通道的值会略有不同。

我们先初始化相关参数,采样噪声纹理的 R、G 通道值,乘以强度系数 strength 后叠加到 UV 上进行扰动:

js 复制代码
CCProgram fs %{
  precision highp float;

  in vec2 uv;

  uniform UBO {
    float strength;  // 控制扰动强度,默认为 0.02
  };

  #pragma rate inputTexture pass
  uniform sampler2D inputTexture;    // 源纹理

  uniform sampler2D noiseTexture;    // 噪声纹理

  vec4 frag () {
    // 读取噪声图,后续用 R 通道控制 X 方向扰动,G 通道控制 Y 方向扰动
    vec2 noise = texture(noiseTexture, uv).rg;
    
    // 扰动后的 UV
    vec2 distortedUV = uv + noise * strength;

    // 使用扰动后的 UV 采样源纹理
    return texture(inputTexture, distortedUV);
  }
}%

💡 本案例使用的后处理管线方案(而非 Render Texture),在摄像头离屏画面上做的扰动处理,因此这里使用的是 inputTexture 来作为源纹理采样器(而非 cc_spriteTexture)。

此时的画面会出现轻微的静态扭曲,但没有任何动态的视觉效果。

这是因为我们在噪声图上采样的 UV 坐标,使用的是源纹理 UV,相当于仅是把噪声纹理拉伸到整个画面,每个画面上的像素,都是被一一对应的、固定的噪声像素干扰。

因此我们需要打破这种「一一对应」的映射关系,其关键就是让噪声纹理采样的 UV 动起来:

js 复制代码
    // 用时间滚动噪声
    vec2 noiseUV = fract(uv + vec2(0.0, time));

    // 读取噪声图,后续用 R 通道控制 X 方向扰动,G 通道控制 Y 方向扰动
    vec2 noise = texture(noiseTexture, noiseUV).rg;

其中 time 是由外部组件脚本传入的时间累加变量(留意后处理管线无法使用 cc_time),它会动态修改噪声纹理的 V 值,搭配 fract 函数会将 V 值限定在 [0, 1] 的范围内不断变化,视觉表现为「噪声纹理在 Y 轴平铺,且不断地向上运动」。

这样噪声纹理对 UV 的扰动就是动态的了。

另外鉴于 noise 的区间在 [0, 1],即 noise 的取值为非负数,会导致对 UV 的扰动实是在单一方向上的。我们可以将它的区间调整到 [-1, 1],使得扰动可以是任意方向(正/负):

js 复制代码
    // 映射 [-1,1],使得扰动可以是任意方向(正/负)
    noise = noise * 2.0 - 1.0;

此时执行项目,可以获得一个动态的热浪扭动空间的效果:

二、VFX 帧动图扰动

2.1 VFX 介绍

VFX 是 Visual effects(视觉特效)的缩写,该术语用于描述在各类电影、游戏或其他媒体所创建、增强的图像(或动画)。

在游戏领域,角色施展技能时伴随的酷炫光效,便是一种 VFX(一般由 AfterEffect、Spine 等专业工具制作):

本文案例需要使用单张的 VFX 大图(俗称雪碧图,即把所有帧合到一张大图上),读者可以自行在网上搜索 "VFX sheet" 获取相关资源。

笔者在某平台上获取了一张 VFX 雪碧图,其原图体积达到了惊人的 6 MB。通过大幅减少其尺寸和压缩(使用 TinyPNG),最终将其控制在 22 KB:

我们会利用这张雪碧图来实现 VFX 扰动 UV 的动画,来实现战斗过程中挥动的武器扭曲周围空间的类似效果:

2.2 着色器实现

VFX 帧动画的核心实现主要有两个:

  • 将序列帧动画的当前帧作为扭曲图,这意味着要想办法拆解雪碧图,并按需抽出当前帧;
  • 利用该帧的颜色数据(RGB)来计算 UV 偏移量,从而对原始画面进行动态的扭曲。

2.2.1 获取当前帧

我们逐步来实现相关的功能,首先是初始化参数并获取当前帧的索引:

js 复制代码
CCProgram fs %{
  precision highp float;

  in vec2 uv;

  uniform UBO {
    vec2 cells;       // 默认为 [6.0, 3.0],表示雪碧图有 3 行 6 列,共 18 帧
    float fps;        // 默认为 30.0,动画帧率
    float time;       // 后处理管线无法使用内置的 cc_time,需要从外部传入
  };

  #pragma rate inputTexture pass
  uniform sampler2D inputTexture;

  uniform sampler2D vfxTexture;    // VFX 帧动画雪碧图

  vec4 frag () {
    float row = cells.x;  // 雪碧图行数
    float col = cells.y;  // 雪碧图列数
    
    float index = floor(time * fps);  // 计算当前帧的索引
  }
}%

鉴于 time 表示当前游戏执行的累计时间,单位为秒,因此乘以一个「每秒执行多少帧」的 fps 变量,就能获得 VFX 帧动画当前帧的索引 index

假设我们的 VFX 动画是要平铺在摄像机离屏画面上的(即无需缩放),那么即算出了帧索引,又知道雪碧图是分为几行几列,就可以算出当前像素在雪碧图上的 UV 坐标是多少:

js 复制代码
// 计算当前像素在雪碧图上的 UV 坐标
vec2 animOffset = vec2(mod(index, col) / col, floor(index / col) / row);
vec2 animUV = uv / cells.yx + animOffset;

其中 uv / cells.yx 将整个屏幕 UV 映射到雪碧图中的一个单元格内,再通过 vec2(mod(index, col) / col, floor(index / col) / row) 计算当前帧在雪碧图中的偏移量:

  • mod(index, col) 表示当前帧在第几列;
  • floor(index/col) 表示当前帧在第几行。

此时如果直接返回对雪碧图的采样结果,可以获得当前帧的动画:

js 复制代码
// 采样当前帧
vec4 vfxColor = texture(vfxTexture, animUV);

return vfxColor;

2.2.2 计算 UV 偏移量

与上一节噪音纹理扭曲 UV 的方案类似,我们依旧可以通过 VFX 纹理的 R、G 通道值,来作为 UV 偏移量的基准:

js 复制代码
vec2 offset = (vfxColor.rg - 0.5) * 2.0 * strength;

留意 (vfxColor.rg - 0.5) * 2.0 依旧是将通道值的区间从 [0, 1] 映射到 [-1, 1],使得扰动可以是任意方向(正/负)。

为了不让偏移量过大,我们额外加上了一个强度系数 strength(默认值为 0.05)进行调整。

此时 VFX 动画帧纯黑的像素偏移值为 0,越亮的的像素偏移值越高,我们利用这个偏移值对源纹理进行采样:

js 复制代码
    vec2 offset = (vfxColor.rg - 0.5) * 2.0 * strength;

    return texture(inputTexture, uv + offset);

返回的画面如下:

可以看到 VFX 明亮的光效部分确实扭曲了画面,但扭曲的部分过于厚重,缺乏 VFX 特效的纹理和轻盈感。

我们可以进一步优化偏移值 offset,让它乘以 VFX 特效的灰度值,来让特效中明亮的地方扭曲的更强烈、暗的地方扭曲的较弱,更符合能量强度的视觉直觉:

js 复制代码
    // 计算灰度值
    float gray = vfxColor.r * 0.299 + vfxColor.g * 0.587 + vfxColor.b * 0.114;

    vec2 offset = (vfxColor.rg - 0.5) * 2.0 * strength * gray;  // 乘以灰度值

另外叠加上 40% 的 VFX 特效色值,可以在扭曲空间的基础上附加发光、能量的视觉效果:

js 复制代码
return texture(inputTexture, uv + offset) + vfxColor * 0.4;

最终效果:

💡 读者可以在线上演示页查看更多动画细节。

相关推荐
Scarlett5 天前
初识cocos,实现《FlappyBird》h5游戏
前端·cocos creator
VaJoy7 天前
Cocos Creator Shader 入门 ⒅ —— 流光动画
cocos creator
成长ing1213811 天前
cocos creator塔防路线 运动路线的编辑和录制
前端·cocos creator
VaJoy12 天前
Cocos Creator Shader 入门 ⒃ —— 有向距离场 SDF
cocos creator
VaJoy19 天前
Cocos Creator Shader 入门 ⒂ —— 自定义后处理管线
前端·cocos creator
Thomas游戏开发22 天前
Cocos Creator 面试技巧分享
面试·微信小程序·cocos creator
IkeShyZz22 天前
cocos creator android项目接入deeplink问题总结
cocos creator
VaJoy1 个月前
Cocos Creator Shader 入门 ⒀ —— UBO 内存布局策略
cocos creator
成长ing121381 个月前
cocos creator 3.x shader 流光
前端·cocos creator