domingo, 9 de junio de 2013

Let’s Build a 3D Graphics Engine: Spaces and Culling

This entry is part 3 of 4 in the series Let's Build a 3D Graphics Engine

Welcome! This is the third part of our series on 3D graphics engines. If you made it this far into the series, you’ll be glad to know that this piece will be much lighter on the math aspect of 3D engines, and instead will take a focus on more practical things – in particular, adding a camera and a basic rendering system.

Tip: If you haven’t read the first two parts yet, I highly suggest that you do before continuing.


Recap

First off, let’s take a look at the classes that we’ve created so far:

  Point Class  {          Variables:                  num tuple[3]; //(x,y,z)            Operators:                  Point AddVectorToPoint(Vector);                  Point SubtractVectorFromPoint(Vector);                  Vector SubtractPointFromPoint(Point);                  Null SetPointToPoint(Point); // move point to specified point            Functions:                  drawPoint; //draw a point at its position tuple  }    Vector Class  {          Variables:                  num tuple[3]; //(x,y,z)            Operators:                  Vector AddVectorToVector(Vector);                  Vector SubtractVectorFromVector(Vector);                  Vector RotateXY(degrees);                  Vector RotateYZ(degrees);                  Vector RotateXZ(degrees);                  Vector Scale(s0,s1,s2); //params: scaling along each axis  }  

Using these two classes on their own has proven a little messy thus far, and drawing every possible point can drain your system’s memory fairly quickly. To solve these problems, we are going to introduce a new class into our game engine: the camera.

Our camera is going to be where all our rendering happens, exclusively; its going to cull all of our objects to the screen, and it is also going to manage a list of all of our points.

But before we can get to all of that, we must first talk a little bit about culling.


London Culling

Culling, by definition, is the selection of objects from a larger group of objects. For our game engine, the small selection that we take will be the points that we want to draw to the screen. The larger group of objects will be every point that exists.

Doing this drastically reduces your engine’s drain on a system’s memory, by drawing only what a player is actually able to see, rather than an entire world’s worth of points. In our engine, we are going to do this by setting parameters for a view space.

Our view space will be defined across all three of the traditional axes: x, y, and z. Its x definition will consist of everything between the window’s left and right boundaries, its y definition will consist of everything between the window’s top and bottom boundaries, and its z definition will be between 0 (where the camera is set) and our player’s view distance (for our demonstration, we will be using an arbitrary value of 100).

Prior to drawing a point, our camera class is going to check to see whether that point lies within our view space. If it does, then the point will be drawn; otherwise, it will not.


Can We Get Some Cameras in Here?

With that basic understanding of culling, we can deduce that our class will look like this, so far:

  Camera Class  {  Vars:      int minX, maxX; //minimum and maximum bounds of X      int minY, maxY; //minimum and maximum bounds of Y      int minZ, maxZ; //minimum and maximum bounds of Z  }  

We are also going to have our camera handle all of the rendering for our engine as well. Depending on the engine, you will find that renderers are often separated from the camera systems. This is typically done to keep the systems encapsulated nicely, since – depending on the scope of your engine – the two could get quite messy if kept together. For our purposes, though, it will be simpler to treat them as one.

First, we’re going to want a function that can be called externally from the class that will draw the scene. This function will cycle through each of the points that exist, compare them to the camera’s culling parameters, and draw them if applicable.

ViewFrustum
Source: http://en.wikipedia.org/wiki/File:ViewFrustum.svg

Tip: If you wanted to separate your camera system from your renderer, you could simply create a Renderer class, have the camera system cull the points, store the ones to be drawn in an array, and then send that array to the draw() function of your renderer.


Point Management

The final piece of our camera class is going to be its point management system. Depending on the programming language you are using, this could just be a simple array of all of the objects that can be drawn (we will be handling more than just points in later parts). Alternatively, you may have to use the language’s default object parent class. If you are super unlucky, you will have to create your own object parent class and have each drawable class (so far only points) be a child of that class.

After adding that into the class, a basic overview of our camera would look like this:

  Camera Class  {      Vars:          int minX, maxX; //minimum and maximum bounds of X          int minY, maxY; //minimum and maximum bounds of Y          int minZ, maxZ; //minimum and maximum bounds of Z          array objectsInWorld; //an array of all existent objects      Functions:          null drawScene(); //draws all needed objects to the screen, does not return anything  }  

With these additions, let’s improve a little upon the program that we made last time.


Bigger and Better Things

We are going to create a simple point drawing program, with the sample program that we created last time as a starting point.

In this iteration of the program, we are going to add in the use of our new camera class. When the D key is pressed, the program will redraw the screen without culling, displaying the number of objects that were rendered in the top right-hand corner of the screen. When the C key is pressed, the program will redraw the screen with culling, also displaying the number of rendered objects.

Let’s take a look at the code:

  main{          //setup for your favorite Graphics API here          //setup for keyboard input (may not be required) here    var camera = new Camera(); //create an instance of the camera class  camera.objectsInWorld[100]; //create 100 object spaces within the camera's array    //set the camera's view space  camera.minX = 0;  camera.maxX = screenWidth;  camera.minY = 0;  camera.maxY = screenHeight;  camera.minZ = 0;  camera.maxZ = 100;            for(int x = 0; x < camera.objectsInWorld.length; x++)          {                  //Set its location to a random point on the screen                  camera.objectsInWorld[x].tuple = [random(-200,1000), random(-200,1000), random(-100,200));          }            function redrawScreenWithoutCulling() //this function clears the screen and then draws all of the points          {                  ClearTheScreen();  //use your Graphics API's clear screen function                  for(int x = 0; x < camera.objectsInWorld.length; x++)                  {                          camera.objectsInWorld[x].drawPoint();   //draw the current point to the screen                  }          }            while(esc != pressed)  // the main loop          {                  if(key('d') == pressed)                  {                          redrawScreenWithoutCulling();                  }                  if(key('c') == pressed)                  {                          camera.drawScene();                  }                  if(key('a') == pressed)                  {                          Point origin = new Point(0,0,0);                          Vector tempVector;                          for(int x = 0; x < camera.objectsInWorld.length; x++)                          {                                  //store the current vector address for the point, and set the point                                  tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin);                                  //reset the point so that the scaled vector can be added                                  camera.objectsInWorld[x].setPointToPoint(origin);                                  //scale the vector and set the point to its new, scaled location                                  camera.objectsInWorld[x].addVectorToPoint(tempVector.scale(0.5,0.5,0.5));                          }                  }                  if(key('s') == pressed)                  {                          Point origin = new Point(0,0,0);  //create the space's origin as a point                          Vector tempVector;                          for(int x = 0; x < camera.objectsInWorld.length; x++)                          {                                  //store the current vector address for the point, and set the point                                  tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin);                                  //reset the point so that the scaled vector can be added                                  camera.objectsInWorld[x].setPointToPoint(origin);                                  //scale the vector and set the point to its new, scaled location                                  camera.objectsInWorld[x].addVectorToPoint(tempVector.scale(2.0,2.0,2.0));                          }                  }                  if(key('r') == pressed)                  {                          Point origin = new Point(0,0,0);  //create the space's origin as a point                          Vector tempVector;                          for(int x = 0; x < camera.objectsInWorld.length; x++)                          {                                  //store the current vector address for the point, and set the point                                  tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin);                                  //reset the point so that the scaled vector can be added                                  camera.objectsInWorld[x].setPointToPoint(origin);                                  //scale the vector and set the point to its new, scaled location                                  camera.objectsInWorld[x].addVectorToPoint(tempVector.rotateXY(15));                          }                  }          }  }  

Now you can see, first-hand, the power of culling! Do note that if you are looking through the sample code, some things are done a bit differently in order to make the demos more web-friendly. (You can check out my simple demo here.)


Conclusion

With a camera and rendering system under your belt, you can technically say that you’ve created a 3D game engine! It may not be overly impressive just yet, but it’s on its way.

In our next article, we’ll be looking at adding some geometric shapes to our engine (namely line segments and circles), and we will talk about the algorithms that can be used to fit their equations to the pixels of a screen.



No hay comentarios:

Publicar un comentario