3D Programming


So how do you get the 3D data imported from a model file like rocket.x on your screen? Your screen can only display 2D data; this means you need a way to convert 3D data to your 2D screen. This is called projection, and there are many ways this can be done. In the early days of 3D games, techniques like ray-casting were used and done all on the CPU. For each pixel column of the screen the upper and lower bounds were calculated by a simple formula and this led to the first 3D games like Ultima Underground, Wolfenstein 3D, and later Doom, which was quite popular.

Not long after that more realistic games like Descent, Terminal Velocity, Quake, and so on were developed, which used a better way to convert 3D data to 2D. In the mid-nineties 3D hardware was becoming more popular and more and more gaming PCs suddenly had the ability to help rendering polygons in a very efficient matter.

Luckily, you don’t have to worry about all the early problems of 3D games anymore. 3D graphics today are all done on the GPU (graphic processing unit on the graphic card). The GPU not only renders polygons on the screen and fills pixels, but it also performs all the transformations to project 3D data to 2D (see Figure 5-7). Vertex shaders are used to transform all 3D points to screen coordinates and pixel shaders are then used to fill all visible polygons on the screen with pixels. Pixel shaders are usually the most interesting and important part of shaders because they can greatly manipulate the way the output looks (changing colors, mixing textures, having lighting and shadows influence the final output, and so on). Older hardware that does not support vertex and pixel shaders is not even supported in XNA, so you don’t have to worry about falling back to the fixed function pipeline or even doing software rendering. For XNA you need at least Shader Model 1.1, which means you need at least a GeForce 3 or ATI 8000 graphic card.

image from book
Figure 5-7

The code and images I’m going to explain in the next pages only cover the basics of 3D programming and how to work with matrices in XNA. If you want to know more I strongly recommend reading the XNA documentation and, even better, the original DirectX documentation, which has very good topics about getting started with 3D programming, matrices, and how everything is handled in the framework (which is very similar to XNA).

I won’t have time to go into great detail about all these calculations and thanks to the many helper classes and methods in the XNA Framework, you can get along without knowing much about matrix transformations. Projecting 3D data usually consists of three steps:

  • Bring the 3D data to the correct 3D position with help of the WorldMatrix. This means for the rocket, which was created in 3D Studio Max, to rotate it in the direction you want, scale it accordingly to fit correctly into your 3D scene, and then position it to wherever you want it to be. You can use the Matrix methods to help you do that; use CreateRotation, CreateScale, and CreateTranslation methods and combine them by multiplying the resulting matrixes:

      BaseGame.WorldMatrix =   Matrix.CreateRotationX(MathHelper.Pi / 2) *   Matrix.CreateScale(2.5f) *   Matrix.CreateTranslation(rocketPosition); 

  • Every single point of your 3D model is then transformed with this WorldMatrix to bring it to the correct position in your 3D world. In DirectX and the fixed function pipeline you didn’t have to do that yourself, but as soon as you write shaders in XNA you have to transform all vertices yourself. And as you know everything in XNA is rendered with shaders, there is no support for the fixed function pipeline.

  • What is visible on the screen depends on the location and orientation of your camera or eye (see Figure 5-7 earlier). Everything is projected to this camera location, but you can also rotate the world, tilt it, or do more crazy things with your camera matrix. You will probably just want to use the Matrix.CreateLookAt method in the beginning to create your camera matrix, which is then used in the vertex shader to transform the 3D world data to the camera space:

      BaseGame.ViewMatrix =   Matrix.CreateLookAt(cameraPosition, lookTarget, upVector); 

  • And finally the last step is to bring the projected camera space data to your screen. The reason why this operation is not already done in the ViewMatrix is to allow the view matrix to be calculated and changed more easily. The visible camera space goes from –1 to +1 for both x and y, and z contains the depth value for each point in relation to the camera position. The ProjectionMatrix converts these values to your screen resolution (for example, 1024×768) and it also specifies how deep you can look into the scene (near and far plane values). The depth is important in helping you find out which object is in front of which other object when you finally render the polygons in the pixel shaders (which are all 2D, because you can only render 2D data on your screen). The projection matrix is constructed this way:

      /// <summary> /// Field of view and near and far plane distances for the /// ProjectionMatrix creation. /// </summary> private const float FieldOfView = (float)Math.PI / 2,   NearPlane = 1.0f,   FarPlane = 500.0f;  aspectRatio = (float)width / (float)height; BaseGame.ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(   FieldOfView, aspectRatio, NearPlane, FarPlane); 

There is an important change in XNA that should be mentioned: In DirectX all the Matrix methods, tutorials, and samples used left-handed matrices, but in XNA everything is right handed; there are no methods to create any left-handed matrices. If you create a new game or if you are new to 3D programming this will probably not matter much to you, but if you have old code or old models, you might run into problems because of the way the 3D data is used. Using left-handed data in a right-handed environment means that everything looks turned inside out if you don’t modify your matrices. Culling is also the other way around in XNA; it works counter-clockwise, which is the mathematical correct way. DirectX used clockwise culling by default. If your models are inside out and you switch from clockwise to counter-clockwise, they will look correct again; they are just mirrored now (the 3D data is still left-handed).

This is especially true if you import .x files, which use, by default, a left-handed system (see Figure 5-8). You can either convert all matrices or points yourself or just live with the fact that everything is mirrored. In this book you will use a lot of .x files and it does not matter if they are left-handed (like the Rocket model from Rocket Commander) or right-handed (like all new 3D content created here) because you do not care if left-handed data is mirrored. If you have to make sure all data in your game is correctly aligned, please make sure you have all the 3D data in an exportable format so you can later re-export any wrong data instead of having to fix it in your code yourself.

image from book
Figure 5-8

You probably remember from school that the left-handed coordinate system is named after the left hand, which can be used to show you the x (thumb), y (forefinger), and z (middle finger) axes. Just hold up your left hand and align these three fingers in 90 degrees to each other and you have your left-handed coordinate system. In school both math and physics used only the right-handed coordinate system and if you were writing with your right hand and held up your left hand to show you the axes, you got it all wrong and that was not cool if you just based your whole solution of an exam on that.

Anyway, the right-handed coordinate system is much cooler because almost all 3D modeling programs work with right-handed data, and most programs except games work with right-handed data too. Right-handed coordinate systems can be shown by using your right hand for the x (thumb), y (forefinger), z (middle finger) axes (see Figure 5-9).

image from book
Figure 5-9

Looks almost the same, but having the z axis coming toward you is kind of unnatural if you worked with left-handed systems before. Another reason why it might be useful to rotate this system is to have it behave the same way as in 3D modeling programs where z points up. I also like x and y lying on the ground for positioning 3D data and building 3D landscapes (see Figure 5-10).

image from book
Figure 5-10

Enough talk about converting 3D to 2D and coordinate systems. You should now know enough about basic 3D calculations to render 3D models. If you are still unsure and want to learn more about 3D math and programming, please pick up a good book about that or read more on the Internet. Many great websites and tutorials are available there; just search for 3D programming.

Model Unit Test

You should now have the Rocket.x model file discussed earlier imported into your project (use the content directory to keep code and content data separated). Rendering the model data is not very complicated, but there is no Render method in the XNA Model class and you want to have such a method for your unit test. To accomplish this and to make it easier to later extend the model rendering, optimizing it and adding more cool features, you write your own Model class (similar to the Texture class you just wrote). It will just use the XNA model class internally and provide you with a Render method and handle all the scaling issues and set all required matrices automatically for you.

It is a good practice to write another unit test just for the class you are currently developing, even if another unit test already covers the basic functionality. In this class unit test you can make more special considerations and check more details. It is also useful to quickly check if new data is valid by just exchanging the content loaded in the unit test. Here is the very simple, but still very useful unit test for the Model class:

  public static void TestRenderModel() {   Model testModel = null;   TestGame.Start("TestRenderModel",     delegate     {       testModel = new Model("Rocket");     },     delegate     {       testModel.Render(Matrix.CreateScale(10));     }); } // TestRenderModel() 

So you just load a test model named Rocket in the content system and then you render it every frame with a scaling of 10 directly in the 3D scene. All the other stuff is handled somewhere else and you don’t have to worry about it. This includes setting shaders, updating matrices, making sure the render states are correct for rendering 3D data, and so on.

The only interesting method in the Model class (see Figure 5-11) is the Render method.

image from book
Figure 5-11

The constructor just loads the model from the content and sets the internal variables to make sure you got the correct object scaling and matrix to make it easier to fit it into your world:

  /// <summary> /// Create model /// </summary> /// <param name="setModelName">Set Model Filename</param> public Model(string setModelName) {   name = setModelName;   xnaModel = BaseGame.Content.Load<XnaModel>(     @"Content\" + name);   // Get matrices for each mesh part   transforms = new Matrix[xnaModel.Bones.Count];   xnaModel.CopyAbsoluteBoneTransformsTo(transforms);   // Calculate scaling for this object, used for rendering.   scaling = xnaModel.Meshes[0].BoundingSphere.Radius *     transforms[0].Right.Length();   if (scaling == 0)     scaling = 0.0001f;   // Apply scaling to objectMatrix to rescale object to size of 1.0   objectMatrix *= Matrix.CreateScale(1.0f / scaling); } // Model(setModelName) 

After the xnaModel instance is loaded from the content directory, the transformations are pre-calculated because you don’t support any animated data here (XNA does not really support it yet anyway). The transforms matrix list holds the render matrix for each model mesh part you have to render (the rocket consists just of one mesh part using only one effect). This transformation is set by the 3D Modeler in the 3D creation program and aligns your model the way the modeler wants it to be. Then you determine the scaling and make sure it is not zero because dividing by zero is not really fun. Finally, you calculate the object matrix, which will be used together with the transform matrices and the render matrix for rendering.

The Render method now just goes through all meshes of your model (for good performance always make sure you have as few meshes as possible) and calculates the world matrix for every mesh. Then each used effect is updated with the current world, view, and project matrix values as well any other dynamic data like the light direction, and then you are good to go. The MeshPart.Draw method calls the internal shader of the model the same way you did earlier for the line rendering. The next two chapters talk in more detail about this process and you re-implement it in your own way to be much more efficient when rendering many 3D models each frame.

  /// <summary> /// Render /// </summary> /// <param name="renderMatrix">Render matrix</param> public void Render(Matrix renderMatrix) {   // Apply objectMatrix   renderMatrix = objectMatrix * renderMatrix;    // Go through all meshes in the model    foreach (ModelMesh mesh in xnaModel.Meshes)    {      // Assign world matrix for each used effect      BaseGame.WorldMatrix =        transforms[mesh.ParentBone.Index] *        renderMatrix;      // And for each effect this mesh uses (usually just 1,multimaterials      // are nice in 3ds max, but not efficiently for rendering stuff).      foreach (Effect effect in mesh.Effects)      {        // Set technique (not done automatically by XNA framework).        effect.CurrentTechnique = effect.Techniques["Specular20"];        // Set matrices, we use world, viewProj and viewInverse in        // the ParallaxMapping.fx shader        effect.Parameters["world"].SetValue(          BaseGame.WorldMatrix);        // Note: these values should only be set once every frame!        // to improve performance again, also we should access them        // with EffectParameter and not via name (which is slower)!        // For more information see Chapter 6 and 7.        effect.Parameters["viewProj"].SetValue(          BaseGame.ViewProjectionMatrix);        effect.Parameters["viewInverse"].SetValue(          BaseGame.InverseViewMatrix);        // Also set light direction (not really used here, but later)        effect.Parameters["lightDir"].SetValue(          BaseGame.LightDirection);      } // foreach (effect)      // Render with help of the XNA ModelMesh Draw method, which goes     // through all mesh parts and renders them (with vertex and index     // buffers).     mesh.Draw();   } // foreach (mesh) } // Render(renderMatrix) 

As you can see there are quite a lot of comments in this code and you will see this in all methods written by me that are a little longer. I think it is very important to understand the code quickly later when you want to change something, refactor code to new requirements, or just for optimizing your code. For most programmers this is a hard step; we all are lazy and it takes a long time to understand how useful these comments can be, not only if other people read your code, but also if you come back to a part of code at a later point in time. Please also try to write your comments in English and use a language that simplifies the reading process and does not use code in the comments.

The render matrix you passed from the unit test (which was just a scaling matrix with the factor of 10) is now multiplied with the object matrix, which again scales the render matrix by the factor required to bring the object to the size 1, so in total your rocket now has a size of 10 units in your 3D world. Then you go through each model mesh (your rocket just has one mesh) and calculate the world matrix for this mesh by using the pre-calculated transforms matrix list and the render matrix. By using the world matrix in the vertex shader you can now be sure that every single 3D point of your rocket is transformed the way you want it to be. Before calling this method the view and projection matrices must obviously be set too because the vertex shader expects all these values to be able to convert the 3D data for the pixel shader to render it onto the screen.

XNA models store all the used effects for each model mesh and you can have multiple effects for one single mesh, but for most models you will ever use this is usually just one effect too. You will later see that the most efficient way to render a lot of 3D data is to render them in a bunch using the same shader for a long time and then flush everything to the screen at once. This allows you to render thousands of objects every frame with a very high frame rate. Please read Chapters 6 and 7 for more information about shaders and how to work with them in an efficient way.

But before you can render your mesh here you have to set the used technique first, which is not done automatically by XNA, even though it is set by the model artist. You will see in Chapter 7 how to get around this issue. Then you set the world, view, and project matrices the way the shader expects these values. For example, the view matrix is never directly required, you just use the view inverse matrix to figure out the camera position and use it for specular light calculations. All these shader parameters are set with the Parameters property of the effect class by using the names of these parameters. This is not a very efficient way to set effect parameters because using strings to access data is a huge performance penalty. You will later find ways to do this way better, but for now it works and you should be able to see your little rocket from the unit test after mesh.Draw is called (see Figure 5-12).

image from book
Figure 5-12

Testing Other Models

If you have other .x files on your disk or if you just want to test other .x files from the Rocket Commander game or one of its many game mods, you can quickly test that by dragging in the .x file to the content directory and changing the unit test to load the new model.

Let’s do that with the Apple.x model from the Fruit Commander mod:

  public static void TestRenderModel() {   Model testModel = null;   TestGame.Start("TestRenderModel",     delegate     {       testModel = new Model("Apple");     },     delegate     {       testModel.Render(Matrix.CreateScale(10));     }); } // TestRenderModel() 

This results in the screen shown in Figure 5-13. Please note that the specular color calculations are based on the normal mapping effect in the shader, which does not work correctly here because the tangent data of the apple is not correct (it was not even imported because XNA models don’t support tangent data, and you have to implement it in your own way in Chapter 7).

image from book
Figure 5-13




Professional XNA Game Programming
Professional XNA Programming: Building Games for Xbox 360 and Windows with XNA Game Studio 2.0
ISBN: 0470261285
EAN: 2147483647
Year: 2007
Pages: 138

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net