摘要:本文介绍了一种对水面波纹的扩散、衰减以及交叠的过程进行计算机模拟的一种实现算法,并对在实现该算法过程中所使用的DirectX系列技术中的DirectDraw技术做了简要的说明。
关键字:Microsoft SDK、DirectX、DirectDraw、水波
一、 引言 现在各种高性能的计算机以其强大的运算能力被广泛应用于各种领域,也可以对许多自然界的物理现象和自然规律进行很好的仿真。但许多专业书籍往往对此类的仿真技术讳莫如深,使不少程序设计人员对此类程序的设计经常会感到无从下手。本文通过对真实水波的产生、扩散、衰减以及多个水波的交迭过程的计算机模拟来对此类程序的设计思路与方法做了简要的描述。在程序的实现过程中为了使仿真的效果更加逼真、使处理数据显示的速度更快使用了DirectX系列技术中的DirectDraw技术通过对硬件加速器的使用来对数据的显示进行加速。本文下面就围绕这些技术的应用展开讨论。
二、 水波模拟的算法设计 要对某种自然现象进行模拟仿真,就必须对该现象的特性有很好的认识。比如对于本文所仿真的对象--水波,就要对水波的诸多特性如扩散性、衰减性、反射性以及水的折射等都要有所认识,并最终通过程序算法体现在程序中。这些关于波的特性属于普通物理的研究范畴,
本文在此不再赘述。根据以上的特性,再利用数学和几何的有关知识就可以在计算机上模拟出真实的水波了。
因为在模拟时需要的是实时的渲染,而每秒种至少要渲染15帧以上才能使水波得以平滑的显示。考虑到普通微型计算机的运算速度,不能用乘、除法,更不可以使用正、余弦函数以精确的公式来构造水波,我们只能通过使用简单而高速的加、减法的近似算法来实现。可以用两个与水池图象一样大小的数组buf1和buf2来保存水面上每一个点(离散化的点,对应于每一个像素)的前、后两时刻的波幅数据。在无外力干扰时的稳定状态下水面是一个平面,水面各点的波幅都为0。当有外力干扰,如向水池投一颗石子会使水面泛起层层的涟漪。我们不能被现象所误导,实际上并非水面上的点在向外扩散,而是仍停在原地上下移动,由于振动幅度的变化而引起视觉上的错觉。而且水波上的任何一点在任何时候都是通过振幅的变化把能量以自己为圆心向四周扩散,我们可以近似认为一个点只会对相临的前、后、左、右四个点有影响。这样我们就可以用归纳法来根据任一点在某时刻周围四点的振幅来求出该点在下一时刻的震动幅度。假设表示该影响关系的公式为:
A0’=a×(A1+A2+A3+A4)+b×A0 (公式一) |
其中a、b为待定系数,A0’为0点下一时刻的振幅,A0、A1、A2、A3、A4均为为当前时刻的周围各点振幅。在不考虑衰减的情况下波的能量守恒,即各点振幅之和守恒(能量通过振幅来体现),可以用公式二表示:
A0’+A1’+...+An’ = A0+A1+...+An (公式二) |
将公式1代入公式2:
(4a+b)×A0+(4a+b)×A1+...(4a+b)×An = A0+A1+...+An |
化简可得4a+b=1, 取a = 1/2、b = -1可以满足条件。而且除2可以用运算速度很快的移位运算符">>"来进行。公式一代入系数可得到无阻尼状态下的周围四点对中心点的影响关系式:
推广到水面任一点:下一时刻任意一点的波幅等于与该点紧邻的前、后、左、右四点的波幅之和的一半与该点在上一时刻的波幅之差。但水在实际中是存在阻尼的,水波会在扩散过程中逐渐衰减直至消失。所以还要对波幅数据进行衰减处理,让每一个点在经过一次运算后,波幅按一定的比例衰减,衰减率经笔者的实验,取1/32比较合适,同时它也可以通过移位运算很快的获得。到此为止,已将水波的扩散和衰减等特型用数学模型表示了出来,下面是具体计算波幅数据的主要代码:
void Spread() { …… 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; …… } |
虽然模拟了对波的传播过程,但如不考虑水面起伏的水波引起的对光的折射也是不逼真的,也正是由于水面上部的光线反射,才使我们感觉到水波的起伏。根据光学有关知识,我们所看到的水下的景物并非在观察点的正下方,而是存在一定的偏移。偏移的程度同水波的斜率,水的折射率和水的深度都有关系,出于对处理速度的考虑同样也不能对其进行精确的模拟。只能做线性的近似处理。因为水面越倾斜,所看到的水下景物偏移量就越大,所以,我们可以近似的用水面上某点的前后、左右两点的波幅之差来代表所看到水底景物的偏移量:
void Render() { …… 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++; } } …… } |
在无外力影响的情况下,是不会自发产生水波的,必须对水面施加某种激励才能引起波源的扩散。而且扩散的速度与范围也是同激励的能量大小与受力范围有关的,我们可以通过在程序中人为的修改振幅缓冲区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; …… } |
虽然在前面的推导中多处采用了看似过分的非常近似的处理,但是完全不必担心,事实证明,用这种方法,在速度和图象上都可以获得非常好的效果。下边就是从其中截取的一帧画面,很逼真的再现了水波的产生过程。
这种用数据缓冲区对图象进行处理的方法的最大的好处就是:程序运算和显示的速度与水波的复杂程度是无关的,用类似的方法完全可以对其他一些物理、自然现象如烟雾、云彩、阳光等进行逼真的模拟。