Chapter 13
Drive-Through
In this chapter you will learn a few new techniques, including how to do a “drive-through” 
or “walk-through” of a scene with OpenGL. In addition, the drive-through will provide the 
medium in which to bring together various techniques and commands you have already 
learned.
Simple Scene
First, a simple scene serves to introduce the basic drive-through requirement. The user does 
not actually change position; to simulate the movement of the user, you will instead move 
the scene in the opposite direction. See Figure 13-1 for the initial appearance.
Project
Create a new directory under the OpenGL directory and name it Chapter.13. In that 
directory, create a directory named Simple. Save a new project therein, naming it 
Simple0.Dpr with a unit named Simple1.Pas. This application will have a light, a floor or 
ground, and an object on the floor. On the form, drop a TOpenGL component, a TLight 
component, three TTransform components, and three TMaterial components.

PAGE 329
 Properties
This project is simple indeed. In addition to lighting for the scene, the components provide a 
broad expanse of floor-like ground and a cube on the ground you can drive around or 
through. Listing 13-1 shows the properties:

object Form1: TForm1	
  Left = 192	
  Top = 107	
  Width = 328	
  Height = 347	
  Caption = 'Form1'	
  Color = clBtnFace	
  Font.Charset = DEFAULT_CHARSET	
  Font.Color = clWindowText	
  Font.Height = -11	
  Font.Name = 'MS Sans Serif'	
  Font.Style = []	
  OldCreateOrder = False	
  OnCreate = FormCreate	
  OnKeyDown = FormKeyDown	
  OnKeyUp = FormKeyUp	
  OnPaint = FormPaint	
  OnResize = FormResize	
  PixelsPerInch = 96	
  TextHeight = 13	
  object OpenGL1: TOpenGL	
    pfdFlags = [DoubleBuffer, Draw_To_Window, Support_OpenGL]	
    PixelType = RGBA	
    Projection = Perspective	
    Enable = [depthTest, Lighting]	
    ClearRed = 0.6	
    ClearGreen = 0.8	
    ClearBlue = 0.8	
    prxLeft = -1	
    prxRight = 1	
    prxBottom = -1	
    prxTop = 1	
    prxNear = 2	
    prxFar = 42	
    ColorBits = 24	
    DepthBits = 32	
    StencilBits = 0	
    Left = 4	
    Top = 8	
  end	
  object TransformLight: TTransform	
    TranY = 5	


PAGE 330
     TranZ = -3	
    AxisX = 1	
    Angle = -20	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformLightPaint	
    Left = 4	
    Top = 44	
  end	
  object Light1: TLight	
    Which = 1	
    Cutoff = 65	
    AmbientRed = 0.2	
    AmbientGreen = 0.2	
    AmbientBlue = 0.2	
    DiffuseRed = 1	
    DiffuseGreen = 1	
    DiffuseBlue = 1	
    SpecularRed = 1	
    SpecularGreen = 1	
    SpecularBlue = 1	
    Direction = True	
    Position = True	
    Spotlight = True	
    Enabled = True	
    Left = 40	
    Top = 44	
  end	
  object MaterialLight: TMaterial	
    ambRed = 1	
    ambGreen = 1	
    ambBlue = 1	
    ambAlpha = 1	
    difRed = 1	
    difGreen = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 76	
    Top = 44	
  end	
 object TransformFloor: TTransform	
    TranY = -1	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	


PAGE 331
     OnPaint = TransformFloorPaint	
    Left = 4	
    Top = 80	
  end	
  object MaterialFloor: TMaterial	
    ambRed = 0.7	
    ambGreen = 0.5	
    ambBlue = 0.1	
    ambAlpha = 1	
    difRed = 0.7	
    difGreen = 0.7	
    difBlue = 0.1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 80	
  end	
  object TransformCube: TTransform	
    TranY = -0.6	
    TranZ = -10	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformCubePaint	
    Left = 4	
    Top = 116	
  end	
  object MaterialCube: TMaterial	
    ambBlue = 1	
    ambAlpha = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 116	
  end	
end
Listing 13-1. Simple drive-through project properties.
Private
The private section of the form type declaration needs a few fields and method declarations. 
The viewer movement uses the GLfloat fields, and the frame rate calculations use the integer 
fields. The Idle method handles the 

PAGE 332
 frame rate calculations and the basic animation including the viewer movement. The angl, 
wx, and wz fields are the key positional fields; the others are helpers. In Figure 13-2 you see 
an example of some accumulation of values for the positional fields.

  private	
    angl,	
    rspeed,	
    speed,	
    tran,	
    rot,	
    wx,	
    wz: GLfloat;	
    frames,	
    elapsed,	
    start: integer;	
    procedure Idle(Sender:TObject;var Done:boolean);	
    procedure Adjust;	
  public	
Variables
In addition to the usual form variable declaration, you should declare pi180. It serves as a 
calculation saver. OpenGL uses degrees and Delphi trigonometric functions use radians. 
Rather than calling the pi function and dividing by 180 degrees (or multiplying by 
0.0055555555556) every frame, just use this pre-initialized variable:

PAGE 333
 
var	
  Form1: TForm1;	
  pi180: double=pi/180.0;	
In Delphi 2 you would use a typed constant rather than an initialized variable:

const	
  pi180: double=pi/180.0;	
Event Handlers
The FormCreate method sets up two-sided lighting and the Idle method:

procedure TForm1.FormCreate(Sender: TObject);	
begin	
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);	
  Application.OnIdle := Idle;	
end;	
The FormPaint method contains the magic that allows you to simulate a moving viewpoint. 
Figure 13-3 shows the viewpoint moved to the back side of the floor area and turned around.

Each Apply method called pushes the ModelView matrix onto the matrix stack, performs the 
transformations, performs the renderings, and pops the matrix stack so that the next 
transformation will start from the same initial condition. The key to the magic is the initial 
condition of each frame. The glMatrixMode command ensures that the next commands will 
affect the ModelView matrix.

First, the glLoadIdentity command makes sure you adjust the ModelView matrix starting 
from the identity matrix each frame. The next two commands rotate and translate the matrix. 
The matrix basically represents the position and orientation of the entire scene, so the 
transformations before the Apply calls move the scene. The subsequent transformations 
place the objects starting from that condition.

PAGE 334
 The angl field changes with user input, and only the y-axis is non-zero, so the user can turn 
the scene about the y-axis, giving the appearance that the user, with a first-person viewpoint, 
turns in the opposite direction. The wx and wz fields also respond to user input, moving the 
scene and giving the appearance the user moves left and right and back and forth, but not up 
and down.

procedure TForm1.FormPaint(Sender: TObject);	
begin	
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);	
	
  glMatrixMode(GL_MODELVIEW);	
  glLoadIdentity;	
  glRotatef(angl,0.0,1.0,0.0);	
  glTranslatef(wx,0.0,wz);	
	
  TransformLight.Apply;	
  TransformFloor.Apply;	
  TransformCube.Apply;	
	
  OpenGL1.Swap;	
  OpenGL1.ErrorCheck('FormPaint');	
end;	
The FormResize method does the usual. The TransformFloorPaint method draws a very 
large horizontal rectangle even with the bottom of the viewport. The TransformLightPaint 
method executes the lighting commands and draws a cube around the light. The 
TransformCubePaint method draws a cube sitting on the “floor.”

procedure TForm1.FormResize(Sender: TObject);	
begin	
  if OpenGL1.Smaller then	
    FormPaint(Sender);	
end;	
	
procedure TForm1.TransformFloorPaint(Sender: TObject);	
begin	
  MaterialFloor.Apply;	
  glBegin(GL_POLYGON);	
    glVertex3f(-29.0,0.0,-29.0);	
    glVertex3f(-29.0,0.0,+29.0);	
    glVertex3f(+29.0,0.0,+29.0);	
    glVertex3f(+29.0,0.0,-29.0);	
  glEnd;	
end;	
	
procedure TForm1.TransformLightPaint(Sender: TObject);	
begin	


PAGE 335
   Light1.Apply;	
  MaterialLight.Apply;	
  Cube;	
end;	
	
procedure TForm1.TransformCubePaint(Sender: TObject);	
begin	
  MaterialCube.Apply;	
  Cube;	
end;	

The FormKeyDown method receives the user input that guides the drive-through as the user 
holds down certain keys. The method does not directly set the wx, wz, and angl fields. 
Instead it sets the tran and rot fields. The tran field is either zero (not moving) or equal to the 
value of the speed field (moving at that rate). The rot field is either zero (not turning) or 
equal to the value of the rspeed field (turning at that rate). The Idle method in turn uses 
those values to adjust the wx, wz, and angl fields.

The Up Arrow key sets a positive speed and the Down Arrow key sets a negative speed for 
backing up (or actually moving the scene forward). The Left Arrow key sets a negative 
rotational speed to turn the scene to the right and make the user seem to turn to the left; the 
Right Arrow key sets a positive rotational speed for the opposite effect. The S key adjusts the 
value of the speed field, increasing it if the user is also holding down the Shift key, 
decreasing it otherwise. Similarly, the R key adjusts the size of the rspeed field.

The Escape key closes the main form, thereby exiting the application.

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;	
  Shift: TShiftState);	
begin	
  case Key of	
    VK_UP: tran := +speed;	
    VK_DOWN: tran := -speed;	
    VK_LEFT: rot := -rspeed;	
    VK_RIGHT: rot := +rspeed;	
    VK_ESCAPE: Close;	
    ord('R'): begin	
      if ssShift in Shift then	
        rspeed := rspeed+0.01	
      else begin	
        if rspeed>0.01 then	
          rspeed := rspeed-0.01;	
      end;	
    end;	
    ord('S'): begin	
      if ssShift in Shift then	


PAGE 336
         speed := speed+0.001	
      else begin	
        if speed>0.001 then	
          speed := speed-0.001;	
      end;	
    end;	
  end; {case}	
end;	
The FormKeyUp method stops any motion when the user releases the keys by setting the 
tran and rot fields to zero:

procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word;	
  Shift: TShiftState);	
begin	
  tran := 0.0;	
  rot := 0.0;	
end;	
The Idle method tracks frame rate just as in the previous chapter. It also calls the Adjust 
method to handle the animation:

procedure TForm1.Idle(Sender:TObject;var Done:boolean);	
const	
  rollover=1000;	
  mspersec=1000.0;	
begin	
  Done := false;	
  Adjust;	
  inc(frames);	
  if frames>=rollover then	
  begin	
    elapsed := TimeGetTime;	
    Caption := Format('fps=%0.1f',[rollover*mspersec/(elapsed-start)]);	
    start := elapsed;	
    frames := 0;	
  end else	
    FormPaint(nil);	
end;	
Other Methods
The Adjust method accumulates the rot and tran fields into the values of the wx, wz, and 
angl fields. When rot and tran are zero, the positional fields remain static, but when they are 
non-zero, the positional fields grow with each frame. Notice that the method uses a little 
standard trigonometry to coordinate the rotation and translation to give a realistic 
appearance of first-person movement.

PAGE 337
 
procedure TForm1.Adjust;	
var	
  theta: GLfloat;	
begin	
  angl := angl+rot;	
  theta := angl*pi180;	
  wx := wx-tran*sin(theta);	
  wz := wz+tran*cos(theta);	
end;	
Results
Save, compile, and run. Figure 13-1 shows the starting view. Hold down a Shift key and tap 
the S key a few times to establish a decent speed, then release. Notice that when you press 
the Up Arrow key, you seem to advance forward into the scene. Hold down the Up Arrow 
key until you are close to the blue cube, but not in it. Figure 13-2 shows this view.
Now hold down the Up Arrow key until you seem to pass through the cube and reach the 
edge of the floor. Hold down a Shift key and tap the R key a few times to set a reasonable 
rotational speed, then release. Turn right by holding down the Right Arrow key until the 
cube is again in the center of the form as in Figure 13-3. From this distant view of the back 
side you can see the elevated cube surrounding the light. The fact that the side toward you is 
white indicates that the light is pointing generally toward you, since the orientation of that 
cube matches the orientation of the light.
Turn left with the Left Arrow key until the blue cube just moves off the right side of the 
form. Go forward a little ways with the Up Arrow key. Try to estimate about a quarter of the 
way across the floor. Turn right until the blue cube fully appears again. Advance forward 
until you just open up a corner of the cube, as in Figure 13-4. Press the Escape key to exit.





    Figure 13-4. Approaching from another angle.

PAGE 338
 Opening
That was one basic technique down, several more to go. The next feature is an opening 
through which the user will view the scene. The challenge is to make the opening shift with 
the user rather than with the rest of the scene.
Project
Create a directory under the Chapter.13 directory and name it Open. Save a new project in it 
named Open0.Dpr with a unit named Open1.Pas. Make this project just like the Simple0 
project. Differences will become evident along the way.
Properties
In addition to the components like those of the previous project, drop on another 
TTransform component and another TMaterial component. Since a few properties on the 
other components are different from those of the previous project, Listing 13-2 includes the 
entire form rather than just the additional components:

object Form1: TForm1	
  Left = 192	
  Top = 107	
  Width = 328	
  Height = 347	
  Caption = 'Form1'	
  Color = clBtnFace	
  Font.Charset = DEFAULT_CHARSET	
  Font.Color = clWindowText	
  Font.Height = -11	
  Font.Name = 'MS Sans Serif'	
  Font.Style = []	
  OldCreateOrder = False	
  OnCreate = FormCreate	
  OnDestroy = FormDestroy	
  OnKeyDown = FormKeyDown	
  OnKeyUp = FormKeyUp	
  OnPaint = FormPaint	
  OnResize = FormResize	
  PixelsPerInch = 96	
  TextHeight = 13	
  object OpenGL1: TOpenGL	
    pfdFlags = [DoubleBuffer, Draw_To_Window, Support_OpenGL]	
    PixelType = RGBA	
    Projection = Perspective	
    Enable = [depthTest, Lighting]	


PAGE 339
     ClearRed = 0.6	
    ClearGreen = 0.8	
    ClearBlue = 0.8	
    prxLeft = -1	
    prxRight = 1	
    prxBottom = -1	
    prxTop = 1	
    prxNear = 2	
    prxFar = 42	
    ColorBits = 24	
    DepthBits = 32	
    StencilBits = 0	
    Left = 4	
    Top = 8	
  end	
  object TransformLight: TTransform	
    TranY = 5	
    TranZ = -3	
    AxisX = 1	
    Angle = -20	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformLightPaint	
    Left = 4	
    Top = 44	
  end	
  object Light1: TLight	
    Which = 1	
    Cutoff = 65	
    AmbientRed = 0.2	
    AmbientGreen = 0.2	
    AmbientBlue = 0.2	
    DiffuseRed = 1	
    DiffuseGreen = 1	
    DiffuseBlue = 1	
    SpecularRed = 1	
    SpecularGreen = 1	
    SpecularBlue = 1	
    Direction = True	
    Position = True	
    Spotlight = True	
    Enabled = True	
    Left = 40	
    Top = 44	
  end	
  object MaterialLight: TMaterial	
    ambRed = 1	
    ambGreen = 1	


PAGE 340
     ambBlue = 1	
    ambAlpha = 1	
    difRed = 1	
    difGreen = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 76	
    Top = 44	
  end	
 object TransformFloor: TTransform	
    TranY = -1	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformFloorPaint	
    Left = 4	
    Top = 80	
  end	
  object MaterialFloor: TMaterial	
    ambRed = 0.7	
    ambGreen = 0.5	
    ambBlue = 0.1	
    ambAlpha = 1	
    difRed = 0.7	
    difGreen = 0.7	
    difBlue = 0.1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 80	
  end	
  object TransformCube: TTransform	
    TranX = 3.5	
    TranY = -0.6	
    TranZ = -10	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformCubePaint	
    Left = 4	
    Top = 116	
  end	
  object MaterialCube: TMaterial	
    ambBlue = 1	


PAGE 341
     ambAlpha = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    mShine = 128	
    Left = 40	
    Top = 116	
  end	
  object TransformCyl: TTransform	
    TranX = -3	
    TranY = -0.6	
    TranZ = -9	
    AxisX = 1	
    Angle = 90	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformCylPaint	
    Left = 4	
    Top = 152	
  end	
  object MaterialCyl: TMaterial	
    ambGreen = 1	
    ambAlpha = 1	
    difGreen = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    mShine = 128	
    Left = 40	
    Top = 152	
  end	
end	
Listing 13-2. Open0 project properties.
Private
The private section of the form declaration has one additional element. The Opening method 
draws polygons to represent the border of the view onto the scene. Think of the polygons as 
part of a bulkhead in your armored exploration vehicle. Figure 13-5 shows how that will 
look.

    procedure Opening;						

PAGE 342
 Event Handlers
The FormCreate method has an additional call to prepare for drawing a cylinder:

procedure TForm1.FormCreate(Sender: TObject);	
begin	
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);	
  Application.OnIdle := Idle;	
  CylinderBase;	
end;	
The FormPaint method now calls the Apply method of the added TTransform component to 
draw a green cylinder in addition to the blue cube. More importantly, the method calls 
Opening to draw the bulkhead. The precise placement of this call is critical. If the call to the 
Opening method appears after the calls to glRotatef and glTranslatef, the polygons will 
move with the rest of the scene. If Opening executes before the call to glLoadIdentity, the 
polygons that compose the bulkhead will be affected by the transformation calls in the 
previous frame. By calling the Opening method just after the call to glLoadIdentity, you 
ensure that the bulkhead remains fixed with respect to the client area of the form, which is 
the viewport.

procedure TForm1.FormPaint(Sender: TObject);	
begin	
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);	
	
  glMatrixMode(GL_MODELVIEW);	
  glLoadIdentity;	


PAGE 343
 	
  Opening; 	
	
  glRotatef(angl,0.0,1.0,0.0);	
  glTranslatef(wx,0.0,wz);	
	
  TransformLight.Apply;	
  TransformFloor.Apply;	
  TransformCube.Apply;	
  TransformCyl.Apply;	
	
  OpenGL1.Swap;	
  OpenGL1.ErrorCheck('FormPaint');	
end;	
Except for one new event handler, the rest of the event handler methods remain the same as 
the ones in the Simple0 project. The new event handler draws the green cylinder:

procedure TForm1.TransformCylPaint(Sender: TObject);	
begin	
  MaterialCyl.Apply;	
  Cylinder;	
end;	
Other Methods
The Adjust method remains the same as before and needs no further elaboration. However, 
the new method does bear a little examination.
Now comes the unveiling of the often-mentioned Opening method. It just draws a few 
rectangular polygons that outline most of the viewport. It does not have its own TMaterial 
component call, so it will respond to the commands from the last used TMaterial component, 
which is MaterialCyl that makes the cylinder green. In a later project you will see why the 
choice of material properties for the bulkhead does not really matter.

procedure TForm1.Opening;	
begin	
  glBegin(GL_POLYGON);	
    glVertex3f(-1.0,+1.0,-2.0);	
    glVertex3f(-1.0,-1.0,-2.0);	
    glVertex3f(-0.7,-1.0,-2.0);	
    glVertex3f(-0.7,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glVertex3f(-0.7,+1.0,-2.0);	
    glVertex3f(-0.7,+0.7,-2.0);	


PAGE 344
     glVertex3f(-0.4,+0.7,-2.0);	
    glVertex3f(-0.4,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glVertex3f(+0.7,+1.0,-2.0);	
    glVertex3f(+0.7,-1.0,-2.0);	
    glVertex3f(+1.0,-1.0,-2.0);	
    glVertex3f(+1.0,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glVertex3f(+0.4,+1.0,-2.0);	
    glVertex3f(+0.4,+0.7,-2.0);	
    glVertex3f(+0.7,+0.7,-2.0);	
    glVertex3f(+0.7,+1.0,-2.0);	
  glEnd;	
end;	
Results
Save, compile, and run. Figure 13-5 shows the initial appearance. Use the commands familiar 
from the last project to drive around the scene. Figure 13-6 shows the scene if you back up 
enough to show the location of the light source.

PAGE 345
 Textured Opening
Now you can add a little more detail to your armored vehicle. When you add texture with 
GL_DECAL to the bulkhead you will recognize that material properties affecting those 
polygons are, in the end, not important.
Project
Create a directory named Texture under the Chapter.13 directory. Save a new project in it 
named Texture0.Dpr, with a unit named Texture1.Pas. Drop on the same components you 
did in the Open0 project and set properties similarly. The only changes are to decrease by 0.2 
the TranY property of all the TTransform components except for TransformLight.
Other Differences
The floor has moved from TranY = –1.0 to TranY = –1.2 so that your viewport is not scraping 
along the surface. Naturally, the cube and the cylinder had to lower their TranY as well, to 
stay in touch with their “roots.”
Copy Texture
Copy Fig13-07.bmp from the companion CD into this project’s directory. Figure 13-7 at right 
shows the bulkhead texture.






         Figure 13-7. Bulkhead texture.
Prepare to Read Texture
Of course you need to read in and process the texture image. First add some constants, 
types, and variables to the top of the implementation section:

PAGE 346
 
implementation	
	
{$R *.DFM}	
	
const	
  Level=0;	
  Border=0;	
  ColorComps=3;	
	
type	
  TRGB=packed record	
    r,g,b: byte;	
  end;	
	
  TWrap=array[0..0]of TRGB;	
	
var	
  sBulk,	
  tBulk: GLsizei;	
  bulk: pointer;	
Add the ReadBitmap procedure just after the variables:

function ReadBitmap(const FilePath:string;var sWidth,tHeight:GLsizei): pointer;	
const	
  szh=SizeOf(TBitmapFileHeader);	
  szi=SizeOf(TBitmapInfoHeader);	
var	
  bmpfile: file;	
  bfh: TBitmapFileHeader;	
  bmi: TBitmapInfoHeader;	
  t: byte;	
  x,	
  size: integer;	
begin	
  assignFile(bmpfile,FilePath);	
  reset(bmpfile,1);	
  size := FileSize(bmpfile)-szh-szi;	
  blockread(bmpfile,bfh,szh);	
  if Bfh.bfType<>$4D42 then	
    raise EInvalidGraphic.Create('Invalid Bitmap');	
  blockread(bmpfile,bmi,szi);	
  with bmi do	
  begin	
    sWidth := biWidth;	
    tHeight := biHeight;	
  end;	
  getmem(result,size);	
  blockread(bmpfile,result^,size);	


PAGE 347
   for x := 0 to sWidth*tHeight-1 do	
  with TWrap(result^)[x] do	
  begin	
    t := r;	
    r := b;	
    b := t;	
  end;	
end;	
Initialize Texture
Add some texture setup code to the top of the FormCreate method. The bulk variable will 
point to a 64x64 texture image. The method uses GL_DECAL so that the bulkhead looks the 
same regardless of the lighting in the rest of the scene. You should replace the simple 
filename with the full path to the file on your computer. You also need to add a 
FormDestroy method to dispose of the bitmap memory. Remember the Delphi 2 adjustments 
if appropriate.

procedure TForm1.FormCreate(Sender: TObject);	
begin	
  glPixelStorei(GL_UNPACK_ALIGNMENT,1);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);	
  glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL);	
  bulk := ReadBitmap('fig13-07.bmp',sBulk,tBulk);	
	
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);	
  Application.OnIdle := Idle;	
  CylinderBase;	
end;	
	
procedure TForm1.FormDestroy(Sender: TObject);	
begin	
  FreeMem(bulk);	
end;	
Apply the Texture
All that is left is to tack the textures onto the vertices of the polygons in the Opening method. 
Be sure to enable texturing (2D) at the top of the method, then disable it at the bottom. Use it 
only where needed, otherwise non-textured surfaces will not display properly. Notice that 
the larger polygons take advantage of GL_REPEAT by using texture coordinates larger than 
1.0.

PAGE 348
 
procedure TForm1.Opening;	
begin	
  glEnable(GL_TEXTURE_2D);	
	
  glTexImage2D(GL_TEXTURE_2D,Level,ColorComps,sBulk,tBulk,Border,	
    GL_RGB,GL_UNSIGNED_BYTE,bulk);	
	
  glBegin(GL_POLYGON);	
    glTexCoord2f(0.0,0.0); glVertex3f(-1.0,+1.0,-2.0);	
    glTexCoord2f(0.0,7.0); glVertex3f(-1.0,-1.0,-2.0);	
    glTexCoord2f(1.0,7.0); glVertex3f(-0.7,-1.0,-2.0);	
    glTexCoord2f(1.0,0.0); glVertex3f(-0.7,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glTexCoord2f(0.0,0.0); glVertex3f(-0.7,+1.0,-2.0);	
    glTexCoord2f(0.0,1.0); glVertex3f(-0.7,+0.7,-2.0);	
    glTexCoord2f(1.0,1.0); glVertex3f(-0.4,+0.7,-2.0);	
    glTexCoord2f(1.0,0.0); glVertex3f(-0.4,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glTexCoord2f(0.0,0.0); glVertex3f(+0.7,+1.0,-2.0);	
    glTexCoord2f(0.0,7.0); glVertex3f(+0.7,-1.0,-2.0);	
    glTexCoord2f(1.0,7.0); glVertex3f(+1.0,-1.0,-2.0);	
    glTexCoord2f(1.0,0.0); glVertex3f(+1.0,+1.0,-2.0);	
  glEnd;	
	
  glBegin(GL_POLYGON);	
    glTexCoord2f(0.0,0.0); glVertex3f(+0.4,+1.0,-2.0);	
    glTexCoord2f(0.0,1.0); glVertex3f(+0.4,+0.7,-2.0);	
    glTexCoord2f(1.0,1.0); glVertex3f(+0.7,+0.7,-2.0);	
    glTexCoord2f(1.0,0.0); glVertex3f(+0.7,+1.0,-2.0);	
  glEnd;	
	
  glDisable(GL_TEXTURE_2D);	
	
end;	
Result
Save, compile, and run. Now you have a fancier display. See Figure 13-8. As you can expect 
with more to render, the frame rate has dropped considerably, although it is still quite good. 
With the slower frame rate you will have to jack up the motion rates with the S and R keys 
quite a bit more in order to move around at a decent speed.

PAGE 349
 
Collision Detection
Perhaps you enjoyed opening up the cylinder and cube with the zNear clipping plane. While 
that can be a very interesting effect, it is not usually the desired effect. You probably have 
thought about the need for preventing your vehicle from traveling through the objects in the 
scene. This is one use of collision detection, in which you use a suitable algorithm to 
determine if two or more objects in your scene intersect. Exact solutions can be 
computationally expensive, so collision detection often involves approximations and 
simplifying assumptions.
Project
Create a new directory under the Chapter.13 directory named Detect. Save a new project in it 
named Detect0.Dpr, with a unit named Detect1.Pas. Make this project initially just like the 
Texture0 project.
Distance
The Adjust method modifies the values of wx and wz, which are the coordinates of the 
viewer, and therefore you might expect them to be the coordinates of the portal through the 
bulkhead of your vehicle. In reality, your portal lies on the zNear clipping plane, which most 
of the example programs, including this one, set at 2.0 units away from the viewer into the 
screen. Your distance calculations must take the extra space into account.

PAGE 350
 Before you change wx and wz, be sure to save them, in savex and savez, naturally. In a 
more elaborate collision detection scheme you would also need to save the value of the angl 
field, but the present approach makes some simplifying assumptions about the shape of the 
vehicle so you will not produce a collision merely by turning.

If the new values of wx and wz would result in a collision, simply restore the former values 
so your vehicle does not move in that frame. Adjust the contents of the Adjust method as 
described in the following paragraphs.

First you need some additional local variables. The viewerx and viewerz variables track the 
viewer’s position in the same coordinate system the objects in the scene use. The deltax and 
deltaz variables store intermediate values in the distance calculations and allow more 
readable code. The distance variable, as you would expect, stores the result of distance 
calculations. You already know about savex and savez.

procedure TForm1.Adjust;	
var	
  theta,	
  viewerx,	
  viewerz,	
  deltax,	
  deltaz,	
  distance,	
  savex,	
  savez: GLfloat;	

Next you need a little nested function to handle restoring old values to prevent movement. It 
is a Boolean function that always returns a result value of true for the sake of simpler if 
statements. As an added touch, it sets the form’s caption to “Bump!” when a collision occurs.

function restore: boolean;	
begin	
  result := true;	
  wx := savex;	
  wz := savez;	
  Caption := 'Bump!'	
end;	

The body of the Adjust method starts out almost the same as you have seen before, except 
that it saves the viewer position variables before changing them, and it converts the viewer 
coordinate system. The wx and wz fields have opposite signs since they are used for moving 
the entire scene in the opposite direction from the apparent movement of the viewer. For the 
sake of calculations of the distance from viewer to object, the viewerx and viewerz variables 
reverse the signs.

PAGE 351
 
begin	
  angl := angl+rot;	
  theta := angl*pi180;	
  savex := wx;	
  wx := wx-tran*sin(theta);	
  savez := wz;	
  wz := wz+tran*cos(theta);	
	
  viewerz := -wz;	
  viewerx := -wx;	

The rest of the Adjust method is strictly new code added since the last project. The following 
block of code calculates the distance from the viewer to the cylinder. The delta variables 
simply receive the difference between each of the viewer coordinates and the center of the 
cylinder. The distance is the square root of the sum of the squares, just like the calculation of 
the hypotenuse of a right triangle. If the distance resulting from the new position is less than 
a specified amount, restore the old positions, alert the user, and exit. Perform no further tests 
once a test fails.

The number 2.515 merits some discussion. Remember that the viewport is always 2.0 units 
“ahead” of the viewer. That explains the 2, and insider knowledge of the drawing of the 
cylinder reveals that the surface will always be 0.4 units (the radius of the base) closer than 
the center in any direction. That accounts for 2.4, but what about the remaining 0.115 units? 
You could call it a “fudge factor,” but if you are in a charitable mood you can consider it an 
allowance for an unspecified rounded bumper below the viewport. This extra distance 
ensures that the vehicle stops at a point where rotation will not be a factor.

You could specify the shape of the vehicle precisely and calculate distances accordingly. 
That would buy you extremely little added realism at the cost of a lot of added complexity of 
code that would merely distract from this introduction to collision detection.

  with TransformCyl do	
  begin	
    deltax := TranX-viewerx;	
    deltaz := TranZ-viewerz;	
    distance := sqrt(deltax*deltax+deltaz*deltaz);	
    if (distance<2.515) and restore then	
      exit;	
  end;	

Collision detection with the cube is similar but has its own unique characteristics. It starts 
out in a familiar way, calculating a distance to the center of the cube. The distance from the 
center to a face is 0.4 units, but the 

PAGE 352
 distance from the center to a corner is about 0.566 units. Add in 2.0 units for distance from 
viewer to viewport, plus a small bumper allowance to obtain the mysterious 2.67. Without 
the bumper allowance you could hit the cube just right without entering it, but a turn to the 
left or right could still tear the cube open.

Notice that approaching within the specified distance does not automatically call the restore 
function and exit. This distance is a worst-case value for when you drive straight toward the 
center of the cube with a corner of the cube exactly in your path. Since approaching from 
other directions could allow you to move closer without a collision, this is just an initial test 
to determine if the method should make further calculations.

  with TransformCube do	
  begin	
    deltax := TranX-viewerx;	
    deltaz := TranZ-viewerz;	
    distance := sqrt(deltax*deltax+deltaz*deltaz);	
    if (distance<2.67) then	
    begin	

If your proposed motion does indeed fail the initial distance test, the method then tests the 
distance from each corner. Again, the whole part of the number (2.0 units) allows for the 
distance from the viewer to the viewport. Allow 0.4 units since that is the distance in the x 
direction and the distance in the z direction from the center of the cube to a given vertex. The 
remainder of the 2.42 is the omnipresent bumper allowance.

For this particular corner, add +0.4 units to TranX and add +0.4 units to TranZ. If the 
proposed distance is less than the given amount, the test has failed; restore the old position 
values, alert the user, and exit. No further tests are useful.

     begin	
       if (distance<2.42) and restore then	
         exit;	
       deltax := TranX+0.4-viewerx;	
       deltaz := TranZ+0.4-viewerz;	
       distance := sqrt(deltax*deltax+deltaz*deltaz);	
       if (distance<2.42) and restore then	
         exit;	

The remaining three corners get the same test. The method specifies each corner by using all 
combinations of +0.4 and –0.4 with the center coordinates. In each case, if the motion fails the 
test for any corner, it does not need to check any remaining corners.

PAGE 353
 
       deltax := TranX+0.4-viewerx;	
       deltaz := TranZ-0.4-viewerx;	
       distance := sqrt(deltax*deltax+deltaz*deltaz);	
       if (distance<2.42) and restore then	
         exit;	
	
       deltax := TranX-0.4-viewerx;	
       deltaz := TranZ+0.4-viewerz;	
       distance := sqrt(deltax*deltax+deltaz*deltaz);	
       if (distance<2.42) and restore then	
         exit;	
When the method tests against the final corner, calling exit is not necessary, but duplicating 
the code for the other corners is convenient:

       deltax := TranX-0.4-viewerx;	
       deltaz := TranZ-0.4-viewerz;	
       distance := sqrt(deltax*deltax+deltaz*deltaz);	
       if (distance<2.42) and restore then	
         exit;	
    end;	
  end;	
end;	
Results	
Save, compile, and run. Drive around as usual. This time you will not be able to penetrate 
the objects in the scene, as Figure 13-9 suggests. Since the program effectively treats the 
vehicle as if it were just as long behind the viewer as in front of the viewer, you will not be 
able to back into any objects either. The shape of the vehicle has become a rounded rectangle.


PAGE 354
 Textured Objects
Are you a little tired of the cylinder and the cube? Take heart; this example will place more 
interesting objects in the scene.
Project
Create a directory under the Chapter.13 directory and name it Real. Save a new project in it 
named Real0.Dpr, with a unit named Real1.Pas. Drop on a TOpenGL component, a TLight 
component, five TTransform components, and four TMaterial components. Make the 
TOpenGL and TLight components just like their counterparts in the previous project, as well 
as the TTransform component for the TLight. Also appropriate TransformFloor and 
MaterialFloor from the previous project.
Properties
The components for the Real0 project set up a textured brick wall, a light pole supporting a 
box around a spotlight, and more. Listing 13-3 shows the properties for this project:

object Form1: TForm1	
  Left = 192	
  Top = 107	
  Width = 328	
  Height = 347	
  Caption = 'Form1'	
  Color = clBtnFace	
  Font.Charset = DEFAULT_CHARSET	
  Font.Color = clWindowText	
  Font.Height = -11	
  Font.Name = 'MS Sans Serif'	
  Font.Style = []	
  OldCreateOrder = False	
  OnCreate = FormCreate	
  OnDestroy = FormDestroy	
  OnKeyDown = FormKeyDown	
  OnKeyUp = FormKeyUp	
  OnPaint = FormPaint	
  OnResize = FormResize	
  PixelsPerInch = 96	
  TextHeight = 13	
  object OpenGL1: TOpenGL	
    pfdFlags = [DoubleBuffer, Draw_To_Window, Support_OpenGL]	
    PixelType = RGBA	
    Projection = Perspective	
    Enable = [depthTest, Lighting]	


PAGE 355
     ClearRed = 0.6	
    ClearGreen = 0.8	
    ClearBlue = 0.8	
    prxLeft = -1	
    prxRight = 1	
    prxBottom = -1	
    prxTop = 1	
    prxNear = 2	
    prxFar = 42	
    ColorBits = 24	
    DepthBits = 32	
    StencilBits = 0	
    Left = 4	
    Top = 8	
  end	
  object TransformLight: TTransform	
    TranY = 5	
    TranZ = -3	
    AxisX = 1	
    Angle = -20	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformLightPaint	
    Left = 4	
    Top = 44	
  end	
  object Light1: TLight	
    Which = 1	
    Cutoff = 65	
    AmbientRed = 0.2	
    AmbientGreen = 0.2	
    AmbientBlue = 0.2	
    DiffuseRed = 1	
    DiffuseGreen = 1	
    DiffuseBlue = 1	
    SpecularRed = 1	
    SpecularGreen = 1	
    SpecularBlue = 1	
    Direction = True	
    Position = True	
    Spotlight = True	
    Enabled = True	
    Left = 40	
    Top = 44	
  end	
  object MaterialLight: TMaterial	
    ambRed = 1	
    ambGreen = 1	


PAGE 356
     ambBlue = 1	
    ambAlpha = 1	
    difRed = 1	
    difGreen = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 76	
    Top = 44	
  end	
  object TransformFloor: TTransform	
    TranY = -0.2	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformFloorPaint	
    Left = 4	
    Top = 124	
  end	
  object MaterialFloor: TMaterial	
    ambRed = 0.7	
    ambGreen = 0.5	
    ambBlue = 0.1	
    ambAlpha = 1	
    difRed = 0.7	
    difGreen = 0.7	
    difBlue = 0.1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 124	
  end	
  object TransformWall: TTransform	
    TranY = -0.2	
    TranZ = -7	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformWallPaint	
    Left = 4	
    Top = 160	
  end	
  object MaterialWall: TMaterial	
    ambRed = 1	
    ambGreen = 1	


PAGE 357
     ambBlue = 1	
    ambAlpha = 1	
    difRed = 1	
    difGreen = 1	
    difBlue = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 160	
  end	
  object TransformPole: TTransform	
    TranY = 1.675	
    TranZ = -3	
    AxisX = 1	
    Angle = 90	
    ScaleX = 0.1	
    ScaleY = 0.1	
    ScaleZ = 7.75	
    OnPaint = TransformPolePaint	
    Left = 4	
    Top = 76	
  end	
  object MaterialPole: TMaterial	
    ambAlpha = 1	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    mShine = 128	
    Left = 40	
    Top = 76	
  end	
  object TransformGlobe: TTransform	
    TranX = -7	
    TranY = 0.8	
    TranZ = -9	
    AxisX = 1	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformGlobePaint	
    Left = 4	
    Top = 196	
  end	
end
Listing 13-3. Real0 project properties.

PAGE 358
 Methods and Other Routines
From the Detect0 project, borrow the ReadBitmap function and the FormKeyDown, 
FormResize, FormKeyUp, TransformLightPaint, TransformFloorPaint, Opening, and Idle 
methods. Also use the Adjust method, although you will need to adapt its collision detection 
for the different objects in the scene.
A Light Pole
One tiny step toward more realism is to stop the light from being a vagabond with no visible 
means of support. The Real0 project borrows the TransformCyl and MaterialCyl components 
from the Detect0 project, renaming them TransformPole and MaterialPole, respectively. The 
Scale properties make the cylinder long and skinny, the material color properties are all 0.0 
to make a black pole, and the Tran properties make the pole line up with the cube around 
the light. The OnPaint event handler method is the same as the previous project’s 
TransformCylPaint method except the names have been changed:

procedure TForm1.TransformPolePaint(Sender: TObject);	
begin	
  MaterialPole.Apply;	
  Cylinder;	
end;	
An Array of Vertices
The next object has a sizable number of vertices; rather than writing a lot of individual 
glVertex3f calls, you can use an array of vertices. First declare a type you can use as a vertex 
ahead of the form type declaration:

type	
  TPoint=packed record	
    x,y,z: GLfloat;	
  end; {TPoint}	
  TForm1 = class(TForm)	
You will code a single call to glVertex3f in a loop, using the array. That means you must 
already have values in the array. In the private section of the form declaration, declare an 
array and a method to fill it:

    wall: array[1..28] of TPoint;	
    procedure BuildWall;	

PAGE 359
 A Wall
You can reuse the TransformCube component and call it TransformWall. Use MaterialCube, 
rename it MaterialWall, and make all the color properties 1.0. In the OnPaint event handler, 
do not call the Cube procedure; you will create a custom object. The wall field contains the 
vertices for drawing the object (a wall, of course). At the bottom of the FormCreate method 
add a call to the BuildWall method:

  CylinderBase;	
  BuildWall;	
end;	
Just above the FormCreate method, code the BuildWall method. The program will render the 
wall using a triangle strip. Since the wall will run parallel to the x-axis and y-axis, the 
algorithm is very simple. The z value retains the zero it had by virtue of being a field of the 
form. The x value stays the same for each two vertices, then increases by one for the next 
pair. The y value alternates between +1 and –1, making a height of two for the wall. Delphi’s 
odd function is very convenient for this pairing and alternating. The first pair of vertices for 
the wall will have an x value of –1.0.

procedure TForm1.BuildWall;	
var	
  q: GLfloat;	
  ix: integer;	
begin	
  q := -2.0;	
  for ix := 1 to 14 do	
  with wall[ix] do	
  if odd(ix) then	
  begin	
    q := q+1.0;	
    x := q;	
    y := +1.0;	
  end else begin	
    x := q;	
    y := -1.0;	
  end;	
The for loop created vertices for a triangle strip 14 units long, but that is not the end of the 
story. The strip must turn a corner, proceed a half unit down the negative z direction, turn 
another corner, and head back in the negative x direction to form a rectangular solid. Jog the 
q value before the next loop so that the x value will start where it left off in the previous 
loop:

PAGE 360
 
  q := q+1.0;	
  for ix := 15 to 28 do	
  with wall[ix] do	
  if odd(ix) then	
  begin	
    q := q-1.0;	
    x := q;	
    y := +1.0;	
    z := -0.5;	
  end else begin	
    x := q;	
    y := -1.0;	
    z := -0.5;	
  end;	
end;	

Once the array has its values, the appropriate OnPaint method can render it as a triangle 
strip. To appear as a brick wall, the object needs a texture, which you can see in Figure 13-10.



You need a pointer variable to point to the texture memory and a couple of variables to store 
the dimensions of the brick texture. Declare them in the implementation section:

var	
  sBulk,	
  tBulk,	
  sBrick,	
  tBrick: GLsizei;	
  globe,	
  bulk,	
  brick: pointer;	
In the FormCreate method, load the bitmap and convert it to a texture right after the texture 
for the bulkhead. Don’t forget to copy Fig13-10.bmp 

PAGE 361
 from the CD to the directory for this program. Also remember to use the full path instead of 
just the simple filename shown below.

  bulk := ReadBitmap('fig13-07.bmp',sBulk,tBulk);	
  brick := ReadBitmap('fig13-10.bmp',sBrick,tBrick);	
The FormDestroy method gives back the texture memory:

procedure TForm1.FormDestroy(Sender: TObject);	
begin	
  FreeMem(bulk);	
  FreeMem(brick);	
end;	

The TransformWallPaint method renders the strip. First enable two-dimensional texturing 
and point to the brick texture:

procedure TForm1.TransformWallPaint(Sender: TObject);	
var	
  ix: integer;	
  s: GLfloat;	
  t: array[false..true] of GLfloat;	
  tt: boolean;	
begin	
  glEnable(GL_TEXTURE_2D);	
  glTexImage2D(GL_TEXTURE_2D,Level,ColorComps,sBrick,tBrick,Border,	
    GL_RGB,GL_UNSIGNED_BYTE,Brick);	

Apply the texture and initialize some local variables. The local variables s and t provide the 
texture coordinates. The s texture coordinate starts with zero and increases by two every 
other vertex. The t texture coordinate alternates between two values. An array from false to 
true provides a convenient repository for the two alternating values. The tt Boolean variable 
provides the switch for selecting between the two t texture values.

  MaterialWall.Apply;	
  s := 0.0;	
  t[false] := 0.0;	
  t[true] := 4.0;	
  tt := false;	

Draw the triangle strip in a loop and tack on the repeating texture image:

  glBegin(GL_TRIANGLE_STRIP);	
    for ix := 1 to 28 do	
    with wall[ix] do	
    begin	
      glTexCoord2f(s,t[tt]);	
      glVertex3f(x,y,z);	
      if tt then	
        s := s+2.0;	


PAGE 362
       tt := not tt;	
    end;	
Close the gap by connecting the dots between the beginning and the end:

    for ix := 1 to 2 do	
    with wall[ix] do	
    begin	
      glTexCoord2f(s,t[tt]);	
      glVertex3f(x,y,z);	
      if tt then	
        s := s+2.0;	
      tt := not tt;	
    end;	
Finish the rendering step and turn off the texturing to protect the rest of the scene:

  glEnd;	
  glDisable(GL_TEXTURE_2D);	
end;	
Paint
The FormPaint method at this point should apply the transformations for the light, the pole, 
the wall, and the floor. Continue using the first-person movement and the buffer swapping.

procedure TForm1.FormPaint(Sender: TObject);	
begin	
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);	
	
  glMatrixMode(GL_MODELVIEW);	
  glLoadIdentity;	
	
  Opening;	
	
  glRotatef(angl,0.0,1.0,0.0);	
  glTranslatef(wx,0.0,wz);	
	
  TransformLight.Apply;	
  TransformPole.Apply;	
  TransformWall.Apply;	
  TransformFloor.Apply;	
	
  OpenGL1.Swap;	
  OpenGL1.ErrorCheck('FormPaint');	
end;	

PAGE 363
 Adjust the Adjust Method
You should make the collision detection in the Adjust method fit the current objects. The 
nested restore function is the same as before. The variables are also the same, but an integer 
joins the list:

procedure TForm1.Adjust;	
var	
  theta,	
  viewerx,	
  viewerz,	
  deltax,	
  deltaz,	
  distance,	
  savex,	
  savez: GLfloat;	
  ix: integer;	
	
function restore: boolean;	
begin	
  result := true;	
  wx := savex;	
  wz := savez;	
  Caption := 'Bump!'	
end;	
The first-person position adjustment code remains the same as before:

begin	
  angl := angl+rot;	
  theta := angl*pi180;	
  savex := wx;	
  wx := wx-tran*sin(theta);	
  savez := wz;	
  wz := wz+tran*cos(theta);	
	
  viewerz := -wz;	
  viewerx := -wx;	
Detecting the pole is very similar to detecting the previous cylinder, except that the radius of 
the pole is smaller:

  with TransformPole do	
  begin	
    deltax := TranX-viewerx;	
    deltaz := TranZ-viewerz;	
    distance := sqrt(deltax*deltax+deltaz*deltaz);	
    if (distance<2.115) and restore then	
      exit;	
  end;	

PAGE 364
 Replace the code for the cube with code for the wall, this time using just the vertices alone. 
Fortunately, those vertices are all available to you in an array. Use the new integer variable 
as the index into the vertex array:

  with TransformWall do	
  begin	
    ix := 0;	
    repeat	
      inc(ix,2);	
      with wall[ix] do	
      begin	
        deltax := x-viewerx;	
        deltaz := TranZ+z-viewerz;	
      end; 	
      distance := sqrt(deltax*deltax+deltaz*deltaz);	
      if (distance<2.42) and restore then	
        exit;	
    until ix=28;	
  end;	
end;	
Run
Save, compile, and run. Figure 13-11 shows the initial view, including the light pole and the 
wall. Drive around a bit. Figure 13-12 shows the wall and the light pole from a different 
angle.


PAGE 365
 More
You are not done yet. That was just a brief rest. For the next object you will need some new 
commands and constants from the OpenGL utility library. Such commands have a glu 
prefix. In the utility library are some convenient objects called quadrics, so named because 
the mathematical description of the three-dimensional surfaces are quadratic equations, which 
take the general form of

a1x2 + a2y2 + a3z2 + a4xy + a5yz + a6xz + a7x + a8y + a9z +a10 = 0	
where each of the subscripted constants could be any number, including zero, which would 
eliminate that term from the equation.
The surface this example program uses is a sphere. Its most familiar equation is:

x2 + y2 + z2 = r2	

which can easily be recast into the general quadratic equation format.

Do not panic if your high school algebra is a bit rusty. The math has been done for you in the 
OpenGL utility library. Here are some OpenGL commands that take out most of the pain of 
drawing quadrics. Naturally the objects will be collections of polygons rather than true 
curved surfaces, but you have already seen that you can do very nice approximations that 
way.

To create a new quadric object, use the gluNewQuadric function. Have a pointer variable 
ready to receive the function result. You can declare your variable to be a GLUquadricObj, 
but a general pointer will do just as well. Delphi’s OpenGL interface unit just declares the 
type as a pointer to an 

PAGE 366
 empty record structure. The region of memory the pointer references contains various 
settings, which you can call a state. If the call fails, it will return nil; otherwise the state 
describes how to draw a quadric, but not what to draw. For that (and other purposes) you 
will pass your pointer variable to other quadric commands wherever they have a parameter 
of that type.

function  gluNewQuadric: GLUquadricObj; stdcall;	

When you want to draw more than one quadric surface, but not at the same time, you can 
simply reuse the pointer, but for multiple quadrics in the same scene at the same time, you 
need to call the function separately for each simultaneous object. When you are finished with 
a given quadric object, and you are not going to reuse it, you need to be a good resource 
citizen and delete it with the gluDeleteQuadric command:

procedure gluDeleteQuadric(state:GLUquadricObj); stdcall;	

You do not have to live with only the default state you get when you first create a quadric 
object. By default your objects will have their skin on; they will appear as collections of filled 
polygons. You do have other choices, however, which you can express with the 
gluQuadricDrawStyle command. You already know the pointer to pass as the quadObject 
parameter. For the drawStyle parameter, you can use one of several OpenGL constants to 
specify whether you want points, lines, or filled polygons.
To render quadrics with filled polygons that have counterclockwise winding, use:

  GLU_FILL = $186AC;	
To render a quadric as a set of points, use:

  GLU_POINT = $186AA;	
To render a quadric as a set of lines, use:

  GLU_LINE = $186AB;	
To render a quadric as a set of lines with certain edges removed, use:

  GLU_SILHOUETTE = $186AD;	
The difference from using GLU_LINE is that any group of lines that form adjacent polygons 
that lie in the same plane will have the shared edges removed to make a single polygon.

Now, here is the command for which you have waiting patiently, to which you pass one of 
those OpenGL constants:

PAGE 367
 
procedure gluQuadricDrawStyle(quadObject:GLUquadricObj; drawStyle: GLenum);	
 stdcall;	
The gluQuadricOrientation function specifies inside/outside orientation for quadrics. Guess 
what: To use it, you need a couple more OpenGL constants. To draw quadrics with normals 
pointing outward (the default), use:

GLU_OUTSIDE = $186B4;	
A normal is a vector used in lighting calculations. Usually it is perpendicular to the face of a 
polygon or to a pair of sides joining at a vertex. Sometimes it is the weighted average of the 
normals for several sides joining at a single vertex, taken two at a time.
To draw quadrics with normals pointing inward, use:

  GLU_INSIDE = $186B5;	
To set the desired orientation of normal vectors for subsequent quadrics, pass one of those 
two OpenGL constants to:

procedure gluQuadricOrientation(quadObject:GLUquadricObj; orientation:	
 GLenum); stdcall;	
To specify what kind of normals a quadric should have, use gluQuadricNormals and pass 
one of the following OpenGL constants to it.
To generate one normal for every vertex of the quadric, use:

  GLU_SMOOTH = $186A0;	
As the name implies, this kind of vertex provides for smooth shading in your lighting 
environment, which is the default.
To generate a normal for every face in a quadric and provide flat shading in your lighting 
environment, use:

  GLU_FLAT = $186A1;	
If you do not intend to use lighting and want no normals generated, use:

  GLU_NONE = $186A2;	
Here is the declaration of the command to which to pass one of those constants:

procedure gluQuadricNormals(quadObject:GLUquadricObj; normals:GLenum);	
  stdcall;	

A really helpful feature is the ability to generate texture coordinates along with each vertex 
in a quadric. If you want to wrap a texture image around a quadric, pass a value 
representing true to:

PAGE 368
 procedure gluQuadricTexture(quadObject:GLUquadricObj; textureCoords: 	
GLboolean );stdcall;	

where

  GLboolean = ByteBool;	

Of course, to turn off texture coordinate generation in quadrics, pass a false (zero) value to 
the same command.

Now that you have seen the commands that determine how to draw various quadrics, you 
probably would like to see some commands that tell OpenGL what objects to draw. To draw 
a cylinder, use:

procedure gluCylinder(quadObject:GLUquadricObj; baseRadius,topRadius,	
 height:GLdouble; slices,stacks:GLint); stdcall;	

You can specify the overall dimensions of the cylinder with the GLdouble parameters, and 
the granularity with the GLint parameters. Notice that you do not just specify the radius of 
the cylinder, but two radii, one for the top and one for the bottom. If they are equal, you will 
have an ordinary cylinder. If one is zero, you generate a cone. Any other pair of values will 
produce a conic frustum.

The height parameter, as you would expect, determines how tall the cylinder will be. 
Actually you might want to think of it as specifying how long the cylinder is, since it will lie 
on its side along the z-axis. If you want it to stand up or appear some place besides the 
origin, you must perform transformations. The cylinder will have no top or bottom; it will be 
like an open-ended pipe or tube.

The slices parameter corresponds to the number of slices with which you divide a pie and 
has the same use in all the quadrics. A larger number makes the object rounder; a smaller 
number makes it blockier. You could use six slices, along with GLU_FLAT for the normals, 
to draw a pencil. For a nice, round cylinder 18 or 20 slices should be enough, while more 
slices would detract needlessly from animation performance.

The stacks parameter tells how many divisions along the length of the cylinder you want. 
You could think of it as the number of short cylinders you stack on top of each other to make 
the longer cylinder.
If you want a circle or disk, use:

procedure gluDisk(quadObject:GLUquadricObj; innerRadius,outerRadius: GLdouble; 
slices,loops:GLint); stdcall;	

This one also has two radii. outerRadius sets the size of the disk, while innerRadius sets the 
size of the hole in the disk. Use zero for the innerRadius parameter if you do not want a hole. 
The loops parameter is 

PAGE 369
 another granularity parameter, determining how many concentric circles with which to 
divide the disk. Since this object is only two-dimensional, orientation needs special 
consideration. The “outside” for purposes of generating normals is the positive z side.
To render a disk that does not make a complete circle, use:

procedure gluPartialDisk(quadObject:GLUquadricObj; innerRadius,	
outerRadius:GLdouble; slices,loops:GLint; startAngle,sweepAngle:	
GLdouble); stdcall;	
The added angle parameters determine how much of a circle you draw. The angles are in 
degrees, as usual. Imagine a clock face on the disk. Noon is at 0 degrees. Three o’clock is at 
90 degrees, six o’clock is at 180 degrees, and nine o’clock is at 270 degrees. The startAngle 
parameter tells where to start drawing the disk; the sweepAngle parameter tells how much 
disk to draw. The disk runs from startAngle to startAngle + sweepAngle.
To draw a sphere or globe, use:

procedure gluSphere(quadObject:GLUquadricObj; radius:GLdouble; slices, loops:GLint); 
stdcall;	
Radius has the obvious meaning, slices has the same meaning as before, and the loops 
parameter corresponds to stacks for the cylinder, like the latitude lines around a globe of the 
earth.
? Caution!
Adding quadrics to your animations may reduce your scene’s overall performance (frame 
rate). Movie films have been acceptable at 18 frames per second, but anything less would 
definitely appear jerky. If your animation’s performance is already borderline, you may have 
to “roll your own” as earlier example programs did with the cylinder. On the other hand, 
static scenes usually do not have this issue. While 4 fps is totally ludicrous for an animation, 
a render time of a fourth of a second for a static scene is usually just fine.
Globe
Near the top of the implementation section, add a few variables:

var	
  sBulk,	
  tBulk,	
  sBrick,	
  tBrick,	
  sWorld,	
  tWorld: GLsizei;	


PAGE 370
   globe,	
  world,	
  bulk,	
  brick: pointer;	
At the bottom of the FormCreate method, create a quadric. Accept the default state except 
for the draw style.

  CylinderBase;	
  BuildWall;	
  globe := gluNewQuadric;	
  gluQuadricDrawStyle(globe,GLU_LINE);	
end;	
At the end give back what you took:

procedure TForm1.FormDestroy(Sender: TObject);	
begin	
  FreeMem(bulk);	
  FreeMem(brick);	
  gluDeleteQuadric(globe);	
end;	
Name the fifth TTransform component on the form TransformGlobe and create an event 
handler for it named TransformGlobePaint:

procedure TForm1.TransformGlobePaint(Sender: TObject);	
begin	
  gluSphere(globe,2.0,18,12);	
end;	
Be sure to call the component’s Apply method in FormPaint:

procedure TForm1.FormPaint(Sender: TObject);	
begin	
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);	
	
  glMatrixMode(GL_MODELVIEW);	
  glLoadIdentity;	
	
  Opening;	
	
  glRotatef(angl,0.0,1.0,0.0);	
  glTranslatef(wx,0.0,wz);	
	
  TransformLight.Apply;	
  TransformPole.Apply;	
  TransformWall.Apply;	
  TransformGlobe.Apply;	
  TransformFloor.Apply;	


PAGE 371
 	
  OpenGL1.Swap;	
  OpenGL1.ErrorCheck('FormPaint');	
end;	
Skeleton
Save, compile, and run. Turn left until you see the sphere. Figure 13-13 shows the natural 
inclination of the sphere, made obvious by GLU_LINE. The gluSphere command builds the 
sphere around the origin and aligns the axis along the z-axis. The transformations put 
the sphere in its final location but did not change its orientation.



Stand Up
This program needs to have the sphere standing up on its axis, so rotate it about the x-axis. 
The Axis fields in the component already have the appropriate values, so just change the 
Angle field to –90.0 degrees.
Save, compile, and run. Turn to the left again to see the sphere standing up as in Figure 13-
14.


PAGE 372
 Skin
Add some skin to the sphere. You can change the GLU_LINE parameter to GLU_FILL in the 
gluQuadricDrawStyle command so that the polygons in the sphere will be filled polygons. 
However, since that is the default state, just delete the command.
Save, compile, run, and turn left as before to see the sphere as in Figure 13-15.


Earth
A skin is nice, but the plan for this sphere is more ambitious. You are going to wrap a map of 
the earth around it to make an earth globe. Not just any map will do. Have you ever seen 
earth maps with the areas near the poles stretched out of proportion? You know, the ones 
where the island of Greenland looks as big as the continent of Africa? That is called a 
Mercator projection. You need a map that is stretched more and more as you move from the 
equator to the poles, because as you wrap a rectangular texture image around a sphere, you 
compress the image more and more as you move from the equator to the poles. Figure 13-16 
shows the texture image. The colors are mostly arbitrary.

Copy Fig13-16.bmp from the CD to the directory for this example program. In the middle of 
the FormCreate method, add a ReadBitmap for this texture image. Again, use the full path 
rather than just a simple filename.

PAGE 373
 
  bulk := ReadBitmap('fig13-08.bmp',sBulk,tBulk);	
  brick := ReadBitmap('fig13-10.bmp',sBrick,tBrick);	
  world := ReadBitmap('fig13-16.bmp',sWorld,tWorld);
To the bottom of the FormCreate method, add a gluQuadricTexture command to turn on 
texture map coordinate generation for the quadric object:

 BuildWall;	
  globe := gluNewQuadric;	
  gluQuadricDrawStyle(globe,GLU_FILL);	
  gluQuadricTexture(globe,byteBool(GL_TRUE));	
end;	
Be sure to give back the texture and quadric resources:

procedure TForm1.FormDestroy(Sender: Tobject);	
begin	
  FreeMem(bulk);	
  FreeMem(brick);	
  FreeMem(world);	
  gluDeleteQuadric(globe);	
end;	
Add texturing ability to the TransformGlobePaint method:

procedure TForm1.TransformGlobePaint(Sender: TObject);	
begin	
  glEnable(GL_TEXTURE_2D);	
  glTexImage2D(GL_TEXTURE_2D,Level,ColorComps,sWorld,tWorld,Border,	
    GL_RGB,GL_UNSIGNED_BYTE,world);	
  gluSphere(globe,2.0,18,12);	
  glDisable(GL_TEXTURE_2D);	
end;	
Save, compile, run, and turn left. Figure 13-17 shows the earth globe in the scene.







       Figure 13-17. A globe of the Earth.

PAGE 374
 Go for a Spin
The finishing touch for this example program is to set the earth spinning and add collision 
detection for it. Add a GLfloat named ang as a new private field of the form to track the 
earth globe’s angular position:

  private	
    ang,	
    angl,	
    rspeed,	
Since your speed of movement within the scene is adjustable to your taste, note the program 
included no attempt to adjust speed to frame rate. Now, however, the globe rotation begs for 
a system-independent speed. You can modify the Idle method to advance the ang field so 
the globe rotates at two revolutions per minute. Since the frame rate has fallen so much with 
the addition of a texture mapped quadric, cut the rollover constant from 1,000 frames to 100 
frames. Add a constant named half to represent the number of milliseconds in half a minute 
(30,000), or one rotation of the globe. The constant uses the reciprocal for computational 
convenience. The fullrot constant represents the number of degrees in one rotation of the 
globe.

procedure TForm1.Idle(Sender:TObject;var Done:boolean);	
const	
  rollover=100;	
  mspersec=1000.0;	
  half=1/30000.0;	
  fullrot=360.0;	
You also need to add some GLfloat variables to participate in the globe rotation calculations:

var	
  deltaAngle,	
  averageTime,	
  timedif: GLfloat;	
Some lines of the body of the method are just rearrangements and some are new. The call to 
TimeGetTime now happens every frame, since the rotation speed should be sensitive to 
fluctuating frame rates. The timedif variable accumulates the time since start rather than just 
the time since the last frame. Start adjusts every rollover frames. Accumulating over a block 
of frames smooths out round-off errors that would happen just calculating the amount of 
angle change from one frame to the next. In essence, the change in angle value varies 
inversely with the frame rate. The higher the frame rate, the less the globe should turn per 
frame.

PAGE 375
 
begin	
  Done := false;	
  Adjust;	
  inc(frames);	
  elapsed := TimeGetTime;	
  timedif := (elapsed-start);	
  averageTime := timedif/frames;	
  deltaAngle := fullrot*averageTime*half;	
  ang := ang+deltaAngle;	
  if ang>=360.0 then	
    ang := ang-360.0;	
	
  if frames>=rollover then	
  begin	
    Caption := Format('fps=%0.1f',[frames*mspersec/timedif]);	
    start := elapsed;	
    frames := 0;	
  end else	
    FormPaint(nil);	
end;	
Now put the ang field to use in the TransformGlobePaint method. This requires some matrix 
stack manipulation as well as a glRotatef command:

procedure TForm1.TransformGlobePaint(Sender: TObject);	
begin	
  glPushMatrix;	
    glRotatef(ang,0.0,0.0,1.0);	
	
    glEnable(GL_TEXTURE_2D);	
    glTexImage2D(GL_TEXTURE_2D,Level,ColorComps,sWorld,tWorld,Border,	
      GL_RGB,GL_UNSIGNED_BYTE,World);	
    gluSphere(globe,2.0,18,12);	
    glDisable(GL_TEXTURE_2D);	
	
  glPopMatrix;	
end;	

At the bottom of the Adjust method, adding collision detection for the globe is very simple 
because it is round. The 4.14 accounts for the radius of the sphere, the viewer offset, and a 
small rounded bumper just above the viewport:

  with TransformGlobe do	
  begin	
    deltax := TranX-viewerx;	
    deltaz := TranZ-viewerz;	
    distance := sqrt(deltax*deltax+deltaz*deltaz);	
    if (distance<4.14) and restore then	
      exit;	


PAGE 376
   end;	
	
end;	
Save, compile, and run. Drive around a while and admire the spinning globe from various 
angles. Figure 13-18 shows collision detection in action.

Who Turned Out the Lights?

You may have noticed that after the addition of the sphere, the floor went uniformly dark, 
no longer demonstrating the shading effect of the light. The reason is the fact that the sphere 
generated normal vectors to give a smooth, round appearance. The last normal vector in 
effect before the drawing of the floor was wrong for the floor. All the sample programs up to 
this time have, by chance, been able to ignore the issue, but now you must explicitly deal 
with normals.

Most surfaces in the scene are covered by decal textures, so they do not show shading 
anyway. The light pole is black, so you can skip it also. Only the floor and one face of the 
cube around the light will show shading once the normals are right. For simplicity, ignore 
the cube and concentrate on the floor.

The glNormal commands are among the few that can fit between glBegin and glEnd, 
although they do not have to. Here is the declaration you need:

procedure glNormal3f(nx,ny,nz:GLFloat); stdcall;	

The floor lies parallel to the x-z plane, so its face normal is a vector that runs parallel to the y-
axis. Make sure the vector is one unit in length, and 

PAGE 377
 make it start at the origin. Only the direction and length matter, so the coordinates of a 
single point are sufficient to specify the vector, with the origin understood as the starting 
point. Add the command to the TransformFloorPaint method:

procedure TForm1.TransformFloorPaint(Sender: TObject);	
begin	
  MaterialFloor.Apply;	
  glBegin(GL_POLYGON);	
    glNormal3f(0.0,1.0,0.0);	
    glVertex3f(-29.0,0.0,-29.0);	
    glVertex3f(-29.0,0.0,+29.0);	
    glVertex3f(+29.0,0.0,+29.0);	
    glVertex3f(+29.0,0.0,-29.0);	
  glEnd;	
end;	
Save, compile, and run. Shading has returned.
Terrain
So far you have been driving around on a surface so flat and featureless you can consider it a 
floor. An outdoor scene would not have a large flat area, even in the Great Plains, unless you 
have an artificial construct such as a concrete tarmac. Normally, an outdoor expanse of 
ostensibly level ground really has its ups and downs. The example program in this section 
will produce an exaggerated rolling grassland.
Project
Create a new directory under the Chapter.13 directory and name it Terrain. In the Terrain 
directory save a new project named Terrain0.Dpr with a unit named Terrain1.Pas. On the 
form, drop a TOpenGL component, a TLight component, two TTransform components, and 
a TMaterial component.
Properties
The Terrain0 project returns to simplicity to illustrate a single concept. Gone are the textured 
objects in the scene and in the opening into the scene, as well as visible constructs for the 
light source. All you have here is a naked light source and a framework for drawing the 
terrain. Listing 13-4 shows the properties:

PAGE 378
 
object Form1: TForm1	
  Left = 192	
  Top = 107	
  Width = 328	
  Height = 347	
  Caption = 'Form1'	
  Color = clBtnFace	
  Font.Charset = DEFAULT_CHARSET	
  Font.Color = clWindowText	
  Font.Height = -11	
  Font.Name = 'MS Sans Serif'	
  Font.Style = []	
  OldCreateOrder = False	
  OnCreate = FormCreate	
  OnKeyDown = FormKeyDown	
  OnKeyUp = FormKeyUp	
  OnPaint = FormPaint	
  OnResize = FormResize	
  PixelsPerInch = 96	
  TextHeight = 13	
  object OpenGL1: TOpenGL	
    pfdFlags = [DoubleBuffer, Draw_To_Window, Support_OpenGL]	
    PixelType = RGBA	
    Projection = Perspective	
    Enable = [depthTest, Lighting]	
    ClearRed = 0.6	
    ClearGreen = 0.8	
    ClearBlue = 0.8	
    prxLeft = -1	
    prxRight = 1	
    prxBottom = -1	
    prxTop = 1	
    prxNear = 2	
    prxFar = 42	
    ColorBits = 24	
    DepthBits = 32	
    StencilBits = 0	
    Left = 4	
    Top = 4	
  end	
  object TransformLight: TTransform	
    TranZ = 2	
    AxisX = 1	
    Angle = -10	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformLightPaint	
    Left = 4	


PAGE 379
     Top = 40	
  end	
  object Light1: TLight	
    Which = 1	
    Cutoff = 55	
    AmbientRed = 0.2	
    AmbientGreen = 0.2	
    AmbientBlue = 0.2	
    DiffuseRed = 1	
    DiffuseGreen = 1	
    DiffuseBlue = 1	
    Direction = True	
    Position = True	
    Spotlight = True	
    Enabled = True	
    Left = 40	
    Top = 40	
  end	
  object TransformLand: TTransform	
    TranY = -0.2	
    ScaleX = 1	
    ScaleY = 1	
    ScaleZ = 1	
    OnPaint = TransformLandPaint	
    Left = 4	
    Top = 72	
  end	
  object MaterialLand: TMaterial	
    ambRed = 0.5	
    ambGreen = 0.5	
    ambBlue = 0.5	
    ambAlpha = 1	
    difRed = 0.7	
    difGreen = 0.7	
    difBlue = 0.7	
    difAlpha = 1	
    aFace = faFront	
    dFace = faFront	
    sFace = faFront	
    Left = 40	
    Top = 72	
  end	
end
Listing 13-4. Terrain0 project properties.

PAGE 380
 Private
In the private section of the form type declaration, you need some fields and methods, most 
of which are already familiar:

private	
    wx,	
    wy,	
    wz,	
    tran,	
    rot,	
    speed,	
    rspeed,	
    theta,	
    angl: GLfloat;	
    start,	
    elapsed,	
    frames: integer;	
    procedure DrawTerrain;	
    procedure BuildTerrain;	
    procedure Adjust;	
    procedure Idle(Sender:TObject;var Done:boolean);	
  public	
Constants, Types, Variables, and Functions
In the implementation section, you need a few identifiers. While you have seen some of them 
in other projects, some of them are unique to this project. The terrain types and the 
corresponding variable help construct triangle strips that will become grassland. The grassy 
variables relate to the texture image intended to suggest a grassy surface. The dl variable 
identifies a display list. Just after those identifiers you need the ReadBitmap function, for 
which you already have listings.

const	
  Level=0;	
  Border=0;	
  ColorComps=3;	
	
type	
  TRGB=packed record	
    r,g,b: byte;	
  end;	
  TWrap=array[0..0]of TRGB;	
  TPoint=packed record	
    x,y,z: GLfloat;	
  end;	
  TTerrainRow=array[1..66] of TPoint;	


PAGE 381
   TTerrain=array[-32..32] of TTerrainRow;	
	
var	
  grass: pointer;	
  sGrass,	
  tGrass: GLsizei;	
  ter: TTerrain;	
  dlTerrain: GLuint;	
  pi180: double=pi/180.0;	
Setup and Shutdown
As usual you will use the FormCreate method to initialize the project, particularly the 
texture and the timing. Copy Fig13-19.bmp from the CD to this project’s directory. Change 
the filename reference to its full path. That Windows bitmap file becomes the texture image 
assigned to the grass variable and shows in Figure 13-19.

procedure TForm1.FormCreate(Sender: TObject);	
begin	
  glPixelStorei(GL_UNPACK_ALIGNMENT,1);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);	
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);	
  glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);	
  grass := ReadBitmap('fig13-19.bmp',sGrass,tGrass);	
  glTexImage2D(GL_TEXTURE_2D,Level,ColorComps,sGrass,tGrass,Border,	
    GL_RGB,GL_UNSIGNED_BYTE,Grass);	
  glEnable(GL_TEXTURE_2D);	


PAGE 382
   Application.OnIdle := Idle;	
  BuildTerrain;	
  start := TimeGetTime;	
end;	
This project has a FormDestroy method to dispose of the grass:

procedure TForm1.FormDestroy(Sender: TObject);	
begin	
  FreeMem(grass);	
end;	
The Usual
The FormResize, FormKeyDown, and FormKeyUp methods are the same as in the last 
project.
FormPaint
The FormPaint method is what you would expect, but does need some attention. The Apply 
method calls position the light and the terrain. The transformation commands enable the 
drive-through. Notice the use of wy instead of a hard-coded zero. For now, the wy field will 
retain its initial value of zero, but keep it in mind for later.

procedure TForm1.FormPaint(Sender: TObject);	
begin	
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);	
  glMatrixMode(GL_MODELVIEW);	
  glLoadIdentity;	
	
  glRotatef(angl,0.0,1.0,0.0);	
  glTranslatef(wx,wy,wz);	
	
  TransformLight.Apply;	
  TransformLand.Apply;	
	
  OpenGL1.Swap;	
  OpenGL1.ErrorCheck('FormPaint');	
end;	
TransformPaint
The OnPaint event handlers for the TTransform components are small. The light source is 
completely unadorned. The event handler defers the rendering code for the terrain to a 
separate method so that later the rendering code can be in a display list.

PAGE 383
 
procedure TForm1.TransformLightPaint(Sender: TObject);	
begin	
  Light1.Apply;	
end;	
	
procedure TForm1.TransformLandPaint(Sender: TObject);	
begin	
  MaterialLand.Apply;	
  DrawTerrain;	
end;	
Idle
The Idle method does not implement the machine-independent speed code of the Real0 
project, so it is as simple as in prior projects. It tracks frame rate and hands off the drive-
through code to the Adjust method:

procedure TForm1.Idle(Sender:TObject;var Done:boolean);	
const	
  rollover=100;	
  mspersec=1000.0;	
begin	
  Done := false;	
  Adjust;	
  inc(frames);	
  if frames>=rollover then	
  begin	
    elapsed := TimeGetTime;	
    Caption := Format('fps=%0.1f',[rollover*mspersec/(elapsed-start)]);	
    start := elapsed;	
    frames := 0;	
  end else	
    FormPaint(nil);	
end;	
Adjust
The Adjust method adjusts the wx and wz fields to perform the drive-through. The private 
form field theta replaces the local variable theta:

procedure TForm1.Adjust;	
begin	
  angl := angl+rot;	
  if angl>360.0 then	
    angl := angl-360.0;	
  if angl<0.0 then	
    angl := angl+360.0;	


PAGE 384
   theta := angl*pi180;	
  wx := wx-tran*sin(theta);	
  wz := wz+tran*cos(theta);	
end;	
Terrain Implementation
Finally comes the distinguishing feature and purpose for this project. The BuildTerrain 
method uses random numbers to modify the vertices in a series of triangle strips. The ter 
variable contains an array of 65 triangle strips that should lie side by side to form a 
continuous stretch of land. The last project used an array to hold a set of programmatically 
generated vertices to build a wall. This terrain generator is an extension of that technique. 
The new twist is to modify the y coordinate of each vertex randomly so the landscape will 
have height variations. The routine also randomizes the x and z coordinates so that each 
triangle has a somewhat different shape, but the variations in height are the most important 
feature.

procedure TForm1.BuildTerrain;	
var	
  row,	
  ix: integer;	
  q: GLfloat;	
begin	
  Randomize;	
  for row := -32 to +32 do	
  begin	
    q := -17.0;	
    for ix := 1 to 66 do	
    with ter[row,ix] do	
    begin	
      y := -1.5+0.001*Random(500);	
      if odd(ix) then	
      begin	
        q := q+1.0;	
        x := q+0.1-0.001*Random(201);	
        z := 0.001*Random(450)+row;	
      end else begin	
        x := q;	
        z := 1.0-0.001*Random(450)+row;	
      end;	
    end; {for}	
  end; {for}	
In order for the scene to look like continuous surface rather than disconnected hash, the 
vertices from the last edge of one strip must match the vertices of the first edge of the next 
strip. An additional pair of for loops takes care of that important detail:

PAGE 385
 
  for row := -32 to +31 do	
  for ix := 1 to 33 do	
    ter[row+1,2*ix-1] := ter[row,2*ix];	
end;	
The FormCreate method calls the BuildTerrain method just once to initialize the triangle 
strips. The FormCreate method indirectly calls the DrawTerrain method each frame. 
DrawTerrain loops though the vertices to pass them to OpenGL in a manner similar to the 
way that BuildTerrain looped through them to give them values. Notice that the 
glBegin/glEnd pair execute once for each separate triangle strip. The inner for loop just 
passes the vertices in the proper order within a strip.

procedure TForm1.DrawTerrain;	
var	
  ix,	
  row,	
  extra,
  start,	
  finish: integer;	
  s: GLfloat;	
  t: array[false..true] of GLfloat;	
  tt: boolean;	
begin	
  start := -32;	
  finish := +32;	
  for row := start to finish do	
  begin	
    s := 0.0;	
    t[false] := 0.0;	
    t[true] := 2.0;	
    tt := false;	
    glBegin(GL_TRIANGLE_STRIP);	
    for ix := 1 to 66 do	
    with ter[row,ix] do	
    begin	
      glTexCoord2f(s,t[tt]);	
      glVertex3f(x,y,z);	
      if tt then	
        s := s+1.0;	
      tt := not tt;	
    end; {for}	
    glEnd;	
  end; {for}	
end;	

PAGE 386
 Run
Save, compile, and run. Drive around a bit. While the unevenness of the terrain is detectable 
but somewhat washed out in the uniform lighting of the area that has full light, it shows 
quite well near the boundary between light and dark, as in Figure 13-20. If you have ever 
looked at the moon through a telescope you are already familiar with such an effect, 
although the reasons for the two cases are not exactly the same. You can see mountains and 
crater rims quite well near the terminator on the moon, but not in the really bright areas.
This terrain illustration is exaggerated for effect. Most grassland is not this lumpy. If you 
drove an M1 Abrams at full throttle over land like this, you would probably shake your 
teeth out.
A Display List
While this scene does not seem to have a lot in it—just rolling grassland—it does have a 
large performance impact. The same computer system that was able to send a 72-triangle 
cylinder tumbling through space at 540 fps dropped to 51 fps with the opening view of this 
scene. In fact, there are a lot of vertices in a large area that OpenGL must process, not to 
mention a great deal of texture. Performance improvements would be welcome. Here are 
some declarations you will need:

procedure glCallList(list:GLuint); stdcall;	
	
procedure glEndList; stdcall;	
	
procedure glNewList(ListIndex:GLuint;mode:GLenum); stdcall;	
OpenGL display lists, containing precompiled rendering code, are supposed to be more 
efficient than calling the rendering code over again each frame. So, instead of calling the 
DrawTerrain method every frame, call it just once at the bottom of the BuildTerrain method 
to put into a display list:

for row := -32 to +31 do	
  for ix := 1 to 33 do	


PAGE 387
     ter[row+1,2*ix-1] := ter[row,2*ix];	
	
  dlTerrain := glGenLists(1);	
  glNewList(dlTerrain,GL_COMPILE);	
    DrawTerrain;	
  glEndList;	
end;	
Now call the display list instead of the DrawTerrain method each frame:

procedure TForm1.TransformLandPaint(Sender: TObject);	
begin	
  MaterialLand.Apply;	
  glCallList(dlTerrain);	
end;	
Of course, since you use resources in creating a display list, you should eventually give them 
back:

procedure TForm1.FormDestroy(Sender: Tobject);	
begin	
  glDeleteLists(dlTerrain,1);	
  FreeMem(grass);
end;	
Save, compile, and run. Compare with the previous frame rates. On the 51 fps machine just 
mentioned, the display list increased the frame rate to 55 fps. While helpful, that is not a 
breath-taking improvement. Try again.
Prefiltered Data
This problem requires some thought. OpenGL receives a large number of vertices from the 
program, but only shows a small subset at any one time. While OpenGL does cull a lot of 
data before final processing, it still has to do a certain amount of work with the entire scene 
before it decides to throw part of it out. If the program could eliminate some of the data 
before even sending it to OpenGL, without much ado, performance should improve.

Simplicity is important. If your program has to perform a lot of complicated calculations to 
filter out just the right amount of data, it will end up doing just about as much extra work as 
it saved. This example program takes simplicity to the extreme, for the sake of clear 
illustration. At the top of the DrawTerrain method you can manipulate the values of start 
and finish so that you only send some of the triangle strips to OpenGL:

begin	
  extra := round(abs(10.0*cos(theta)));	
  start := round(-wz);	


PAGE 388
   finish := start;	
  start := start-10-extra;	
  if start<-32 then	
    start := -32;	
  finish := finish+10+extra;	
  if finish>32 then	
    finish := 32;	
	
  for row := start to finish do	

Basically this eliminates some of the triangle strips behind you, and the most distant ones 
ahead of you. This algorithm is a bit crude; as you drive forward you will see a hint of new 
strips materializing. However, you can treat that as more surface coming into view above the 
horizon as you travel over a round earth rather than a flat one.
The display list is no longer useful since it contains the entire scene as an indivisible unit. 
Take its creation out of the BuildTerrain method and its destruction out of the FormDestroy 
method, and restore the call to the DrawTerrain method in TransformLandPaint. Also 
remove dlTerrain from the variable declarations.

procedure TForm1.TransformLandPaint(Sender: TObject);	
begin	
  MaterialLand.Apply;	
  DrawTerrain;	
end;	
Save, compile, and run. Again compare frame rates. On the previously referenced 51 fps 
machine, this technique almost doubled the rate to 96 fps.
A Bumpy Ride
Have you noticed that you are getting a very smooth ride for such a hilly area? That will 
now change. Do you remember the wy field that was being left at zero? By manipulating the 
value of wy in

glTranslatef(wx,wy,wz);	

you can make your view rise and fall with the terrain you pass over. The Adjust method will 
now act as if the tracks of the vehicle were at the location of wx and wz and will find the y 
value of the nearest terrain vertex under that point.
The top of the method stays the same:

procedure TForm1.Adjust;	
var	
  minx,	


PAGE 389
   deltaX: GLfloat;	
  ix,	
  start,	
  finish,	
  strip: integer;	
begin	
  angl := angl+rot;	
  if angl>360.0 then	
    angl := angl-360.0;	
  if angl<0.0 then	
    angl := angl+360.0;	
  theta := angl*pi180;	
  wx := wx-tran*sin(theta);	
  wz := wz+tran*cos(theta);	

The value of strip is the array position of the triangle strip over which your vehicle is 
passing. The values of start and finish are the search range in a loop to find the vertex in the 
strip horizontally closest to wx. A search is necessary since the vertices randomly deviated 
from an even spacing down the strip. Initialize the value of minx so that the first deltaX will 
be less that this sliding minumum.

  strip := round(-wz);	
  start := round(-wx)+34;	
  finish := start+5;	
  dec(start,5);	
  minx := 99999.9;	

Check for boundary conditions and search from start to finish for the minimum deltaX. Each 
time you find a candidate, set a new trial value for wy. The offset allows for the average 
distance of the ground below the zero level. The sign reversal allows for the fact that when 
you seem to move your vehicle in one direction, you are actually moving the scene in the 
opposite direction.

  if (finish<1) or (start>66) or (strip<-32) or (strip>32) then	
    wy := 0	
  else	
  for ix := start to finish do	
  with ter[strip,ix] do	
  begin	
    deltaX := abs(x-wx);	
    if deltaX<minx then	
    begin	
      minx := deltaX;	
      wy := -1.45-y;	
    end;	
  end; {else for}	
end;	

PAGE 390
 Save, compile, run, and go for a spin. You might want to take some Dramamine along.
Identifier Reference
Identifier			Description
GLboolean
An OpenGL type equivalent to Delphi ByteBool.
glCallList
An OpenGL command to execute a display list.
glEndList
An OpenGL command that concludes a series of OpenGL commands that go into a display 
list.
glNewList
An OpenGL command that starts a series of OpenGL commands to go into a display list.
glNormal
An OpenGL command to create and make current an OpenGL vector called a normal, which 
is used for lighting calculation.
gluCylinder
An OpenGL Utility library command to draw a cylinder.
gluDisk
An OpenGL Utility library command to draw a disk or circle, possibly with a hole in it.
gluDeleteQuadric
An OpenGL Utility library command that deletes a specified quadric object.
gluNewQuadric
An OpenGL Utility library command that creates a new quadric object.
gluPartialDisk
An OpenGL Utility library command to draw an arc or slice of a disk.
gluQuadricDrawStyle
An OpenGL Utility library command to set the drawing style of the given quadric object, so 
that a subsequent drawing command will render points, lines, or filled polygons.
gluQuadricNormals
An OpenGL Utility library command to set the kind of generation of normals during the 
drawing of a quadric. The command determines whether drawing generates no normals, a 
normal for each face, or a normal for each vertex.

PAGE 391
 
Identifier			Description
GLUquadricObj
An OpenGL pointer type. Pointers of this type should point to the memory region of a 
quadric object.
gluQuadricOrientation
An OpenGL Utility library command to determine whether generated normals should point 
inside or outside the quadric to be drawn.
gluQuadricTexture
An OpenGL Utility library command to determine whether or not generation of vertices in 
drawing a quadric should also generate texture coordinates.
gluSphere
An OpenGL Utility library command to draw a sphere.
GLU_FILL
An OpenGL Utility library constant to specify filled polygons in the drawing of a quadric.
GLU_FLAT
An OpenGL Utility library constant to specify face normals in the drawing of a quadric.
GLU_INSIDE
An OpenGL Utility library constant to specify generation of inward pointing normals in the 
drawing of a quadric.
GLU_LINE
An OpenGL Utility library constant to specify lines (like a wire frame) in the drawing of a 
quadric.
GLU_NONE
An OpenGL Utility library constant to specify no generation of normals in the drawing of a 
quadric.
GLU_OUTSIDE
An OpenGL Utility library constant to specify generation of outward pointing normals in the 
drawing of a quadric.
GLU_POINT
An OpenGL Utility library constant to specify points in the drawing of a quadric.
GLU_SILHOUETTE
An OpenGL Utility library constant like GL_LINES except that adjacent co-planar polygons 
will be consolidated by the elimination of shared edges.
GLU_SMOOTH
An OpenGL Utility library constant to specify vertex normals in the drawing of a quadric.

PAGE 392
 Summary

In this chapter you learned a few new commands and some techniques for using the 
commands you have learned in new ways, with a drive-through as the means of this 
learning. These example programs are, of course, just simple starting points; where you take 
them is limited only by your imagination. As a further stimulus, Figure 13-21 shows one of 
the vehicle simulations with a front or hood added.

Naturally, such an extension forward adds complication to the collision detection 
calculations. That, as they say, is left as an exercise for the reader.


PAGE 393
 

PAGE 394

