目前,各种高性能计算机正以其强大的运算能力被广泛应用于各种领域,其中对自然界的物理现象和自然规律进行仿真是主要应用之一。由于许多专业书籍对此类仿真技术讳莫如深,使不少程序设计人员对此类程序的设计问题感到无从下手。本文以对真实水波的产生、扩散、衰减以及多个水波的交迭过程的计算机模拟为例,介绍此类程序的设计思路与解决方法。在程序的实现过程中,为了使仿真的效果更加逼真、处理数据显示的速度更快,本文使用了DirectX中的DirectDraw技术利用硬件加速器对数据的显示进行加速。
扩散及衰减处理
要对某种自然现象进行仿真,就必须对该现象的特性有很好的认识。比如对于本文所仿真的对象水波而言,就要对水波的诸多特性,如扩散性、衰减性、反射性以及水的折射等都要有所认识,并最终通过程序算法体现在程序中。这些关于波的特性属于普通物理的研究范畴,本文不再赘述。根据以上的特性,再利用计算机、数学和几何等有关知识就可以在计算机上模拟出真实的水波了。
由于在模拟时需要的是实时的渲染,所以每秒种至少要渲染15帧以上的画面才能使水波平滑地显示。考虑到普通计算机的运算速度较慢,所以不能用乘、除法,更不可以使用正、余弦函数以精确的公式来构造水波,只能通过使用简单而高速的加、减法的近似算法来实现。
首先,可以用两个与水池图像一样大小的数组buf1和buf2来保存水面上每一个点(对应于每一个像素的离散化点)的前、后两个时刻的波幅数据。在无外力干扰的稳定状态下,水面是一个平面,水面各点的波幅都为0。当有外力干扰时,如向水池投一颗石子会使水面泛起层层的涟漪,实际上并非水面上的点在向外扩散,而是仍停在原地上下移动,由于振动幅度的变化而引起视觉上的变化。由于水波上的任何一点在任何时候都是通过振幅的变化把能量以自己为中心向四周扩散,所以可以近似认为一个点只会对相邻的前、后、左、右4个点有影响。这样我们就可以用归纳法来根据任一点在某时刻周围4点的振幅来求出该点在下一时刻的振动幅度。假设表示该关系的公式为:
A0’=a×(A1+A2+A3+A4)+b×A0 (公式1)
其中a、b为待定系数,A0’为0点下一时刻的振幅,A0、A1、A2、A3、A4均为当前时刻周围各点振幅。在不考虑衰减情况下波的能量守恒,即上下时刻各点振幅之和守恒,这可以用公式2表示:
A0’+A1’+...+An’= A0+A1+...+An(公式2)
将公式1代入公式2:
(4a+b)×A0+(4a+b)×A1+...(4a+b)×An = A0+A1+...+An
化简公式可得4a+b=1, 取a = 1/2、b = -1时可以满足条件,而且除以2可以用运算速度很快的移位运算符“”来进行。这样,向公式1中代入系数可得到无阻力状态下的周围4点对中心点的影响关系式:
A0’=(A1+A2+A3+A4)/ 2- A0
因此,水面上下一时刻任意一点的波幅等于与该点紧邻的前、后、左、右4点的波幅之和的一半与该点在上一时刻的波幅之差。但在实际中水是存在阻力的,水波会在扩散过程中逐渐衰减直至消失。所以,还要对波幅数据进行衰减处理,让每一个点在经过一次运算后,波幅按一定的比例衰减。笔者实验发现,经验系数(衰减率)取1/32比较合适,同时它也可以通过移位运算很快地获得。下面是具体计算波幅数据的主要代码:
void Spread()
{
……
for(int i=BACKWIDTH;i
/td
{
//能量的扩散
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
{
for (int j=0; j
{
//计算偏移量
xoff = buf1[k-1]-buf1[k+1];
yoff = buf1[k-BACKWIDTH]-buf1[k+BACKWIDTH];
//判断坐标是否在窗口范围内
if ((i+yoff )
if ((i+yoff ) BACKHEIGHT) {k++; continue;}
if ((j+xoff )
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
/td
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)
return;
……
for (int posx=x-stonesize; posx
for (int posy=y-stonesize; posy
if ((posx-x)*(posx-x) + (posy-y)*(posy-y)
buf1[BACKWIDTH*posy+posx] = -stoneweight;
……
}
虽然在上述的推导中多处采用了看似过分的近似处理,但是完全不必担心效果,事实证明,用这种方法,在速度和图像上都可以获得非常好的效果。图1就是从其中截取的一帧画面,很逼真地再现了水波的产生过程。
水波模拟
这种用数据缓冲区对图像进行处理的方法的最大的好处就是:程序运算和显示的速度与水波的复杂程度无关,用类似的方法完全可以对其他一些物理和自然现象,如烟雾、云彩、阳光等,进行逼真的模拟。
加速显示
由于动画对处理速度要求比较严格,所以要竭尽所能来提高数据的处理速度,不仅在算法上如此,在显示上更是如此。普通的GDI函数的处理速度在此类计算中是无法容忍的,为此,笔者采用了DirectX中的DirectDraw技术来对图形进行加速处理。DirectDraw是DirectX SDK中的一员,也是其中最主要的一个部件。它允许程序员直接操作显存、硬件位图映射以及硬件覆盖和换页技术。它在提供直接访问显示设备的同时,与GDI相兼容,提供了一种与设备无关的途径,以访问特定的显示设备的某些高级特性。
本文例子采用Microsoft SDK编码。首先,对DirectDraw环境进行初始化设置,对各页面进行创建和初始化:
BOOL InitDDraw(void)
{
DDSURFACEDESC ddsd;
HRESULT ddrval;
……
//创建DirectDraw对象
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
……
//取得全屏独占模式
ddrval = lpDD-SetCooperativeLevel(hwndful, DDSCL_EXCLUSIVE | DSCL_FULLSCREEN );
……
//设置显示器显示模式
ddrval = lpDD-SetDisplayMode( DISPLAYMODEWIDTH,DISPLAYMODEHEIGHT, 24);
……
//填充主页面信息
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS ;
ddsd.ddsCaps.dwCaps=
DDSCAPS_PRIMARYSURFACE;
//创建主页面对象
ddrval = lpDD-CreateSurface( &ddsd, &lpDDSPrimary, NULL );
……
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT |DDSD_WIDTH;
ddsd.ddsCaps.dwCaps=
DDSCAPS_OFFSCREENPLAIN |
DDSCAPS_SYSTEMMEMORY ;
……
lpDD-CreateSurface(&ddsd, &lpDDSPic1, NULL) ;
……
lpDD-CreateSurface(&ddsd, &lpDDSPic2, NULL);
……
lpDD-CreateClipper(0, &lpClipper, NULL);
lpClipper-SetHWnd(0, hwndful);
lpDDSPrimary-SetClipper(lpClipper);
……
//初始化页面图像
DDReLoadBitmap(lpDDSPic1, MAKEINTRESOURCE(IDB_BITMAP1));
DDReLoadBitmap(lpDDSPrimary,MAKEINTRESOURCE(IDB_BITMAP2));
……
ZeroMemory(buf1,
BACKWIDTH*BACKHEIGHT*sizeof(short));
ZeroMemory(buf2,
BACKWIDTH*BACKHEIGHT*sizeof(short));
……
return TRUE;
}
由于必须在应用程序处于激活状态时不断地进行更新,方可完成对整个过程的模拟,所以对数据的实时处理及对页面的渲染在更新帧的函数中完成,最重要