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;

最终效果:

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

相关推荐
LcGero20 天前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero21 天前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero22 天前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero23 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero23 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero25 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei25 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
CodeCaptain4 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy5 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator