藉助DirectDraw實現對水波的計算機模擬(轉)

post0發表於2007-08-12
藉助DirectDraw實現對水波的計算機模擬(轉)[@more@]

  目前,各種高效能運算機正以其強大的運算能力被廣泛應用於各種領域,其中對自然界的物理現象和自然規律進行模擬是主要應用之一。由於許多專業書籍對此類模擬技術諱莫如深,使不少程式設計人員對此類程式的設計問題感到無從下手。本文以對真實水波的產生、擴散、衰減以及多個水波的交迭過程的計算機模擬為例,介紹此類程式的設計思路與解決方法。在程式的實現過程中,為了使模擬的效果更加逼真、處理資料顯示的速度更快,本文使用了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 )< 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/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)<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;

  ……

  }

  雖然在上述的推導中多處採用了看似過分的近似處理,但是完全不必擔心效果,事實證明,用這種方法,在速度和影像上都可以獲得非常好的效果。圖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;

  }

  由於必須在應用程式處於啟用狀態時不斷地進行更新,方可完成對整個過程的模擬,所以對資料的實時處理及對頁面的渲染在更新幀的函式中完成,最重

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-951905/,如需轉載,請註明出處,否則將追究法律責任。

相關文章