OpenGL最初是由Silicon圖形公司開發的底層圖形庫規范。你的系統中准確實現這個規范的部分,通常被稱為OpenGL驅動,它允許你使用幾何集合(點,線,多邊形,圖像等等)來描述你希望表現的場景。讓肉眼觀察起來較為舒適的中等規模場景,通常在毫秒級的速度上實現,這意味著該庫文件有足夠的能力來支持你創建一個生機勃勃的虛擬世界。
OpenGL驅動一般以二進制庫文件的形式提供。它能夠動態的連接到你的程序中。在Windows平台上,它將是成為DLL的形式(在你的系統目錄下檢查opengl.dll)。自從Delphi能夠使用任何DLL開始,它對OpenGL 3D編程的能力就像其他任何語言一樣容易了。本文將幫助你獲得在Delphi中進行OpenGL開發的有效知識。
數學基礎
OpenGL擁有強大的數學基礎,因此對它功能的限制完全取決於你的想象能力(譯者注:沒有做不到,只有想不到)。對於理解那些公理和引理,更好的是讓我們立刻認識一個簡單的3D坐標系統,它是3D編程中慣用的坐標系統。如下:
你應該如何理解你的屏幕(藍色的方塊)在場景中的放置位置呢?發出四條射線並形成屏幕的那個點,是該想象空間中的視點(point of view)。OpenGL讓你調用兩個簡單的函數來定義這個場景
glMatrixMode(GL_PROJECTION);
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);
在這個調用的過程中的-0.1,0.1,-0.1,0.1定義了這個可視屏幕的左上角和右下角坐標;0.3指定視點到屏幕的距離(就好象“近剪貼板”(near clipping plane))同時25.0指定“遠剪貼板”(far clipping plane)。任何近剪貼板前面的物體以及遠剪貼板後面的物體都將不可見。當然,你能夠任意擺弄這些數字,以使他們適合你需要的場景。
從基本元素(primitive)到對象
現在開始最有意思的部分:對象。OpenGL僅僅支持以下幾種基本幾何圖形:點,線和多邊形。沒有表面或者更高級的圖形(比如球狀圖形)能被作為基本圖形元素繪制。但是它們能夠用多邊形完美的模仿出來。隨意看看現代3D游戲,你會發現它們完全由三角形建立。因此,我們不會被此限制所約束。
對象的繪制非常類似Pascal語言編程。每個塊都應該被begin-end包含著,更為確切的說是glBegin()和glEnd()。如同下面的例子:
const S=1.0; D=5.0;
...
glBegin(GL_TRIANGLES);
glVertex3f( -S, 0, D); glVertex3f(S, 0, D); glVertex3f(0, S, D);
glEnd;
這是個簡單的三角形。它距離你的視點有5個單位,自身高1個單位,寬2個單位。
這是屏幕截圖:
即使它看起來不象3D圖形,但它是我們的初始塊。在下面你可以看到這個例子的源代碼。
在你開始鑽研代碼前,還有些話要說。每次OpenGL編程,都包含一些初始化輸出設備的OS設定(OS-specific)代碼。如果你使用Win32,你將需要設置像素格式以及建立顯示上下文環境脫離windows設備上下文環境。如果windows系統級編程你並不很在行,你可以把如下的代碼作為模版使用。FormCreate中被調用函數的詳細信息可以參考幫助文檔。
FILE: Tri.pas
unit Tri;
interface
uses
OpenGL, Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, ComCtrls;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
procedure Draw; //Draws an OpenGL scene on request
public
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure setupPixelFormat(DC:HDC);
const
pfd:TPIXELFORMATDESCRIPTOR = (nSize:sizeof(TPIXELFORMATDESCRIPTOR); // size
nVersion:1; // version
dwFlags:PFD_SUPPORT_OPENGL or PFD_DRAW_TO_WINDOW or PFD_DOUBLEBUFFER; // support double-buffering
iPixelType:PFD_TYPE_RGBA; // color type
cColorBits:24; // preferred color depth
cRedBits:0; cRedShift:0; // color bits (ignored)
cGreenBits:0; cGreenShift:0;
cBlueBits:0; cBlueShift:0;
cAlphaBits:0; cAlphaShift:0; // no alpha buffer
cAccumBits: 0;
cAccumRedBits: 0; // no accumulation buffer,
cAccumGreenBits: 0; // accum bits (ignored)
cAccumBlueBits: 0;
cAccumAlphaBits: 0;
cDepthBits:16; // depth buffer
cStencilBits:0; // no stencil buffer
cAuxBuffers:0; // no auxiliary buffers
iLayerType:PFD_MAIN_PLANE; // main layer
bReserved: 0;
dwLayerMask: 0;
dwVisibleMask: 0;
dwDamageMask: 0; // no layer, visible, damage masks
);
var pixelFormat:integer;
begin
pixelFormat := ChoosePixelFormat(DC, @pfd);
if (pixelFormat = 0) then
exit;
if (SetPixelFormat(DC, pixelFormat, @pfd) <> TRUE) then
exit;
end;
procedure GLInit;
begin
// set viewing projection
glMatrixMode(GL_PROJECTION);
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);
// position viewer
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
end;
procedure TForm1.FormCreate(Sender: TObject);
var DC:HDC;
RC:HGLRC;
i:integer;
begin
DC:=GetDC(Handle); //Actually, you can use any windowed control here
SetupPixelFormat(DC);
RC:=wglCreateContext(DC); //makes OpenGL window out of DC
wglMakeCurrent(DC, RC); //makes OpenGL window active
GLInit; //initialize OpenGL
end;
procedure TForm1.Draw;
const S=1.0; D=5.0;
begin
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
glTranslatef(0.0, 0.0, -12.0);
glBegin(GL_TRIANGLES);
glVertex3f( -S, 0, D); glVertex3f(S, 0, D); glVertex3f(0, S, D);
glEnd;
SwapBuffers(wglGetCurrentDC);
end;
procedure TForm1.FormPaint(Sender: TObject);
begin
Draw;
end;
end.
FILE: Tri.dfm
object Form1: TForm1
BorderStyle = bsDialog
Caption = 'BASIC OpenGL Program'
ClientHeight = 318
ClientWidth = 373
OnCreate = FormCreate
OnPaint = FormPaint
end
3D歷險
好了,讓我們開始真正的3D吧。將先前的代碼作為框架,我們增加一些畫線的代碼建立一個帶陰影面的四面體。應該如何用基本圖形元素來構建呢?我們使用四個三角形。一個在底部,另外三個作為側面。這裡就是生成他們的代碼:
procedure TForm1.Draw;
const D=1.5;
H1=D/1.732;
H2=D*1.732-H1; // D/H = tg(30) = 1/sqrt(3)
HY=3.0;
const //vertexes
a1:TGLArrayf3=(-D, 0, -H1); //bootom left
a2:TGLArrayf3=( D, 0, -H1); //bootom right
a3:TGLArrayf3=( 0, 0, H2); //bootom back
a4:TGLArrayf3=( 0, HY, 0); //top
begin
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
glTranslatef(0.0, 0.0, -12.0);
glBegin(GL_TRIANGLES);
glVertex3fv(@a1); glVertex3fv(@a3); glVertex3fv(@a2);
glVertex3fv(@a1); glVertex3fv(@a2); glVertex3fv(@a4);
glVertex3fv(@a2); glVertex3fv(@a3); glVertex3fv(@a4);
glVertex3fv(@a3); glVertex3fv(@a1); glVertex3fv(@a4);
glEnd;
SwapBuffers(wglGetCurrentDC);
end;
雖然看起來有點復雜,不過當你面對下面這張圖時,它就很容易理解了。
我們定義頂點a1 – a4同時依據4個頂點位置建立指定的三角形。當你定義自己的三角形(或者其他的多邊形),請使用如下的規則:始終按照逆時針順序排列定點序號,就像你正在外部觀看側面一樣。通過這個規則,我們可以指定指定a1-a2-a4,a1-a3-a2(仰視),a2-a3-a4和a3-a1-a4。
現在就替換Tri.pas中TForm1.Darw()部分,程序運行的效果不會體現出過多的變化。它看起來仍然不象三維圖形。這是因為我們還沒有設定任何光源。
LIGHTS! CAMERA! OPENGL!
在OpenGL中光源模式有兩部分:光源自身(顏色,強度等等)和對象材質。材質,依次包括顏色,一些物理參數(比如不透明性光澤性)以及紋理。深入其中,這會是一個巨大的世界,我們將一步步地接近。
定義一個光源相當容易。
procedure GLInit;
const
light0_position:TGLArrayf4=( -8.0, 8.0, -16.0, 0.0);
ambient: TGLArrayf4=( 0.3, 0.3, 0.3, 0.3);
begin
// set viewing projection
glMatrixMode(GL_PROJECTION);
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);
// position viewer */
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
// set lights
glEnable(GL_LIGHTING);
glLightfv(GL_LIGHT0, GL_POSITION, @light0_position);
glLightfv(GL_LIGHT0, GL_AMBIENT, @ambient);
glEnable(GL_LIGHT0);
end;
代碼內的兩個常量是必須的。一個定義光源位置(位於視點的後面的左上角),另外一個定義環境光線。這將產生少量的散亂光線,使你能夠看到完全位於陰影中的某些物體。
雖然你可以使用光照設定光源,可是物體仍然沒有繪制陰影。這是因為OpenGL需要知道你指定的每個多邊形的“normal”以便進行光線計算(Normal是一個與表面正交的向量) 。如果你沒有自己的向量函數庫,可以使用以下方法計算三角形中三個頂點的normal。這個函數是以定點逆時針排列為基礎的,因為normal是一個向量的叉積,如果你不遵守該規則,會使向量指向四面體內部。
function getNormal(p1,p2,p3:TGLArrayf3):TGLArrayf3;
var a,b:TGLArrayf3;
begin
//make two vectors
a[0]:=p2[0]-p1[0]; a[1]:=p2[1]-p1[1]; a[2]:=p2[2]-p1[2];
b[0]:=p3[0]-p1[0]; b[1]:=p3[1]-p1[1]; b[2]:=p3[2]-p1[2];
//calculate cross-product
result[0]:=a[1]*b[2]-a[2]*b[1];
result[1]:=a[2]*b[0]-a[0]*b[2];
result[2]:=a[0]*b[1]-a[1]*b[0];
end;
使用這個函數,就可以設定所有的計算光線必需的信息了:
procedure TForm1.Draw;
const D=1.5;
H1=D/1.732;
H2=D*1.732-H1; // D/H = tg(30) = 1/sqrt(3)
HY=3.0;
const //vertexes
a1:TGLArrayf3=(-D, 0, -H1);
a2:TGLArrayf3=(D, 0, -H1);
a3:TGLArrayf3=(0, 0, H2);
a4:TGLArrayf3=(0, HY, 0);
var n1, n2, n3, n4: TGLArrayf3; //normals
begin
n1 := getNormal(a1,a3,a2);
n2 := getNormal(a1,a2,a4);
n3 := getNormal(a2,a3,a4);
n4 := getNormal(a3,a1,a4);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glEnable(GL_NORMALIZE);
glShadeModel(GL_FLAT);
glCullFace(GL_BACK);
glLoadIdentity;
glTranslatef(0.0, 0.0, -12.0);
glBegin(GL_TRIANGLES);
glNormal3fv(@n1);
glVertex3fv(@a1); glVertex3fv(@a2); glVertex3fv(@a3);
glNormal3fv(@n2);
glVertex3fv(@a1); glVertex3fv(@a2); glVertex3fv(@a4);
glNormal3fv(@n3);
glVertex3fv(@a2); glVertex3fv(@a3); glVertex3fv(@a4);
glNormal3fv(@n4);
glVertex3fv(@a3); glVertex3fv(@a1); glVertex3fv(@a4);
glEnd;
SwapBuffers(wglGetCurrentDC);
end;
這便是以上代碼的效果:
現在,使用一點Delphi VCL提供的的東西。在窗體上放一個Timer,指定一個類成員“angle:single”並在每次Timer觸發時讓他增加1:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
angle:=angle+1.0;
Draw;
end;
離一個充滿生氣的OpenGL僅差條線:
glRotatef(angle, 0.0, 1.0, 0.0);
把它放在glBegin()內三角開始繪制前的位置上,這樣你的陰影部分就可以旋轉了,至此,一切結束。