| -Writing an OpenGL Wrapper- |
|
To make any decent OpenGL based programs, we must have some decent OpenGL tools. This tutorial takes you through the steps for writing a wrapper class around OpenGL, sorta like like DXDraw that comes with DelphiX (if you dont know what DelphiX is, what planet have you been on ?). Of course, it is perfectly viable to use a totally API based framework, but I find that VCL and class based code is a lot easier to understand, maintain and modify. So I'm going to stick with this paradigm (until I'm convinced otherwise :) |
| You will probably see a lot of missing things in the wrapper. This is due to two reasons. |
| A wrapper with all the frills would require a lot more code and explanation than one tut could accomodate.I find it best to learn when the necessity arises, that way, you are able to use what you have learned right away. |
| Another
thing. This tut will not try to explain what a rendering context is, and
stuff. All it will do is try to make a concrete class, one that can be used
time and again (and extended). A basic working knowledge of OpenGL is assumed.
Anyway, here goes nothing. |
| The basic methods we would for initialization and cleanup are InitGL and CleanupGL. InitGL has to be called after the class is created, but CLeanUPGL automatically happens when the class is destroyed. Another method, InitGLFromHandle is useful when you want to render to some object other than the form's surface (mostly a TPanel). For fullscreen rendering (will work only when OpenGL is inited with a form.), we call ChangeDisplaySettingsA from User32.dll. This function can arbitrarily change the desktop resolution and colordepth. We will now walk through the declaration of our wrapper, and see what it's made of. |
TOpenGL=class(TObject)
private
//The device context
fDC:HDC;
//The OpenGL rendering context that we will init
fHRC:HGLRC;
//Palette in case the user needs color-index mode
fPalette:HPALETTE; //The color using which the screen will be cleared
fClearColor:TColorf; //Fullscreen? YES! YES!
fFullScreen:boolean;
//Resolution at which to go fullscreen
fXRes,fYRes:integer;
//If OpenGL inited using a form, a pointer to it
fOwner:TForm;
//Was OpenGL inited from just a handle?
fFromHandle:boolean;
//The handle from which OpenGL was inited
fHandle:THandle;
protected
//Make our rendering context the current one.
procedure MakeCurrent; //Access methods for some properties
procedure SetClearColor(aColor:TColorf);
procedure SetFullScreen(v:boolean); public
//Sets up basic info.
constructor Create(aOwner:TForm);
//DUH!
destructor Destroy;override; //Initialize OpenGL with the given info
procedure InitGL;
//Initialize OpenGL with the given info
procedure InitGLFromHandle(aHandle:THandle);
//Clean up after OpenGL
procedure CleanUpGL; //Flip the backbuffer to the front buffer
procedure Flip;
//Clear all the inited buffer
procedure ClearAll;
//Clear the ZBuffer alone
procedure ClearZBuffer;
//Clear the Color buffer alone
procedure ClearColorBuffer;
//Clear the stencil buffer alone
procedure ClearStencilBuffer;
//The device context
property DC:HDC read fDC;
//The rendering context
property RC:HGLRC read fHRC;
//The palette
property Palette:HPALETTE
read fPalette
write fPalette;
//The clear color
property ClearColor:TColorf
read fClearColor
write SetClearColor;
//An alien from mars...
//Just checking to see if you are really reading these silly
//comments
property XRes:integer
read fXRes
write fXRes
default 640;
//Y resolution
property YRes:integer
read fYRes
write fYRes
default 480;
//FullScreen property
property FullScreen:boolean
read fFullScreen
write SetFullScreen;
end;
|
| So at the end of all this, we can initialze OpenGL and shut it down. What good is that going to do? We need to render something, right. At least just to prove to someone that you dont simply goof of in front of the computer? |
| We need to set up some more stuff before we can actually draw some stuff to the screen. The most basic is a projection matrix. Instead of using gluPerspective all over the program, what we will do is write another small class that sets this up for us. A plus of making a class from this is that whenever we write a camera, we can declare a TViewVolume (that's what we're gonna call the class) instance inside a TCamera so that the internal details are all hidden. All we do is call Camera.SetView, and the camera takes care of the rest of the things. This will be especially useful when you have to render 2D text on top of a 3D scene. All we would have to do is,(I'm assuming we don't have the camera class, but the technique would essentially remain the same). Declare and create two instances of TViewVolume. Set up one for 2D rendering, andthe other for 3D . To render the 3D scene, call the appropriate instance of TViewVolume's SetMatrix method. Simple, isnt it? Instead of maintaining flags and such?! |
| Now for a walkthrough of the TViewVolume declaration. This is, again, a primitive version that supports only perspective viewing volumes.When we come around to rendering text, we will extend this class, as we will a great many others. |
//A **really** simple class, but goddamn helpful around the place
TViewVolume=class
private
//The field-of-view
fFOV,
//distance to the near clipping plane
fHither,
//distance to the far clipping plane
fYon:double;
//Viewport information
fX,fY,
fWidth,
fHeight:integer;
public
//I really should stop commenting each line :)
constructor Create;
//Set the projection params
procedure SetMatrix;
//Properties for the private member vars
//described above
property FOV:double
read fFOV
write fFOV;
property Hither:double
read fHither
write fHither;
property Yon:double
read fYon
write fYon;
property X:integer
read fX
write fX;
property Y:integer
read fY
write fY;
property Width:integer
read fWidth
write fWidth;
property Height:integer
read fHeight
write fHeight; end;
|
| We are finally ready to render something onto the screen. All we will render is a GLUT teapot, Stop groaning, GLUT objects are an extremely easy way of testing rendering code. Or would you rather sit up another couple of hours and code a file format loader? (If you stick with me, I'll make you do that too). What I want you to note is that, the main rendering code will look really neat. All the messy details are tucked away inside classes. That's the way it should be. I'm presenting the code for a simple example here. The component names in the code should be self-descriptive. |
unit Main;interfaceuses
Windows,
Messages,
SysUtils,
Classes,
Graphics,
Controls,
Forms,
Dialogs,
GL,
GLU,
GLUT,
GLClass,
ExtCtrls;type
TMainForm = class(TForm)
Timer: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure TimerTimer(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
private
{ Private declarations }
public
{ Public declarations }
//Declare an instance so we can use it
GL:TOpenGL;
//we need to set up a projection matrix, right?
ViewVolume:TViewVolume; //Some mouse movement to add spice to this drab tut
MousePressed:boolean;
xStart,xDelta,yStart,yDelta:integer;
xRot,yRot:single;
end;var
MainForm: TMainForm;implementation{$R *.DFM}procedure TMainForm.FormCreate(Sender: TObject);
begin
//Create it. We are going to render to the form.
GL:=TOpenGL.Create(Self);
//Check if the user wants to run fullscreen or not.
GL.FullScreen:=Application.MessageBox('Would you like to run fullscreen?',
'TOpenGL Demo',
MB_YESNO or MB_ICONQUESTION)=ID_YES;
//That's it. Cool, aint it?
//Any changes you want to make will never muck up this code.
GL.InitGL; //Create it and set up the width and height of the window
ViewVolume:=TViewVolume.Create;
//we could also do this if we wanted to resize the window
ViewVolume.Width:=ClientWidth;
ViewVolume.Height:=ClientHeight; MousePressed:=false;
xRot:=0;
yRot:=0; //Let the flipping begin
Timer.Enabled:=true;
end;procedure TMainForm.FormDestroy(Sender: TObject);
begin
//Stop the rendering. We dont want to try and render
//a destroyed object, right?!!
Timer.Enabled:=false; if assigned(GL) then GL.Destroy;
end;procedure TMainForm.TimerTimer(Sender: TObject);
begin
//Clear contents of previous render
GL.ClearColorBuffer; //Set the projection matrix
ViewVolume.SetMatrix; //We will simply translate ourselves and watch a little teapot
glTranslatef(0,0,-10);
glRotatef(xRot,1,0,0);
glRotatef(yRot,0,1,0);
GLUTWireTeapot(1.0); //Flip the screen
GL.Flip;
end;procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
//Now quit, we should...like any good app would :)
VK_ESCAPE:Close;
end;
end;procedure TMainForm.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
//Mouse button is being held down
MousePressed:=true;
//Note the starting position
xStart:=X;
yStart:=Y;
end;procedure TMainForm.FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
//User released the mouse
MousePressed:=false;
end;procedure TMainForm.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
//Dragging is happening only if the mouse button is down
if not MousePressed then exit;
//Calculate how much the mouse has moved
xDelta:=xStart-X;
yDelta:=yStart-Y; //Adjust rotation and scale so it is'nt too fast
xRot:=xRot-yDelta/2;
yrot:=yRot-xDelta/2; //Next time, we start from here
xStart:=X;
yStart:=Y;
end;end.
|
| If you notice, the only messy pieces of code are where we do the rotation and set up the viewing matrix. Why, you ask, didnt I write a camera as well, after making you sit through all this? The reason is that a camera is so integral to everything that we will write, so we will devote an entire tutorial to it. Coming soon. |
| >>Download the tutorial
source You'll also need, in case you dont have them already >>Download the OpenGL 1.2 headers >>Download the GLUT pack |