Using IwGx to render 3D graphics

Remember when we were looking at 2D graphics rendering in Chapter 2, Resource Management and 2D Graphics Rendering, I said we would be using IwGx because it would make the transition to rendering 3D graphics that much easier. Now's the time to see if my claim was true!

In this section, we shall look at how we can implement the 3D equivalent of the "Hello World" program—a spinning cube.

Preparing IwGx for 3D rendering

As with 2D rendering, the very first thing we need to do is initialize the IwGx API by calling IwGxInit, and of course we should call IwGxTerminate at the end of our program.

With IwGx ready to go we next need to set up our projection. We're going to be using a perspective projection, so we need to be able to specify the perspective multiplier value that we want to use. The code to do this is as follows:

IwGxSetPerspMul((float) IwGxGetScreenWidth() * 0.5f);

This line of code sets the perspective multiplier up, to provide a 90 degree field of view. See the section Converting Between Coordinate Systems earlier in this chapter for more information on how to calculate the required perspective multiplier value.

Next we have to set the far and near clipping planes' distances. For our demo purposes we'll choose a value of 10 for the near plane and 1000 for the far plane; these values are set as follows:

IwGxSetFarZNearZ(1000.0f, 10.0f);

These values are in view space units and can be set to any value greater than zero (the far value should be greater than the near value too!) that works well for the needs of our game. Normally it is the far clip distance that is most important, as it needs to be set far enough out that our world is rendered satisfactorily, but not so far that the frame rate suffers because we are rendering too much.

Note

You may be wondering why these numbers have been written as 10.0f and not just 10 or 10.0? The reason is to ensure that the compiler treats these values as a single precision float value. The latter two forms will both be interpreted as a double and this can lead to a time consuming conversion from double to float.

Setting lighting information

In order to make our spinning cube look a little more attention grabbing, we'll set up some lights so that as the cube spins its faces change color accordingly. The lighting support provided by Marmalade may look a little limited, but is generally adequate for most mobile games' needs.

Marmalade only allows us to define a single ambient light and a single diffuse light. Let's start by setting the global ambient lighting value.

The first function we call is IwGxSetLightType, which takes an ID number to identify the light we wish to modify and a definition describing the type of light we are specifying. Presumably this API has been chosen so that Marmalade can easily be made to support more lights in the future, but for now the ID number can only be zero or one, and the light type must be one of IW_GX_LIGHT_AMBIENT, IW_GX_LIGHT_DIFFUSE, or IW_GX_LIGHT_UNUSED. The latter value can be used to disable the light.

With the type of light taken care of, we set the color of light using the function call IwGxSetLightCol. There are two versions of this function. Both take the ID of the light we wish to modify, but the RGB color of the light can either be specified as three uint8 values for red, green, and blue, or a pointer to a CIwColour instance can be supplied instead.

The following code sets the light with ID zero to be an ambient light with a mid-grey color:

IwGxSetLightType(0, IW_GX_LIGHT_AMBIENT);
IwGxSetLightCol(0, 128, 128, 128);

Now let's create a diffuse light with a specular highlight. We'll need two additional functions to do this, IwGxSetLightSpecularCol to set the color of the specular highlight, and IwGxSetLightDirn to set the direction in which the light is pointing. The direction is specified as a unit vector in terms of world space coordinates. Here's some code to illustrate this:

CIwFVec3 lLightDir(1000.0f, 0.0f, 1000.0f);
lLightDir.Normalise();
 
IwGxSetLightType(1, IW_GX_LIGHT_DIFFUSE);
IwGxSetLightCol(1, 128, 128, 128);
IwGxSetLightSpecularCol(1, 200, 200, 200, 255);
IwGxSetLightDirn(1, &lLightDir);

This code snippet sets up the light ID one to be a diffuse light with mid-grey intensity and a brighter grey specular highlight. The light is pointing at a 45 degree angle between the x and z axes of the world.

Our lights have now been initialized, so all that is left to do is let Marmalade know we want to switch them on! There are a number of functions available to allow us to do this. We can either use IwGxLightingOn and IwGxLightingOff to enable or disable all the initialized light sources, or we can enable each part of the lighting model independently. The following example code disables emissive lighting but enables ambient, diffuse, and specular lighting:

IwGxLightingAmbient(true);
IwGxLightingDiffuse(true);
IwGxLightingEmissive(false);
IwGxLightingSpecular(true);

Since we are using specular lighting, there is one more thing to do. The material that is used to render our polygons must have a specular color and power specified. The material's specular color is used to modulate the global specular color, while the power value indicates how close the vertex normal must be to the light direction for the specular highlight to kick in. The power value is a uint8 value and only very low values (that is, less than 8) produce notable differences in the rendered effect. Here is the code to illustrate this:

CIwMaterial* lpMaterial = new CIwMaterial;
lpMaterial->SetColSpecular(255, 255, 255);
lpMaterial->SetSpecularPower(3);

Note

The previous examples are just making use of Marmalade's built-in lighting model since it is easy to use and works well enough for most needs. However, there is absolutely no reason we have to use this lighting model, as there is nothing stopping us from generating our own color stream using whatever lighting algorithm we want to use. Alternatively we could employ OpenGL ES 2.0 shaders, although discussion of this particular topic is beyond the scope of this book.

Model data for the cube

We're going to render a lit cube with a different color on each face, so we need to provide some data streams for the vertices, colors, and normals, and an index stream to show how this data should be interpreted by the rendering engine. Since we are not using textures in this example, there is no need to provide a UV stream.

We also want to be as efficient as possible in our drawing, so our aim is to draw the entire cube with just a single call to IwGxDrawPrims. To do so we'll need to have three copies of each vertex (one for each face that the vertex is part of) so we can assign different colors and normals to it, and we'll also need to specify some degenerate triangles in our index stream to join all the faces together into one big triangle strip.

Let's start with the vertex stream. We allocate an array of CIwFVec3 and initialize it with the vertex data. The cube pivot point will be dead center, so all the vertex coordinates will have the same magnitude.

const uint32 lVertexCount = 24;
CIwFVec3* v = new CIwFVec3[lVertexCount];
v[0].x = 100.0f;    v[0].y = -100.0f;  v[0].z = -100.0f;
v[1].x = -100.0f;   v[1].y = -100.0f;  v[1].z = -100.0f;
v[2].x = 100.0f;    v[2].y = 100.0f;   v[2].z = -100.0f;
v[3].x = -100.0f;   v[3].y = 100.0f;   v[3].z = -100.0f;
v[4].x = 100.0f;    v[4].y = -100.0f;   v[4].z = 100.0f;
v[5].x = 100.0f;    v[5].y = -100.0f;   v[5].z = -100.0f;
v[6].x = 100.0f;    v[6].y = 100.0f;    v[6].z = 100.0f;
v[7].x = 100.0f;    v[7].y = 100.0f;    v[7].z = -100.0f;
v[8].x = 100.0f;    v[8].y = -100.0f;   v[8].z = 100.0f;
v[9].x = 100.0f;    v[9].y = 100.0f;    v[9].z = 100.0f;
v[10].x = -100.0f;  v[10].y = -100.0f;  v[10].z = 100.0f;
v[11].x = -100.0f;  v[11].y = 100.0f;   v[11].z = 100.0f;
v[12].x = -100.0f;  v[12].y = -100.0f;  v[12].z = 100.0f;
v[13].x = -100.0f;  v[13].y = 100.0f;   v[13].z = 100.0f;
v[14].x = -100.0f;  v[14].y = -100.0f;  v[14].z = -100.0f;
v[15].x = -100.0f;  v[15].y = 100.0f;   v[15].z = -100.0f;
v[16].x = -100.0f;  v[16].y = 100.0f;   v[16].z = -100.0f;
v[17].x = -100.0f;  v[17].y = 100.0f;   v[17].z = 100.0f;
v[18].x = 100.0f;   v[18].y = 100.0f;   v[18].z = -100.0f;
v[19].x = 100.0f;   v[19].y = 100.0f;   v[19].z = 100.0f;
v[20].x = -100.0f;  v[20].y = -100.0f;  v[20].z = -100.0f;
v[21].x = -100.0f;  v[21].y = -100.0f;  v[21].z = 100.0f;
v[22].x = 100.0f;   v[22].y = -100.0f;  v[22].z = -100.0f;
v[23].x = 100.0f;   v[23].y = -100.0f;  v[23].z = 100.0f;

The vertices have been ordered a face at a time, so the first four vertices form the front of the cube, the next four the right hand face, and so on. You are free to specify the order however you see fit, since ultimately it will be the index stream that determines how the individual triangles will be rendered.

Now we'll create the normal stream. Normals in Marmalade are also specified as instances of CIwFVec3, and they are expected to have unit length. This means that the magnitude of the vector should be one. Here's a code snippet that will do the job:

CIwFVec3* n = new CIwFVec3[lVertexCount];
n[0].x = 0.0f;      n[0].y = 0.0f;    n[0].z = -1.0f;
n[1].x = 0.0f;      n[1].y = 0.0f;    n[1].z = -1.0f;
n[2].x = 0.0f;      n[2].y = 0.0f;    n[2].z = -1.0f;
n[3].x = 0.0f;      n[3].y = 0.0f;    n[3].z = -1.0f;
n[4].x = 1.0f;      n[4].y = 0.0f;    n[4].z = 0.0f;
n[5].x = 1.0f;      n[5].y = 0.0f;    n[5].z = 0.0f;
n[6].x = 1.0f;      n[6].y = 0.0f;    n[6].z = 0.0f;
n[7].x = 1.0f;      n[7].y = 0.0f;    n[7].z = 0.0f;
n[8].x = 0.0f;      n[8].y = 0.0f;    n[8].z = 1.0f;
n[9].x = 0.0f;      n[9].y = 0.0f;    n[9].z = 1.0f;
n[10].x = 0.0f;     n[10].y = 0.0f;   n[10].z = 1.0f;
n[11].x = 0.0f;     n[11].y = 0.0f;   n[11].z = 1.0f;
n[12].x = -1.0f;    n[12].y = 0.0f;   n[12].z = 0.0f;
n[13].x = -1.0f;    n[13].y = 0.0f;    n[13].z = 0.0f;
n[14].x = -1.0f;    n[14].y = 0.0f;    n[14].z = 0.0f;
n[15].x = -1.0f;    n[15].y = 0.0f;    n[15].z = 0.0f;
n[16].x = 0.0f;     n[16].y = 1.0f;    n[16].z = 0.0f;
n[17].x = 0.0f;     n[17].y = 1.0f;    n[17].z = 0.0f;
n[18].x = 0.0f;     n[18].y = 1.0f;    n[18].z = 0.0f;
n[19].x = 0.0f;     n[19].y = 1.0f;    n[19].z = 0.0f;
n[20].x = 0.0f;     n[20].y = -1.0f;   n[20].z = 0.0f;
n[21].x = 0.0f;     n[21].y = -1.0f;   n[21].z = 0.0f;
n[22].x = 0.0f;     n[22].y = -1.0f;   n[22].z = 0.0f;
n[23].x = 0.0f;     n[23].y = -1.0f;   n[23].z = 0.0f;

Now we need a color stream. Just as with 2D rendering, this requires an array of CIwColour instances. Here comes the code snippet!

CIwColour* c = new CIwColour[lVertexCount];
c[0].Set(255, 0, 0, 255);
c[1].Set(255, 0, 0, 255);
c[2].Set(255, 0, 0, 255);
c[3].Set(255, 0, 0, 255);
c[4].Set(255, 255, 0, 255);
c[5].Set(255, 255, 0, 255);
c[6].Set(255, 255, 0, 255);
c[7].Set(255, 255, 0, 255);
c[8].Set(0, 255, 0, 255);
c[9].Set(0, 255, 0, 255);
c[10].Set(0, 255, 0, 255);
c[11].Set(0, 255, 0, 255);
c[12].Set(0, 0, 255, 255);
c[13].Set(0, 0, 255, 255);
c[14].Set(0, 0, 255, 255);
c[15].Set(0, 0, 255, 255);
c[16].Set(0, 255, 255, 255);
c[17].Set(0, 255, 255, 255);
c[18].Set(0, 255, 255, 255);
c[19].Set(0, 255, 255, 255);
c[20].Set(255, 128, 0, 255);
c[21].Set(255, 128, 0, 255);
c[22].Set(255, 128, 0, 255);
c[23].Set(255, 128, 0, 255);

Finally, it's time for the index stream to be created. Again, as with 2D rendering, this is just an array of uint16 values which indicate the order in which elements of the streams should be accessed. Here's the code:

const uint32 lIndexCount = 34;
uint16* i = new uint16[lIndexCount];
 
// Front face (red)
i[0] = 0;  i[1] = 1;  i[2] = 2;  i[3] = 3;
// Degenerate
i[4] = 3;  i[5] = 7;
// Right face (yellow)
i[6] = 7;  i[7] = 6;  i[8] = 5;  i[9] = 4;
// Degenerate
i[10] = 4;  i[11] = 9;
// Back face (green)
i[12] = 9;  i[13] = 11;  i[14] = 8;  i[15] = 10;
// Degenerate
i[16] = 10;  i[17] = 12;
// Left face (blue)
i[18] = 12;  i[19] = 13;  i[20] = 14;  i[21] = 15;
// Degenerate
i[22] = 15;  i[23] = 16;
// Bottom face (cyan)
i[24] = 16;  i[25] = 17;  i[26] = 18;  i[27] = 19;
// Degenerate
i[28] = 19;  i[29] = 23;
// Top face (orange)
i[30] = 23;  i[31] = 21;  i[32] = 22;  i[33] = 20;

Note than the first four values in the stream define the first full face of the cube. The next two values form a degenerate triangle that allows us to link the first face to the second face without actually rendering anything. As we saw earlier in this chapter, the easiest way to link two triangle strips is to repeat the last index of the first strip and start the next strip with two copies of its first index. This pattern continues until we've drawn the last face of the cube.

The order in which the vertices are specified is the most important consideration, as we must ensure we get this correct for the culling mode we'll be using. For back-face culling (so faces that are away from the camera are not rendered) we need the vertex order to be in anti-clockwise order for the first triangle specified.

As we are using triangle strips, the order of the vertices actually alternates between anti-clockwise and clockwise. Normally we don't have to worry about this too much since the natural order of the vertices in the strip takes care of it, but it can cause problems when you try to join together triangle strips that contain an odd number of vertices.

Note

The general rule for joining triangle strips with degenerate triangles is that a strip with an odd number of points will require the order of the points in the next strip to be reversed. For example, if your first triangle strip contains an odd number of points, the first triangle of the next strip will need to be specified in clockwise rather than anti-clockwise order; otherwise it will not be culled correctly.

The view matrix

When rendering 3D graphics, we need to be able to provide a position and direction that we want to view our game world from. We do this by supplying a view or camera matrix; in Marmalade this can be done using an instance of the CIwFMat class.

The CIwFMat class represents a 4 x 4 matrix using a 3 x 3 array of float for the rotation part, and CIwFVec3 for the translation part. The remaining elements of the 4 x 4 matrix (that is, the right-most column of numbers) are fixed to be the same as the identity matrix (0, 0, 0, and 1 from top to bottom of the column). These values never have any influence on normal 3D transformations; so by leaving them out we save memory, and also the matrix multiplication code can be made slightly more efficient by not having to perform multiplications for these parts of the matrix.

Time to create a suitable view matrix. For the purposes of our spinning cube, it would be good if we could specify a position for the camera and then calculate the correct rotation for the matrix to view our cube. Luckily the matrix classes have a method called LookAt that makes this easy to do:

CIwFMat vm;
vm.t.x = 0.0f;  vm.t.y = 0.0f;  vm.t.z = -400.0f;
vm.LookAt(vm.t, CIwFVec3::g_Zero, CIwFVec3::g_AxisY);

The previous code declares a new CIwMat instance and sets its translation to (0, 0, -400). We then call the LookAt method, which is passed the position we want the camera to be placed at, the point in space we want it to be orientated towards, and a unit vector in the vertically up direction.

Marmalade's default coordinate system when rendering in 3D has the x axis positive direction running from left to right across the screen, while the z axis positive direction runs into the screen. However, the positive y axis runs in a direction from the top of the screen to the bottom, which may not be what you initially expect. We are used to thinking about the height above the ground as a positive number, but in Marmalade it would be negative.

Once we have a view matrix, we can call the function IwGxSetViewMatrix with a const pointer to the matrix.

The model matrix

The model matrix is used to position our 3D model in the world and allow it to be rotated or scaled as desired. As with the view matrix, the model matrix can be specified using a CIwFMat instance.

For our spinning cube we will create a matrix that spins the cube around the x and y axes. We do this by creating two matrices, one for x axis rotation and another for y axis rotation, which we then multiply together. We will be positioning our cube at the world origin.

CIwFMat lModelMatrix;
lModelMatrix.SetRotY(lRotationY);
CIwFMat lRotX;
lRotX.SetRotX(lRotationX);
lModelMatrix.PreMult(lRotX);

The code shown declares two instances of CIwFMat and uses the methods SetRotY and SetRotX to generate the rotation matrices around the y and x axes respectively. The rotation angles are provided by two variables lRotationY and lRotationX, which are both of the type float and represent an angle (in radians) to rotate by. If we increase the values of these two variables with each iteration of the main game loop, it will change the orientation of the cube and make it appear to rotate when rendered.

Note

Be careful when using the SetRotX, SetRotY, and SetRotZ methods of the matrix classes. These methods take two further bool parameters that allow the translation part of the matrix and any elements of the 3 x 3 rotation part of the matrix that are not used in the rotation to be zeroed. Both of these parameters default to true; so, in particular, if you set up a translation in the matrix before calling one of these methods, it will get lost unless you specify false as the second parameter.

Once we have our two rotation matrices, we multiply them together to generate the final model matrix using the PreMult method. The order in which matrices are multiplied together is very important as the end rotation will vary depending on the order used. Marmalade provides us with PreMult and PostMult methods to allow us to determine whether the calling matrix is the first matrix or the second in the multiplication.

When we have our model matrix ready, we just call IwGxSetModelMatrix to use it for rendering.

Rendering the model

All the hard work is now done and we can finally submit our cube for rendering. The following code will submit all our streams and our cube will be rendered. Hopefully you'll see just how close it is to the code we used for rendering in 2D:

IwGxSetColClear(128, 190, 220, 255);
IwGxClear();

IwGxSetMaterial(lpMaterial);
IwGxSetVertStreamModelSpace(v, lVertexCount);
IwGxSetNormStream(n, lVertexCount);
IwGxSetColStream(c);
IwGxDrawPrims(IW_GX_TRI_STRIP, i, lIndexCount)

IwGxFlush();
IwGxSwapBuffers();
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.188.238