这个名字是我私取的。所谓抖动波,是通过在某点“人为地”搅动而产生向四周传播波,如同握绳子某点摇动而产生的物理波。回顾其实现方式,以及把算法移植到shader,让GPU高速运算。——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/water-simulation-3.html
涟漪在物理意义上也就是这么一个抖动波,但是在上篇[水效果Ⅱ - 涟漪]中,涟漪的实现不是采用波的传播方式,而是采用一种类似电场般迅速扑开的“场”,把整个水面纳入计算,根据距波源的距离、时间等模拟出某一时刻的顶点高度。所以说,它体现出传播的特点其实只是波源公式里各个衰减因子的功效而已。对于抖动波,我们就“无法”控制每个顶点的高度,在给予了最初的一个抖动后就应该“任由其发展”。说是最初的抖动,其实不过给予波源点一个向上或向下的速度罢了。假设执行波的传播计算的是RenderWave这么一个每帧执行的函数,它的功能就是在网格中带动一个连锁反应:波源点的上升会带动它所连接的四个相邻点上升,然后这四个相邻点的每一个又带动它的四个相邻点(包括带动它的波源点)……这样就把波传递下去了(传到网格边缘则从对边继续)。之后,再保证波源点具有向上速度的同时具有向下的加速度(即每帧速度都在减小),一个物理波的传播模型便形成了。波源在速度变为0的时候到达最高点并在反向速度的拉力下回落,继续扯动相邻点……
实现的算法的获得应该追溯到一年前,课程三大DEMO之一的完善需要一种简单的海水运动模拟,于是上网找算法找水运动的DEMO,并找到了一个日本人写的这个抖动波算法实现。比较单纯的正弦余弦波结合的方法,这种抖动波能产生更加让人信服的缓波浪。现在再次运用这种算法,本是希望模拟池水轻微的表面浮动,但是结果不理想,一来要等到整个“造浪”过程到达平缓阶段需要好一段时间,如上所述,抖动传播的过程是无法人为干扰的,改变传播速度也没办法,且最初波源中心的突兀而起是很显然的;二来因为没有一个精确的物理式子描述,所以在大波浪运动之下,组成的每个顶点相连并不光滑,造成很多小触点,尽管在海水中这种触点能恰好加强了波的细节描述,但在池水这种水面一般需要很很光滑的场合这样就不好了。
在给出代码之前说一下,如果目的是造成网格各处起伏的波浪的话,波源最好不要位于网格中心,或者网格不要是个正方形。因为波到达边缘要回弹或者从对边继续开始同向拂来,如果网格和波源位置过于“工整”则只会造成波波抵消之类的效果,最初给的抖动(或者说,能量)没办法分散,所以始终会表现为一种重复的运动——波源引导最大振幅的波动。
以下代码是我对原始代码简化后的版本。在DEMO中为了简捷我还是取规正网格和中心波源,没所谓了 - -
- //波动开始的时刻
- mCurrentWaveDelta[0] = 2.0;//假设0索引是波源点,设定初速
- //渲染过程:
- for(z = 1; z < mGridScaleZ - 1; ++z)
- for(x = 1; x < mGridScaleX - 1; ++x)
- {
- factor.set(0.0, 0.0, 0.0);
- index = x + z * mGridScaleX;
- deltaVec = mOldCurrentOriGrid[index] - mOldCurrentOriGrid[index + 1];
- vecDis = deltaVec.abs() * NormalLengthPerGridFactor;
- factor += deltaVec.normalize() * (1.0 - vecDis);
- deltaVec = mOldCurrentOriGrid[index] - mOldCurrentOriGrid[index - 1];
- vecDis = deltaVec.abs() * NormalLengthPerGridFactor;
- factor += deltaVec.normalize() * (1.0 - vecDis);
- deltaVec = mOldCurrentOriGrid[index] - mOldCurrentOriGrid[index + mGridScaleX];
- vecDis = deltaVec.abs() * NormalLengthPerGridFactor;
- factor += deltaVec.normalize() * (1.0 - vecDis);
- deltaVec = mOldCurrentOriGrid[index] - mOldCurrentOriGrid[index - mGridScaleX];
- vecDis = deltaVec.abs() * NormalLengthPerGridFactor;
- factor += deltaVec.normalize() * (1.0 - vecDis);
- mCurrentWaveDelta[index] += factor * 0.2f * velocity;
- mCurrentOriGrid[index] += mCurrentWaveDelta[index];
- }
mOldCurrentOriGrid是用于计算的当前时刻的网格高度,deltaVec是相邻网格顶点之间的向量;NormalLengthPerGridFactor是一小网格的边长之倒数,是为了规范化vecDis到一个与1靠近的值;factor收集了四相邻点的扯动效果,影响该点速度(mCurrentWaveDelta)方向,是正扯反扯就看vecDis与1.0比的大小以及deltaVec方向——看官可以回忆高中学的机械波传播过程,速度,加速度,位置变化等信息。事实上我觉得自己还是不怎么能完全理解的,所以解释的话就如此从简了。
由截图可见波是从波源一直传播开来并扩散的,最后达到算是比较和缓的状态。这就是CPU上实现的抖动波。
接下来是GPU上实现的抖动波。整整折磨了我暑假里不止一个星期啊。
把算法搬到shader上,但是这里每个顶点都要知道相邻点的情况,所以不能交给并行处理顶点的vertex shader来做。方法是:
1.独立设一个shader对象,把全部顶点打包成数组(也就是纹理,见[Vertex Texture Fetch 顶点纹理拾取] ,每个纹素包含一个顶点信息,如果有N*N个顶点,就弄一张N*N大小的纹理好了),传入该fragment shader;
2.把上面的算法应用到这张纹理上(这是难点,就是这里搞死我的就是这里!),注意纹理可以随便检索,因此通过纹理坐标的偏移可以获得相邻像素里的对应的顶点信息,结果(代表每个顶点的位置)作为gl_FragData渲染到应用程序的一张空纹理上(FBO的应用,见[学一学,FBO] );
3.最后是处理网格顶点的shader对象的vertex shader对此处理过的纹理进行VTF(顶点纹理获取,同见[Vertex Texture Fetch 顶点纹理拾取] ),把得到的顶点位置数据作为对应顶点的结果。
当然,编程细节可多了。不可能一一在此阐述,但大体思路就是这样。作为效果演示,以下DEMO的抖动波效果是夸张了点,但是它能让你感性了解什么是抖动波。(众:喂,这个名词不是你自己乱取的咩~)
下篇预告:[水效果Ⅳ - GPU水面波]
DEMO提示:可按提示文字操作,右键是涟漪效果(基于CPU计算的)[注意别连续按];D键是抖动波演示[注意别多按]。
本DEMO用了不少OpenGL技术和GPU技术,所以对显卡的要求颇高的,(另外,ATI显卡目前连VTF都不一定支持,汗~)我只能保证NV 9 series以上大概能正常观看了。
下载点My Google Code - Downloads: 1. Water-Simulation-抖动波
http://code.google.com/p/zwqxindemos/downloads/list
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/water-simulation-3.html