这一篇文章作为介绍海面渲染的提前篇,也作为第二篇的补充和修正。在第二篇中,介绍了 FFT的计算方法。然而,在深入学习了海面渲染方法时,发现了之前的一些问题。关于第二篇的内容,在这里就不回顾了,感兴趣的可以先粗略阅读。(跳转链接)
先说明第二篇存在的问题及造成该问题的原因。然后再介绍蝶形变换计算的本质和降低计算量方法。
存在的问题
第二篇提供的分析是正确的。但是实现的算法时间复杂度是O(n^2log(n))。而算法的时间复杂度是可以降低到O(nlog(n))的。第二篇之所以没有意识到这个问题,是由于没有考虑到W_N^k的周期性。这里W_N^k有一个性质,即:
W_N^{k}=W_N^{k+N}
在网上公开的许多资料中,都提到了蝶形变化和比特反转。第二篇并没有提及这些内容,没有提到这些内容的原因是我认为资料中给出的介绍没有很好地解释这些内容,有的只是给了一张图,并没有解释图的含义。这里简单介绍一下比特反转,及我在学习比特翻转时发现的一些特点。
比特翻转
比特翻转就是把一个由n bit 组成的整数的比特位进行翻转,例如一个3 bit组成的整数100,经过比特翻转后变成001。
回到第二篇的递归推导。这里展示了8个数的傅立叶变换的递归计算过程。

这里看最下面一层,顺序是0,4,2,6,1,5,3,7。最上一层的顺序是0,1,2,3,4,5,6,7。比特翻转的意义就是能够由最上层的下标,可以计算出最下层的下标。4的比特翻转结果是1,所以在最下一层,x[4]在下标为1的位置。比特翻转在这里就是能提前计算出输入序列在 FFT最低一层的计算中的组合方式。
不过这过计算过程,只是计算单个频率傅立叶变换的结果。即下面公式中,的某个X[i]的值,而这样的计算还有n个。
X[i] = \sum\limits_{n=0}^{N-1}x[n]{e^\frac{-jkn2\pi}{N}}
因此,不嫌麻烦地,我把八个数的FFT递归的每一项的计算过程都写出来。并把每一层底递归的求和结果按顺序展开,得到如下结果。从这个结果中可以总结出一些规律。
X[0] = x[0]+x[1]W_8^0+x[2]W_4^0+x[3]W_4^0W_8^0+x[4]W_2^0+x[5]W_2^0W_8^0+x[6]W_2^0W_4^0+x[7]W_2^0W_4^0W_8^0\\
X[1] = x[0]+x[1]W_8^1+x[2]W_4^1+x[3]W_4^1W_8^1+x[4]W_2^1+x[5]W_2^1W_8^1+x[6]W_2^1W_4^1+x[7]W_2^1W_4^1W_8^1\\
X[2] = x[0]+x[1]W_8^2+x[2]W_4^2+x[3]W_4^2W_8^2+x[4]W_2^2+x[5]W_2^2W_8^2+x[6]W_2^2W_4^2+x[7]W_2^2W_4^2W_8^2\\
X[3] = x[0]+x[1]W_8^3+x[2]W_4^3+x[3]W_4^3W_8^3+x[4]W_2^3+x[5]W_2^3W_8^3+x[6]W_2^3W_4^3+x[7]W_2^3W_4^3W_8^3\\
X[4] = x[0]+x[1]W_8^4+x[2]W_4^4+x[3]W_4^4W_8^4+x[4]W_2^4+x[5]W_2^4W_8^4+x[6]W_2^4W_4^4+x[7]W_2^4W_4^4W_8^4\\
X[5] = x[0]+x[1]W_8^5+x[2]W_4^5+x[3]W_4^5W_8^5+x[4]W_2^5+x[5]W_2^5W_8^5+x[6]W_2^5W_4^5+x[7]W_2^5W_4^5W_8^5\\
X[6] = x[0]+x[1]W_8^6+x[2]W_4^6+x[3]W_4^6W_8^6+x[4]W_2^6+x[5]W_2^6W_8^6+x[6]W_2^6W_4^6+x[7]W_2^6W_4^6W_8^6\\
X[7] = x[0]+x[1]W_8^7+x[2]W_4^7+x[3]W_4^7W_8^7+x[4]W_2^7+x[5]W_2^7W_8^7+x[6]W_2^7W_4^7+x[7]W_2^7W_4^7W_8^7\\
展开之后,每一项的结果一目了然。
从比特翻转中总结出的规律
根据上文中,比特翻转的介绍及递归过程的展开结果来看。可以看出FFT和比特位有一定的关系。进一步总结,可以总结出下面的规律。
X[0] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^0+x[2]W_2^0W_4^0W_8^0+x[3]W_2^0W_4^0W_8^0+x[4]W_2^0W_4^0W_8^0+x[5]W_2^0W_4^0W_8^0+x[6]W_2^0W_4^0W_8^0+x[7]W_2^0W_4^0W_8^0 \\ X[1] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^1+x[2]W_2^0W_4^1W_8^0+x[3]W_2^0W_4^1W_8^1+x[4]W_2^1W_4^0W_8^0+x[5]W_2^1W_4^0W_8^1+x[6]W_2^1W_4^1W_8^0+x[7]W_2^1W_4^1W_8^1 \\
X[2] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^2+x[2]W_2^0W_4^2W_8^0+x[3]W_2^0W_4^2W_8^2+x[4]W_2^2W_4^0W_8^0+x[5]W_2^2W_4^0W_8^2+x[6]W_2^2W_4^2W_8^0+x[7]W_2^2W_4^2W_8^2 \\
X[3] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^3+x[2]W_2^0W_4^3W_8^0+x[3]W_2^0W_4^3W_8^3+x[4]W_2^3W_4^0W_8^0+x[5]W_2^3W_4^0W_8^3+x[6]W_2^3W_4^3W_8^0+x[7]W_2^3W_4^3W_8^3 \\ X[4] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^4+x[2]W_2^0W_4^4W_8^0+x[3]W_2^0W_4^4W_8^4+x[4]W_2^4W_4^0W_8^0+x[5]W_2^4W_4^0W_8^4+x[6]W_2^4W_4^4W_8^0+x[7]W_2^4W_4^4W_8^4 \\X[5] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^5+x[2]W_2^0W_4^5W_8^0+x[3]W_2^0W_4^5W_8^5+x[4]W_2^5W_4^0W_8^0+x[5]W_2^5W_4^0W_8^5+x[6]W_2^5W_4^5W_8^0+x[7]W_2^5W_4^5W_8^5 \\X[6] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^6+x[2]W_2^0W_4^6W_8^0+x[3]W_2^0W_4^6W_8^6+x[4]W_2^6W_4^0W_8^0+x[5]W_2^6W_4^0W_8^6+x[6]W_2^6W_4^6W_8^0+x[7]W_2^6W_4^6W_8^6 \\X[7] = x[0]W_2^0W_4^0W_8^0+x[1]W_2^0W_4^0W_8^7+x[2]W_2^0W_4^7W_8^0+x[3]W_2^0W_4^7W_8^7+x[4]W_2^7W_4^0W_8^0+x[5]W_2^7W_4^0W_8^7+x[6]W_2^7W_4^7W_8^0+x[7]W_2^7W_4^7W_8^7 \\
由上,可以看到FFT的过程,就是对输入数组中每一个数乘以一个W_N^k。在上面的例子中,数组长度为8,比特位数为3位。下标的最高位为1的数,要乘以一个W_2^k,否则乘以W_2^0 (结果为1),第二位为1的,要乘以W_4^k,否则乘以1,第三位为1的要乘以W_8^k,否则要乘以1。这样就可以总结出一个规律:输入数组中每个元素的下标的比特位,决定了它应该乘的W_N^k还是W_N^0,而N从比特位的高位依次排到低位为,2,4,8……输入数组的长度保持为二的次幂,假设长度l=2^n,则比特数为n。上面的例子中,比特数为3.
蝶形变换
蝶形变换的过程,正是利用了上面总结的规律进行优化计算的。由于W_N^k具有周期性,因此有的成积不需要多次计算,只进行一次计算复用即可。这样一维的FFT只需要O(Nlog(n))次运行即可得出结果。感兴趣的可以自己动手计算一下,感受这个过程,结合周期性去总结规律。我会在后面介绍海面渲染的文章中,讲解这个优化过程,并介绍如何利用Compute Shader优化这个计算过程。ComputeShader的计算很快,可以在帧循环中实时更新。