
做 Cocos 开发的人几乎都会遇到“让画面抖一下”的需求。受击反馈、爆炸震屏、Boss 出场、手机震动提示、UI 警告……这些效果做不好游戏手感会立刻变假。我最初也是老老实实写个 Tween 直接改节点 position让整个 Sprite 左摇右晃直到项目里同时要处理几十个单位受击、再把镜头震动和 UI 提示叠上去CPU 逻辑帧被吃干净帧率开始心电图式乱跳。从那以后我认真用 Cocos Shader 来做抖动把原本 CPU 干的活挪给 GPU也因此第一次理解了“分层抖动”这个概念。这篇文章就围绕 Cocos Shader 抖动的原理、代码、参数和坑按我实际项目经历讲一遍。适合已经会 Cocos Creator 基础操作、想开始碰 Shader 又不知道从哪下手的开发者。你可以直接复制代码调试也可以只读原理部分判断自己项目到底该不该用 Shader。1. 先分清顶点抖动、像素抖动还是相机震屏接到“做个抖动”需求时第一步不是找代码而是问自己到底要抖哪个层很多人一搜“cocos shader 抖动”就埋头找现成 effect结果照着别人写的顶点偏移贴上去发现 UI 全乱了或者挂一个全屏后处理整个页面花屏。原因往往不是代码不对而是抖错了层。1.1 节点层抖动什么时候够用最基础、新手最容易想到的做法是在update、协程或者 Tween 里改节点 transform。每个逻辑帧把 position 往随机方向偏一下下一帧拉回来。这种写法在个别对象上完全没问题优点是想都不用想、不引入渲染概念、任意节点都能用。缺点也明显它在 CPU 逻辑帧执行如果同一帧有十几个对象一起抖压力就会压到主线程而且它只能让整个节点整体平移、旋转、缩放做不到局部肌肉抖动。我一般只会在满足这几种情况时优先用节点层需要抖动的对象很少比如一个主角、一个镜头抖动频率不需要超过 60Hz不需要局部形变只需要整体位移团队对 Shader 不熟不想引入额外维护成本。如果条件不满足就直接滑到下一节。1.2 顶点层抖动GPU 在动形状Shader 顶点抖动是把位置计算放到顶点着色器里。GPU 在渲染三角形的时候会先用你写的逻辑重新算每个顶点的位置再经过 MVP 矩阵投影到屏幕上。对 3D 模型来说顶点多可以做局部鼓起、波浪形变对 2D Sprite 来说只有四个顶点能做的动作有限但可以实现整体偏移、轻微拉伸、按顶点相位错开的形变。最关键的是它不占用逻辑帧节点实际坐标也没变碰撞、点击、物理系统都不受影响。所以如果你需要大批量单位同时受击抖动或者不希望抖动影响逻辑判定顶点层是首选。代价是团队里得有一个人懂 GLSL调试时没有脚本断点那么直观参数只能靠一遍遍刷新看效果。1.3 像素层与后处理让整个画面哆嗦还有一类需求是整个画面震UI、角色、背景一起晃。此时逐个 Sprite 做顶点抖动太累通常用一个覆盖全屏的 Shader 层在片元阶段把 UV 统一偏移让贴图像素错位视觉上就是一个全局震屏。2D 游戏可以在场景最前面放一个全屏透明节点挂材质3D 项目一般要走 Cocos 自定义渲染管线做全屏后处理。像素层最大的优点是效果统一不改变任何对象的 transform不影响 UI 布局、不打断动画。缺点是它只产生视觉错位无法改变真实几何遮挡关系如果要求只有背景震而 UI 不震还要配合遮罩或分层。所以接到需求时第一件事永远是问自己我到底要让哪一层在动。这个问题的答案直接决定你接下去几小时写的是脚本还是 Shader。2. Shader 抖动原理时间、噪声、顶点偏移有了分层思路再看原理就清楚了。Shader 里做抖动本质上就是“让某个数值随时间变化”再把这个数值加到顶点位置或 UV 上。听起来很简单但有几个关键点容易卡住。2.1 抖动的时间来源cc_time 怎么用Shader 本身不知道时间过了多久Cocos 引擎在渲染时会往每个 Pass 注入一个内置 uniform叫cc_time。类型是 vec4cc_time.x是时间累计值单位秒。顶点着色器和片元着色器都能读它。我习惯把cc_time.x理解成“全局节拍器”所有顶点、所有像素都按同一个节拍走所以画面不会出现各抖各的、互相打架。有一点要注意cc_time.x不会因为节点暂停或隐藏而停表。如果你想让抖动在节点隐藏时也停止需要在脚本里用自定义时间变量传给 Shader。我的做法是持续循环效果直接用cc_time.x一次性受击效果用脚本传入起点时间或剩余时间可控性更强。2.2 sin 和噪声自然抖动的数学如果只要“会动”一行sin(cc_time.x * speed)就够了得到的是沿一个轴的正弦往返。正弦波看着平滑做漂浮、呼吸很合适但做受击效果偏“机械”。真正的打击感不是匀速正弦而是方向随机、幅度先大后小的衰减过程。GLSL 没有现成的随机函数但有个经典 trick用 sin 的分数部分生成伪随机float hash11(float p) { return fract(sin(p * 127.1) * 43758.5453); }hash11(整数)返回一个 [0, 1] 的伪随机数输入同一个整数永远返回同样的结果。受击时我们把每次受击的种子塞进去每次触发的抖动方向都不一样。为了让抖动连续而不是帧间硬跳我会在随机点之间用 smoothstep 插值让它像连续变化的噪声。在实际项目里我通常叠加三层信号一层低频 sin 负责大范围位移给人明显的冲击感一层中频 sin 让方向微转避免低频太单一一层高频噪声负责细节毛刺。三层乘上不同权重再合起来效果比单一正弦丰富很多。2.3 顶点偏移规则为什么幅度不能乱填顶点着色器里操作的是模型空间的a_position经过cc_matViewProj变成裁剪空间坐标。如果你在投影之后再去改pos坐标空间就乱了画面会飞。正确顺序是在 MVP 变换前对pos.xy做加减再交给投影矩阵。另一个必须理解的点是“同一向量偏移”和“按顶点相位偏移”的区别。同一向量偏移相当于整个物体平移Sprite 的四个顶点都加同一个值按顶点相位偏移会形成形变比如左上角向右、右下角向左产生拉伸、倾斜、波浪的效果。很多初学者以为顶点抖动就是前后平移其实结合顶点自身坐标作为相位参数可以让 2D 精灵也拥有“肌肉抽动”的质感。当然四个顶点能表达的形变很有限想要更复杂的花样还是得用 3D 模型。3. Cocos Creator 实战从复制内置 Sprite effect 到调出效果下面进入可以落地的部分。我以 Cocos Creator 3.x 为例说明怎么在引擎自带的 Sprite effect 基础上加入抖动。老 2.x 项目的差异我会在文末单独讲。3.1 从内置 sprite.effect 复制别从零写不建议从零写一个 effect。Cocos 的 effect 文件结构里CCEffect块负责渲染状态和属性表CCProgram块负责 GLSL 代码。官方已有的sprite.effect处理了纹理采样、颜色混合、内置宏直接拿到底层复制修改就好。你可以在资源管理器的 Shader 创建面板里选择 Sprite 模板或者去引擎安装目录找internal下的 Sprite effect 资源复制一份改名为shake.effect。复制出来以后核心结构长这样CCEffect %{ techniques: - passes: - vert: sprite-vs frag: sprite-fs depthStencilState: depthTest: false depthWrite: false blendState: targets: - blend: true blendSrc: src_alpha blendDst: one_minus_src_alpha rasterizerState: cullMode: none properties: _TintColor: { value: [1, 1, 1, 1], editor: { type: color } } }%这里几个关键点blend 开启深度测试关闭cullMode 为 none。properties里列出的属性会出现在材质面板上供美术和策划随时调整。3.2 增加可调参数在 properties 里暴露在properties里追加三个参数抖动幅度、抖动速度、随机种子。属性名和默认值根据项目需要来我给一个常用配置properties: _TintColor: { value: [1, 1, 1, 1], editor: { type: color } } _ShakeAmp: { value: 0.02, editor: { displayName: 抖动幅度 } } _ShakeSpeed: { value: 12.0, editor: { displayName: 抖动速度 } } _ShakeSeed: { value: 1.0, editor: { displayName: 随机种子 } }注意属性名、uniform 名、CCProgram 里声明的名字必须完全一致。这里没有自动纠错写错了引擎不会报错但面板参数传不到 Shader 里效果就会一直不对。我见过不少人在这一步卡很久反复确认代码逻辑没问题结果只是下划线拼错了。3.3 顶点着色器加入抖动逻辑可直接复制把顶点着色器里的main函数改一版。下面是一个可以直接复制的整体偏移抖动方向由伪随机噪声控制CCProgram sprite-vs %{ precision highp float; #include builtin/uniforms/cc-global #include builtin/uniforms/cc-local in vec4 a_position; in vec2 a_uv0; out vec2 v_uv0; uniform ShakeArgs { float _ShakeAmp; float _ShakeSpeed; float _ShakeSeed; }; float hash11(float p) { return fract(sin(p * 127.1) * 43758.5453); } void main () { vec4 pos a_position; float t cc_time.x * _ShakeSpeed; float h1 hash11(floor(t) * _ShakeSeed); float h2 hash11((floor(t) 1.0) * _ShakeSeed); float w fract(t); float noise mix(h1, h2, smoothstep(0.0, 1.0, w)); float angle noise * 3.1415926 * 2.0; vec2 offsetVec vec2(cos(angle), sin(angle)) * _ShakeAmp * 0.01; pos.xy offsetVec; v_uv0 a_uv0; gl_Position cc_matViewProj * pos; } }%这个逻辑分三步读时间、算连续伪随机噪声、把顶点沿随机方向平移。其中mix和smoothstep的作用是让随机点之间平滑过渡抖动是连续的而不是每帧一个方波跳变。如果你想做“不规则波形形变”让四个顶点各抖各的就把偏移改成依赖顶点位置float fx sin(cc_time.x * _ShakeSpeed a_position.x * 50.0 a_position.y * 30.0) * _ShakeAmp * 0.01; float fy cos(cc_time.x * _ShakeSpeed * 0.8 a_position.y * 50.0 a_position.x * 20.0) * _ShakeAmp * 0.01; pos.xy vec2(fx, fy);这里要注意振幅是相对模型坐标系的。Sprite 的模型单位基本等同像素所以 0.02 到 0.05 这样的小数值已经能看出效果。调到 1.0 以上图片会直接飞出屏幕。3.4 片元着色器 UV 抖动让图案自身扭曲顶点抖动只是让整个图形动纹理里的内容不会动。如果想让图案发生波浪形扭曲就在片元里给 UV 加偏移。把两段逻辑结合视觉层次会丰富很多CCProgram sprite-fs %{ precision highp float; in vec4 v_color; in vec2 v_uv0; uniform sampler2D texture; uniform vec4 _TintColor; uniform ShakeArgs { float _ShakeAmp; float _ShakeSpeed; float _ShakeSeed; }; void main () { vec2 uv v_uv0; uv.x sin(cc_time.x * _ShakeSpeed * 0.6 uv.y * 20.0) * _ShakeAmp * 0.5; uv.y cos(cc_time.x * _ShakeSpeed * 0.5 uv.x * 20.0) * _ShakeAmp * 0.5; vec4 o texture(texture, uv); o * v_color; o * _TintColor; gl_FragColor o; } }%UV 抖动可以精确控制局部内容扭曲比如爆炸特效内部火焰乱窜、受击角色脸部变形。但有坑如果 SpriteFrame 是从图集里切出来的UV 偏移可能会越出当前子图区域采样到图集里相邻的贴图表现就是画面里闪一下其他物体的颜色。所以要给图集加 2~4 像素 padding或者让这种效果使用独立纹理。3.5 建材质、挂 Sprite、脚本控制衰减把 effect 资源拖进材质管理器或者新建 Material 时选择这个 Shader然后把它赋给 Sprite 组件的 CustomMaterial。在编辑器的材质面板上你就能直接看到抖动幅度、速度、随机种子这些参数。美术调色时很需要这种渲染上的实时调整。运行时想控制衰减普通做法是脚本每帧改材质属性const mat this.sprite.getMaterial(0) as Material; mat.setProperty(_ShakeAmp, currentAmp);受击瞬间把_ShakeAmp设成 2.0随机种子换成当前时间戳然后用 0.2 秒的 tween 把_ShakeAmp拉回 0。这样一次受击抖动就有了“爆发-衰减-归零”的完整过程。如果你不想每帧 set也可以把衰减时间作为参数传进 Shader在 Shader 内部计算。我的经验是脚本里做衰减更灵活方便和动画状态机配合Shader 内部做衰减更省逻辑调用适合批量对象。项目初期用脚本方案就行遇到性能瓶颈再平移进 Shader。4. 参数调优如何不“抖晕”有了能跑的代码之后后面就是手感问题。我见过很多项目“做了抖动却像卡顿”不是 Shader 写错而是参数没配合好。4.1 幅度、速度、衰减的常用参数表幅度直接决定“有多用力”。对 UI 元素比如按钮按下去震一下幅度建议在 0.5 到 2 像素对角色受击2 到 6 像素比较明显对全屏震屏6 到 12 像素才够沉浸。这个没有绝对标准因为最终显示尺寸和设备像素密度有关。我的判断方式很简单做到“能察觉但不过度”如果玩家的眼睛已经需要停下来确认信息那就过头了。速度方面正弦波频率 6 到 10Hz 是“硬朗受击”的常用区间。低于 3Hz 像漂浮高于 20Hz 人眼基本看不出波形只会觉得画面发虚。衰减决定抖动什么时候停止前 0.1 秒满振幅中间 0.1 秒线性降末尾 0.1 秒用 ease-out 平滑收尾。周期性的持续抖动比如中毒状态则不需要衰减反而要保持稳定。参数受击震屏UI 点击反馈角色持续喘气幅度4~80.5~1.51~2速度10~148~102~4衰减快衰减更快衰减不衰减稳定循环随机种子每次受击变化固定或轻微变化固定主要偏移层顶点UV 混合顶点平移低频顶点具体参数要按项目美术风格去调但方向是一致的有明确次数的“事件型抖动”要快起慢收持续不停的“状态型抖动”要小幅度、低频率。4.2 自然感从机械振荡到多层噪声只用 sin 做受击效果会像音叉共振来回振荡非常规律玩家很快会觉得恶心。我建议至少叠加两层信号一层稳定低频负责形体位移一层高频噪声负责细节毛刺。如果不想写复杂最低限度是每次受击都把_ShakeSeed随机换一个值让每次触发相位都不同。另外衰减曲线的形状很关键。线性衰减听起来没错但实际观看会觉得抖动“断”得很突兀。用乘方衰减比如amp * (1 - t)^2让末尾慢下来画面会自然很多。Cocos 的tween支持自定义 curve可以在脚本层做这件事也可以直接在 Shader 里传入剩余时间。4.3 抖动分层相机、UI、角色别一起抽风项目里真正复杂的不是单个 Sprite 抖动而是多个层同时要抖。如果 Boss 出场地板要震UI 提示不想跟着晃角色轻微晃动镜头还要不要动两种抖动叠加容易变成“癫痫观感”。我的建议是分层隔离背景和地面用一个带顶点抖动的全屏层或独立 Sprite 处理。相机或 Canvas只处理镜头级震屏幅度再大也不要超出 UI 安全区。UI 元素只在重要事件里短暂出现永远不要跟层叠动画同时开启。这样每层都有自己的衰减节奏比如场景震动逐渐平息时UI 可以紧接着做一个强提示震动。如果你从 Unity 转来会发现 Cocos 的 Shader 概念和 Unity 有很多对应关系cc_time类似_Time.ycc_matViewProj大体相当于UNITY_MATRIX_MVP。读懂了这条对应关系很多 Unity 的抖动、扭曲 Shader 源码都能快速移植到 Cocos。5. 常见问题速查我踩过的坑这部分是整理我的踩坑实录都是能直接拿去当排查手册的。5.1 材质挂了但完全没有抖动效果十有八九是材质没有真正赋到组件上。Cocos 的 Sprite 有自己的默认材质你新建 Material 后必须拖进 Sprite 组件的 CustomMaterial 槽里。检查方式运行时选中 Sprite看材质实例里的属性是否存在修改后画面是否变化。第二个高频原因是properties里的属性名和 CCProgram 里的 uniform 名不一致。引擎不会报错只会忽略面板传值使用 Shader 默认值效果当然出不来。5.2 抖动幅度太大导致图形飞出屏幕这个八成是振幅填太大或者偏移逻辑没有围绕 0 值波动。如果你的噪声函数是每帧累加随机数顶点就会越跑越远最后飞出裁剪空间。正确做法是让偏移保持在 0 附近振荡用 sin/cos 或者带限噪声而不是无脑累加。另外确认顶点偏移写在gl_Position cc_matViewProj * pos;之前。投影后再修改坐标本质上就是把最终屏幕上已经计算好的位置强行挪走必然出问题。5.3 UV 抖动时闪出别的贴图这是图集问题。检查 SpriteFrame 是不是从图集里切的如果是要么换成独立纹理要么给图集加 padding。闪错图还有一个常见原因纹理过滤模式设置不合适UV 偏移跑到纹理边缘。测试阶段把过滤从 Bilinear 改成 Point 更容易看到问题正式发布再按效果切回。5.4 编辑器流畅真机掉帧主要是渲染压力差异。如果你项目里用了很多全屏后处理和带顶点 Shader 的 Sprite真机 GPU 负载会明显上升。优先做几件事降低振屏层的覆盖范围把“每帧计算随机噪声”改成低频缓存噪声减少片元着色器里的复杂数学。Cocos 项目打包 Android 后 Shader 在编辑器正常、真机不显示很多时候不是 Shader 本身问题而是纹理压缩格式、宏开关、设备兼容性的差异。可以先跑 Debug 模式真机看 Console 里 Shader 编译日志。5.5 Cocos Creator 2.x 老项目怎么迁移2.x 的 effect 文件语法和 3.x 有差异CCEffect的 properties 更像数组但核心思路一样。cc_time、cc_matViewProj这些名字通用。如果你手头是 2.x 老项目照着 3.x 的逻辑写把CCProgram里的属性名换成 2.x 内置的a_position、a_uv0即可大部分 GLSL 能平移到 2.x。5.6 最后分享一个小技巧我发现真正好用的抖动 Shader参数永远要“小而多”不要“大而少”。幅度小一点速度多一点衰减柔和一点。如果做一个持续效果比如角色踩在毒液区建议在抖动之外叠加一层明暗闪烁或颜色偏移比单纯抖更能传递伤害信息。多通道刺激一起上视觉负担反而比单纯猛抖要小。做 Shader 抖动不是追求夸张而是追求可控的手感。