C++模拟水波的形成

波有如下几个特性:

扩散:当你投一块石头到水中,你会看到一个以石头入水点为圆心所形成的一圈圈的水波,这里,你可能会被这个现象所误导,以为水波上的每一点都是以石头入水点为中心向外扩散的,这是错误的。实际上,水波上的任何一点在任何时候都是以自己为圆心向四周扩散的,之所以会形成一个环状的水波,是因为水波的内部因为扩散的对称而相互抵消了。 
衰减:因为水是有阻尼的,否则,当你在水池中投入石头,水波就会永不停止的震荡下去。 
水的折射:因为水波上不同地点的倾斜角度不同,所以,因为水的折射,我们从观察点垂直往下看到的水底并不是在观察点的正下方,而有一定的偏移。如果不考虑水面上部的光线反射,这就是我们能感觉到水波形状的原因。 
反射:水波遇到障碍物会反射。 
衍射:忽然又想到这一点,但是在程序里却看不到,如果能在水池中央放上一块礁石,或放一个中间有缝的隔板,那么就能看到水波的衍射现象了。 
好了,有了这几个特性,再运用数学和几何知识,我们就可以模拟出真实的水波了。但是,如果你曾用3DMax做过水波的动画,你就会知道要渲染出一幅真实形状的水波画面少说也得好几十秒,而我们现在需要的是实时的渲染,每秒种至少也得渲染20帧才能使得水波得以平滑的显示。考虑到电脑运算的速度,我们不可能按照正弦函数或精确的公式来构造水波,不能用乘除法,更不能用sin、cos,只能用一种取近似值的快速算法,尽管这种算法存在一定误差,但是为了满足实时动画的要求,我们不得不这样做。

首先我们要建立两个与水池图象一样大小的数组buf1[PoolWidth*PoolHeight]和buf2[PoolWidth*PoolHeight](PoolWidth=水池图象的象素宽度、PoolHeight=水池图象的象素高度),用来保存水面上每一个点的前一时刻和后一时刻波幅数据,因为波幅也就代表了波的能量,所以以后我们称这两个数组为波能缓冲区。水面在初始状态时是一个平面,各点的波幅都为0,所以,这两个数组的初始值都等于0。

下面来推导计算波幅的公式

我们假设存在这样一个一次公式,可以在任意时刻根据某一个点周围前、后、左、右四个点以及该点自身的振幅来推算出下一时刻该点的振幅,那么,我们就有可能用归纳法求出任意时刻这个水面上任意一点的振幅。如左图,你可以看到,某一时刻,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
=>4a+b=1

找出一个最简解:a = 1/2、b = -1
因为1/2可以用移位运算符“>>”来进行,不用进行乘除法,所以,这组解是最适用的而且是最快的。那么最后得到的公式就是:
X0‘=(X1+X2+X3+X4)/ 2- X0

好了,有了上面这个近似公式,你就可以推广到下面这个一般结论:已知某一时刻水面上任意一点的波幅,那么,在下一时刻,任意一点的波幅就等于与该点紧邻的前、后、左、右四点的波幅的和除以2、再减去该点的波幅。

应该注意到,水在实际中是存在阻尼的,否则,用上面这个公式,一旦你在水中增加一个波源,水面将永不停止的震荡下去。所以,还需要对波幅数据进行衰减处理,让每一个点在经过一次计算后,波幅都比理想值按一定的比例降低。这个衰减率经过测试,用1/32比较合适,也就是1/2^5。可以通过移位运算很快的获得。

到这里,水波特效制作中最艰难的部分已经度过了,下面是源程序中计算波幅数据的代码。

//*******************************************************
//计算波能数据缓冲区
//*******************************************************
void RippleSpread()
{
for (int i=BACKWIDTH; i<BACKWIDTH*BACKHEIGHT-BACKWIDTH; i++)
{
//波能扩散
buf2[i] = ((buf1[i-1]+
buf1[i+1]+
buf1[i-BACKWIDTH]+
buf1[i+BACKWIDTH])
>>1)
- buf2[i];
//波能衰减
buf2[i] -= buf2[i]>>5;
}

//交换波能数据缓冲区
short *ptmp =buf1;
buf1 = buf2;
buf2 = ptmp;
}

好了,下面再来根据算出的波幅数据对页面进行渲染。

因为水的折射,当水面不与我们的视线相垂直的时候,我们所看到的水下的景物并不是在观察点的正下方,而存在一定的偏移。偏移的程度与水波的斜率,水的折射率和水的深度都有关系,如果要进行精确的计算的话,显然是很不现实的。同样,我们只需要做线形的近似处理就行了。因为水面越倾斜,所看到的水下景物偏移量就越大,所以,我们可以近似的用水面上某点的前后、左右两点的波幅之差来代表所看到水底景物的偏移量。

在程序中,用一个页面装载原始的图象,用另外一个页面来进行渲染。先用Lock函数锁定两个页面,取得指向页面内存区的指针,然后用根据偏移量将原始图象上的每一个象素复制到渲染页面上。进行页面渲染的代码如下:(下面的代码为了便于理解,并没有进行优化,实际上,优化后的代码比它要麻烦许多)

//*******************************************************
//根据波能数据缓冲区对离屏页面进行渲染
//*******************************************************
void RenderRipple()
{
//锁定两个离屏页面
DDSURFACEDESC ddsd1, ddsd2;
ddsd1.dwSize = sizeof (DDSURFACEDESC);
ddsd2.dwSize = sizeof(DDSURFACEDESC);
lpDDSPic1->Lock(NULL, &ddsd1, DDLOCK_WAIT, NULL);
lpDDSPic2->Lock(NULL, &ddsd2, DDLOCK_WAIT, NULL);

//取得页面象素位深度,和页面内存指针
int depth=ddsd1.ddpfPixelFormat.dwRGBBitCount/8;
BYTE *Bitmap1 = (BYTE*)ddsd1.lpSurface;
BYTE *Bitmap2 = (BYTE*)ddsd2.lpSurface;

//下面进行页面渲染
int xoff, yoff;
int k = BACKWIDTH;
for (int i=1; i<BACKHEIGHT-1; i++)
{
for (int j=0; j<BACKWIDTH; j++)
{
//计算偏移量
xoff = buf1[k-1]-buf1[k+1];
yoff = buf1[k-BACKWIDTH]-buf1[k+BACKWIDTH];

//判断坐标是否在窗口范围内
if ((i+yoff )< 0 ) {k++; continue;}
if ((i+yoff )> BACKHEIGHT) {k++; continue;}
if ((j+xoff )< 0 ) {k++; continue;}
if ((j+xoff )> BACKWIDTH ) {k++; continue;}

//计算出偏移象素和原始象素的内存地址偏移量
int pos1, pos2;
pos1=ddsd1.lPitch*(i+yoff)+ depth*(j+xoff);
pos2=ddsd2.lPitch*i+ depth*j;

//复制象素
for (int d=0; d<depth; d++)
Bitmap2[pos2++]=Bitmap1[pos1++];
k++;
}
}
//解锁页面
lpDDSPic1->Unlock(&ddsd1);
lpDDSPic2->Unlock(&ddsd2);
}

增加波源

俗话说:无风不起浪,为了形成水波,我们必须在水池中加入波源,你可以想象成向水中投入石头,形成的波源的大小和能量与石头的半径和你扔石头的力量都有关系。知道了这些,那么好,我们只要修改波能数据缓冲区buf,让它在石头入水的地点来一个负的“尖脉冲”,即让buf[x,y]=-n。经过实验,n的范围在(32~128)之间比较合适。

控制波源半径也好办,你只要以石头入水中心点为圆心,画一个以石头半径为半径的圆,让这个圆中所有的点都来这么一个负的“尖脉冲”就可以了(这里也做了近似处理)。

增加波源的代码如下:

//*****************************************************
//增加波源
//*****************************************************
void DropStone(int x,//x坐标
int y,//y坐标
int stonesize,//波源半径
int stoneweight)//波源能量
{
//判断坐标是否在屏幕范围内
if ((x+stonesize)>BACKWIDTH ||
(y+stonesize)>BACKHEIGHT||
(x-stonesize)<0||
(y-stonesize)<0)
return;

for (int posx=x-stonesize; posx<x+stonesize; posx++)
for (int posy=y-stonesize; posy<y+stonesize; posy++)
if ((posx-x)*(posx-x) + (posy-y)*(posy-y) < stonesize*stonesize)
buf1[BACKWIDTH*posy+posx] = -stoneweight;
}

 

好了,至此,水波特效的制作原理就此就全部揭示了。在上面的推导中,每一步都进行了很多看似非常过分的近似处理,但是,你完全不必担心,事实证明,用这种方法,在速度和图象上都可以获得非常好的效果。源程序中有非常详尽的注释,仔细推敲一下,看懂它们应该不成问题。

这个程序是Win32下的DirectX编程,没有使用任何包装库。在我的电脑上(AMDK6-200、2MVRam、64MSRam),320x240的画面大小,每秒可以达到25帧。与前几个程序不一样,这个程序使用了窗口模式,所以调试起来很方便。如果你对窗口模式编程不熟悉,这个程序也是一个很好的例子。

这种用数据缓冲区对图象进行水波处理的方法,有个最大的好处就是,程序运算和其示的速度与水波的复杂程度是没有关系的,无论水面是风平浪静还是波涛汹涌,程序的fps始终保持不变,这一点你研究一下程序就应该可以看出来。实际上,如果你掌握了这种方法,将这种方法推广一下,完全可以做出另外一些特殊的效果,如烟雾、大气、阳光等,我现在也正在研究这些特效的制作,相信不久以后就会有新的收获。

时间: 2024-12-19 16:29:15

C++模拟水波的形成的相关文章

android:模拟水波效果的自定义View

Github地址:https://github.com/nuptboyzhb/WaterWaveView 欢迎Fork,欢迎Star 1.先看效果 2.再看关键代码 描绘函数y = Asin(wx+d)+offset /** * 使用路径描绘绘制的区域 * * @return */ private Path getFristWavePath() { // 绘制区域1的路径 if (firstWavePath == null) { firstWavePath = new Path(); } fir

HTML学习总结(四)【canvas绘图、WebGL、SVG】

一.Canvas canvas是HTML5中新增一个HTML5标签与操作canvas的javascript API,它可以实现在网页中完成动态的2D与3D图像技术.<canvas> 标记和 SVG以及 VML 之间的一个重要的不同是,<canvas> 有一个基于 JavaScript 的绘图 API,而 SVG 和 VML 使用一个 XML 文档来描述绘图.SVG 绘图很容易编辑与生成,但功能明显要弱一些. canvas可以完成动画.游戏.图表.图像处理等原来需要Flash完成的一

使用opengl制作简单的控制杆

这一次数字媒体的作业是在模拟水波的基础上(波浪相关实现参考:http://www.cnblogs.com/linchw3/p/4840995.html)使用控制杆进行相关控制,原本以为opengl应该会有相关的函数可以直接实现控制杆,可是找了好久没有找到(应该是没有这种函数,不太肯定),所以只好自己手打控制杆,后来发现其实,很简单,其实控制杆就是一个矩形(代表杆),一个随着鼠标移动的矩形(代表滑块),还有一个鼠标监控,以实现波浪高度的调节杆为例子: 首先是绘制一个控制杆(矩形): 1 glBeg

HTML5 学习总结(四)——canvas绘图、WebGL、SVG

HTML5 学习总结(四)--canvas绘图.WebGL.SVG 目录 一.Canvas 1.1.创建canvas元素 1.2.画线 1.3.绘制矩形 1.4.绘制圆弧 1.5.绘制图像 1.6.绘制文字 1.7.随机颜色与简单动画 二.WebGL 2.1.HTML5游戏开发 2.2.1.Cocos2D-HTML5 2.2.2.Egret(白鹭引擎) 三.SVG 3.1.SVG Hello Wrold 3.2.多种引入SVG的方法 3.3.画直线 3.4.画椭圆 3.5.文本与矩形 3.6.向

Atitit (Sketch Filter)素描滤镜的实现 &#160;图像处理 &#160;attilax总结

Atitit (Sketch Filter)素描滤镜的实现  图像处理  attilax总结 Sch lg java d sketch filter aigo se ,ma sinsho ..byedu 3page ma ... 素描滤镜的实现方法比较简单,这里我们直接写出算法过程如下: 1,对原图S进行去色命令得到灰度图A: 2,对A进行反色得到图像B: 3,对B进行高斯模糊得到图C: 4,将C与B进行颜色减淡的图层混合算法: P(x,y) = Pb(x,y) + (Pb(x,y)* Pc(x

cocos2d-x:Particle System(粒子系统)

一.粒子系统简介: 粒子系统最早出现在80年代,主要用于解决由大量按一定规则运动(变化)的微小物质在计算机上的生成和显示问题.Particle System的应用非常广泛,大的可以模拟原子弹爆炸,星云变化,小的可以模拟水波.火焰.烟火.云雾等,而这些自然现象用常规的图形算法是很难逼真再现的. Particle System可以说是一种基于物理模型来解决问题的方法,它的核心不在于如何显示,而在用于对微小物质模型的规则提取. 粒子运动(变化)的规则可以很简单也可以很复杂,这取决你所模拟的对象.举例来

手把手教你画一个 逼格满满圆形水波纹loadingview Android

才没有完结呢o( ̄︶ ̄)n .大家好,这里是番外篇. 拜读了爱哥的博客,又学到不少东西.爱哥曾经说过: 要站在巨人的丁丁上. 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱哥看到别打我). 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50523713 上一篇 带领大家做了一款炫酷的loading动画view 手把手带你做一个超炫酷loading成功动画view  不知道大家跟着做了一遍没有呢? 在开始之

HTML5 学习笔记(四)——canvas绘图、WebGL、SVG

一.Canvas canvas是HTML5中新增一个HTML5标签与操作canvas的javascript API,它可以实现在网页中完成动态的2D与3D图像技术.<canvas> 标记和 SVG以及 VML 之间的一个重要的不同是,<canvas> 有一个基于 JavaScript 的绘图 API,而 SVG 和 VML 使用一个 XML 文档来描述绘图.SVG 绘图很容易编辑与生成,但功能明显要弱一些. canvas可以完成动画.游戏.图表.图像处理等原来需要Flash完成的一

来来来!游戏场景风格暴露你的年纪

大家是不是不知道什么是低多边形风格游戏呢,唉,还是太年轻啊. 记得当年极为火热暗黑破坏神.红色警戒.塞尔达传说等等经典游戏都可以归为低多边形风格游戏.当然了,当年的主要问题却是电脑配置跟不上了. 随着电脑硬件的逐渐升级,游戏画面的精度却是越来越高,低多边形游戏也就慢慢地变成了"Low"的代言人了. 但是,近期随着<纪念碑谷><我的世界><战争模拟器><besiege>等大名鼎鼎的游戏问世,低多边形游戏却是另辟蹊径地以另一种方式成为了一种新