尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Cocos Shader抖动实现指南:顶点位移、UV扰动与故障特效

发布时间:2026/9/29 15:32:01

资讯中心
01
ARTICLE

Cocos Shader抖动实现指南:顶点位移、UV扰动与故障特效

Cocos Shader抖动实现指南:顶点位移、UV扰动与故障特效
“cocos shader抖动”这个话题其实我一开始是有点抗拒的。因为大多数情况下游戏里的“抖”根本不该用 shader 做直接 Tween 移动节点就行便宜又见效。直到后来做卡牌战斗的受击反馈策划要求怪物在挨打的那一帧出现“身体不动、贴图乱抖”的故障感用节点位移怎么做都不对味。这才老老实实把抖动写进 shader走顶点、片元两条路线分别折腾了一遍才搞清楚这东西的边界在哪里。这篇就聊 Cocos Creator 里用 shader 做抖动的完整思路什么时候该上 shader、effect 文件怎么写、正弦抖动和噪声抖动分别怎么玩、故障块怎么做以及我实测踩过的坑。内容以 Cocos Creator 3.x 的语法为主线2.x 的老法师可以参考括号里的差异思路完全通用。1. 抖动需求拆解为什么要把这个效果做到 Shader 里先把丑话说在前面并不是所有“抖动”都值得写进 shader。你只要一个圆形图标挨打后震两下老老实实 Tween 就够用上 shader 反而给自己找麻烦。但有些场景是纯节点位移搞不定的。1.1 三种常见实现方式对比我在项目里把抖动方案分成了三类各有各的使用边界实现方式常见做法优势坑点节点位移/旋转Tween 改变 position、rotation实现简单成本最低动效可控性好改变包围盒、点击热区物理体跟着动逻辑层容易误判Shader 顶点抖动在顶点着色器里改顶点坐标渲染层抖动节点逻辑层不受影响适合表现“画面坏了”效果越界后穿帮需要一个可调的幅度限制Shader UV 抖动在片元着色器里改采样 UV不动几何体只有贴图内容在揉搓水波纹、故障屏首选采样会越界导致闪出透明或拉伸幅度必须压小我自己划分的标准很简单如果希望“这个物体本身还在原地但视觉上已经不正常”那必须上 shader。比如角色被眩晕身体不能乱飘还要吃技能判定但画面里整个人在重影抖动这时候在顶点阶段做高频低幅度抖动就非常合适。如果只是镜头震一下、UI 弹一下那还是用节点的 Tween 或摄像机的位移别折腾 shader。1.2 适合用 Shader 抖动的实际场景先看几个典型场景你在做项目时可以直接对号入座。受击后的故障反馈是我最常遇到的。玩家打中怪物怪物做一个 0.1 秒左右的抖动同时身体颜色闪白。如果直接移动节点的 position会出现两个问题一是怪物模型的包围盒也动了游戏逻辑里射线检测、技能命中判定会跟着漂二是如果怪物身上挂了粒子系统粒子会跟着节点一起甩出去特别容易穿帮。而顶点 shader 抖动只影响渲染层的网格位置碰撞体纹丝不动粒子系统也可以选择独立挂接整体就稳很多。第二个场景是故障屏和电击效果比如 UI 上突然出现信号干扰或者角色被电击时屏幕边缘出现撕裂感。这种效果适合用 UV 抖动来做因为你要的不是“画面里的某一个东西在动”而是“整块画面在视觉上出问题”。对 UI 节点做 UV 扰动能让角色的纹理边界出现偏移和模糊屏幕就有了那种受干扰的质感。第三个场景是大招震屏。有些项目会把整个场景的摄像机位置拿来做 Sin 波抖动这个效果直观且低成本但如果你把镜头移动幅度调大场景边缘和远景背景之间容易露出一条缝。这时候我更推荐把抖动写到摄像机的后处理 shader 里直接对屏幕像素做偏移既不会露出边缘还能顺便做震屏模糊。2. 动手前先看懂 .effectCocos Creator 的 Shader 文件结构很多朋友一听说写 shader 就打退堂鼓其实 Cocos Creator 的 effect 文件没有想象中那么难。它的结构非常固定你只要记住“一套材质 一个属性声明 一个顶点着色器 一个片元着色器”就够了。2.1 Cocos Creator 3.x 的 effect 整体骨架下面是一个最精简的 effect 文件功能是给精灵纹理加透明度混合CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true src: srcAlpha dst: oneMinusSrcAlpha rasterizerState: cullMode: none properties: texture: { value: white } shakeAmp: { value: 0.02, editor: { tooltip: 抖动幅度 } } }% CCProgram vs %{ precision highp float; #include cc-global #include cc-local in vec3 a_position; in vec2 a_uv0; in vec4 a_color; out vec2 v_uv0; out vec4 v_color; void main() { vec4 worldPos cc_matWorld * vec4(a_position, 1.0); gl_Position cc_matViewProj * worldPos; v_uv0 a_uv0; v_color a_color; } }% CCProgram fs %{ precision highp float; in vec2 v_uv0; in vec4 v_color; uniform sampler2D texture; void main() { vec4 o texture(texture, v_uv0) * v_color; gl_FragColor o; } }%这里面有几个关键结构你要慢慢嚼。CCEffect %{ ... }%是整个材质的元信息区里面声明了渲染管线要用的技术、Pass、混合状态、剔除状态以及我们可以从外部修改的 properties。properties 里的shakeAmp会在编辑器材质面板里显示成一个可调的数值也可以被代码通过setProperty动态修改。CCProgram vs是顶点着色器。它做的事情是把模型顶点从局部坐标变换到裁剪空间。如果你要改动一个模型的形状、大小、位置就改这里。cc_matWorld和cc_matViewProj是引擎内置的 MVP 矩阵不用自己传。CCProgram fs是片元着色器。它决定每个像素最终的颜色也就是“贴图内容怎么被采样、变成什么颜色”。要做 UV 扭曲、触摸扰动、像素故障就往这里写。2.2 内置的 cc_time 与 Shader 属性怎么用做抖动动画必须有时间变量。Cocos Creator 3.x 里通过#include cc-global拉入全局 uniform其中就有一个vec4 cc_timecc_time.x自应用启动后经过的秒数最常用cc_time.y全局时间以 60 秒为一个周期cc_time.z引擎内部时间适合做帧率冻结类效果我一般用cc_time.x * 速度作为抖动的时间驱动。注意一点cc_time.x是只增不减的所以如果你想做一个“受击后持续 0.3 秒停止”的衰减效果不能让 shader 自己维护时间状态必须在脚本侧控制幅度参数后面第 5 节会细讲。在 properties 里声明的属性会和 CCProgram 中同名 uniform 自动绑定。比如你在 properties 里写了shakeAmp在片元着色器里就可以这样取uniform ShakeParams { float shakeAmp; };使用时直接shakeAmp即可。属性名、uniform 名、脚本里 setProperty 的传参名三者必须完全一致否则会拿到默认值。2.3 5 分钟验证版一个最小可用的抖动材质先用一个最简单、最少代码的版本跑通流程。新建一个 effect 资源把材质拖给任意 Sprite然后把这个顶点着色器替换成下面这个CCProgram vs %{ precision highp float; #include cc-global #include cc-local in vec3 a_position; in vec2 a_uv0; in vec4 a_color; out vec2 v_uv0; out vec4 v_color; uniform ShakeParams { float shakeAmp; float shakeFreq; float shakeSpeed; }; void main() { vec3 p a_position; float t cc_time.x * shakeSpeed; float offset sin(p.y * shakeFreq t) * shakeAmp; p.x offset; vec4 worldPos cc_matWorld * vec4(p, 1.0); gl_Position cc_matViewProj * worldPos; v_uv0 a_uv0; v_color a_color; } }%sin(p.y * shakeFreq t)的意思是不同的高度 p.y 对应不同的正弦相位同一时刻不同位置的像素水平位移不一样这样整个精灵不是整体平移而是像软体一样左右扭动非常有“信号不稳”的质感。在 2.x 的 Cocos Creator 里上面代码有两处差异矩阵变量要改成matViewProj * matWorld采样函数要改成texture2D其他地方思路一样。3. 三种抖动实现路径顶点位移、UV 扰动、旋转缩放有基础概念之后我们来看三条主要的实现路径。你可以把它们理解成三种“抖动语言”顶点位移是让模型本身在空间里晃UV 扰动是让贴图内容在表面乱窜旋转缩放是让整体做一个失稳的甩动。三种能组合出非常丰富的反馈。3.1 顶点位移抖动让整个精灵跟着震顶点位移是性价比最高的做法也是我项目里用得最多的。它的核心就是在顶点着色器里给a_position加一个偏移量。上一节那个版本只是水平方向的扭动实际项目中我一般会用两个方向、多频率叠加避免看起来像机械波void main() { vec3 p a_position; float t cc_time.x * shakeSpeed; float waveX sin(p.y * shakeFreq t) sin(p.x * shakeFreq * 1.7 t * 3.0); float waveY cos(p.x * shakeFreq t * 1.5) sin(p.y * shakeFreq * 0.9 t * 2.3); p.x waveX * shakeAmp * 0.5; p.y waveY * shakeAmp * 0.5; vec4 worldPos cc_matWorld * vec4(p, 1.0); gl_Position cc_matViewProj * worldPos; v_uv0 a_uv0; v_color a_color; }为什么叠加两个不同频率的波因为单一正弦波的时间规律太强人眼很容易看出“再来一次”的节奏这是机械感的最大来源。把频率设定为 1.7、3.0、2.3 这类不规整倍数波形叠加后看起来就混乱了很多更像真实震动。这里有三个参数供你调试shakeAmp振幅单位是顶点局部坐标单位。Sprite 的 PPU 默认是 100所以一个 100 像素高的精灵顶点坐标范围大概是 0.5 到 -0.5。我第一次调的时候直接给了 0.1结果模型直接飞到屏幕外。建议先用 0.01 这个量级起步再慢慢加。shakeFreq空间频率决定波在模型表面上的密度。越大越像碎纸屑越小越像整块木板在晃。shakeSpeed时间速度决定波形变化的快慢。受击抖动我喜欢 6 到 10地震震屏用 2 到 4 反而更有厚重感。注意顶点位移的偏移量加到顶点上之后整个包围盒的渲染范围也会变化。虽然碰撞逻辑不受影响但如果你在 shader 之外还挂了一个用于裁剪的 Mask 组件边缘穿帮会非常明显需要把材质幅度控制在合理范围内。3.2 UV 扰动抖动画面内纹理抖动不动边界顶点位移解决的是“物体在空间里抖”UV 扰动解决的是“空间里的物体没动但贴图像被电磁干扰一样乱抖”。这是我最喜欢的一类效果因为看起来特别“魔法”。核心思路很简单在片元着色器里采样贴图之前先给 UV 坐标加一个偏移CCProgram fs %{ precision highp float; in vec2 v_uv0; in vec4 v_color; uniform sampler2D texture; uniform ShakeParams { float shakeAmp; float shakeFreq; float shakeSpeed; }; void main() { vec2 uv v_uv0; float t cc_time.x * shakeSpeed; uv.x sin(uv.y * shakeFreq t) * shakeAmp; uv.y cos(uv.x * shakeFreq t) * shakeAmp; vec4 o texture(texture, uv) * v_color; gl_FragColor o; } }%这段代码的物理含义是原本在 UV 坐标 (0.5, 0.5) 处采样的像素因为偏移去采了 (0.52, 0.48) 的附近像素视觉上就是贴图内容发生了局部揉搓。UV 扰动最典型的应用是做“水波感”和“信号感”。控制幅度非常重要因为 UV 坐标超出 0 到 1 之后采样会拉到贴图边缘或者透明区域闪出黑色或者透明破洞。我通常的做法是加一个 clampuv.x sin(uv.y * shakeFreq t) * shakeAmp; uv.y cos(uv.x * shakeFreq t) * shakeAmp; uv clamp(uv, vec2(0.0), vec2(1.0));这样抖动会稍微损失一点“溢出感”但至少不会出现难看的闪边。想要更好的边缘表现可以传一个textureSize进来手动做边缘反射但大部分游戏项目根本没必要做到那份上clamp 就够了。3.3 绕中心旋转 缩放抖动做出失控感第三种路线是让节点围绕自身中心做极小幅度的旋转再叠加一点缩放制造“这个东西快失控了”的感觉。很多 Boss 狂暴前的蓄力表现其实就是这种旋转抖动加上闪白。旋转抖动的核心是旋转矩阵。注意这里操作的是模型局部坐标大多数 Sprite 的中心就在原点所以可以直接转void main() { vec3 p a_position; float t cc_time.x * shakeSpeed; float angle sin(t * shakeSpeed) * shakeAmp; float c cos(angle); float s sin(angle); mat2 rot mat2(c, -s, s, c); p.xy rot * p.xy; p.z * 1.0 sin(t * shakeSpeed * 2.0) * shakeAmp * 0.5; vec4 worldPos cc_matWorld * vec4(p, 1.0); gl_Position cc_matViewProj * worldPos; v_uv0 a_uv0; v_color a_color; }这里面shakeAmp不再是像素位移而是旋转角的弧度。0.01 弧度大约 0.57 度视觉上是轻微摆动0.05 弧度接近 3 度已经有明显的失控感。缩放抖动我叠加在 z 轴上配合模型本身的比例能做出一种呼吸式的不稳定感。有一个最容易被忽略的坑如果精灵的锚点不在中心而是在左下角或者任意位置旋转矩阵会绕锚点转效果看起来就是整个图在画圈圈而不是原地晃动。这种情况要么在脚本里先给节点补一个中心偏移要么在 shader 里手动把顶点先平移到模型中心、旋转后再平移回来。对 Spite 节点锚点居中的情况来说直接旋转就是对的不用纠结。3.4 三合一衰减模型实战里三种抖动很少单独用更常见的做法是把它们组合成一个“震动模型”再搭配一个衰减因子。我常用的组合是顶点位移做主体振幅从 0.04 衰减到 0持续 0.3 秒UV 扰动做辅助只在最后 0.1 秒出现幅度 0.01 左右制造残余的“重影感”旋转抖动做一个 0.05 弧度的小摆动频率比位移高一倍让受击方看起来更慌乱衰减因子可以放进 shader由脚本每帧传入uniform ShakeParams { float shakeAmp; float shakeFreq; float shakeSpeed; float shakeDecay; };在顶点位移时给最终偏移乘上shakeDecay。脚本侧每帧让shakeDecay * 0.85大概 8 帧后衰减到 0.27肉眼就能感知“震感在快速消失”。4. 让抖动“不机械”噪声函数、故障块与帧冻结如果你只用正弦波做抖动前期可能有新鲜感但用久了会发现所有震动都长得一模一样因为正弦波太规律了。要做出“随机、混乱、故障”的感觉得引入噪声和区块化操作。4.1 为什么 sine 看起来“机械”直观解释正弦波是一个固定频率、固定振幅的完美周期函数人眼对周期非常敏感第一轮可能觉得是震动盯着看三秒就会发现它在“循环”。尤其是游戏里的受击反馈玩家看几十次动画流畅的循环感会直接摧毁“突变”的冲击力。所以我的习惯是正统地震类效果保留低频正弦受击、故障、电击这类突发反馈一律上噪声。4.2 用 hash 做随机抖动GPU 上最常用的伪随机函数是 hash。它不产生真正的随机但足够把视觉预期打乱。一个非常经典的一行哈希是float hash21(vec2 p) { p fract(p * vec2(123.34, 456.21)); p dot(p, p 45.32); return fract(p.x * p.y); }这个函数的作用是对于任意 UV 坐标返回一个 0 到 1 之间看起来毫无规律的数值。用它替换正弦偏移就是最基础的“随机噪声抖动”float t cc_time.x * shakeSpeed; float r hash21(vec2(v_uv0.x * 20.0, floor(v_uv0.y * 20.0 t))); uv.x (r - 0.5) * shakeAmp;这里我特意把 UV 乘了 20相当于把画面划分成 20 列每一列用一个随机数驱动眨眼之间就能看到整幅画面在“数字式”地跳变。相比正弦波它几乎不可能被眼睛预测。4.3 故障特效按区块抖动“故障块”是另一个高频需求直接把 hash 和 UV 区块结合就能做。思路是把 UV 划分为网格每个网格是一个区块每个区块使用自己的随机偏移量vec2 uv v_uv0; float t cc_time.x * 15.0; vec2 grid vec2(4.0, 6.0); vec2 cell floor(uv * grid); vec2 cellUv fract(uv * grid); float rand hash21(cell floor(t)); float shift step(0.5, rand); uv.x (rand - 0.5) * shift * shakeAmp * 0.15; vec4 o texture(texture, uv) * v_color;这段代码的效果是整幅图被切成 4 列 6 行的方块每个方块的颜色可能被水平错开一部分。当t随时间变化floor(t)改变之后所有区块的哈希值重新洗牌视觉上就会像屏幕一帧一帧“坏掉”。这里有一个非常有用的调节技巧grid的数值决定故障块的粗细。想要大的碎裂感用vec2(3.0, 4.0)想要细腻的像素雪花用vec2(24.0, 16.0)。实际项目的故障画面通常需要两层配合一层是粗块位移另一层是细粒度噪点叠加起来才有那种“数字错误”的粒状感。4.4 帧冻结 shader 的时间技巧故障效果里经常需要“顿挫感”也就是画面不是平滑变化而是每隔一小段时间突然跳变一次像坏掉的视频播放器。这个在 shader 里用一个简单的floor就能做float snapTime floor(cc_time.x * 12.0) / 12.0; float t snapTime * shakeSpeed;cc_time.x先乘 12 再向下取整再除 12时间就变成了每秒跳 12 次的量化值。放到噪声函数里随机偏移每 1/12 秒才重新计算一次画面就会有“一顿一顿”的故障风格。对电击、黑客入侵、大脑被控制这类演出这个效果非常管用。5. 脚本控制像做受击反馈一样调参shader 写好了最后一步是在 Cocos Creator 里让它可以被玩法逻辑触发。工程实践中直接用编辑器拖参数是不够的得写一个脚本组件来控制衰减和结束。5.1 材质实例隔离先啰嗦一个很多人踩过的坑不要直接修改共享的材质资源。如果你在编辑器里把同一个材质拖给多个 Sprite然后脚本里执行this.material.setProperty(shakeAmp, 0.1);这个修改会让所有使用该材质的 Sprite 一起抖。正确做法是拿到精灵自己独立的材质实例。在 Cocos Creator 3.x 里可以这样import { _decorator, Component, Sprite, Material } from cc; const { ccclass, property } _decorator; ccclass(ShaderShake) export class ShaderShake extends Component { private shakeMat: Material null; private timer 0; private duration 0; private maxAmp 0; start() { const sprite this.getComponent(Sprite); this.shakeMat sprite.getMaterial(0); } playShake(duration 0.3, amplitude 0.04, speed 6) { this.duration duration; this.timer duration; this.maxAmp amplitude; this.shakeMat.setProperty(shakeAmp, amplitude); this.shakeMat.setProperty(shakeSpeed, speed); } update(dt: number) { if (this.timer 0) return; this.timer - dt; const progress this.timer / this.duration; const amp this.maxAmp * progress * progress; this.shakeMat.setProperty(shakeAmp, amp); if (this.timer 0) { this.shakeMat.setProperty(shakeAmp, 0); } } }这里sprite.getMaterial(0)返回的是精灵当前使用的材质实例不是共享资源所以改属性不会影响其他节点。注意 Cocos Creator 的 UI 组件渲染和 Sprite 渲染在getMaterial上有些细节差异如果节点挂的是 UI 相关组件换成this.node.getComponent(UIOpacity)之类的 API 之前先确认组件有没有暴露材质接口。写个工具函数打日志看看getMaterial返回的是不是 null是 null 就需要走setMaterial重新绑一次。5.2 参数衰减与玩法对接衰减用progress * progress是为了让震感在前半段衰减快、后半段保留一点余震尾巴时间进度从 1 到 0progress * progress会让数值快速变小但不会瞬间归零0.4、0.3 这种后段数值虽然小但视觉上依然能感受到“余颤”刚好配合受击反馈的收尾实战中playShake(0.35, 0.05, 8)是击打反馈里比较通用的参数。如果是 Boss 砸地这种大重量演出我更喜欢playShake(0.8, 0.08, 3)振幅大、频率低震起来有“地动山摇”的感觉。6. 常见问题与排坑记录这部分都是实际项目里踩过的、拿出来一定能帮你省时间的经验。6.1 Shader 变全黑或者材质面板没有属性先检查几个低级但高频的问题properties 里的属性名和 CCProgram 里的 uniform 名是否完全一致大小写和拼写都不能错是否存在引用了但没有真正用到的 uniform。部分渲染后端会把无用 uniform 优化掉导致脚本 setProperty 不生效在CCProgram fs里面声明了sampler2D texture但顶点着色器没有输出 v_uv0采样结果就是乱七八糟的颜色如果材质面板不显示属性大概率是 properties 声明放错了层。把 properties 放在 passes 下是对的别放到 techniques 外层编辑器认的是 passes 里的 properties。6.2 抖动幅度过大导致贴图闪出透明不管是顶点位移还是 UV 扰动幅度一旦超过贴图范围就会出现采样越界表现是边缘疯狂闪透明或者黑线。解决办法有这么几个给幅度加 clamp把最大偏移限制在贴图尺寸的 5% 以内UV 扰动采样前强制 clamp顶点位移时用模型原本的顶点范围计算一个最大偏移限制比如amp min(amp, 0.1)6.3 版本差异Cocos Creator 2.x 与 3.x2.x 的老项目建议把文中的矩阵和采样函数替换一下再直接使用项2.x3.x纹理采样texture2D(texture, uv)texture(texture, uv)顶点矩阵matViewProj * matWorld * poscc_matViewProj * cc_matWorld * pos时间统一变量访问cc_time.xcc_time.x顶点输入attribute写法或内置简写in写法3.x 里 2.x 的matViewProj直接引用会编译报错别问我是怎么知道的。6.4 一点经验总结我做这个效果的体会是shader 抖动本质上是给游戏画面加“质感粗粝感”的手段它和 Tween 不是替代关系而是互补。Tween 负责让一个大物体整体动起来shader 负责让材质内部“坏掉”。受击、故障、电击、大脑被控制这些瞬间用 shader 抖动的表现力和冲击力完全不是节点位移能比的。但代价是要严格管理材质实例和幅度边界否则排查起来比写 shader 本身还痛苦。实际开发中我最后还会留一个调试开关mat.setProperty(shakeAmp, 0)可以让所有抖动效果瞬时归零方便美术在非受击状态下正常截图、调 UI。这个细节很小但项目后期对接时特别管用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。