Sunday, April 24, 2011

Drawing a simple cube

The most simple 3D rendering example (I can think of) is that of a colored cube:

Rendering a simple cube

Backrefenreces:
 Before starting you should download that initial source files from here. The solution already has the required vertex buffer containing 8 vertexes with position and color and the index buffer of 36 indices representing all 6 faces of the cube. It also has 2 constants for the numbers of vertices and indices.

To draw this cube we will have to follow the following steps:
  1. Understand the transformation matrices (optional but recommended)
  2. Create a new effect file and load it
  3. Create the transformation Matrices and initialize them
  4. Use input to modify the World matrix (optional)
  5. Set the buffers, effect parameters and effect pass
  6. Call the draw function
1. Understand the transformation matrices (optional but recommended)

 Now I want to tell you the roles of the 3 matrices: World, View and Projection, if you already know what they do just skip over this section.

  The World matrix transforms the object from local to global coordinates. For example let's image we have a box that we want to move. To move the box we can change the values of the vertex buffer to move it where we need. But what happens if we have 2 boxes? We will also need 2 vertex buffers, this may sound OK for 2 boxes, but it won't work for more complex items (you will waste a lot of memory for an army of identical looking soldiers). So it becomes apparent that it's better to just hold the vertex information once and move it to where we need in the space. This is the role of the world transform, it contains all necessary  transformations (like scale, rotation and translation) for that instance of the object. By multiplying with this matrix you will put the object in the place it has to be in the world.

 The view matrix is a change of coordinate transformation matrix. This matrix is used to determine the position of objects relative to the camera. So it changes all coordinates from the world to where they are relative to the camera because we need to know how far an object is from the observer, and also if it is higher or lower, if it should be more to the right or left on the screen or maybe it isn't in our sight at all.

 After this transformation we need to determine what objects are in the view frustum. This tells the graphics card if the objects is too close or too far to draw, or if it is too high or too low so that it doesn't reach the screen. The projection matrix defines how much of the world we can see.

2. Create a new effect file and load it

 To create a new file right click on the content project -> Add -> New Item and select Effect File from the list. Name it SimpleColor.fx.
Now notice that we multiply with each of the matrices one after another. This isn't very useful as we can just pack all transformations into one (by multiplying them). So what we will have to do is replace the 3 matrices at the top with only one called WVP (short for WorldViewProjection). Now change the 3 multiplications from the vertex shader with only one (where I have '...' it means that I didn't change the code there):

float4x4 WVP;
... 
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    output.Position = mul(input.Position, WVP);
  
    return output;
}

Our shaders do not have support for colors at the moment, to pass colors through the rendering pipeline we will have to add a Color component with a COLOR0 semantic to the structures, so they will become something like this:

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float4 Color    : COLOR0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 Color    : COLOR0;
};

Now that our structures are equipped with color components we must pass the color from input to output in both vertex and pixel shader. Yup, we just pass it along, the color combination is done by interpolation automatically by the graphics card. So our final shaders will be:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    output.Position = mul(input.Position, WVP);
 
 output.Color = input.Color;
  
    return output;
}
 
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return input.Color;
}

To load our file in the game we have to first create an Effect object (at game class level):

     Effect simpleColorEffect;

After this we must use Load function of the Content object to load our effect. The load function has an generic that specifies the type of object to load. The parameter to this function is a string representing the ASSET NAME of the file and NOT the file name. The asset name can be viewed by right click-> Properties on the item we want to load, you can also change it here (I recommend you always have the properties windows visible for this). Just add this line to the LoadContent function:

    simpleColorEffect = Content.Load<Effect>("SimpleColor");

3. Create the transformation Matrices and initialize them

Creating these matrices at the class level is very straightforward:

    Matrix World, View, Projection;

Then we go in our LoadContent method and set them to the desired values:

    World = Matrix.Identity;
    View = Matrix.CreateLookAt(new Vector3(0,0,5),Vector3.Zero,Vector3.Up);
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1, 10);

  We set World to the identity matrix (no transformation). The view matrix is created using the CreateLookAt function of the Matrix structure. The first parameter represents where the camera (eye) is in the world, the second parameter of this function is the point in space we are looking at. These 2 points determine the direction we are looking at. The third parameter represents the up direction of the camera (not the world). We can give arbitrary up directions to simulate an upside-down camera or a crooked camera. The projection matrix is constructed using the CreatePerspectiveFiledOfView (create a view frustum based on an view angle). The first parameter is an angle (in radians) from the direction we are looking at to the top-most or bottom most direction we can see. How it determines how up far up and down we can draw. The second parameter is the aspect ratio of the drawing surface. We need to set this correctly so the image won't appear distorted. It will also determine the limit of how far left and right we can see. The third parameter is the distance from the camera to the near plane, it represents the closest point that we can see. The last parameter is the far plane and it represents how far from the camera we can see.

4. Use input to modify the World matrix (optional)

 We will have to check of a certain key (the directional keys) is pressed and multiply the world matrix by a rotation matrix around the X or Y axis. To create the transformation matrices we will call the CreateRotation functions of the Matrix structure:

    if (currentKeys.IsKeyDown(Keys.Up))
        World *= Matrix.CreateRotationX(-0.05f);
    if (currentKeys.IsKeyDown(Keys.Down))
        World *= Matrix.CreateRotationX(0.05f);
    if (currentKeys.IsKeyDown(Keys.Left))
        World *= Matrix.CreateRotationY(-0.05f);
    if (currentKeys.IsKeyDown(Keys.Right))
        World *= Matrix.CreateRotationY(0.05f);

 The parameter of these functions is a float and it represents the angle to rotate around that axis in radians. The above lines should be places in the update() function.

5. Set the buffers, effect parameters and effect pass

All these settings will be made in the Draw function right before drawing the object on screen.
To be able to render our cube we must specify the buffers that we want to use to our Graphics Device like so:

    GraphicsDevice.SetVertexBuffer(vertices);
    GraphicsDevice.Indices = indices;

After that we need to set our matrix. There is a array of elements in the Effect class called Parameters and you can probably guess what we will use it for; to set our effect parameters. We can access elements in it by index (int) or by name (string). I recommend you use strings as the effects might be changed at a certain point and the index can will change if you add another parameter at the beginning but the name will stay the same. We can't directly change the elements in the array, we have to call the (overloaded) SetValue function like so:

    simpleColorEffect.Parameters["WVP"].SetValue(World * View * Projection);

As you can see we have combined all 3 matrices into 1 by multiplying them.

The last thing we have to do before drawing is to call the Apply function of the pass we will use to draw, we can only set passes from the current technique and they set the Vertex and Pixel shaders we will use to draw.

    simpleColorEffect.CurrentTechnique.Passes[0].Apply();

We have only one technique so it already is selected as the current technique. Also we only have one pass so we can just apply it directly.

6. Call the draw function

In XNA we have more than just one draw function. Because we want to draw something with indices we will use DrawIndexedPrimitives from the GraphicsDevice class

    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, number_of_vertices, 0, number_of_indices / 3);

So we can see a lot of parameters:
  • The first one is an enum called PrimitiveType, it represents how the vertices will be assembled. In XNA we can wither draw lines or triangles as lists or strips. Lists means that each 2 vertices will form a line or each 3 vertices will form a triangle, so they don't have to be connected. Strip means they are continuous and connected, so the first 2 vertices will draw a line, and the next vertices will form a line from them to the vertex before them (or the 2 vertices before them in case of triangle strips). We don't want them to be connected right now so we draw as a TriangleList. 
  • The next parameter (base vertex) is an offset that will be added to each index in the index buffer. So if the offset is 2 and we had 2 1 3 in the index buffer we will draw the 4 3 5 vertices. 
  • The next parameter represents lowest index (added to base) that we will draw in this call. 
  • The fourth parameter represents the number of vertices that we will use from the vertex buffer in this draw call.
  • The fifth parameter represents from which index to start reading. If we have 2 1 9 4 7 6 in our index buffer and we set this parameter to 3 we will only use 4 7 6.
  • The last parameter represent how many primitives (triangles or lines) we are going to draw. Because we are using indices and drawing a line list we have 3 times more indices than triangles, that'w why we divide the number of indices by 3.
That should be it. You can download the finished version from here.

In case you are wondering you should get something like this:

12 comments:

  1. Hi,

    You can go here: http://iloveshaders.blogspot.com/p/xna-topics.html for a list of all xna topics.

    Thanks for your interest!

    ReplyDelete
  2. Hi. tx for sharing your knowledge, Nice job!!,
    Enrico /italy
    (little knowledge of Assembly,game cracking experience,C#, Xna,isp hardware programming)
    always tuned to this world :)^_^
    Tvm for code too:)^_^

    ReplyDelete
  3. hi, how can I draw multiple cubes using this method?
    thanks.

    ReplyDelete
    Replies
    1. After the draw primitives call you can change the world matrix (or use a second, different one altogether) and set it in the shader, then call the draw method again.

      Delete
    2. protected override void LoadContent()
      {

      simpleColorEffect = Content.Load("SimpleColor");

      World = Matrix.Identity;

      View = Matrix.CreateLookAt(new Vector3(0, 2, 10), Vector3.Zero, Vector3.Up);

      Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1, 10);


      //Cube 1
      CreateCubeVertexBuffer();
      CreateCubeIndexBuffer();
      World *= Matrix.CreateTranslation(2, 2, 2);


      //Cube 2
      CreateCubeVertexBuffer();
      CreateCubeIndexBuffer();
      World *= Matrix.CreateRotationZ(0.04f);
      }


      That's my code. Actually I want to draw two cubes. The first cube is translated by (2,2,2) and the second one is rotated 0.04f to Z.
      But using that code, I just get one cube that is rotated and translated.

      How to do that right?

      thanks for your help.

      Delete
    3. Try this:

      protected override void LoadContent()
      {

      simpleColorEffect = Content.Load("SimpleColor");

      World1 = Matrix.Identity;
      World2 = Matrix.Identity;

      View = Matrix.CreateLookAt(new Vector3(0, 2, 10), Vector3.Zero, Vector3.Up);

      Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1, 10);


      //Cube 1
      CreateCubeVertexBuffer();
      CreateCubeIndexBuffer();
      World1 *= Matrix.CreateTranslation(2, 2, 2);


      //Cube 2
      CreateCubeVertexBuffer();
      CreateCubeIndexBuffer();
      World2 *= Matrix.CreateRotationZ(0.04f);
      }


      Another way would be to modify World in the Draw function, right before you draw each cube.

      Delete
    4. It works.

      Thank you very much, you are my savior.

      Delete
  4. hi adrian, not a biggie, but in the optional part where you add the lines to be able to move the world matrix, you forgot to mention that there also needs to be 2 additional lines of code to make it work (i found them in your code example)

    in the game class : KeyboardState currentKeys;
    in the update class (before the key definitions):
    currentKeys = Keyboard.GetState();

    Other than that, excellent tutorial ;) thanks!

    ReplyDelete
    Replies
    1. I didn't consider that part of this tutorial when I wrote it, sorry about that.

      Thanks for catching it!

      Delete
  5. Thank for tutorial, how can i change color of cube? 1 color per face.

    ReplyDelete