直接用正弦函数做抖动显得假,因为纯sin(t)运动匀速来回,而真实抖动起始猛烈、衰减迅速且带高频微颤;应采用指数衰减包络叠加噪声的物理模拟方案。

为什么直接用正弦函数做抖动会显得假
因为纯 sin(t) 的运动是匀速来回的,人眼对加速度敏感——真实爆炸或撞击引发的抖动,起始猛烈、衰减迅速,中间还带点高频微颤。直接套正弦波,抖完还“弹”两下,或者衰减太慢,看着像屏幕在呼吸。
手写抖动算法的关键不是“画个波形”,而是模拟物理响应:初始位移大 + 加速度高 + 衰减非线性 + 可叠加噪声。
- 别用
amplitude * sin(frequency * t)当主力,它只适合循环背景抖动(比如恐怖游戏UI),不适合事件触发型抖动 - 时间参数
t应该是自抖动开始的**已过去毫秒数**,不是全局时间戳,否则多次触发会相位错乱 - 振幅必须随时间衰减,但不能用简单线性衰减(
amp = max_amp * (1 - t / duration)),它后半段拖沓无力
推荐用指数衰减 + 高频叠加的组合公式
这是实测在 Unity/C++ 项目中反馈最自然的写法,核心是两层:主衰减包络 + 微扰噪声。C++ 实现时建议封装成可重入的结构体,避免全局状态:
struct ScreenShake {
float amp = 0.f;
float decay = 0.98f; // 每帧衰减率,0.97~0.99 之间调
float noise_freq = 23.0f; // 伪随机噪声频率,质数防周期感
uint64_t seed = 0;
<pre class="brush:php;toolbar:false;">float evaluate(float t) const {
if (t <p>};</p>说明:
-
std::pow(decay, t)中t是 float 秒数,不是帧数;若用帧数,改用std::pow(decay, frame_count)更稳 - 高频噪声乘以
envelope,保证微颤随主抖动一起消失,不会残留“嗡嗡”感 - 最后一行的
std::sin(8.0f * t)是低频方向调制,让抖动不是纯左右/上下,而是略带旋转倾向,更接近物理惯性
如何安全集成到 C++ 渲染循环中
抖动值最终要加到相机/视口矩阵上,但必须注意三件事:坐标系、累积和重置。
- 别直接修改
view_matrix,先算出偏移量offset_x,offset_y,再在顶点着色器或 UI 布局阶段应用——否则 3D 物体透视会畸变 - 多个抖动源(比如枪击+爆炸)要**叠加振幅,不叠加时间**:用一个主
ScreenShake实例,每次触发调用shake.amp = std::max(shake.amp, new_amp),并重置start_time - 务必设超时保护:即使
amp理论上永不归零,也要在t > 1.5f后强制归零,防止浮点误差累积导致抖动“阴魂不散”
容易被忽略的平台细节
移动端和 PC 端的输入延迟、帧率稳定性差异很大,同一套参数在 iOS 上可能抖过头,在 Windows 上又不够劲。
- 别硬编码
decay = 0.98f,按实际帧率动态调整:decay = std::pow(0.98f, delta_time * 60.f),把设计值锚定在 60fps - Android 上
std::sin在某些 NDK 版本里有精度 bug,建议用查表法或FastSin近似函数替代 - 如果用了 Vulkan 或 Metal,抖动偏移量最好走 uniform buffer 传入,别在 CPU 端每帧重算整个 MVP 矩阵——小抖动不值得扛矩阵乘法开销
真正难的不是写出抖动公式,而是让每一次爆炸都让人肩膀一缩,而不是盯着屏幕想“这抖得不太对”。参数调校没有银弹,但只要盯住「起始陡、衰减快、末尾干净」三个点,就踩不到大坑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











