Home > Tutorials > Displaying 3D Objects
1.1. Modern Computer Graphics Basics
In this tutorial you will learn the basics of modern 3D computer graphics.



Click here to go to the forum discussion of this tutorial


 

1.1.1. Prerequisites
default.jpg
Before you start this tutorial make sure that you have:
  • Installed XNA Game Studio 3.1
  • Downloaded the starter project from the downloads section
  • Successfully run the starter project
   
 

1.1.2. Organization of an XNA application
XNA applications are organized in callback methods. A callback method is a method that you write but you never call it directly. You can think that you write this methods so that XNA can call them. The callback methods that you will find in the starter project are:

The Initialize method

This method is called once. In it you must load all your graphics assets and perform the calculations that are to be done only once in the application.

The OnActivated method

This method is called every time that the application window you are working on is not the center of attention of your computer. If another program is launched, you minimize the window, another program puts a popup window in front of your application; this method will be called once you select your application window back.

The Update method

This is a method that runs in an infinite cycle; what you must understand is that this cycle is different than the one in which the Draw method is running. In this method you should do tasks like update the position of the camera, update the location of the objects in the scene, update the particle systems, and all logic that changes the environment. Its important to do this kind of stuff in the update method because if it is done in the Draw method, the application can suffer performance drawbacks.

The Draw Method

The Draw method is an infinite cycle where you draw your scene.
 
 

1.1.3. The using statements
The using statements informs XNA that we are going to need some code; in this tutorial, this code is given by the following lines:

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;

using BetaCell.Environment.Mesh;
using BetaCell.Environment;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex
;

 

1.1.4. Game Attributes 
Ok, lets get into business. We will start by defining the attributes of the Game1 class that are to be used in the sample.

Right now you can see the following attributes in the Game1 class of the starter project:

GraphicsDeviceManager graphics;
GraphicsDevice device;
ContentManager content;

BCFirstPersonHumanCamera camera;
 
The first attribute manages all the graphic devices in your system, chances are, you won't be needing to accesss it directly.

The second attribute is the device. This object is very important in XNA as it is used to both define how the drawing takes place and actually draw the scene.

The third attribute is the content object. This object is used to import assets like meshes or textures into the application.

The fourth attribute is the camera used to capture the scene, you can think of it as your video camera.

The attributes specific to this tutorial are:
 
//This tutorial's attributes
BCMesh sphere1;

BCMesh sphere2;
BCMesh sphere3;
BCMesh sphere4;

These attributes are of type BCMesh which is a representation of an object built from an approximation of triangles, for example, the screen shot of this tutorial has 4 approximations of a sphere, the one in the top left part has only 16 triangles, so it's difficult to associate it with a sphere. The sphere in the lower right part is a good representation of a sphere but 1024 triangles were needed.

 

1.1.5. What's an effect shader?
Back in the old days, all the work in a computer graphics application was done by the CPU, if you wanted the latest games to run smoothly, you would have to buy the latest Pentium 2 processor now including MMX routines!; back then the graphics card was only an interface between the processor and the computer screen. Things have changed since then, now days the graphics cards have their own processor called GPU and the computer graphics related work of an application can either be done by the CPU or the by the GPU, and guess what, the GPU is many times faster.

So the question is, how do we split the computer graphics related work from all the other tasks to assign it to the GPU? The answer is that we create a different program for it. So thats the main characteristic of modern computer graphics, now you have more work! Instead of writing a single program you have to write two! Hooray!!!!.

I was just kidding, any programmer with little experience in modern computer graphics can tell you that this approach has two main advantages:
  • More flexibility: Now you can program the tasks that the graphics card can perform, giving almost limitless possibilities on the end results that can be achieved.
  • More performance: We can now assign tasks to the GPU that were to complex for the CPU to compute in an acceptable response time.
In XNA the programs that are to be executed in the GPU (AKA shaders) are written in a very simple c-like language called HLSL (High Level Shading Language).

What you do is; you write a shader and then you tell the GPU that this shader needs to be applied to anything that has to be drawn (In fact is a little more complex than that but as you progress in the tutorials you'll get the big picture).

default.jpg
For example, the graphic shows the same cube with 4 different shaders. In this case the pseudo code could be as follows:

1. Set the wireframe shader
2. draw a cube in the upper left part
3. Set the texture shader
4. draw a cube in the upper right part
5. Set the light shader
6. draw a cube in the lower left part
7. Set the normal map shader
8. draw a cube in the lower right part

      

Now, one thing is missing, notice that the effects have to be customized; for example, the texture effect should apply any texture, not just the crate of the example, the light position in the light effect should be able to change, and generally every effect has to be parameterizable, meaning that it should have parameters. So for example the portion of the code that draws the textured cube can be as follows:

3. set texture shader
3.1. set the crate texture as a parameter
4. draw the cube in the upper right part

 

Advertisement
 

1.1.6. The Initialize method 
So, how are effects applied in BetaCell? Fortunately, BetaCell distinguishes the application developer and the effect developer roles; in the tutorials you'll start at the application developer position; meaning that many effects have already been done for you, and that BetaCell provides a framework to use them effectively.

The first part of the initialize method is the basic initialization:

//base initialization
device = graphics.GraphicsDevice;
BCLogger.instance.init(device, content);
BCInitializationManager.initialize(content);

OnActivated(null, null);


The only parts that you should care about are the initialization of the BCLogger and the BCInitializationManager initialization method. The BCLogger class has a singleton instance (a lonely static public object) that has methods to print information in the form of text in the screen, this is very useful to debug programs.

The initialize method of the BCInitializationManager class loads the necessary data to:
  • Reads the necessary data to compose effects
  • Loads the effects declared in the BCEffectStore
default.jpg
The Effect Store is a file inside Content>BetaCell>, it contains the definitions of the effects that are compiled at content creation time (when the game is built). This is useful for the XBOX360 because it can't compile effects at runtime. The default behavior in the PC is to compile the effects at runtime, if you want to use only the effects in the effectStore.bce file, set the following flag to false:

BCGlobalInfo.instance.runtimeEffects = false;

The flag can't be set in the XBOX360 because you can't choose to compile effects at runtime on it.

The declaration of the effects in the file are very cryptic, to add an effect in it see [1.1.7].

The next part of the initialization method is the creation of the effect:

//Base black color for the lines
BCColorSet blackBase = new BCColorSet(Color.Black);
//Function part that represents the base color of the objects
BCFunction colorFunction =
blackBase.getFunction("Shader.ColorSet");
//Effect representation
BCDynamicEffectVisitor colorSetter =
    new BCDynamicEffectVisitor(
colorFunction , blackBase, 0);

Remember that effects have to be parametrized? for example, the light effect can have the following parameters: light position, light color, light range, etc. well in BetaCell the object responsible for setting the parameters of an effect is called an effect feeder and is represented by the BetaCell.Effects.BCEffectFeeder interface.

Now think about three different effects, the point light, the directional light and the spot light. These effects can share the same feeder, in this case a light feeder, so the effect feeder is a good place to administer effects that have common characteristics.

In this tutorial, the BCColorSet is an effect feeder, it is created to feed the effect with a black color to draw the black lines in the wireframe spheres. So we have the feeder but how about the effect?

The effect in this case has to set a constant color across the objects that are drawn with it. In BetaCell, the functions that are part of an effect are represented by the BCFunction class. Notice that we obtain the function from the feeder passing the name of the function.

We have to create an object capable of holding both the effect function and the feeder in order to apply the effect to different objects in the scene, in this tutorial that object is an effect visitor, and is represented by the class BetaCell.Environment.Mesh.Visitors.BCDynamicEffectVisitor.

Now we will discuss the parameters of the visitor's constructor one by one:

colorFunction: For now, think that the BCEffectFeeder.getFunction method returns an effect that applies color to the objects. Remember that the effect feeder administers the effects? well blackBase is a feeder.

blackBase: since the lines have to be black, this feeder will feed the effect with a black color.

0: This parameter will be dealt with later in the tutorials.

So now we have the colorSetter object that makes black everything it touches!!!

The last part of the initialization method is the construction of the spheres; note that only the first sphere will be explained. 

//Creating a sphere
sphere1 = ProceduralModelers.SPHERE_MODELER.createSlicedSphere(4, 2, 1, new                   BCVertexPosContainer());

The BetaCell.Util.Procedural.ProceduralModelers has many modelers, one of them is the SPHERE_MODELER which is used to create the sphere of this example. The method  createSlicedSphere
receives the number of slices, you can think of them as the vertical slices that yo make to an orange with a knife, the number of vertical divisions specified in the following form: divs = pow(2,n-1) being n the second parameter, what this means is that if you state 2 you will have 2 divisions, 3 will have 4 divisions, 4 will have 8 divisions, 5 will have 16  divisions, etc. the next parameter is the radix and the last one is a vertex container. vertex containers are discussed later in the tutorials.

//Initialization
BCMeshGraphicUtil.init(sphere1, device);

This method prepares the sphere to be drawn, it's very important. The reason why it's necessary will be explained later in the tutorials.

//Setting the effect
sphere1.visit(colorSetter);

With BetaCell you set the effect to the mesh, and when you draw the mesh, the effect is automatically set to the graphics card. So the line where the effect is set is just painting the sphere black.

//Setting the transform
sphere1.transform = Matrix.CreateTranslation(-1.25f, 1.25f, 0);

The last part is to set the transform, in this case we are setting the center of the sphere to 1.25f to the left and 1.25f up to draw the sphere in the upper left part.


 

 1.1.7. Adding effects to the effect store

Use the BetaCell API as if effects could be compiled at runtime, even if you are working on the XBOX360. When you run the game an exception will be shown:

effect store exception

The exception will show all the effect declarations that are missing in the effectStore.bce file. You must copy all the exception message after the ´Please add the following declarations to the 'effectStore.xml' to let the content pipeline create and compile them:´ line to the effectStore.bce file.

exception message 2
Don't forget to copy all the way down, or the message will appear again with the declarations that you missed.

Once you have updated the effectStore.bce file, save it and run the application again.

 

 1.1.8. The OnActivated method
Now lets discuss a little math. You can think of a vertex as a point in the 3D space, a face is a group of three of these points creating a triangle. So in a very simplistic way, a 3D scene is an approximation of points. A point can be transformed (bet you heard that one before), so you can move it, rotate it, and apply all kind of transformations; a convenient form to represent transformations is a Matrix.

A special form of transformation is the one that the 3D points in the world go through in order to become points in a 2D screen; this transformation is encapsulated by the camera object in BetaCell (as usual is a little more complicated, for more information see Computer Graphics by Foley).

The following specifies the position of the camera

default.jpg
Vector3 pos = new Vector3(0, 0, -7);

Now the direction to which the camera is looking:

Vector3 look = new Vector3(0, 0, 1);
look.Normalize();

This is the constructor; the rest of the parameters of the camera deserve a tutorial of their own.

camera = new BCFirstPersonHumanCamera(look,
    pos, MathHelper.PiOver4,
    (float)this.Window.ClientBounds.Width /
    (float)this.Window.ClientBounds.Height,
    1f, 200); 

So, the rest is telling BetaCell the Matrices that represent the camera transformation; these are called the view and projection.
default.jpg
BCGlobalInfo.instance.setMatrix(
    BCGlobalInfo.VIEW_INDEX, camera.getViewMatrix(-1)
);    
BCGlobalInfo.instance.setMatrix(    BCGlobalInfo.PROJECTION_INDEX,
   camera.getProjectionMatrix(-1)
);

The BetaCell.Util.Globalinfo.BCGlobalInfo class is a common place where data is being posted an read all the time, here we are stating for every object in the world that the view matrix is camera.getViewMatrix() and the projection matrix is camera.getProjectionMatrix(). 

 

 1.1.9. The Draw method
The important lines of the Draw method are:

This line states that we are going to draw lines instead of filled triangles
device.RenderState.FillMode = FillMode.WireFrame;

For each sphere, this is the line that draws it
sphere1.Draw(gameTime);

This line states that we are going to draw solid back again to display the FPS information
device.RenderState.FillMode = FillMode.Solid;

This prints the frames per second
BCLogger.instance.printFPS(gameTime);

This paints the information of the log
BCLogger.instance.flush();

 

 1.1.10. Conclusion
At this point you should be able to play around with BetaCell, I don't expect you to fully understand all the API calls of this tutorial but there will be more tutorials for you to do so.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

1.1.11. Complete source code
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;

using BetaCell.Environment.Mesh;
using BetaCell.Environment;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex;

namespace Starter

{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        //This tutorial's attributes
        BCMesh sphere1;
        BCMesh sphere2;
        BCMesh sphere3;
        BCMesh sphere4;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);
#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
#else
            graphics.PreferredBackBufferWidth = 852;
            graphics.PreferredBackBufferHeight = 480;
#endif
        }

        protected override void Initialize()
        {
            //base initialization
            device = graphics.GraphicsDevice;

            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

            //Base black color for the lines
            BCColorSet blackBase = new BCColorSet(Color.Black);
            //Function part that represents the base color of the objects
            BCFunction colorFunction = blackBase.getFunction("Shader.ColorSet");
            //Effect representation
            BCDynamicEffectVisitor colorSetter = new BCDynamicEffectVisitor(colorFunction, blackBase, 0);

            //Creating a sphere
            sphere1 = ProceduralModelers.SPHERE_MODELER.createSlicedSphere(4, 2, 1,
                new BCVertexPosContainer());
            //Initialization
            BCMeshGraphicUtil.init(sphere1, device);
            //Setting the effect
            sphere1.visit(colorSetter);
            //Setting the transform
            sphere1.transform = Matrix.CreateTranslation(-1.25f, 1.25f, 0);

            sphere2 = ProceduralModelers.SPHERE_MODELER.createSlicedSphere(8, 3, 1,
                new BCVertexPosContainer());
            BCMeshGraphicUtil.init(sphere2, device);
            sphere2.visit(colorSetter);
            sphere2.transform = Matrix.CreateTranslation(1.25f, 1.25f, 0);

            sphere3 = ProceduralModelers.SPHERE_MODELER.createSlicedSphere(16, 4, 1,
                    new BCVertexPosContainer());
            BCMeshGraphicUtil.init(sphere3, device);
            sphere3.visit(colorSetter);
            sphere3.transform = Matrix.CreateTranslation(-1.25f, -1.25f, 0);

            sphere4 = ProceduralModelers.SPHERE_MODELER.createSlicedSphere(32, 5, 1,
                new BCVertexPosContainer());
            BCMeshGraphicUtil.init(sphere4, device);
            sphere4.visit(colorSetter);
            sphere4.transform = Matrix.CreateTranslation(1.25f, -1.25f, 0);

            base.Initialize();
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            buildViewMatrix();
            base.OnActivated(sender, args);
        }

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(0, 0, -7);
            Vector3 look = new Vector3(0, 0, 1);
            look.Normalize();

            camera = new BCFirstPersonHumanCamera(look, pos,
                MathHelper.PiOver4,
                (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height,
                1f, 200);

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.VIEW_INDEX,
                camera.getViewMatrix(-1)
            );

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.PROJECTION_INDEX,
                camera.getProjectionMatrix(-1)
            );
        }

        protected override void Update(GameTime gameTime)
        {
        }

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            device.RenderState.FillMode = FillMode.WireFrame;
            sphere1.Draw(gameTime);
            sphere2.Draw(gameTime);
            sphere3.Draw(gameTime);
            sphere4.Draw(gameTime);
            device.RenderState.FillMode = FillMode.Solid;

            BCLogger.instance.printFPS(gameTime);
            BCLogger.instance.flush();

            base.Draw(gameTime);
        }
    }
}