傅里叶变换:游戏开发中的频域思维
从 Waveform 与 Spectrum 出发,理解傅里叶变换为什么把时域问题转换到频域,以及 FFT、采样、窗口、STFT、卷积和频域海洋在游戏开发中的意义。
傅里叶变换(Fourier Transform)经常以一种令人望而生畏的方式出现:积分、复数、指数函数,以及一连串 FT、DFT、FFT、STFT 的缩写。
但对游戏程序员来说,更值得首先理解的并不是如何手算傅里叶变换,而是一个更基础的问题:
为什么我们要把一个信号从原来的表示方式,转换到“频率”这个表示空间?
这背后其实是一种非常通用的工程思想:
当一个问题在原来的坐标系里很复杂时,换一种表示,可能会让它的结构突然变得简单。
声音是理解这一点最直观的入口。
一段音频既可以表示为随时间变化的波形,也可以表示为不同频率成分的组合:
它们不是两份不同的数据所描述的两种声音,而是同一个信号的两种表示。
傅里叶变换,就是连接这两种表示最重要的数学工具之一。
1. 为什么需要频域?
1.1 计算机看到的声音是什么?
数字音频最直接的形式,其实只是一串数字:
0.02, 0.11, 0.37, 0.68, 0.42, -0.15, -0.61, ...每一个数值代表某个离散时刻的采样值。
如果把它画出来,就是我们熟悉的 Waveform(波形):
Amplitude
↑
│ /\ /\
│ /\ / \ /\ / \
────┼──/──V────\_/──\_/──────→ Time
│横轴是时间:
纵轴是振幅:
因此时域中的信号回答的是:
某个时刻,信号的值是多少?
这里的纵轴并不是“音高”。
音高来自振动的周期性:波形重复得越快,对应的频率通常越高。
一个纯正弦信号可以写成:
其中:
| 符号 | 含义 |
|---|---|
| 振幅(Amplitude) | |
| 频率(Frequency) | |
| 相位(Phase) | |
| 时间 |
例如,一个 的纯正弦波意味着它每秒完成 440 个周期。
问题在于,现实中的声音几乎从来都不像单一正弦波那么简单。
它通常更像:
大量频率同时叠加之后,时域波形会迅速变得复杂。
1.2 时域能告诉我们什么,又不能告诉我们什么?
假设现在有这样一个信号:
它是两个简单正弦波的叠加:
- 一个 ;
- 一个 ,振幅只有前者的一半。
如果直接观察 Waveform,我们看到的是两者叠加之后的一条复杂曲线。
但如果问题变成:
这段声音里到底包含哪些频率?
时域就不再是最自然的表示。
我们更希望得到类似:
Magnitude
↑
│ █
│ █
│ █ █
│ █ █
────┼───█─────────────────█────→ Frequency
100 Hz 1000 Hz这就是 Spectrum(频谱)。
它不再主要回答:
每个时刻发生了什么?
而是在回答:
这个信号由哪些频率组成,每个频率的成分有多强?
这便是从 Time Domain(时域) 转向 Frequency Domain(频域) 的动机。
1.3 从 Waveform 到 Spectrum:换一种方式观察同一个信号
Spectrum 并不是 Waveform 的平均值。
它们之间也不是:
更准确的理解是:
可以类比一个三维向量。
同一个向量 可以在一组坐标轴下写成:
如果换一组基底,它又可能写成:
向量没有改变。
改变的是描述它的坐标系。
傅里叶变换所做的事情与此非常相似:
不再问信号在每个时间位置上是多少,而是问它在每一个“频率基函数”方向上有多少分量。
Spectrum 不是 Waveform 的平均。
Waveform 与 Spectrum 是同一个信号的两种表示。 前者以时间为坐标,后者以频率成分为坐标。
2. 傅里叶变换到底在做什么?
2.1 复杂信号可以由简单周期波组成
傅里叶分析最重要的思想之一,是把复杂函数表示为大量周期成分的组合。NumPy 的 FFT 文档也将 Fourier Analysis 概括为:用周期成分的和表示函数,并能够从这些成分恢复原函数。1
最熟悉的形式是正弦和余弦:
如果使用复数,表达会更加简洁。
根据 Euler 公式:
一个频率成分可以写成:
于是傅里叶分析实际上是在问:
对于每一个频率 ,信号 中到底含有多少 ?
这就是频率分解的核心。
2.2 频率、振幅与相位
如果只看常见的频谱可视化,很容易产生一个误解:
傅里叶变换的输出就是“每个频率有多强”。
实际上,完整的傅里叶变换结果通常是复数。
可以写成:
其中:
代表 Magnitude;
而:
代表 Phase。
因此,一个频率成分并不只有“有多少”,还有:
这个周期波相对于其他周期波处在什么相位。
NumPy 的定义中:
abs(A)对应 amplitude spectrum;abs(A) ** 2对应 power spectrum;angle(A)对应 phase spectrum。1
游戏里的频谱柱状图通常只关心 Magnitude,因此 Phase 很容易从直觉中消失。
但如果要把频谱重新还原成原始信号,相位是不能随意丢掉的。
2.3 从“分解信号”理解傅里叶变换
连续傅里叶变换的一种常见定义是:
乍看起来,这只是一个复杂积分。
但如果把它理解成:
直觉就会清楚很多。
当 中确实包含某个频率时,它与这个频率的复指数会产生较强的对应结果。
如果不存在这个频率,相互抵消之后的结果就会较小。
于是:
可以理解成:
从“每个时间点有多少”,转换为“每个频率方向上有多少”。
2.4 傅里叶变换也可以理解为换基
对于游戏程序员来说,“换基”可能比“频率分解”更加统一。
在线性代数里,一个向量可以投影到不同基底:
傅里叶分析只是把基底换成了一组不同频率的周期函数。
因此可以粗略理解为:
这也是为什么傅里叶思想并不限于声音。
声音的自变量是时间,所以我们讨论:
图像的自变量是空间位置,所以可以讨论:
海面同样是空间中的高度场,也能够用不同波长、方向和波数的波来表示。
2.5 逆傅里叶变换:还能变回去吗?
如果频域真的是同一个信号的另一种完整表示,那么理论上当然应该能变回来。
连续情况下存在逆傅里叶变换:
于是整个过程是:
这点非常重要。
傅里叶变换并不是单纯的“分析工具”。
它实际上建立了两种表示之间的双向转换。
后面我们会看到,FFT Ocean 正是先在频域构造海浪,再通过逆变换生成空间中的海面。
3. 从 Fourier Transform 到 FFT
3.1 Fourier Series、FT、DFT 是什么关系?
工程里经常混在一起出现的几个名词,其实处在不同层次。
| 概念 | 典型对象 | 核心意义 |
|---|---|---|
| Fourier Series | 周期函数 | 用离散的谐波序列表示周期函数 |
| Fourier Transform | 连续信号 | 从连续原始域转换到连续频域 |
| DTFT | 离散时间、无限序列 | 离散信号对应的连续周期频谱 |
| DFT | 有限个离散采样点 | 得到有限个离散频率分量 |
| FFT | 算法 | 高效计算 DFT |
实际游戏程序里,输入通常是:
float samples[N]而不是数学意义上无限连续的 。
所以程序真正计算的通常是 DFT(Discrete Fourier Transform)。
3.2 为什么计算机实际使用 DFT?
假设我们有 个采样:
一种常见的 DFT 定义是:
其中:
这里的每一个 就对应一个离散频率位置。
因此:
N 个时域采样
↓
DFT
↓
N 个复数频域系数对于实值音频信号,还存在一个很重要的性质:
即负频率与正频率具有共轭对称性。
因此对真实音频,很多 FFT API 只需要保存从 到 Nyquist Frequency 的一半频谱;例如 NumPy 的 rfft 对 个实值输入只返回非负频率部分。1
3.3 FFT 不是另一种变换
这是名字上最容易造成误解的地方。
FFT(Fast Fourier Transform)不是一种与 DFT 并列的新变换。
它是一类:
快速计算 DFT 的算法。
因此:
而是:
1965 年,James Cooley 与 John Tukey 发表的经典论文 An Algorithm for the Machine Calculation of Complex Fourier Series 系统描述了通过分解问题,把计算量从与 成比例降到与 成比例的方法。2
3.4 FFT 为什么能比直接计算 DFT 快?
直接按照 DFT 定义计算:
有 个 。
而每个 又需要遍历 个输入。
于是大约需要:
次运算。
FFT 利用了复指数中的周期性与对称性,把大的 DFT 拆成许多更小的 DFT,并复用中间结果。
经典 radix-2 思路会不断拆分:
最终复杂度降低到:
这使频域方法真正成为实时计算中的实用工具。
FFT 并不要求数学上的 DFT 长度必须是 2 的幂。
某些 FFT 算法和具体 API 对 长度特别友好,甚至直接要求如此,但现代 FFT 库也可以处理任意长度。FFTW 例如明确支持 arbitrary length,并为所有长度采用 级算法。3
因此“FFT 必须是 2 的幂”是具体实现限制,不是 DFT 本身的数学要求。
4. 现实中的 FFT 为什么没有那么完美?
理论中的例子经常是:
然后傅里叶变换得到一个干净的频率尖峰。
但真正把音频数据丢进 FFT 后,经常会发现:
- 一个频率扩散到了附近很多 Bin;
- 同一个频率换个 FFT Size,结果似乎变了;
- 峰值并不刚好落在目标 Hz 上;
- 改一个 Window,频谱形状也发生变化。
问题并不在 FFT。
问题在于:
计算机处理的是有限长度、离散采样的信号。
4.1 Sample Rate:离散采样意味着什么?
假设连续信号为:
采样间隔为:
数字信号实际上记录的是:
采样率则是:
如果:
就意味着每秒获得 48000 个样本。
这也是数字音频从连续世界进入计算机的第一步。
4.2 Nyquist Frequency 与 Aliasing
离散采样并不能无条件保存任意高的频率。
对于采样率 ,Nyquist Frequency 为:
例如:
则:
SciPy 的 FFT 文档同样把:
定义为 Nyquist Frequency。4
超过可表示范围的频率如果未经带限处理就进入采样系统,会折叠成较低频率,这就是 Aliasing(混叠)。
这与实时图形学中的空间混叠其实是同一类数学问题:
原始信号包含的变化速度,超过了离散采样网格能够表达的上限。
4.3 FFT Bin 是什么?
FFT 不会告诉你“所有连续频率”的大小。
它只计算一组离散频率。
如果采样率为 ,FFT 长度为 ,那么相邻频率采样之间的间隔为:
第 个 Bin 对应:
假设:
并且:
那么:
FFT 并不是在问:
440.000 Hz 有多少?
441.000 Hz 有多少?
442.000 Hz 有多少?而是在固定的离散网格上进行测量。
这也解释了为什么真实频率往往不会刚好落在某个 Bin 中心。
4.4 FFT Size 为什么影响频率分辨率?
在采样率固定时:
因此 越大,Bin 间距越小。
但这里有一个非常重要的工程细节:
Bin 更密,不一定意味着获得了更多真实频率信息。
如果 增加是因为真正观察了更长的信号,那么:
也随之增加。
更长的时间窗口允许我们更可靠地区分彼此接近的频率。
但如果原本只有 1024 个样本,只是在后面补 3072 个零:
真实数据
████████
Zero Padding
████████000000000000000000FFT 输出确实会拥有更多采样点。
但原始观测时间没有增加。
因此它主要是在:
更密地采样原有频谱形状,而不是创造新的频率信息。
SciPy 的频谱分析文档也把 zero-padding 描述为对频谱的过采样,而真正的泄漏与主瓣宽度仍然由有限观测窗口及窗口函数决定。5
4.5 为什么会出现 Spectral Leakage?
想象一个无限持续的纯正弦波:
它的理想频谱可以非常集中。
但计算机不可能观察:
我们实际上只截取:
这等价于把原始信号乘上一个矩形窗口:
而“时域相乘”在频域中对应卷积。
矩形窗口自身拥有 sinc 形状的频谱,因此原本集中的频率会向周围扩散。
这就是:
Spectral Leakage(频谱泄漏)。
SciPy 的信号处理教程明确指出,有限时间区间造成的截断会产生旁瓣和频谱泄漏;信号越短,这种影响通常越明显。5
4.6 Window Function 到底解决了什么?
既然突然截断信号会造成强烈的不连续,我们可以在 FFT 前逐渐压低窗口两端:
Rectangular
████████████████████
Hann-like
▂▄▆██████▆▄▂也就是:
常见窗口包括:
| Window | 大致特点 | 常见考虑 |
|---|---|---|
| Rectangular | 主瓣窄、旁瓣高 | 频率刚好对齐时简单直接 |
| Hann | 比较均衡 | 通用频谱分析 |
| Hamming | 与 Hann 类似 | 常见 DSP 窗口 |
| Blackman / Blackman-Harris | 更强旁瓣抑制 | 更关注泄漏控制 |
| Flat Top | 更强调峰值幅度精度 | 牺牲较多频率分辨能力 |
窗口并不能“消灭傅里叶误差”。
它是在两件互相冲突的事情之间做权衡:
也就是:
更容易区分两个接近频率,还是更容易发现强频率旁边的弱频率?
SciPy 的窗口分析同样指出,抑制旁瓣通常需要付出主瓣变宽的代价。5
5. 如何真正读懂声音的 Spectrum?
5.1 基频与泛音
如果播放一个合成的纯 正弦波,频谱主要会在对应频率附近出现能量。
但真实乐器不是纯正弦发生器。
一个周期性声音通常包含:
以及:
这样的谐波成分。
其中 通常称为 Fundamental Frequency(基频)。
后面的整数倍频率则属于 Harmonics。
因此即使两个声音具有相似基频,它们的频谱形状仍然可能完全不同。
5.2 为什么相同音高可以拥有不同音色?
假设两个声音都有:
但第一个的频率成分可能更接近:
440 ███████
880 ██
1320 █
1760 ▏另一个可能是:
440 █████
880 ██████
1320 ████
1760 ██它们拥有相同或接近的基频,却拥有不同的:
- Harmonic Distribution;
- Attack;
- Decay;
- Noise;
- Phase;
- 非周期成分。
因此“音色”不能简单等同于“泛音列表”,但频谱结构确实是其中极其重要的一部分。
5.3 Magnitude Spectrum 与 Power Spectrum
完整 FFT 输出为复数:
最常见的频谱可视化使用:
也就是 Magnitude Spectrum。
如果关心功率,则常使用:
也就是 Power Spectrum。
因此常见的“频谱柱状图”只是完整 Fourier 表示的一部分。
它把复数:
压缩成:
于是相位信息不再显示。
5.4 Phase 为什么不能完全忽略?
如果只做:
- 音乐可视化;
- Bass 能量检测;
- 粗略音色分析;
Magnitude 往往已经足够。
但如果要重建信号,就不能认为:
Magnitude 就是整个 Spectrum。
傅里叶系数本质上包含:
不同频率之间如何在时间上对齐,会直接改变最终生成的波形。
这也是为什么:
相同,并不保证两个时域信号完全相同。
5.5 “低频、中频、高频”究竟意味着什么?
FFT 得到的是许多离散 Bin。
而 Bass、Mid、Treble 通常是人为定义的频带。
例如一个游戏可以定义三个区间:
然后把对应 Bin 的能量累积起来:
这样就能得到:
Low █████████
Mid █████
High ██需要注意的是:
Bass / Mid / Treble 并不是 FFT 天生提供的三个值。
FFT 给的是频率 Bin。
如何把它们聚合成游戏需要的频带,是上层设计。
实际音乐可视化中,也经常使用对数或接近 octave 的频带划分,而不是把整个 Hz 范围机械地平均切成几块。
6. Spectrum 的问题:它没有告诉你“什么时候”
6.1 对整段音乐做一次 FFT 会发生什么?
假设一段 30 秒音乐中:
0s ───── 10s ───── 20s ───── 30s
鼓 人声 高频打击如果把整个 30 秒一次性 FFT:
我们能够知道:
这 30 秒总体存在什么频率。
但会失去一个极其重要的问题:
这些频率是什么时候出现的?
Spectrum 只有频率轴。
它没有时间轴。
6.2 STFT:把音频切成一个个短窗口
解决方法非常直接:
不要一次分析整段声音,而是不断分析一小段。
Audio
──────────────────────────────────
Window 1
████████
Window 2
████████
Window 3
████████每一个窗口分别进行 FFT:
这就是 STFT(Short-Time Fourier Transform)。
SciPy 将 STFT 描述为:使用滑动窗口遍历输入,并顺序计算 FFT,从而量化频谱如何随时间变化。6
于是原本的一维频谱:
变成:
6.3 从一串 Spectrum 到 Spectrogram
如果把每一个时间窗口得到的 Spectrum 横向排列:
Time →
Frequency
↑
高 │ ░░▒▒██▒░░
│ ░▒████▒░░
│ ░██▒▒██▒░
低 │ ███▒░███▒
└────────────就得到 Spectrogram(时频谱图)。
它通常编码:
- 横轴:Time;
- 纵轴:Frequency;
- 颜色或亮度:Magnitude / Power。
因此:
分别对应:
6.4 时间分辨率与频率分辨率为什么互相牵制?
假设窗口长度为 ,采样率为 。
窗口持续时间:
频率 Bin 间隔:
于是:
窗口变长:
频率区分能力更好,但一个窗口覆盖更长的时间。
窗口变短:
能够更准确定位瞬间变化,但频率分辨能力下降。
这就是 STFT 设计中最基本的权衡之一。
你不能无限提高:
- 时间分辨率;
- 频率分辨率;
而完全不付代价。
窗口长度、Window Function 与 Hop Size,本质上共同决定你“以什么尺度观察信号”。
7. 游戏音频中的频域分析
7.1 Audio Reactive Visuals
游戏里最直观的 FFT 应用,就是让视觉元素响应音乐。
例如夜店场景可以设计:
每帧或每个音频分析周期,取得最近一段音频:
进行窗口处理:
然后 FFT:
再从指定频带累积能量。
例如:
最后把:
映射到:
- Light Intensity;
- Emission;
- Particle Spawn Rate;
- Camera Effect;
- Material Parameter;
- Gameplay Trigger。
这就是一套最基本的 Audio Reactive Pipeline。
7.2 如何得到稳定的 Bass、Mid、Treble?
直接把某个 FFT Bin 映射到灯光通常效果并不好。
现实中至少还需要考虑三件事。
首先,一个声音的能量经常分布在多个 Bin,而不是刚好集中在一个位置。
因此更合理的是做频带累积:
其次,音频强度范围很大。
常常需要进行:
或其他动态范围压缩。
最后,频谱本身变化很快。
直接驱动视觉效果容易产生抖动,所以可以增加时间平滑:
其中:
- :当前频带能量;
- :平滑后的值;
- :平滑系数。
于是一个可用的音乐响应系统通常不是:
FFT → Light而更接近:
FFT
→ Band Aggregation
→ Normalization
→ Log / Compression
→ Temporal Smoothing
→ Visual Mapping7.3 Unity 中的 Spectrum Data
以 Unity 6.0 的 API 文档为例,AudioSource.GetSpectrumData 可以直接取得正在播放的 AudioSource 的频谱数据,并允许指定 FFTWindow。Unity 文档指出,该 API 使用 AudioSettings.outputSampleRate,并将频带分布在 到采样率一半的范围内。7
一个最小示意:
using UnityEngine;
public class AudioSpectrum : MonoBehaviour
{
[SerializeField] private AudioSource audioSource;
private readonly float[] spectrum = new float[1024];
private void Update()
{
audioSource.GetSpectrumData(
spectrum,
0,
FFTWindow.BlackmanHarris
);
float bass = BandEnergy(40f, 200f);
// Example:
// light.intensity = bass;
}
private float BandEnergy(float minHz, float maxHz)
{
float nyquist = AudioSettings.outputSampleRate * 0.5f;
int minBin = Mathf.Clamp(
Mathf.FloorToInt(minHz / nyquist * spectrum.Length),
0,
spectrum.Length - 1
);
int maxBin = Mathf.Clamp(
Mathf.CeilToInt(maxHz / nyquist * spectrum.Length),
minBin + 1,
spectrum.Length
);
float energy = 0f;
for (int i = minBin; i < maxBin; i++)
{
energy += spectrum[i] * spectrum[i];
}
return energy;
}
}这里的重点不是具体频段数值,而是数据流:
Unity 6.0 文档对 GetSpectrumData 还有几个值得注意的工程限制:
传入数组长度必须是 2 的幂,并限制在 64 到 8192;API 使用输出采样率而不是 AudioClip 自身采样率;文档还特别提醒,它访问的是短历史窗口,并不适合要求严格时序或低延迟的关键实时分析。7
因此真正的节奏游戏、DSP 或需要 sample-accurate timing 的系统,不应该简单把 Update() 中的频谱查询当成完整音频分析架构。
7.4 Beat、Onset 与 Pitch Detection
FFT 能提供频域信息,但很多游戏真正关心的是更高级的问题:
鼓点什么时候发生?
BPM 是多少?
现在唱的是哪个音?
这些问题并不等于:
做一次 FFT
→ 找最大值
→ 完成例如 Onset Detection 可以观察相邻 STFT Frame 之间频谱能量的突然变化。
一种常见思路是计算类似 Spectral Flux 的量:
当大量频率能量突然增加时,往往意味着新的音符或打击开始。
Beat Detection 则通常还需要进一步分析 Onset 随时间形成的周期。
Pitch Detection 也不能简单认定:
最大的 FFT Peak 就是基频。
真实声音可能出现:
- 高次谐波比基频更强;
- 基频很弱;
- 噪声;
- 非整数 Bin;
- Vibrato;
- 多个声源。
因此 FFT 更像是:
许多音频分析算法的前端表示。
而不是所有问题的完整答案。
7.5 FFT、EQ 与滤波器是什么关系?
理解频域后,很容易产生另一个误区:
既然滤波就是改变不同频率,那所有滤波是不是都应该先 FFT?
不是。
对于一个实时低通滤波器:
一个简单 IIR / Biquad 每个采样只需要非常少的计算。
如果为了它:
Buffer
→ FFT
→ 修改频谱
→ IFFT反而可能更加复杂,并引入 Buffering Latency。
可以粗略这样理解:
| 问题 | 常见合适方法 |
|---|---|
| 简单 Low-pass / High-pass / EQ | IIR / Biquad |
| 短 FIR | 直接 FIR |
| 频谱分析 | FFT / STFT |
| 很长的 FIR / Impulse Response | FFT Convolution |
| 离线频域编辑 | FFT |
| 长窗口相关分析 | FFT 往往很有价值 |
傅里叶变换不是“频率问题的唯一算法”。
它提供的是一个新的表示空间。
8. 卷积:为什么频域还能让计算变简单?
频域最重要的价值之一,并不只是“看 Spectrum”。
而是:
有些在时域里计算非常昂贵的运算,到了频域之后会变得异常简单。
最经典的例子就是 Convolution(卷积)。
8.1 卷积在描述什么?
离散卷积可以写成:
直觉上,它是在做:
把一个 Kernel 在整个信号上滑动,并不断计算加权叠加。
这与图像卷积、FIR Filter 的思想完全一致。
如果:
是输入,
是系统对一个单位冲激的响应,
那么:
描述的就是输入经过这个线性时不变系统之后的结果。
8.2 Impulse Response 与空间声学
假设在一个大厅中播放一个极短的脉冲。
声音会:
- 直接到达听者;
- 从墙壁反射;
- 从天花板反射;
- 多次衰减;
- 最后形成长长的混响尾部。
把这个响应记录下来,就得到:
Impulse Response,IR。
如果一个房间近似为线性时不变系统,那么任意声音经过这个空间之后可以表示为:
其中:
- :原始声音;
- :空间 IR;
- :带有空间响应的声音。
这就是 Convolution Reverb 的基本思想。
8.3 为什么长卷积计算很贵?
假设输入长度大约为 ,Impulse Response 长度为 。
直接计算卷积需要大量乘加:
如果 IR 很长,例如包含数秒混响,直接逐采样计算会迅速变得昂贵。
这时傅里叶变换的一个重要定理开始发挥作用。
8.4 为什么时域卷积会变成频域乘法?
卷积定理告诉我们:
也就是说:
于是原本的:
大量滑动
大量 Multiply-Accumulate可以转换成:
在频域中,卷积变成了:
也就是简单的逐点乘法。
NumPy 的 FFT 文档也明确把“大型卷积可以通过 FFT 加速”列为高维 Fourier 方法的重要用途之一。1
8.5 FFT Convolution 的工程价值
如果两个序列长度同阶,直接卷积大约是:
而使用 FFT:
可以做到接近:
这就是长 FIR 和 Convolution Reverb 中 FFT 极其重要的原因。
不过工程实现还必须处理一个问题:
DFT 天然对应的是 Circular Convolution。
如果想得到普通的 Linear Convolution,需要进行足够的 Zero Padding。
实时音频还不能等到整段声音结束以后再一次性 FFT,因此还会使用:
- Overlap-Add;
- Overlap-Save;
- Partitioned Convolution;
把长信号分块处理。
于是这里可以第一次非常直观地看到:
频域并不只是让数据“更容易看懂”。
它甚至会改变一个问题的计算结构与复杂度。
9. 频率不只存在于声音中
9.1 Spatial Frequency:图片也有“频率”
声音的输入变量是:
所以我们讨论每秒振动多少次。
图片的输入变量是:
因此同样可以讨论:
图像在空间中变化得有多快?
例如一大片缓慢变化的天空:
████████████████████属于较低的 Spatial Frequency。
而细密栅栏:
||||||||||||||||||||则包含大量高 Spatial Frequency。
二维傅里叶变换可以写成:
于是图像也能够:
9.2 模糊、细节与高低频
一张图像中:
- 平缓的大面积变化主要集中在低频;
- 快速变化的边缘和细节对应更高频率。
因此从频域角度看:
Blur 可以理解成抑制高频:
而很多 Sharpening 方法,则可以理解成提高高频成分相对于低频的权重。
当然,这并不意味着游戏里的每次 Gaussian Blur 都应该 FFT。
对于很小的卷积 Kernel:
3 × 3
5 × 5直接 Shader 卷积通常更加简单。
只有 Kernel 很大、卷积很长或者频域本身就是目标表示时,FFT 的优势才逐渐明显。
9.3 FFT Ocean:海浪为什么也可以放到频域?
海面是傅里叶思想在图形学里最漂亮的例子之一。
假设海面高度是:
其中:
一个复杂海面可以理解成大量不同:
- Wave Vector;
- 波长;
- 方向;
- 振幅;
- 相位;
的波叠加。
可以写成类似:
这里:
是空间中的 Wave Vector。
其方向表示波传播方向,长度与空间波长有关。
Jerry Tessendorf 在经典 SIGGRAPH Course Notes Simulating Ocean Water 中系统描述了一类频域海面方法:先在 Fourier Space 中构造与时间演化海浪频谱,再通过 Fourier 变换生成空间中的海面高度。8
于是每一帧可以概念性地理解成:
这和音频里的:
本质上是相同的数学结构。
9.4 FFT Ocean 不等于通用流体模拟
这里也需要避免另一个常见误解:
使用 FFT 的海洋,并不是在一般意义上完整求解所有水体运动。
这类频谱海面非常适合描述:
- 大尺度海洋表面;
- 大量叠加波;
- 风浪频谱;
- 周期性大面积 Height Field。
但它并不天然解决:
- 水撞击障碍物;
- 杯子里的水;
- 海浪沿复杂岸线破碎;
- 任意拓扑液体;
- 强烈局部流体交互。
因此它和 SPH、FLIP、Eulerian Fluid Simulation 等方案解决的不是完全相同的问题。
这再次说明:
选择频域不是因为 FFT 很高级,而是因为问题本身具有适合频域表达的结构。
10. 游戏程序员什么时候应该想到 FFT?
傅里叶变换真正值得掌握的,不是“以后遇到声音就用 FFT”。
更好的判断方式是问:
这个问题在频域中是否拥有比原始域更简单的结构?
可以建立这样一个粗略判断表:
| 问题 | 是否应该优先想到频域? | 原因 |
|---|---|---|
| 看一段声音包含哪些频率 | 是 | 频域直接表达频率组成 |
| 音乐可视化 | 是 | 需要频带能量 |
| Spectrogram | 是 | 本质是 STFT |
| 很长的卷积 | 是 | 卷积变乘法 |
| FFT Ocean | 是 | 海面天然适合波谱表示 |
| 简单低通滤波 | 通常不是 | Biquad 更直接 |
| 3×3 图像模糊 | 通常不是 | Spatial Kernel 更便宜 |
| 一次普通碰撞检测 | 不是 | 与频率结构无关 |
| 任意液体局部交互 | 通常不是 | 频谱方法不自然 |
可以进一步把问题抽象成三个判断。
第一,这个系统是否存在明显的周期、振动或尺度结构?
如果有,频率可能是自然坐标。
第二,原始域中的复杂运算是否在频域中会简化?
卷积定理就是最经典的情况。
第三,转换成本是否值得?
FFT 与 IFFT 本身并不是免费的。
如果时域里只需要几个乘加,那么转换来转换去反而是在制造复杂度。
10.1 FFT、IIR/FIR 与直接计算如何选择?
可以把它们理解成不同层次的工具,而不是竞争关系。
IIR 擅长用很少状态实现高效实时滤波。
FIR 可以通过有限冲激响应直接描述系统,并能获得一些 IIR 不容易得到的特性。
FFT 擅长:
- 观察频率结构;
- 批量处理很多频率;
- 加速长卷积;
- 处理频域天然存在的问题。
因此不存在:
这样的简单排序。
选择取决于问题结构。
10.2 从傅里叶变换到“频域思维”
理解傅里叶变换之后,真正应该形成的思维方式不是:
“看到声音就 FFT。”
而是:
一个问题现在的表示方式,是不是让它显得比实际更复杂?
例如:
时域中的长卷积很复杂:
换到频域后变成:
复杂海面在空间里似乎有无数顶点。
换到频域后,可以理解成许多波:
一段看似混乱的声音波形:
_/\/\__/\/\/\/\___/\/\_/\/\_换到频域以后:
100 Hz ███████
440 Hz ████
1 kHz ██
4 kHz █结构突然出现了。
11. 总结:改变的不是信号,而是观察信号的方式
傅里叶变换最容易被误解成:
一个“用来分析声音频率”的算法。
它当然可以做到这一点。
但这只是表面应用。
更一般地说,傅里叶变换是在建立:
声音中:
图像中:
海洋模拟中:
而卷积则告诉我们,转换表示甚至可以改变计算本身:
因此学习傅里叶变换时,最值得带走的不是某一个积分公式。
而是这样一种更普遍的工程直觉:
当一个问题在当前表示下很复杂时,不妨问:是否存在另一种基底、另一种坐标系或另一种表示,让问题本身变简单?
傅里叶变换只是这个思想最经典、最强大的例子之一。
注释
-
NumPy Documentation,Discrete Fourier Transform (
numpy.fft),NumPy 2.2 Manual。该文档给出了 DFT 定义、Spectrum / Frequency Domain 的术语、Magnitude / Power / Phase 的关系、实值输入的共轭对称性,以及 FFT 在高维分析与长卷积中的用途。 ↩ ↩2 ↩3 ↩4 -
James W. Cooley, John W. Tukey,An Algorithm for the Machine Calculation of Complex Fourier Series,Mathematics of Computation,Vol. 19, No. 90, 1965, pp. 297–301,doi:10.1090/S0025-5718-1965-0178586-1。论文描述了把运算量从与 成比例降低到与 成比例的快速 Fourier 算法。 ↩
-
Matteo Frigo, Steven G. Johnson,FFTW 3.3.11 Documentation,Introduction。FFTW 官方文档说明其 DFT 实现支持 arbitrary length,并为所有长度使用 级算法,因此“FFT 必须使用 2 的幂”不是普遍数学限制。 ↩
-
SciPy Documentation,Discrete Fourier Transforms (
scipy.fft)。文档给出了采样频率、DFT 频率间隔以及 的定义。 ↩ -
SciPy Documentation,Signal Processing (
scipy.signal) — Spectral Analysis。教程解释了有限观测区间如何产生 Spectral Leakage、Windowing 如何通过降低旁瓣来缓解泄漏,以及主瓣宽度与泄漏之间的权衡;同时展示了 Zero Padding 对频谱采样密度的作用。 ↩ ↩2 ↩3 -
SciPy Documentation,
scipy.signal.ShortTimeFFT与 Signal Processing Tutorial 的 STFT 章节。文档将 STFT 描述为沿信号滑动窗口并顺序进行 FFT,从而分析 Spectrum 随时间的变化。 ↩ -
Unity Technologies,Unity 6.0 Scripting API,
AudioSource.GetSpectrumData。文档说明该 API 返回正在播放 AudioSource 的 Spectrum Data,使用AudioSettings.outputSampleRate,支持 FFT Window;传入数组长度要求为 2 的幂、范围 64–8192,并注明其短历史缓冲不适合要求严格时序或低延迟的关键实时分析。 ↩ ↩2 -
Jerry Tessendorf,Simulating Ocean Water,SIGGRAPH Course Notes,1999–2004。该系列课程笔记系统介绍了以 Fourier-Space Ocean Spectrum 表示、演化并重建海面的方法,是计算机图形学中频域海洋模拟的经典资料。 ↩
CONTINUE EXPLORING