Sol's Graphics for Beginners

(ch20.cpp)

(prebuilt win32 exe)

20 - Simple 3d Transformations

(lastrevised 9. August 2005)

3d graphics is something I'd very much like to avoid in this tutorial as much as possible. I don't consider myself an expert in the subject, and there's plenty of material about 3d graphics out there (NeHe for instance). In any case, if you wanted to do 3d, you'd be better off using OpenGL or Direct3D anyway.

Anyway, it would be nice to visualize the ball's rolling somehow. I'm afraid this will eventually require quaternions and other fun stuff like that, depending on what kind of physics model the ball will end up having. But for now, let's do things in very, very simple manner.

Let's start from the chapter 17 skeleton one more time. Make a copy of some earlier project and replace the source code with the skeleton.

Go to 07 - Shall We Play a Game and copy-paste the drawcircle() and drawrect() functions into the source, above render().

First we'll need to define a vertex. Vertex is just a point in space. It has x, y and z coordinates. Copy-paste the following near the top of the source file (after the #defines):

// Vertex structure
struct vertex
{
  float x, y, z;
};

Next, we'll need to define some vertices. Add the following two tables after the vertex structure definition:

// Original vertices
vertex *gVtx;
// Transformed vertices
vertex *gRVtx;

We'll need space for both 'original' and 'transformed' coordinates. I'll explain this in a bit.

Paste the following into init():

gVtx = new vertex[100];
gRVtx = new vertex[100];

int i;
for (i = 0; i < 100; i++)
{
  gVtx[i].x = (rand() % 32768) - 16384.0f;
  gVtx[i].y = (rand() % 32768) - 16384.0f;
  gVtx[i].z = (rand() % 32768) - 16384.0f;
  float len = (float)sqrt(gVtx[i].x * gVtx[i].x + 
                          gVtx[i].y * gVtx[i].y + 
                          gVtx[i].z * gVtx[i].z);
  if (len != 0)
  {
    gVtx[i].x /= len;
    gVtx[i].y /= len;
    gVtx[i].z /= len;
  }
  }

We allocate the arrays for 100 vertices. For each vertex, we first assign the x, y and z variables random values from -16384 to 16384. Then, using three-dimensional version of the pythagoras' theorem, we calculate the distance from origo (0,0,0) to our random point.

Then, unless the length was zero (which is rather unlikely), we divide the coordinates by the length. After this we have 100 random positions on the surface of a sphere with a radius of 1.

Let's render the vertices so we can see what's happening. Find the 'rendering here' comment in render() and paste the following after it:

drawrect(0, 0, WIDTH, HEIGHT, 0);

drawcircle(WIDTH / 2, HEIGHT / 2, WIDTH / 4, 0x3f3f3f);

memcpy(gRVtx, gVtx, sizeof(vertex) * 100);

int i;

for (i = 0; i < 100; i++)
{
  int c = 0xffffff;
  if (gRVtx[i].z < 0)
    c = 0x7f7f7f;
  drawcircle((int)(gRVtx[i].x * (WIDTH / 4) + WIDTH / 2),
              (int)(gRVtx[i].y * (WIDTH / 4) + HEIGHT / 2),
              2, c);
}

Here we clear the screen with a drawrect, and then draw a dark grey circle in the center with a radius of WIDTH / 4. This circle will be our 'ball'.

Then we loop through the vertices and draw them on top of our circle. Since the vertices' coordinates will always be in the range of [-1..1], multiplying the coordinate values with our ball's radius will always render the vertices inside the ball. The vertices will be rendered in white, unless their Z coordinate value is below zero, in which case the color is mid-grey.

Compile and run.

The choise of colors we've made for the vertices and the ball result in an illusion of translucency, even though there's no blending involved.

Now that we have the basic set up done, let's go into transformations. Staying in 2d as long as possible, we'll start with z rotation. In our case, Z axis is considered to be the depth, so rotating around the z axis means 2d rotation around the origo.

Paste this above render():

void rotate_z(float angle)
{
  float ca = (float)cos(angle);
  float sa = (float)sin(angle);
  int i;
  for (i = 0; i < 100; i++)
  {
    float x = gRVtx[i].x * ca - gRVtx[i].y * sa;
    float y = gRVtx[i].x * sa + gRVtx[i].y * ca;
    gRVtx[i].x = x;
    gRVtx[i].y = y;
  }
}

Here we first calculate the sine and cosine values for the desired angle. For each vertex, we calculate x1 = x0 * cos(a) - y0 * sin(a), y1 = x0 * sin(a) + y0 * cos(a).

Consider a point in (1,0). The formulas simplify to x1=1cos(a)-0, y1=1sin(a)+0.

If you draw something in x=cos(a)*radius+middlePointX, y=sin(a)*radius+middlePointY, the object will rotate around the middle point at 'radius' distance. (You probably have already seen this in practise if you've followed this tutorial).

Finally, paste the following after the memcpy in render():

rotate_z(tick * 0.0005f);

Compile and run. The vertices should now be rotating around the center.

Now then, we have several reasons to have the 'original' versus 'rotated' data. This way we can control the exact orientation of the object relative to the original orientation. Consider if we wanted to render 50 copies of the same 3d object in different orientations! Accuracy of the floating point math is also an issue. If we were to constantly overwrite our rotated data, errors would eventually creep in.

Rotating around the Y and X axes works pretty much the same way as rotating around the Z axis. Paste these functions after the rotate_z one:

void rotate_y(float angle)
{
  float ca = (float)cos(angle);
  float sa = (float)sin(angle);
  int i;
  for (i = 0; i < 100; i++)
  {
    float z = gRVtx[i].z * ca - gRVtx[i].x * sa;
    float x = gRVtx[i].z * sa + gRVtx[i].x * ca;
    gRVtx[i].z = z;
    gRVtx[i].x = x;
  }
}

void rotate_x(float angle)
{
  float ca = (float)cos(angle);
  float sa = (float)sin(angle);
  int i;
  for (i = 0; i < 100; i++)
  {
    float y = gRVtx[i].y * ca - gRVtx[i].z * sa;
    float z = gRVtx[i].y * sa + gRVtx[i].z * ca;
    gRVtx[i].y = y;
    gRVtx[i].z = z;
  }
}

As you can see, the functions are very similar. Consider looking at the coordinate system from the direction of the y or x axis.

Next, find and replace the rotate_z call in render() with the following:

rotate_x(tick * 0.001f);
rotate_y(tick * 0.00025f);
rotate_z(tick * 0.0005f);

Compile and run again. Play with the values, remove some rotations, change the order of the rotations.

Once you're done playing, replace the rotations with these:

float rotz = tick * 0.0005f;
float roty = tick * 0.002f;

rotate_y(roty);
rotate_z(rotz);

When run, you'll see that the ball is rotating quickly around its 'y' axis and slowly around the 'z' axis. To mark the point towards which we're rolling, add the following lines after the loop in which we're rendering the vertices:

drawcircle((int)((WIDTH / 3) * cos(rotz) + WIDTH / 2),
            (int)((WIDTH / 3) * sin(rotz) + HEIGHT / 2), 6, 0xffffff);

Compile and run again. Now, what happens if you swap the order of the rotations? Can you figure out why?

That's all I'm going to cover this time. Note that we're not doing any perspective projection, but I don't think it'll be needed, as the game's ball will be very small in any case.

We won't be able to model any "spin" on the ball this way, but at least we get direction and the "rolling". If the player performs a 180 degree turn (by, say, colliding with a wall or something), the ball will snap around, which may end up looking bad, but it's hard to say at this point. To fix these issues we'd need to define the ball's rotation as a quaternion. (I'm quite likely wrong at this, and it's entirely possible to do everything with plain matrices, but in any case it can only get more complex from here).

For one pretty complete demosceneish 3d math tutorial, try to find a copy of 3dica.

Additional things to try:

  • Adjust the number of vertices.
  • Create different kinds of patterns with the vertices. For instance, try setting 'x' to sin(i*PI/50); and 'y' to cos(i*PI/50); and 'z' to 0. Another example is sin(i*PI/20), cos(i*PI/20), (i-50)/50.0f;
  • Can you optimize the rotations by combining them?
  • Adjust the vertex sizes depending on the z values.
  • Since growing the circles will make our lack of sorting obvious, sort the vertices by their z values and render in the sorted order.
  • Add perspective projection.

That's it for the effects for now. In part D we'll start plugging these effects to the game, along with other things.

Having problems? Improvement ideas? Just want to discuss this tutorial? Try the forums!

Any comments etc. can be emailed to me.