-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 |