浅述Delphi下的OpenGL图形开发
OpenGL 是一个低层的图形库,最初由Silicon Graphics Inc. 公司开发。在用户操作系统上对OpenGL具体的实现一般被称为OpenGL驱动,它允许用户使用一组几何元素(点,线,多边形,图象等等)来描述需要绘制的场景。使用OpenGL对一个复杂的场景进行可视化通常只需要毫秒级的时间,这就意味着OpenGL图形库有足够的性能来支持用户创建动画和虚拟世界。
OpenGL驱动通常以二进制格式的库文件提供给用户,用户可以在应用程序中动态的连接这个库文件。在Windows*台上,这个库文件的形式是一个DLL(就是用户系统目录下的opengl.dll文件)。因为Delphi可以调用DLL文件,所以用Delphi进行OpenGL图形开发就像使用其它语言进行OpenGL图形开发一样容易。这篇文章会帮助你熟悉使用Delphi进行OpenGL图形开发的过程。
数学基础
OpenGL 具有很强的数学背景,你使用OpenGL进行图形开发所仅仅受到你想象力的限制。在进入OpenGL的世界之前,先让我们先来看一个被大多数3D程序员使用的非常简单的3D坐标系统:
这幅图描述了在绘制场景中屏幕的位置。图中四条线汇集在一起的点就是虚拟世界中的观察者的视点。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 定义了视点到屏幕的距离(等于*剪裁*面); 25.0 定义了远剪裁*面。在*端剪裁*面之前和远端剪裁*面之后的场景都不会被显示出来。当然,你也可以根据自己的需要来设置这些参数。
从图形元素到物体
现在到了具有挑战性的一个部分:物体。OpenGL仅仅支持以下几种图形元素:点,线和多边形,它并不支持直接绘制复杂的曲面(例如球面等等)。但是,我们仍然可以用多边形来*似模拟这些曲面,通常是使用三角形来进行模拟(你可以在现在的3D游戏当中看到游戏中的物体都是由三角形面片所构成的),所以这对我们来说不是问题。
进行物体的绘制与使用Pascal编程非常相似。在Pascal语法中,每一个程序块都以begin开头,以end结尾;同样,在OpenGL中,每一个物体的绘制都以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个单位宽。
这里是绘制的结果:
虽然它看起来还不是三维的,但起码是一个开始。下面是绘制的源代码:
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; // 绘制OpenGL场景
public
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure setupPixelFormat(DC:HDC); // 设置象素格式
const
pfd:TPIXELFORMATDESCRIPTOR = (
nSize:sizeof(TPIXELFORMATDESCRIPTOR); // 结构大小
nVersion:1; // 版本号
dwFlags:PFD_SUPPORT_OPENGL or PFD_DRAW_TO_WINDOW or
PFD_DOUBLEBUFFER; // 支持双缓存
iPixelType:PFD_TYPE_RGBA; // 颜色类型
cColorBits:24; // 颜色深度
cRedBits:0; cRedShift:0; // 颜色位数)
cGreenBits:0; cGreenShift:0;
cBlueBits:0; cBlueShift:0;
cAlphaBits:0; cAlphaShift:0; // 没有Alpha缓存
cAccumBits: 0;
cAccumRedBits: 0; // 没有积累缓存
cAccumGreenBits: 0;
cAccumBlueBits: 0;
cAccumAlphaBits: 0;
cDepthBits:16; // 深度缓存
cStencilBits:0; // 没有模板缓存
cAuxBuffers:0; // 没有辅助缓存
iLayerType:PFD_MAIN_PLANE; // 主层
bReserved: 0;
dwLayerMask: 0;
dwVisibleMask: 0;
dwDamageMask: 0;
);
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
// 设置观察投影模式
glMatrixMode(GL_PROJECTION);
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);
// 观察的位置
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
end;
procedure TForm1.FormCreate(Sender: TObject);
var DC:HDC;
RC:HGLRC;
i:integer;
begin
DC:=GetDC(Handle); //你可以在这里使用任何的窗口控制
SetupPixelFormat(DC);
RC:=wglCreateContext(DC); // 创建RC
wglMakeCurrent(DC, RC); // 激活OpenGL 窗口
GLInit; // 初始化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
在三维世界中冒险
现在就让我们进入真正的三维世界。我们可以使用前面的程序作为一个模板,添加一些代码来创建一个*滑具有阴影的四面体。我们如何通过使用基本的图素来构建这个四面体呢?我们可以使用四个三角形,一个三角形位于底面,其它的三角形位于侧面。这里使绘制的代码:
procedure TForm1.Draw;
const D=1.5;
H1=D/1.732; // H1 = D * tg(30)
H2=D*1.732-H1; // D/H = tg(30) = 1/sqrt(3), H2 = tg(60)*D – H1
HY=3.0;
const // 定义顶点坐标
a1:TGLArrayf3=(-D, 0, -H1); // 左下顶点
a2:TGLArrayf3=( D, 0, -H1); // 右下顶点
a3:TGLArrayf3=( 0, 0, H2); // 后下顶点
a4:TGLArrayf3=( 0, HY, 0); // 上顶点
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,并且通过它们来构建三角形。无论什么时候定义自己的三角形(或者是多边形)必须遵循以下准则:按逆时针方向进行绘制,以你从外面看物体表面为准。 根据这个准则,我们像这样绘制:a1-a2-a4, a1-a3-a2 (从下往上看), a2-a3-a4, and a3-a1-a4。
只需要把原来Tri.pas单元里的TForm1.Draw()中语句换成上面的语句就可以了。它看起来似乎还不是三维的,这是因为我们还没有定义光照。正是光展示了物体的形状和表面的反射属性。
光照!相机!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
// 设置观察投影模式
glMatrixMode(GL_PROJECTION);
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);
// 观察位置
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
// 设置光照
glEnable(GL_LIGHTING);
glLightfv(GL_LIGHT0, GL_POSITION, @light0_position);
glLightfv(GL_LIGHT0, GL_AMBIENT, @ambient);
glEnable(GL_LIGHT0);
end;
需要定义两个常量。一个定义光源的位置,另一个定义环境光。所谓环境光就是它会产生一些分散的光,这样即使物体处于阴影当中也可以被看见。
虽然你已经启用了光照也定义了光源,但是物体仍然没有阴影。这是因为OpenGL不知道每一个多边形的朝向(就是法向量,即与物体表面相垂直的矢量)。如果你还没有自己的法向量计算函数,那么可以看看下面的代码。这个函数用来计算一个三角形的法向量。这个三角形必须是以逆时针方向进行绘制,因为法向量的计算是根据两个向量的叉积得到的。所以如果你按顺时针方向的话这个三角形的法向量就会指向三角形的里面,而不是外面。
function getNormal(p1,p2,p3:TGLArrayf3):TGLArrayf3;
var a,b:TGLArrayf3;
begin
// 得到两个向量a 和 b
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];
// 计算叉积
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 // 顶点
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; // 法向量
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提供的一些功能。放置一个定时器控件在你的窗体上,然后添加以下代码:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
angle:=angle+1.0;
Draw;
end;
你只需要一行代码就可以制作OpenGL动画:
glRotatef(angle, 0.0, 1.0, 0.0);
把这一行代码放置在glBegin() 语句之前,这样,这个应用程序就算完成了。