摘要:本文介绍了一种对水面波纹的扩散、衰减以及交叠的过程进行计算机模拟的一种实现算法,并对在实现该算法过程中所使用的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可以用运算速度很快的移位运算符">>"来进行。公式一代入系数可得到无阻尼状态下的四周四点对中心点的影响关系式:
A0’=(A1+A2+A3+A4)/ 2- A0
推广到水面任一点:下一时刻任意一点的波幅等于与该点紧邻的前、后、左、右四点的波幅之和的一半与该点在上一时刻的波幅之差。但水在实际中是存在阻尼的,水波会在扩散过程中逐渐衰减直至消失。所以还要对波幅数据进行衰减处理,让每一个点在经过一次运算后,波幅按一定的比例衰减,衰减率经笔者的实验,取1/32比较合适,同时它也可以通过移位运算很快的获得。到此为止,已将水波的扩散和衰减等特型用数学模型表示了出来,下面是具体计算波幅数据的主要代码:
void SPRead()
{
……
for (int i=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
{
for (int j=0; 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
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
for (int posy=y-stonesize; posy
if ((posx-x)*(posx-x) + (posy-y)*(posy-y) < stonesize*stonesize)
buf1[BACKWIDTH*posy+posx] = -stoneweight;
……
}
虽然在前面的推导中多处采用了看似过分的非常近似的处理,但是完全不必担心,事实证实,用这种方法,在速度和图象上都可以获得非常好的效果。下边就是从其中截取的一帧画面,很逼真的再现了水波的产生过程。
这种用数据缓冲区对图象进行处理的方法的最大的好处就是:程序运算和显示的速度与水波的复杂程度是无关的,用类似的方法完全可以对其他一些物理、自然现象如烟雾、云彩、阳光等进行逼真的模拟。 三、 利用DirectDraw完成对图形的加速
从前面可以看出,我们一直在处理速度上比较严格。竭尽所能来提高数据的处理速度,不仅在算法上如此,在显示上更是如此。普通的GDI函数的处理速度在这里是无法容忍的,对此采用了DirectX系列技术中的DirectDraw技术来对图形进行加速处理。
DirectDraw是DirectX SDK系列中的一员,也是其中最主要的一个部件。它答应程序员直接的操作显存、硬件位图映射以及硬件覆盖和换页技术。它在提供直接访问显示设备的同时,与GDI相兼容,它只是一个软件接口,提供了一种与设备无关的途径,以获得访问特定的显示设备的某些高级特性的能力。大体上DirectDraw可以提供了以下几个功能,而这些功能以前只有那些专为特定显示设备所写的软件中才能使用:
·支持双缓冲和换页图形
·访问、控制显示卡的位图映射
·支持3D z-buffers (z缓存)
·支持z方向(z-ordering)硬件辅助覆盖。
·访问图形缩放硬件
·仿真访问标准的和增强的显示设备内存空间
下面就对DirectDraw的在图形加速方面中的具体应用结合本程序的具体实例来进行介绍,为方便起见,本工程采用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;
}
另外,必须在应用程序活跃时不断的进行更新,方可完成对整个过程的模拟,在更新帧的函数中负责完成对数据的实时处理及对页面的渲染,最重要的环节--将离屏页面2装载到主页面的工作也将在这里完成:
……
DropStone((int)(BACKWIDTH*((short)rand()/32767.0)),
(int)(BACKHEIGHT*((short)rand()/32767.0)),
2,64);//激励点为随机产生,模拟雨点落在水面的情形
Spread();//计算振幅数据缓冲区
Render();//页面渲染
//在window内显示页面2
lpDDSPrimary->Blt(&Window, lpDDSPic2, NULL, DDBLT_WAIT, NULL);
……
在前面的处理光的折射的Rander()函数中也应增添对页面的渲染部分的编码,可以用一个页面来装载原始的图象,用另外一个页面来进行渲染。
先用Lock函数锁定两个页面,取得指向页面内存区的指针,然后根据偏移量将原始图象上的每一个象素复制到渲染页面上:
……
//锁定两个离屏页面
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;
……
//中间为对光的折射模拟过程的代码
……
//解锁页面
lpDDSPic1->Unlock(&ddsd1);
lpDDSPic2->Unlock(&ddsd2);
最后,为了养成良好的编程习惯,应在程序退出之前释放掉所使用过的资源,在这里主要为对各个页面的释放以及对DirectDraw对象lpDD的释放:
……
lpDDSPrimary->Release();
lpDDSPic1->Release();
lpDDSPic2->Release();
lpClipper->Release();
lpDD->Release();
……
经过上述几步,已基本完成了对水波的模拟工作。通过对DirectDraw技术的应用,在取得了对图形的硬件加速支持后可以在计算机上很流畅地完成对水波自产生、扩散到消失的全部过程的仿真模拟。
小结:
通过本文所述的对自然事物的计算机模拟的实现过程,可以对仿真模拟类程序的一般实现过程有基本的熟悉,在理解本程序的设计思路与方法的前提下也可以用类似的方法设计出对其他事物的模拟算法。本程序结合具体程序设计要求合理和有效的利用DirectDraw的图形硬件加速等特性很好的实现了对水波模拟的过程显示,对于其他仿真程序的设计也可以根据实际灵活的选用诸如OpenGL、Direct3D等不同的软件接口。本文所述程序在windows 2000下,在DirectX 9.0的支持下,由Microsoft Visual C++ 6.0编译调试通过。