OpenGL alapok

Top  Previous  Next

 

 

OpenGL with Delphi

 

By: Alexei Semichastny

 

Abstract: Get acquainted with OpenGL 3D graphics programming with this cookbook approach for Delphi developers. By Alex Semichastny.

 

OpenGL is a low-level graphics library specification originally developed by Silicon Graphics Inc. Your system's particular implementation of this specification, often called OpenGL driver, allows you to use a set of geometric primitives (points, lines, polygons, images, and so on) to describe the scene you wish to draw. Visualizing a scene of moderate complexity takes mere milliseconds, which means the library has sufficient performance to support you in the creation of animations and virtual worlds.

 

The OpenGL driver is generally provided as a library in binary format. It can be linked to your program dynamically. On the Windows platform, it will be a DLL (check for opengl.dll in your system directory). Since Delphi can use any DLL, it is as simple as with any other language to program OpenGL 3D graphics. This article will help you to get acquainted with OpenGL techniques in Delphi.

 

MATHEMATICAL FUNDAMENTALS

 

OpenGL is based on strong mathematical fundamentals so you are limited only by your imagination. Rather then getting into axioms and lemmas, let's just proceed with a simple 3D coordinate system, as used by most 3D programmers. Here it is:

 

clip0007

 

This is how you should imagine the placement of your screen (the blue square) in your scene. The point from which the four rays extend to create the screen is your "point of view" in the imaginary world.OpenGL lets you define this situation with two simple function calls

 

glMatrixMode(GL_PROJECTION);

glFrustum(-0.1, 0.1, -0.1, 0.1, 0.3, 25.0);

 

In these calls, -0.1, 0.1, -0.1, 0.1 define the size of virtual screen in left, right, bottom, top sequence; 0.3 defines viewpoint-to-screen distance (same as near clipping plane) and 25.0 defines the far clipping plane. Anything in front of the near clipping plane or behind the far clipping plane will be invisible. Of course, you can play with these numbers to suit your needs for your scene dimensions.

FROM PRIMITIVES TO OBJECTS

 

Now comes the challenging part: the objects. OpenGL supports only the following geometrical primitives: points, lines and polygons. No surface of higher order (such as sphere) can be drawn as a primitive. But it can be perfectly approximated with polygons. Take a look at any modern 3D game and you'll find it is completely built from triangles. So we won't be limited by this restriction.

 

The object drawing is very similar to Pascal programming. Each block should be surrounded by a begin-end pair...or actually, a glBegin() and glEnd() pair. Take a look at this:

 

const S=1.0; D=5.0;

...

glBegin(GL_TRIANGLES);

glVertex3f( -S, 0, D); glVertex3f(S, 0, D); glVertex3f(0, S, D);

glEnd;

 

It's a simple triangle. It is five units away from your viewpoint; it is one unit tall and two units wide.

 

 

Even if it doesn't look much like 3D, it is our starting piece. Below you will find the source code for the example.

 

A few words should be said prior to letting you dig into the code. Every OpenGL program contains some OS-specific code that initializes the output device. If you use Win32, you'll need to set up the pixel format and create a rendering context out of the windows device context. If you are not so good in system-level Windows programming, just use following program as a template. For more info read the help on each function called in 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

 

ADVENTURES IN THE THIRD DIMENSION

 

OK, lets do real 3D now. Using the previous program as a skeleton, we add a few lines of code to create a flat shaded tetrahedron. How can we build it out of available primitives? We will use four triangles. One for the base and three on the sides. Here is the code that produces it:

 

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;

 

Even if it looks a little complex, it is easier to understand when you have a picture in front of you.
 

clip0006

 

We define the vertices a1 - a4 and build triangles out of them by specifying triples of points. Whenever you define your triangles (or other polygons), use the following rule: always specify points in counterclockwise order as if you were looking at each side from the outside. According to this rule, we specify a1-a2-a4, a1-a3-a2 (looking from beneath), a2-a3-a4, and a3-a1-a4.

 

Just replacing TForm1.Draw() in Tri.pas will not make a lot of changes. It still does not look three-dimensional. That's because we have not defined any lights yet.

LIGHTS! CAMERA! OPENGL!

 

The lighting model in OpenGL has two parts: the lights themselves (color, intensity etc.) and materials of the bodies. Material, in turn, contains colors, some physical parameters (like opacity or glossiness) and textures. It is a huge world to play with, we'll go step by step.

 

Defining a light source is rather easy.

 

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;

 

Two constants were needed. One defines the light position (to the left, top, back from viewer) and another defines ambient lighting. It produces some small amount of scattered light, which allows you to see something even if it is completely in shadow.

 

Although you enabled lighting and specified a light source, the body is still not drawn shaded. This is because OpenGL needs to know "normal" to each polygon you specify to do lighting math. (Normal is a vector perpendicular to the surface.) If you have no library of vector functions of your own, just use the following method to calculate a normal having 3 points of triangle. This function needs them to be specified counterclockwise, because the normal is produced as a cross product of the vectors and it will point inside the tetrahedron if you don't obey this rule.

 

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;

 

Using this function, you can now specify all the information needed to calculate the lighting:

 

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;

 

clip0005

 

That's it. The code produces a view like this:

 

And now let us use something offered by Delphi VCL. Put the timer on your window, specify class member "angle:single" and increment it by 1.0 each time the timer ticks:

 

procedure TForm1.Timer1Timer(Sender: TObject);

begin

angle:=angle+1.0;

Draw;

end;

 

You are just one line away from making an OpenGL animation:

 

glRotatef(angle, 0.0, 1.0, 0.0);

 

Put this line just before you start your triangles in a glBegin() and your rotating shaded-body application is finished.

 

If you like this stuff, you can play a little with this application. You can download it from CodeCentral: Step1, 2.6K Try to understand how it works and why you use this code or that. If you want to go further, there are plenty of books and tutorials. Check out www.opengl.org if you haven't done so yet.

 

If you have questions or comments about this article email me at alex@nichewo.com. Thanks for your time. I hope you enjoyed this article.