Opengl 最初是由 Silício 图形公司开发的底层图形库规范。你的系统中准确实现这个规范的部分 , 通常被称为 OpenGl 驱动 , 它允许你使用几何集合 (点 , 线 , 多边形 , 图像等等) 来描述你希望表现的场景。让肉眼观察起来较为舒适的中等规模场景 , 通常在毫秒级的速度上实现 , 这意味着该库文件有足够的能力来支持你创建一个生机勃勃的虚拟世界。
OpenGL 驱动一般以二进制库文件的形式提供。它能够动态的连接到你的程序中。在 Windows 平台上 , 它将是成为 dll 的形式 (在你的系统目录下检查 Opengl.dll) 。自从 Delphi 能够使用任何 dll 开始 , 它对 OpenGL 3D 编程的能力就像其他任何语言一样容易了。本文将帮助你获得在 Delphi 中进行 Opengl 开发的有效知识。
数学基础
Opengl 拥有强大的数学基础 , 因此对它功能的限制完全取决于你的想象能力 (译者注 : : 没有做不到 , 只有想不到) 。对于理解那些公理和引理 , 更好的是让我们立刻认识一个简单的 3d 坐标系统 , 它是 3d 编程中惯用的坐标系统。如下:
你应该如何理解你的屏幕 (蓝色的方块) 在场景中的放置位置呢?发出四条射线并形成屏幕的那个点 , 是该想象空间中的视点 (ponto de vista) 。Opengl 让你调用两个简单的函数来定义这个场景
GlMatrixMode (GL_PROJECIÇÃO);
glfrustum (-0,1, 0,1, -0,1, 0,1, 0,3, 25,0);
在这个调用的过程中的 -0.1,0.1, -0.1,0,1 定义了这个可视屏幕的左上角和右下角坐标; 0,3 指定视点到屏幕的距离 (就好象 就好象 近剪贴板 近剪贴板 "(perto do plano de recorte )) 同时 25.0 指定 “远剪贴板” (plano de corte distante) 。任何近剪贴板前面的物体以及远剪贴板后面的物体都将不可见。当然 , 你能够任意摆弄这些数字 , 以使他们适合你需要的场景。
从基本元素 (primitivo) 到对象
现在开始最有意思的部分 : 对象。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 Específico) 代码。如果你使用 Win32 , 你将需要设置像素格式以及建立显示上下文环境脱离 Windows 设备上下文环境。如果 Windows 系统级编程你并不很在行 , 你可以把如下的代码作为模版使用。FormCreate 中被调用函数的详细信息可以参考帮助文档。
Arquivo: TRI.PAS
unidade tri;
interface
usos
OpenGL, Windows, Mensagens, Sysutils, Classes, Gráficos, Controles, Formulários, Diálogos,
Stdctrls, extctrls, comctrls;
tipo
Tform1 = classe (tform)
Procedimento FormCreate (remetente: Tobject);
Procedimento FormPaint (remetente: Tobject);
privado
Procedimento Desenho; // desenha uma cena do OpenGL a pedido
público
fim;
var
Form1: TForm1;
implementação
{$ R *.dfm}
Procedimento setuppixElFormat (DC: HDC);
const
pfd: tpixelformatDescriptor = (
nsize: sizeof (tpixelformatDescriptor); // tamanho
nversion: 1; // versão
dwflags: pfd_support_opengl ou pfd_draw_to_window ou
Pfd_doublebuffer; // suporta buffer duplo
ipixeltype: pfd_type_rgba; // tipo de cor
ccolorbits: 24; // profundidade de cor preferida
credbits: 0; CredShift: 0; // bits coloridos (ignorados)
cgreenbits: 0; CGREENSHIFT: 0;
CBluebits: 0; cblueshift: 0;
Calfabits: 0; Calphashift: 0; // sem buffer alfa
Caccumbits: 0;
Caccumredbits: 0; // sem buffer de acumulação,
caccumgreenbits: 0; // accum bits (ignorado)
Caccabumbluebits: 0;
Caccumalphabits: 0;
CDEPTHBITS: 16; // buffer de profundidade
CSTECHISKBITS: 0; // sem buffer de estêncil
cauxbuffers: 0; // Sem buffers auxiliares
ILAYERTYPE: PFD_MAIN_PLANE; // camada principal
Breserved: 0;
dwlayerMask: 0;
dwvisiblemask: 0;
dwdamageMask: 0; // sem camada, visível, máscaras de dano
);
var Pixelformat: Inteiro;
começar
Pixelformat: = ChoicePixElFormat (DC, @pfd);
if (pixelformat = 0) então
saída;
if (setpixelformat (dc, pixelformat, @pfd) <> true) então
saída;
fim;
Procedimento Glinit;
começar
// Defina a projeção de visualização
GlMatrixMode (GL_PROJECIÇÃO);
glfrustum (-0,1, 0,1, -0,1, 0,1, 0,3, 25,0);
// Visualizador de posição
GlMatrixMode (GL_MODelView);
glenable (gl_depth_test);
fim;
procedimento TForm1.FormCreate (remetente: Tobject);
var DC: HDC;
RC: HGLRC;
I: Inteiro;
começar
Dc: = getDC (alça); // Na verdade, você pode usar qualquer controle em janela aqui
SetuppixElFormat (DC);
Rc: = wglcreateContext (dc); // faz janela OpenGL para fora de DC
wglMakecurrent (DC, RC); // Torna a janela OpenGL ativa
Glinit; // inicialize o OpenGL
fim;
procedimento TFFORM1.DRAW;
const s = 1,0; D = 5,0;
começar
GLCLEAR (GL_COLOR_BUFFER_BIT ou 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);
fim;
procedimento TForm1.FormPaint (remetente: Tobject);
começar
Empate;
fim;
fim.
Arquivo: TRI.DFM
Objeto Form1: TForm1
BordersTyle = Bsdialog
Legenda = 'Programa Básico OpenGl'
ClientHeight = 318
ClientWidth = 373
OnCreate = FormCreate
OnPaint = FormPaint
fim
3d 历险
好了 , 让我们开始真正的 3D 吧。将先前的代码作为框架 , 我们增加一些画线的代码建立一个带阴影面的四面体。应该如何用基本图形元素来构建呢?我们使用四个三角形。一个在底部 , 另外三个作为侧面。这里就是生成他们的代码:
procedimento TFFORM1.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 // vértices
A1: TGLARRAYF3 = ( -D, 0, -H1); // Bootom esquerdo
A2: TGLARRAYF3 = (D, 0, -H1); // bootom à direita
A3: TGLARRAYF3 = (0, 0, H2); // botoom de volta
A4: TGLARRAYF3 = (0, HY, 0); //principal
começar
GLCLEAR (GL_COLOR_BUFFER_BIT ou 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);
fim;
虽然看起来有点复杂 , 不过当你面对下面这张图时 , 它就很容易理解了。
我们定义顶点 A1 - A4 同时依据 4 个顶点位置建立指定的三角形。当你定义自己的三角形 (或者其他的多边形) , 请使用如下的规则: 始终按照逆时针顺序排列定点序号 , 就像你正在外部观看侧面一样。通过这个规则 , 我们可以指定指定 A1-A2-A4 , A1-A3-A2 (仰视) , A2-A3-A4 和 A3-A1-A4。
现在就替换 Tri.Pas 中 TFFORM1.DARW () 部分 , 程序运行的效果不会体现出过多的变化。它看起来仍然不象三维图形。这是因为我们还没有设定任何光源。
Luzes! CÂMERA! OpenGL!
在 OpenGl 中光源模式有两部分 : 光源自身 (颜色 , 强度等等) 和对象材质。材质 , 依次包括颜色 , 一些物理参数 (比如不透明性光泽性) 以及纹理。深入其中 , 这会是一个巨大的世界 , 我们将一步步地接近。
定义一个光源相当容易。
Procedimento Glinit;
const
Light0_Position: TGLARRAYF4 = (-8,0, 8.0, -16,0, 0,0);
ambiente: tglarrayf4 = (0,3, 0,3, 0,3, 0,3);
começar
// Defina a projeção de visualização
GlMatrixMode (GL_PROJECIÇÃO);
glfrustum (-0,1, 0,1, -0,1, 0,1, 0,3, 25,0);
// Visualizador de posição */
GlMatrixMode (GL_MODelView);
glenable (gl_depth_test);
// Luzes definidas
Glenable (gl_lighting);
gllightfv (gl_light0, gl_Position, @Light0_Position);
gllightfv (gl_light0, gl_ambient, @Ambient);
glenable (gl_light0);
fim;
代码内的两个常量是必须的。一个定义光源位置 (位于视点的后面的左上角) , 另外一个定义环境光线。这将产生少量的散乱光线 , 使你能够看到完全位于阴影中的某些物体。
虽然你可以使用光照设定光源 , 可是物体仍然没有绘制阴影。这是因为 OpenGl 需要知道你指定的每个多边形的 “Normal” 以便进行光线计算 (Normal 是一个与表面正交的向量) 。如果你没有自己的向量函数库 , 可以使用以下方法计算三角形中三个顶点的 Normal 。这个函数是以定点逆时针排列为基础的 , Normal 是一个向量的叉积 , 如果你不遵守该规则 , 会使向量指向四面体内部。
função getNormal (p1, p2, p3: tglarrayf3): tglarrayf3;
var a, b: tglarrayf3;
começar
// Faça dois vetores
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];
// Calcule o produto cruzado
resultado [0]: = a [1]*b [2] -a [2]*b [1];
resultado [1]: = a [2]*b [0] -a [0]*b [2];
resultado [2]: = a [0]*b [1] -a [1]*b [0];
fim;
使用这个函数 , 就可以设定所有的计算光线必需的信息了:
procedimento TFFORM1.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 // vértices
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; // normais
começar
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 ou 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);
fim;
这便是以上代码的效果
现在 , 使用一点 Delphi vcl 提供的的东西。在窗体上放一个 timer , 指定一个类成员 “ângulo: único” 并在每次 timer 触发时让他增加 1:
procedimento TFFORM1.TIMER1TIMER (remetente: Tobject);
começar
ângulo: = ângulo+1,0;
Empate;
fim;
离一个充满生气的 Opengl 仅差条线:
glrotatef (ângulo, 0,0, 1,0, 0,0);
把它放在 Glbegin () 内三角开始绘制前的位置上 , 这样你的阴影部分就可以旋转了 , 至此 , 一切结束。