声明:这个特效不是我原创的,当然我也不知道这个特效的最初制作者是谁。我仅仅试图解释这个水波特效实现的原理。
我曾经从vchelp网站下载过这样一个水波特效的demo(c#写的),当我第一次看到这个控件的时候,感觉很不起眼,但是当我无意中用鼠标在控件上一拖,oh,it 's really amazing! 一层层水波荡漾开去,不得不说,我有些惊讶它的表现力! 我再次看了一下这个控件的代码,只有区区200多行,但是初看一眼,由于里面注释很少,没有马上读懂。后来我又找到了一些国内的网页,我对图像处理也有一些涉及和兴趣,这样一看也就明白了它的实现原理。所以本文中没有任何我自己的创造,只是希望用更通俗的语言解释一下其实现的过程。
为了更直观一点,还是先放一下效果图:
现在开始解释,我看到的中文解释网址如下:
特效天地——水波
http://www.itcai.net/D01/7/540.shtml
在这里对基本的铺垫省略,这个特效实现的依据如下:我们看到物理世界中的水波,是因为水波使水面产生起伏,从而导致光的折射方向不一,使水底景物的像在局部产生不同程度的偏移,形成水波的视觉效果。
设想一下,没有任何波纹的平面水面,则我们透过一个平面的玻璃去看水下的景物,将产生一副任何偏移的水底景物的画面。而较小的水波产生较轻微扭曲的画面,较大的水波产生扭曲更大的画面。因此,我们可以通过像素的偏移大小来模拟水波视觉。好了,这句话就是这个特效的模拟原理!
------------------------------------------------------------------------------------------------------------------------------
请注意,这个原理实际上和photoshop中置换滤镜的原理完全相同,我对该滤镜具有非常深入的理论和应用性研究,我在中国photoshop联盟论坛中有这个滤镜的教程,我将在以后讲解这个非常有趣的滤镜。并且这个特效也会促进我对该滤镜产生新的应用研究。
-------------------------------------------------------------------------------------------------------------------------------
从中学物理学科可知,波在媒介中传导时,媒介中的每一点都在原处做一种简谐振动。因此我们就对图像的每一点建立一个变量,它可以理解为表示该点的能量或振幅。这样所有点在一起形成一个wave [,]数组,wave[i, j]对应于像素(i, j)。由于水波在传导过程中,所有点随时间推移而改变,因此wave数组的内容需要发生改变。这里又引入一个类似信号处理中的递推公式的假设,假设下一时刻的wave值可以根据当前时刻的值经过推算得出,根据直观的感觉,这种递推,主要和相邻不远的临域位置相关,因此提出这样一个递推公式:这里我成段引用上面的原文的推导过程,
------------------------------------------------------------------------------------------------------------------------------
【-----------------------------------------------------------------------------------------------------------------------
某一时刻,X0点的振幅除了受X0点自身振幅的影响外,同时受来自它周围前、后、左、右四个点(X1、X2、X3、X4)的影响(为了简化,我们忽略了其它所有点),而且,这四个点对a0点的影响力可以说是机会均等的。那么我们可以假设这个一次公式为:
X0’=a(X1+X2+X3+X4)+bX0 (公式1)
a、b为待定系数,X0’为0点下一时刻的振幅
X0、X1、X2、X3、X4为当前时刻的振幅
下面我们来求解a和b。
假设水的阻尼为0。在这种理想条件下,水的总势能将保持不变。也就是说在任何时刻,所有点的振幅的和保持不变。那么可以得到下面这个公式:
X0’+X1’+...+Xn’ = X0+X1+...+Xn
将每一个点都象公式1那样计算,然后代入上式,得到:
(4a+b)X0+(4a+b)X1+...(4a+b)Xn = X0+X1+...+Xn
-----------------------------------------------------------------------------------------------------------------------】
-----------------------------------------------------------------------------------------------------------------------------
(这里的迭代理论基础我尚没完全搞清楚有待继续研究,但是可以直观的这样去设想)。
这里引入这样一个假设,下一时刻的值近似的用上下左右四个临域位置参与计算。设想这个平面无限大,则代入上式以后,可见每个元素各被上下左右相邻元素计算时引入乘以a一次,被自身计算时引入乘以b一次,因此就得到上式。因此可以得到在震荡不衰减的情况下:在四邻域情况下:(4a+b)=1。对这个关系式,会有很多满足的a和b,为了简化计算量,选择了a=1/2,b=-1.
对公式1的模式,实在太过熟悉,它就是数字图像处理中的非常常用的模板法的形式。在photoshop的自定义滤镜就是典型的模板算法。
它相当于这样一个计算模板:
[0 1 0] [0 0 0]
[1 0 1]/2+ [0 -1 0]
[0 1 0] [0 0 0]
如果引入八临域:则可以选择如下模板:
[1 1 1] [0 0 0]
[1 0 1]/4+ [0 -1 0]
[1 1 1] [0 0 0]
当然模板里的系数是一种加权值,前面都为1,说明我们认定这些临域对中心位置的影响因子相同,如果引入更大的模板,模型会更复杂,从直观感觉上来说可能会更精确,但是显然计算量也会更大。在上面的演示中使用的模板是上面的第二个。
这样的递推计算,是指水波不衰减的情况,而实际自然界由于各种阻力使能量逐渐衰减。因此上面的计算结果将进行一次衰减。例如将结果减去1/16. 选择这样的2的某次幂值完全是因为可以用移位这种比较高效的cpu指令。
最后,由于wave数组的内容需要迭代,我们可以使用两个同样规模的数组wave(t)和wave(t+1),它们两个可以交替迭代,从而使数据沿着时间向前推进,在demo中,将这两个数组合并成了一个三维数组,即wave[ bitmapWidth , bitmapHeight, 2]。使用第三个下标在0和1之间切换来完成交替。
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
23
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
24
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
26
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
其次我们需要触发一个水波,也就是投入一个初始的值到wave数组中,在自然界中显然水波是圆形的,所以我们在图形中的一个圆形区域内给它一初值。然后让wave数组进行迭代,计算出新的时刻的wave数组。
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
最后,我们需要在视觉效果上反应水波,我们的wave数组这时用来计算像素的偏移。这个假设就是水波特效的原理:wave数组的同行相邻元素的差值反应了水平偏移量,同列相邻元素的差值反应了垂直偏移量。这里得到偏移量以后,和photoshop中的置换滤镜方式就完全一样,查找到偏移后的位置,并进行边界判断,然后复制源图中该位置的像素,这部分代码不帖了。
对以上的过程,最神奇的是用递推式计算出下一时刻的wave数据,并且实际效果证明这种模拟效果很好。但是对于这一步,只能从感觉上理解,我尚没能够从用数学验证它。
这里同时也在photoshop领域衍生了一种新的应用,即制作水波效果的置换图。关于置换滤镜的原理,涉及到了photoshop领域里面比较深入的部分,不是三言两语能讲清的,这里暂时不详细讲解,直接给出结果吧:
我们用以下代码来生成置换图:
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedSubBlock.gif)
![](https://www.cnblogs.com/Images/dot.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
23
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
24
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
26
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
27
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
28
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
29
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
30
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
31
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
32
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
33
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
34
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
35
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
36
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
37
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
38
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
39
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
40
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
41
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
42
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
43
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
44
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
这样我们可以把这个置换图保存为Photoshop的PSD格式,即可被置换滤镜拿来置换了,下面是用这个置换图和它在photoshop中应用的效果,当然了,和这个控件产生的效果应该是差不多的。
这是生成置换图的工具:
这是使用代码生成的置换图:
应用于photoshop的置换滤镜以后,显然,它是预料中的结果~:
【注】上文中的水波置换图生成工具的源代码(位于菜单中):
http://files.cnblogs.com/hoodlum1980/PsFilters.rar