Home   Donators   Contact Us       

<< Previous Tutorial     Next Tutorial >>

Tutorial 9 - Order in chaos

Theory

A well designed 3D engine makes it easy for a 3D developer to build interactive 3D applications. It is also a great time saver to re-use a well tested 3D engine in different 3D projects.

Before we build the first components of your re-usable 3D engine, let's take one step back and have another look at one of the most basic 3D models, a cube.

From the image above it is easy to see that a basic cube has 6 faces... front, left, top, right, back and bottom. The right, back and bottom faces are hidden from view in the image. Each face requires 4 vertices to form a 4-sided polygon. If we quickly did the math, we realize that we need a way to store 6x4=24 vertices and 6 polygons in memory.

It is critical that a 3D engine uses the least amount of memory possible. The bigger your 3D scenes become, the more memory you need to store all the data. Let's see if we can somehow reduce the amount of items needed to represent a cube in memory.

When looking at the top left hand corner of the cube, we notice something interesting... 3 of the vertices share the same position in 3D space. If we use one vertex, instead of three, we can reduce the total number of vertices required to 8 vertices (see image on right). This results in a massive 67% saving in memory usage when storing vertices.

Besides for storing vertex and polygon information in memory, we also need to store the polygon normals for lighting calculations and the color information of the polygons. If you remember from Tutorial 3 - A splash of color, RGB colors are created from combining red, green and blue components.

A final piece of information that your first 3D engine will also need to store in memory, is the position (a vector) of a model in 3D space.

In this tutorial we will design and implement the following classes to store all the required information:

  • R3DTVector
  • R3DTColor
  • R3DTPolygon
  • R3DTModel

It will become more clear to you how these classes relates and interacts with each other, at the end of the tutorial. Once we've implemented the classes, all that remain is to write a rendering routine that can render any given R3DTModel object to an OpenGL surface.

This tutorial shows you how to write a very primitive 3D engine, for models built from colored polygons, but forms a very important foundation for future tutorials. As the tutorials progress we will continue to expand, improve and optimize the engine.

Tutorial Steps
Tutorial created with Real Studio 2011 Release 4.3.
1. Open Real Studio.
2. Choose the "Desktop" project template.
3. Add an OpenGLSurface control to Window1.
4. Resize and position OpenGLSurface1 to fill the whole form.
5. Tick the LockRight and LockBottom properties of OpenGLSurface1.
6. Add a Timer control to Window1.
7. Change the Period property of Timer1 from 1000 to 200.
8. Add a class named "R3DTColor" to the project. (Select the Project tab and then "Add Class")
9. Add a property named "Red" of type Double to the R3DTColor class. (On the Project tab double click the R3DTColor class and select "Add Property")
10. Repeat step 9 and add a property named "Green" of type Double to the R3DTColor class.
11. Repeat step 9 and add a property named "Blue" of type Double to the R3DTColor class.
12. Add a method named "Constructor" to the R3DTColor class with a parameter list of "initRed As Double, initGreen As Double, initBlue As Double".
13. Add the following code to R3DTColor.Constructor:
// initialize the color with the given RGB values

Red = initRed
Green = initGreen
Blue = initBlue
14. Add a class named "R3DTVector" to the project.
15. Add a property named "x" of type Double to the R3DTVector class.
16. Repeat step 15 and add a property named "y" of type Double to the R3DTVector class.
17. Repeat step 15 and add a property named "z" of type Double to the R3DTVector class.
18. Add a method named "Constructor" to the R3DTVector class with a parameter list of "initX As Double, initY As Double, initZ As Double".
19. Add the following code to R3DTVector.Constructor:
// initialize the vector with the given values

x = initX
y = initY
z = initZ
20. Add a class named "R3DTPolygon" to the project. (Select the Project tab and then "Add Class")
21. Add a property named "Normal" of type R3DTVector to the R3DTPolygon class.
22. Repeat step 21 and add a property named "SurfaceColor" of type R3DTColor to the R3DTPolygon class.
23. Repeat step 21 and add a property array named "VertexIndex()" of type Integer to the R3DTPolygon class.
24. Add a method named "Constructor" to the R3DTPolygon class with a parameter list of "initNorm As R3DTVector, initColor As R3DTColor".
25. Add the following code to R3DTPolygon.Constructor:
// initialize the normal with the given value

Normal = initNorm

// initialize the surface color with the given value

SurfaceColor = initColor
26. Add a class named "R3DTModel" to the project.
27. Add a property named "Position" of type R3DTVector to the R3DTModel class.
28. Repeat step 27 and add a property array named "Vertex()" of type R3DTVector to the R3DTModel class.
29. Repeat step 27 and add a property array named "Polygon()" of type R3DTPolygon to the R3DTModel class.
30. Add a method named "Constructor" to the R3DTModel class with a parameter list of "initPos As R3DTVector".
31. Add the following code to R3DTModel.Constructor:
// initialize the model with the given value

Position = initPos
32. Add a method named "AppendVertex" to the R3DTModel class with a parameter list of "x As Double, y As Double, z As Double".
33. Add the following code to R3DTModel.AppendVertex:
// instantiate a new vector with the given values, and append it to the vertex array

Vertex.Append new R3DTVector(x, y, z)
34. Add a method named "SetPosition" to the R3DTModel class with a parameter list of "newX As Double, newY As Double, newZ As Double".
35. Add the following code to R3DTModel.SetPosition:
' assign the new position values to the position object

Position.x = newX 
Position.y = newY
Position.z = newZ
36. Add a method named "SetColor" to the R3DTModel class with a parameter list of "newRed As Double, newGreen As Double, newBlue As Double".
37. Add the following code to R3DTModel.SetColor:
Dim i as Integer ' loop variable
Dim poly As R3DTPolygon ' temporary polygon object

' loop through all the polygons and update their colors with the new values

for i = 0 to Polygon.Ubound

  poly = Polygon(i) ' get next polygon

  ' assign the color values

  poly.SurfaceColor.Red = newRed
  poly.SurfaceColor.Green = newGreen
  poly.SurfaceColor.Blue = newBlue

next i
38. Add a module named "R3DT_Models" to the project. (Select the Project tab and then "Add Module")
39. Add a method named "R3DT_GetModel_Cube" to the R3DT_Models module. (On the Project tab double click the R3DT_Models module and select "Add Method")
40. Set the return type of R3DT_Models.R3DT_GetModel_Cube to R3DTModel.
41. Add the following code to R3DT_Models.R3DT_GetModel_Cube:
Dim model As new R3DTModel(new R3DTVector(0, 0, 0)) // new model at origin, to be returned to caller
Dim poly As R3DTPolygon // temporary object to hold the polygons we create

// first we add all the vertices we need to create a 3D cube

// front vertices

model.AppendVertex 1, 1, 1 // vertex 0
model.AppendVertex -1, 1, 1 // vertex 1
model.AppendVertex -1, -1, 1 //vertex 2
model.AppendVertex 1, -1, 1 // vertex 3

// back vertices

model.AppendVertex 1, 1, -1 // vertex 4
model.AppendVertex -1, 1, -1 // vertex 5
model.AppendVertex -1, -1, -1 //vertex 6
model.AppendVertex 1, -1, -1 // vertex 7

// now we build polygons with these vertices and add them to our model
// IMPORTANT: remember to always give the vertices of polygons in an anti-clockwise direction

' front polygon

poly = new R3DTPolygon(new R3DTVector(0, 0, 1), new R3DTColor(1, 1, 1)) // white polygon with a normal of (0, 0, 1)
poly.VertexIndex.Append 0 // point to vertex (1, 1, 1)
poly.VertexIndex.Append 1 // point to vertex (-1, 1, 1)
poly.VertexIndex.Append 2 // point to vertex (-1, -1, 1)
poly.VertexIndex.Append 3 // point to vertex (1, -1, 1)
model.Polygon.Append poly // add the polygon to our model

// right polygon

poly = new R3DTPolygon(new R3DTVector(1, 0, 0), new R3DTColor(1, 1, 1)) // white polygon with a normal of (1, 0, 0)
poly.VertexIndex.Append 0 // point to vertex (1, 1, 1)
poly.VertexIndex.Append 3 // point to vertex (1, -1, 1)
poly.VertexIndex.Append 7 // point to vertex (1, -1, -1)
poly.VertexIndex.Append 4 // point to vertex (1, 1, -1)
model.Polygon.Append poly // add the polygon to our model

// left polygon

poly = new R3DTPolygon(new R3DTVector(-1, 0, 0), new R3DTColor(1, 1, 1)) // white polygon with a normal of (-1, 0, 0)
poly.VertexIndex.Append 1 // point to vertex (-1, 1, 1)
poly.VertexIndex.Append 5 // point to vertex (-1, 1, -1)
poly.VertexIndex.Append 6 // point to vertex (-1, -1, -1)
poly.VertexIndex.Append 2 // point to vertex (-1, -1, 1)
model.Polygon.Append poly // add the polygon to our model

// back polygon

poly = new R3DTPolygon(new R3DTVector(0, 0, -1), new R3DTColor(1, 1, 1)) // white polygon with a normal of (-1, 0, 0)
poly.VertexIndex.Append 4 // point to vertex (1, 1, -1)
poly.VertexIndex.Append 7 // point to vertex (1, -1, -1)
poly.VertexIndex.Append 6 // point to vertex (-1, -1, -1)
poly.VertexIndex.Append 5 // point to vertex (-1, 1, -1)
model.Polygon.Append poly // add the polygon to our model

// top polygon

poly = new R3DTPolygon(new R3DTVector(0, 1, 0), new R3DTColor(1, 1, 1)) // white polygon with a normal of (0, 1, 0)
poly.VertexIndex.Append 0 // point to vertex (1, 1, 1)
poly.VertexIndex.Append 4 // point to vertex (1, 1, -1)
poly.VertexIndex.Append 5 // point to vertex (-1, 1, -1)
poly.VertexIndex.Append 1 // point to vertex (-1, 1, 1)
model.Polygon.Append poly // add the polygon to our model

// bottom polygon

poly = new R3DTPolygon(new R3DTVector(0, -1, 0), new R3DTColor(1, 1, 1)) // white polygon with a normal of (0, -1, 0)
poly.VertexIndex.Append 3 // point to vertex (1, -1, 1)
poly.VertexIndex.Append 2 // point to vertex (-1, -1, 1)
poly.VertexIndex.Append 6 // point to vertex (-1, -1, -1)
poly.VertexIndex.Append 7 // point to vertex (1, -1, -1)
model.Polygon.Append poly // add the polygon to our model

return model // return the model to the caller
42. Add a module named "R3DT_OpenGL" to the project.
43. Add a method named "R3DT_RenderModel" to the R3DT_OpenGL module with a parameter list of "model As R3DTModel".
44. Add the following code to R3DT_OpenGL.R3DT_RenderModel:
Dim i As Integer // loop variable
Dim j As Integer // loop variable
Dim poly As R3DTPolygon ' temporay polygon used for drawing
Dim vertex As R3DTVector ' temporary vertex used for drawing

OpenGL.glPushMatrix ' save the current matrix

' set the position of the model

OpenGL.glTranslatef model.Position.x, model.Position.y, model.Position.z

' loop through all the polygons of the model and draw each one

for i = 0 to model.Polygon.Ubound

  ' get the next polygon from our model

  poly = model.Polygon(i)

  ' set the color of the polygon

  OpenGL.glColor3d poly.SurfaceColor.Red, poly.SurfaceColor.Green, poly.SurfaceColor.Blue

  ' set the normal of the polygon

  OpenGL.glNormal3d poly.Normal.x, poly.Normal.y, poly.Normal.z

  ' draw the polygon

  OpenGL.glBegin OpenGL.GL_POLYGON ' start drawing of polygon

  ' loop through all the vertices of the current polgon

  for j = 0 to poly.VertexIndex.Ubound
  
    ' get the next vertex of the polygon
  
    vertex = model.Vertex(poly.VertexIndex(j))
  
    ' add the vertex to the OpenGL vertex list for the current polygon
  
    OpenGL.glVertex3d vertex.x, vertex.y, vertex.z
  
  next j

  OpenGL.glEnd ' end drawing of polygon

next i 

OpenGL.glPopMatrix ' restore the matrix we saved before rendering the model
45. Add the following code to the Paint event of Window1:
' refresh the OpenGL surface

OpenGLSurface1.Render
46. Add the following code to the Open event of OpenGLSurface1:
' make sure only back faces are culled and enable culling

OpenGL.glCullFace OpenGL.GL_BACK
OpenGL.glEnable OpenGL.GL_CULL_FACE

' instantiate memory blocks used to configure our light with

Dim light_position As new MemoryBlock(16)
Dim light_ambience As new MemoryBlock(16)
Dim light_diffANDspec As new MemoryBlock(16)

' define position of the light

light_position.SingleValue(0) = 0.0
light_position.SingleValue(4) = 0.0
light_position.SingleValue(8) = 1.0
light_position.SingleValue(12) = 0.0

' define ambience of the light

light_ambience.SingleValue(0) = 0.0
light_ambience.SingleValue(4) = 0.0
light_ambience.SingleValue(8) = 0.0
light_ambience.SingleValue(12) = 1.0

' define diffuse and specular of the light

light_diffANDspec.SingleValue(0) = 1.0
light_diffANDspec.SingleValue(4) = 1.0
light_diffANDspec.SingleValue(8) = 1.0
light_diffANDspec.SingleValue(12) = 1.0

' apply light settings

OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_POSITION, light_position ' set position
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_AMBIENT, light_ambience ' set ambience
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_DIFFUSE, light_diffANDspec ' set diffuse
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_SPECULAR, light_diffANDspec ' set specular

' enable our light

OpenGL.glEnable OpenGL.GL_LIGHT0

' enable lighing

OpenGL.glEnable OpenGL.GL_LIGHTING

' enable overwriting of material properties with vertex colors

OpenGL.glEnable OpenGL.GL_COLOR_MATERIAL
47. Add a property array named "MemModels()" of type R3DTModel to Window1.
48. Add the following code to the Resized event of OpenGLSurface1:
' set the viewport rectangle

OpenGL.glViewport 0, 0, OpenGLSurface1.Width, OpenGLSurface1.Height

' set up the perspective projection settings

OpenGL.glMatrixMode OpenGL.GL_PROJECTION
OpenGL.glLoadIdentity
OpenGL.gluPerspective 60.0, OpenGLSurface1.Width / OpenGLSurface1.Height, 1, 100.0

' select and reset the modelview matrix

OpenGL.glMatrixMode OpenGL.GL_MODELVIEW
OpenGL.glLoadIdentity
49. Add the following code to the Render event of OpenGLSurface1:
  
  Dim i As Integer ' loop variable
  
  OpenGL.glPushMatrix ' save matrix
  
  ' clear the background
  
  OpenGL.glClearColor(0, 0, 0, 1)
  OpenGL.glClear(OpenGL.GL_COLOR_BUFFER_BIT)
  
  ' move back a bit so that we can see the object
  
  OpenGL.glTranslatef 0.0, 0.0, -5.0
  
  ' loop through all the models
  
  for i = 0 to MemModels.Ubound
    
    ' render the next model
    
    R3DT_RenderModel MemModels(i)
    
  next i
  
  OpenGL.glPopMatrix ' restore matrix
50. Add the following code to the Action event of Timer1:
Dim i As Integer ' loop variable
Dim xStep As Double ' x step variable
Dim yStep As Double ' y step variable
Dim zStep As Double ' z step variable
Dim model As R3DTModel ' temporary model to calculate new position
Dim rnd as New Random ' object to generate random numbers with

' move all the cubes in memory in random directions

for i = 0 to MemModels.Ubound

  ' calculate random x, y and z move steps

  xStep = (rnd.InRange(0, 2) - 1) / 5 ' generate a random x step
  yStep = (rnd.InRange(0, 2) - 1) / 5 ' generate a random y step
  zStep = (rnd.InRange(0, 2) - 1) ' generate a random z  step

  ' get the next model

  model = MemModels(i)

  ' move the current model with the random steps

  model.Position.x = model.Position.x + xStep ' take the x step
  model.Position.y = model.Position.y + yStep ' take the y step
  model.Position.z = model.Position.z + zStep ' take the z step

next i 

OpenGLSurface1.Render  ' refresh the OpenGL surface
51. Add the following code to the Open event of Window1:
Dim model As R3DTModel ' tempory model object

' create some models (cubes) in memory

' red cube at position (7, 6, -15)

model = R3DT_GetModel_Cube ' instantiate a new cube model
model.SetPosition 7, 6, -15 ' set the position of the cube
model.SetColor 1, 0, 0 ' red
MemModels.Append model ' add the model to the model array

' green cube at position (-5, 2, -8)

model = R3DT_GetModel_Cube ' instantiate a new cube model
model.SetPosition -5, 2, -8 ' set the position of the cube
model.SetColor 0, 1, 0 ' green
MemModels.Append model ' add the model to the model array

' blue cube at position (-3, -2, -6)

model = R3DT_GetModel_Cube ' instantiate a new cube model
model.SetPosition -3, -2, -6 ' set the position of the cube
model.SetColor 0, 0, 1 ' blue
MemModels.Append model ' add the model to the model array

' yellow cube at position (3, -2, -5)

model = R3DT_GetModel_Cube ' instantiate a new cube model
model.SetPosition 3, -2, -5 ' set the position of the cube
model.SetColor 1, 1, 0 ' yellow
MemModels.Append model ' add the model to the model array
52. Save and run the project.

Code Analysis

This tutorial is a lot to take in at first, but an important step to developing serious 3D applications.

Not all the code in this tutorial is part of the 3D engine, and we are only concerned with the 4 classes R3DTVector, R3DTColor, R3DTPolygon and R3DTModel, as well as the two modules R3DT_Models and R3DT_OpenGL.


R3DTVector - Used to store vector information.
x  Stores the x value of the vector.
y  Stores the y value of the vector.
z  Stores the z value of the vector.
Constructor(initX, initY, initZ)  Initializes the x, y and z values of the vector, when a new R3DTVector object is instantiated.


R3DTColor - Used to store color information.
Blue  Stores the blue component of the color.
Green  Stores the green component of the color.
Red  Stores the red component of the color.
Constructor(initRed, initGreen, initBlue)  Initializes the red, green and blue values (RGB) of the color, when a new R3DTColor object is instantiated.


R3DTPolygon - Used to represent a single polygon.
Normal  Stores the normal vector of the polygon.
SurfaceColor  Stores the color information of the polygon.
VertexIndex()  Stores index pointers to a vertex array stored in a parent R3DTModel object. This R3DTModel parent object stores all the vertices used by all the polygons that forms part of the final 3D model.
Constructor(initNorm, initColor)  Initializes the normal vector and surface color of the polygon, when a new R3DTPolygon object is instantiated.


R3DTModel - Used to represent a 3D model.
Polygon()  An array that stores all the polygons of the model.
Position  A vector storing the (x,y,z) position of the model in 3D space.
Vertex()  An array that stores all the vertices of the model. The VertexIndex() property array in the R3DTPolygon class points to this array.
Constructor(initPos)  Initializes the model at a position in 3D space, when a new R3DTModel object is instantiated.
AppendVertex(x, y, z)  Appends a new vertex to the vertex array.
SetColor(newRed, newGreen, newBlue)  Changes all the colors of all the polygons in the model to a new color.
SetPosition(newX, newY, newZ)  Moves the model to a new position in 3D space.


The diagram below visually illustrates how the classes interact with each other.

The R3DT_Models module holds methods that can be used to create primitive models on the fly. These functions are not part of the functionality of the 3D engine, and merely acts as helper functions for testing. R3DT_GetModel_Cube returns a cube model instance of type R3DTModel.

The functions contained in the R3DT_OpenGL module do however play an important role in the 3D engine. Currently it only contains one method, R3DT_RenderModel, that accepts a single R3DTModel object as a parameter. The method is capable of drawing any given R3DTModel object. This is the "time-saving" part of the engine. You can now build up any model as a R3DTModel object, and then draw it to the OpenGL surface, by passing it to R3DT_RenderModel.

The best way to learn and become comfortable with the classes is to experiment with the code. See if you can add a purple cube to the existing array of cube models (Tip: Look at the code in the Window1.Open event). If you are really adventurous, why not try adding a model other than a cube.

Project Downloads

<< Previous Tutorial     Next Tutorial >>


 
All the content on Real 3D Tutorials, with the exception of the SyntaxHighlighter which is licensed under the MIT License, is provided to the public domain and everyone is free to use, modify, republish, sell or give away this work without prior consent from anybody. Content is provided without warranty of any kind. Under no circumstances shall the author(s) or contributor(s) be liable for damages resulting directly or indirectly from the use or non-use of the content.
Should you find the content useful and would like to make a contribution, you can show your support by making a donation.