本例從零開始實現了一個比較完整的聲影並茂的OpenGL小游戲。實現的OpenGL星體沒有什麼使用功能,而本例的星空閃電圖則有一定的實用價值,因為不僅實現了背景貼圖和聲音資源的播放,還實現在背景上加載了一個OpenGL幾何實體。隨著閃電的不斷旋轉,背景和閃電的顏色都不斷忽明忽暗地變化著,很有星空閃電的效果。本例程序運行結果如圖所示。
不一樣的地方就是它是完全在Delphi環境下實現的。因此,我們可以從這個實例中一步一步地學會如何在Delphi環境下創建一個聲影具備的OpenGL動畫游戲。下面是這個實例的一些關鍵技術。
1、在控制台環境下創建自定義的窗口。
2、Delpi環境下OpenGL初始環境設置。
3、用OpenGL技術繪制場景和閃電。
4、背景貼圖技術。
5、聲音媒體的播放。
新建一個控制台應用程序。代碼編寫步驟:
(1)在控制台環境下創建自定義的窗口,並根據具體情況初始化OpenGL設備環境。
function glCreateWnd(Width, Height : Integer; Fullscreen : Boolean; PixelDepth : Integer) : Boolean; var wndClass : TWndClass; // 窗口類型 dwStyle : DWORD; // 窗口風格 dwExStyle : DWord; // 窗口擴張風格 dmScreenSettings : DEVMODE; // 屏幕設置 PixelFormat : GLuint; // OpenGL渲染設置 h_Instance : HINST; // 當前實例 pfd : TPIXELFORMATDESCRIPTOR; //OpenGL窗口相關設置 ResHandle : THandle; //聲音資源句柄 begin //設置OpenGL窗口 with pfd do begin nSize:= SizeOf(TPIXELFORMATDESCRIPTOR); nVersion := 1; dwFlags:= PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER; iPixelType := PFD_TYPE_RGBA; // RGBA顏色格式 cColorBits := PixelDepth; // OpenGL顏色深度 cDepthBits := 16; // 指定深度緩沖區的深度 iLayerType := PFD_MAIN_PLANE; end; PixelFormat := ChoosePixelFormat(h_DC, @pfd); //設置剛才設置的象素格式 if (PixelFormat = 0) then begin glKillWnd(Fullscreen); MessageBox(0, '不能找到一個合適的象素格式!', '錯誤', MB_OK or MB_ICONERROR); Result := False; Exit; end; //設置象素格式 if (not SetPixelFormat(h_DC, PixelFormat, @pfd)) then begin glKillWnd(Fullscreen); MessageBox(0, '不能設置象素格式', '錯誤', MB_OK or MB_ICONERROR); Result := False; Exit; end; //創建一個OpenGL 渲染環境 h_RC := wglCreateContext(h_DC); if (h_RC = 0) then begin glKillWnd(Fullscreen); MessageBox(0, '不能創建一個OpenGL 渲染環境', '錯誤', MB_OK or MB_ICONERROR); Result := False; Exit; end; //設置當前的設備環境 if (not wglMakeCurrent(h_DC, h_RC)) then begin glKillWnd(Fullscreen); MessageBox(0, '不能激活OpenGL渲染設備', '錯誤', MB_OK or MB_ICONERROR); Result := False; Exit; end; SetTimer(h_Wnd, FPS_TIMER, FPS_INTERVAL, nil); //初始化計算幀數的定時器 ResHandle := FindResource(hInstance, 'Electric', 'WAVE'); //初始化聲音設置,加載聲音資源 SndHandle := LoadResource(hInstance, ResHandle); ShowWindow(h_Wnd, SW_SHOW); SetForegroundWindow(h_Wnd); //設置當前窗口一直保持在最前面 SetFocus(h_Wnd); //保證窗口可以正確地縮放 glResizeWnd(Width, Height); glInit(); Result := True; end; (2)用OpenGL技術繪制場景和閃電。程序運行後,場景和閃電的顏色都是不斷變化的。 procedure glDraw(); //繪制真實場景 var I, J : Integer; rnd: glFloat; begin glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度緩沖區 glLoadIdentity(); // 重新設置視圖 glTranslatef(0.0, 0.0, -3); {-------------------------------畫場景----------------------------------} glEnable(GL_TEXTURE_2D); glDisable(GL_BLEND); glColor3f(Random(10)/10,0.5,1); //場景顏色不斷變化 glBegin(GL_QUADS); glTexCoord(0, 0); glVertex(-4, -4, -5); glTexCoord(1, 0); glVertex( 4, -4, -5); glTexCoord(1, 1); glVertex( 4, 4, -5); glTexCoord(0, 1); glVertex(-4, 4, -5); glEnd; glrotate(ElapsedTime/40, 0, 1, 0); // 旋轉場景 for I :=1 to STEPS-2 do // 計算新的Y坐標 begin y[I] :=y[I] + 0.1*(random-0.5); if y[I] > y[I-1] + 0.075 then y[I] :=y[I-1]+0.075; if y[I] < y[I-1] - 0.075 then y[I] :=y[I-1]-0.075; if y[I] > y[I+1] + 0.075 then y[I] :=y[I+1]+0.075; if y[I] < y[I+1] - 0.075 then y[I] :=y[I+1]-0.075; if y[I] > 0.5 then y[I] :=0.5; if y[I] < -0.5 then y[I] :=-0.5; end; {-----------------------繪制閃電----------------------------------} glDisable(GL_TEXTURE_2D); glEnable(GL_BLEND); glColor3f(Random(10)/10, Random(10)/20+0.3, 0.6); //閃電顏色不斷變化 for I :=1 to SparkCount do begin glBegin(GL_TRIANGLE_STRIP); for J :=0 to STEPS-1 do begin rnd :=0.04*(Random-0.5); glVertex3f(-1 + 2*J/STEPS + rnd, -0.01 + y[J] + rnd, rnd); glVertex3f(-1 + 2*J/STEPS + rnd, +0.01 + y[J] + rnd, rnd); end; glEnd(); end; end;
(3)背景貼圖技術的實現首先要將BMP位圖轉換成可以被OpenGL環境可以調用的格式。這是通過自定義函數LoadBitmap來實現的。
procedure LoadBitmap(Filename: String; out Width: Cardinal; out Height: Cardinal; out pData: Pointer); var FileHeader,InfoHeader: TBITMAPINFOHEADER; Palette: array of RGBQUAD; BitmapFile: THandle; BitmapLength,PaletteLength,ReadBytes: LongWord; begin BitmapFile:= CreateFile(PChar(Filename), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0); if (BitmapFile = INVALID_HANDLE_VALUE) then begin MessageBox(0, PChar('打開錯誤' + Filename), PChar('錯誤提示1'), MB_OK); Exit; end; //獲取位圖頭信息 ReadFile(BitmapFile, FileHeader, SizeOf(FileHeader), ReadBytes, nil); ReadFile(BitmapFile, InfoHeader, SizeOf(InfoHeader), ReadBytes, nil); //獲取調色板信息 PaletteLength := InfoHeader.biClrUsed; SetLength(Palette, PaletteLength); ReadFile(BitmapFile, Palette, PaletteLength, ReadBytes, nil); if (ReadBytes <> PaletteLength) then begin MessageBox(0, PChar('讀取調色板錯誤'), PChar('錯誤提示2'), MB_OK); Exit; end; Width := InfoHeader.biWidth; Height := InfoHeader.biHeight; BitmapLength := InfoHeader.biSizeImage; if BitmapLength = 0 then BitmapLength := Width * Height * InfoHeader.biBitCount Div 8; //獲取真實象素數據 GetMem(pData, BitmapLength); ReadFile(BitmapFile, pData^, BitmapLength, ReadBytes, nil); if (ReadBytes <> BitmapLength) then begin MessageBox(0, PChar('位圖文件數據讀取錯誤'), PChar('錯誤提示3'), MB_OK); Exit; end; CloseHandle(BitmapFile); SwapRGB(pData, Width*Height); //位圖信息保存為BGR模式,而不是RGB模式 end;
將位圖格式轉換成可以被OpenGL環境可以調用的格式之後,然後再構造一個LoadTexture函數用於加載位圖文件充當背景圖。
function LoadTexture(Filename: String; var Texture: GLuint): Boolean; var pData: Pointer; Width:, Height: LongWord; begin pData :=nil; LoadBitmap(Filename, Width, Height, pData); if (Assigned(pData)) then Result := True else begin Result := False; MessageBox(0, PChar('不能打開文件:' + filename), '加載材質', MB_OK); Halt(1); end; glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); //材質和對象背景混合 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gluBuild2DMipmaps(GL_TEXTURE_2D, 3, Width, Height, GL_RGB, GL_UNSIGNED_BYTE, pData); end;
(3)在創建新窗口、設置OpenGL顯示和渲染設備和進行背景貼圖之後,就可以進入程序的主體設計部分了。這個部分其實還是一個自定義的函數——WinMain。在這個部分,實現了整個游戲的所有內容,同時也加載了聲音資源。
function WinMain(hInstance : HINST; hPrevInstance : HINST; lpCmdLine : PChar; nCmdShow : Integer) : Integer; stdcall; var msg : TMsg; finished : Boolean; DemoStart, LastTime : DWord; ResPtr : PChar; begin finished := False; //執行程序初始化設置 if not glCreateWnd(800, 600, FALSE, 32) then begin Result := 0; Exit; end; ResPtr := LockResource(SndHandle); //播放聲音 sndPlaySound(ResPtr, SND_MEMORY OR SND_ASYNC OR SND_LOOP OR SND_NODEFAULT); DemoStart := GetTickCount(); //計時開始 //主消息循環 while not finished do begin if (PeekMessage(msg, 0, 0, 0, PM_REMOVE)) then //捕捉窗口消息 begin if (msg.message = WM_QUIT) then //退出程序 finished := True else begin TranslateMessage(msg); DispatchMessage(msg); end; end else begin Inc(FPSCount); LastTime :=ElapsedTime; ElapsedTime :=GetTickCount() - DemoStart; //計算間隔時間 ElapsedTime :=(LastTime + ElapsedTime) DIV 2; //計算平滑移動的時間 glDraw(); //繪制場景 SwapBuffers(h_DC); //顯示場景 if (keys[VK_ESCAPE]) then //按ESC退出程序 finished := True else ProcessKeys; //檢測鍵盤消息 end; end; glKillWnd(FALSE); Result := msg.wParam; end; WinMain函數構造之後, 在程序的主體執行部分就只有一句代碼了,那就是 Begin WinMain( hInstance, hPrevInst, CmdLine, CmdShow ); End.
本例的一個特點就是根據具體情況構造了大量的函數。這也是大中型程序的特點,程序大了之後,系統本身提供的那些API函數就不夠用了或是顯得過於復雜。這時就不得不構造一些適合自己開發習慣和開發功能的函數和方法了。在習慣了這種做法之後,你會發現,其實它讓我們節約了很多時間和精力。