Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

3D Graphics for Windows Phone 7 Using the XNA Framework

0.00/5 (No votes)
2 Apr 2011 6  
Learn how to use 3D graphics and effects for Windows Phone 7.

Introduction

Applications with 3D graphics are very popular today, one of the best examples being modern video games. We have already got used to games for personal computers (or modern game consoles) that amaze us by realistic 3D graphics and the beauty of visual effects while games for mobile phones are considerably behind in the visual plan.

With the development of mobile technologies, there is now a possibility to place powerful hardware within the tiny cases of phones, and because of that, modern mobile phones can cope with the problem of rendering rather difficult three-dimensional scenes.

In this article, we are going to look at the Microsoft XNA Framework which allows you to develop applications with 3D graphics for the new Windows Phone 7 devices.

Prerequisites

Before starting to work with examples from this article, please make sure that your PC meets the requirements described below and all the required software is installed.

As mentioned earlier, we will use XNA Framework 4.0 in this article. Code will be developed in the Visual Studio 2010 IDE with the additional Windows Phone Developer Tools.

System requirements for Windows Phone Developer Tools are listed in the Release Notes for the product (http://download.microsoft.com/download/1/7/7/177D6AF8-17FA-40E7-AB53-00B7CED31729/Release%20Notes%20-%20WPDT%20RTM.htm); only the main items will be listed here:

  • Operating System: Windows Vista SP2 (except Starter), Windows 7 (except Starter)
  • You can develop games for Windows or Xbox 360 with XNA Framework 4.0 on Windows XP, but developing for Windows Phone 7 is supported only in Windows Vista/7.

  • Hardware: Video card with DirectX 10.1 support is needed to run the Windows Phone Emulator.
  • You can find information about your video card on the web site of the manufacturer of the video card. It is recommended that you update your video card drivers.

You can find information about the supported DirectX version in the DirectX Caps Viewer which is a part of the DirectX SDK. Go to DXGI 1.1 Devices\[Your Video Card Name], and if you see Direct3D 10.1, then everything should go fine.

1

Now we are ready to install Windows Phone Developer Tools, which can be found at http://create.msdn.com/en-us/resources/downloads. I also recommend installing the latest DirectX SDK before installing the Windows Phone Developer Tools.

After installation has completed, create a new Windows Phone Game (4.0) project in Visual Studio.

2

Hit the Run button, and if everything is done correctly, then you should see a Windows Phone Emulator with a blue screen (the first launch may take longer).

3DGraphicsWP7/3.png

To decrease the launch time of the application, don't close the emulator after the first launch, simply stop debugging in Visual Studio.

Basics of the XNA Framework

The XNA Framework allows the developer to create applications for Windows, Xbox 360, and Windows Phone 7. And this technology was specially created so that developing applications for various hardware platforms should be simple and convenient as possible. (And that's true to my mind.)

The following scheme describes the principle of work of the XNA Framework:

4

The XNA Framework for Windows is based on the .NET Framework and on the .NET Compact Framework for Xbox 360 and Windows Phone 7. The graphics system is based on DirectX 9. Applications are developed in a Visual Studio 2010 extension named XNA Game Studio which adds support for the XNA Framework, new project templates, and other useful stuff.

The following scheme shows the major elements of the XNA Framework.

5

The XNA Framework contains all the necessary elements for game development like math library for working with vectors and matrixes, a library for unified work with different input controllers, etc. Moreover, the XNA Framework contains additional elements (Extended Framework) which solve many problems which developers face when developing games.

The Extended Framework consists of the Application Model and the Content Pipeline.

Application Model - is a framework (template) for an application. Each new XNA Game project already has a Game1 class which has a set of methods. Each has its own purpose. For example, the Draw method should be used to render everything to the screen, LoadContent should be used to load all game content such as models, images, songs, etc.

The purpose of this framework is that the developer doesn't need to think about problems like:

  • How do I create a game loop?
  • When do I need to process user input?
  • How do I synchronize rendering speed with video adapter refresh rate?

Take a look at the following scheme which describes the flow of an XNA application:

app_model

Content Pipeline - unifies game content processing. All game contents are placed in a special storage and are processed with importers and processors which are already included into the XNA Game Studio. Thus you don't need to spend time to create your own importers.

Foundations of 3D computer graphics

In my opinion, 3D graphics programming is a quite complex theme, so before starting to develop your first application with 3D graphics, you have to gain an understanding of the foundations of 3D computer graphics.

This part of the article is a very quick tutorial about 3D graphics.

I strongly recommend you read some books and articles on 3D graphics prior to starting work on your own projects, this tutorial should only help you understand the rest of this article. If you are familiar with 3D graphics, you can skip this section of the article.

Coordinate systems

The first thing to mention is that almost all modern 3D graphics is polygonal graphics. This means that all models are made of polygons (usually triangles) which are made of vertexes. Those vertexes are positioned in the 3D coordinate system.

There are two types of coordinate systems: in left-handed coordinate system, the Z axis is directed "inwards the screen" (when Y axis is directed upwards and X axis is directed to the right), and "outwards" in right-handed. The XNA Framework only uses right-handed coordinate systems.

coord

Vertex operations

Working with vertexes (and thus with 3D models) usually consists of changing the vertex position which is equal to changing the coordinate system. These operations might be described in matrix form (this is great for hardware) as multiplication of basic transformation matrixes such as Rotation matrix, Translation matrix, and Scale matrix.

For example, if you want to translate a vertex by (dx1,dy1,dz1), then rotate over X axis by A radians, and then translate it by (dx2,dy2,dz2), you have to multiply the original vertex position vector by the translation matrix, then by the rotation matrix, then by the translation matrix again.

Make sure you keep the order of these operations in the matrix form. Matrix multiplication is a non-commutative operation.

This is how it may look like in the XNA Framework:

Vector3 position = new Vector3(x, y, y);
Matrix transformationMatrix = Matrix.CreateTranslation(dx1, dy1, dz1) * 
       Matrix.CreateRotationX(a) * Matrix.CreateTranslation(dx2, dy2, dz2);
Vector3 newPosition = Vector3.Transform(position, transformationMatrix);

Actually, almost all the necessary operations might be described by Translation, Rotation, and Scale Matrixes.

Main problem of 3D computer graphics

Now with some basic knowledge of 3D graphics theory, we can proceed to the main problem of 3D computer graphics: How to create our big 3D game world and then render it onto the 2D screen?

In brief, we have to position each model in 3D space first, then position the camera somewhere in this world, and finally project everything from the camera view area onto the screen. Everything starts when we create out models (or vertexes or primitives) in some modeling tool or in our source code. At this time, each vertex is positioned in its local coordinate system.

For example, when we create a model of a robot, we usually place it in the center on its local coordinate system. Each part of this robot (hand, foot, or head) is positioned relative to this model itself.

This is the local coordinate system:

11

Then we have to place this model somewhere in the game level. To achieve this, we need to translate, scale, and rotate our model so that it is placed in the desired position in the world coordinate system.

This may be done by multiplying the model's coordinates by the World matrix.

12

When every vertex is placed in the desired position, we need to decide what part of our game world should be rendered. We usually know the virtual position of the player in the game world so we place the virtual camera in this position. The virtual camera is described by its position, direction, and orientation. Here, we came to the camera coordinate system.

Actually, transformation into camera coordinate system might be acheived using the same translation, rotation, and scaling operations which together form the View matrix.

In the XNA Framework, the View matrix is created from the following parameters: camera position, camera target, camera up vector (camera orientation). When we have the camera view area, we place far and near clipping planes just to make further operations a little easier for the hardware.

Then everything from the created truncated pyramid must be projected onto the projection plane. This is done by multiplying coordinates from the camera coordinate system by the Projection matrix.

Projection matrix is a little more complex than World or View matrix. I will not describe it in detail here. The only thing to know now is that the matrix for perspective projection might be created by the XNA Framework from the field of the view angle, aspect ratio, and distances to the near and far planes.

13

Since the projection plane is 2D, we can easily translate it to the 2D screen.

And to summarize: the position of the 3D vertex on the 2D screen might be acquired by the multiplication of its position in local coordinate system by World matrix, View Matrix, and then Projection Matrix.

Basics of 3D graphics in the XNA Framework

Now we are ready to look at the basic principles of 3D graphics in the XNA Framework.

First of all, we will load the 3D model of a teapot into our project.

As mentioned earlier, game contents are processed with the Content Pipeline. There is a new type of project in XNA Game Studio 4.0 - Content Project. You can see it in the Solution Explorer when you create a new XNA project (WindowsPhoneGame1Content on the picture).

6

Now add the model (Add -> Existing Item) into the content project. We will not look at the properties of the Content Processor and Content Importer (which are available in the Properties tab) for now. XNA Game Studio will choose the correct importer and processor without our assistance. We just need to associate our model with a variable in the source code and render it.

That's how we do it:

Model model;
protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    model = Content.Load<Model>("teapot");
}

Before moving to the render method, we will set the screen resolution it is made to set the needed screen orientation.

protected override void Initialize()
{
    this.graphics.PreferredBackBufferHeight = 800;
    this.graphics.PreferredBackBufferWidth = 480;
    base.Initialize();
}

From now on, in vertical orientation of the emulator, Y axis in 3D space will be directed up.

Now let's go to the drawing.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds);
    Matrix view = Matrix.CreateLookAt(new Vector3(0,0,2), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
           MathHelper.ToRadians(45), 
           GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
    model.Draw(world, view, projection);

    base.Draw(gameTime);
}

Let's take a closer look at this new code.

The first line of the Draw method simply clears the frame buffer, filling it with blue color.

The next line creates the World matrix. In this case, the World matrix is set as a matrix of rotation over Y axis to make the picture more "live". The rotation angle is equal to the amount of seconds passed from the moment of application launch. We didn't set any translation in the World matrix, it means that the model will be placed in the center of the coordinate system.

After that, we set the View matrix. The camera is positioned a little away from the center of the coordinate system in the direction of the observer and is directed to the center of the coordinate system.

Right after that, we create the Projection matrix. In this case, we set very common values: 45 degrees field of view, aspect ratio taken from the current viewport. Distances to near and far planes are set so that our model should be within the view area.

Now when all major matrixes are set, we call the Draw method of the model which renders the model to the screen.

We should see the following picture on the emulator screen:

7

Now, just to make the picture look nicer, we will change our matrixes a little. We position our model lower by multiplying the World matrix by the translation matrix. We will also move the camera a little closer to the model's position.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
        * Matrix.CreateTranslation(0,-0.4f,0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0,0,1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45), 
      GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    model.Draw(world, view, projection);

    base.Draw(gameTime);
}

We should get something like this:

simpleRender

This is the easiest method of rendering 3D models in the XNA Framework. As you can see, our model is not lighted and because of that, it doesn't look very nice. We will solve this problem later; now let's talk about what happens when we call the Draw method. If you are familiar with any older version of the XNA Framework, you may be surprised by the fact that there is no code that goes through all the model meshes and sets the effect parameters.

The matter is that all this code is hidden now in the Draw method of the model. On the other hand, if you use this new Draw method, you lose the possibility to adjust effect parameters.

To have this possibility again, we will apply an old method of rendering using a basic effect (this effect is actually called BasicEffect). This is how it looks:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    Matrix world = Matrix.CreateRotationY(
        (float)gameTime.TotalGameTime.TotalSeconds)
        * Matrix.CreateTranslation(0,-0.4f,0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0,0,1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45), 
      GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.PreferPerPixelLighting = true;

            effect.LightingEnabled = true;
            effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);

            effect.SpecularColor = new Vector3(1, 1, 1);
            effect.SpecularPower = 24;

            effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
            effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
            effect.DirectionalLight0.SpecularColor = new Vector3(1, 1, 1);

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;
        }
        mesh.Draw();
    }

    base.Draw(gameTime);
}

This code will take effect only if BasicEffect is set as the default effect in the properties of the Content Processor.

8

Before going further, we need to understand the code which we just created because we will need it later.

Each model consists of meshes which are positioned in the local coordinate system of the model. So before rendering each mesh, we need to transform its coordinate system to world space. The model also contains Bones which are the transformation matrixes for the meshes.

You can find the description of the Model class here: http://msdn.microsoft.com/en-us/library/dd904249.aspx.

So first of all, we need to store the model bones:

Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);

Then go through each mesh and render it with the given effect (which is usually set in the properties of the Content Processor). We also need to set all the needed parameters for those effects. The major parameters are World, View, and Projection matrix.

When setting the World matrix for the model effect, make sure you use the transformation matrix from the Bones array.

foreach (ModelMesh mesh in model.Meshes)
{
    foreach (BasicEffect effect in mesh.Effects)
    {
        // set effect parameters
        effect.View = view;
        effect.Projection = projection;
        effect.World = transforms[mesh.ParentBone.Index] * world;
    }
    mesh.Draw();
}

3D effects for Windows Phone 7 in the XNA Framework

XNA Framework for Windows and Xbox 360 fully supports custom shader effects, but for Windows Phone 7, only five built-in effects are supported now. In this part of the article, we will go though each of these effects.

BasicEffect

We already saw BasicEffect at work in the last example, now we are going to look at this effect in detail because this effect is used most often. It's also the default effect in the XNA Game Studio.

BasicEffect implements Blinn-Phong light model with up to three directional lights both per-vertex and per-pixel. It supports models with or without textures.

Let's start with positioning the light sources.

We will have one red light source to left, one blue to the right, and a green light source just in front of our model. Actually, we need to set the direction (not position) of the light, but here I use position just to make things a little more clear.

This is how we can do it in code, just set the DirectionalLight0, DirectionalLight1, and DirectionalLight2 parameters:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY(
        (float)gameTime.TotalGameTime.TotalSeconds)
        * Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(
         new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
         MathHelper.ToRadians(45), 
         GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.PreferPerPixelLighting = true;

            effect.LightingEnabled = true;
            effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);

            effect.SpecularColor = new Vector3(1, 1, 1);
            effect.SpecularPower = 24;

            // Set direction of light here, not position!
            effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
            effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
            effect.DirectionalLight0.SpecularColor = new Vector3(1, 0, 0);
            effect.DirectionalLight0.Enabled = true;

            // Set direction of light here, not position!
            effect.DirectionalLight1.Direction = new Vector3(0, -1, -1);
            effect.DirectionalLight1.DiffuseColor = new Vector3(0, 1, 0);
            effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
            effect.DirectionalLight1.Enabled = true;

            // Set direction of light here, not position!
            effect.DirectionalLight2.Direction = new Vector3(-1, -1, 0);
            effect.DirectionalLight2.DiffuseColor = new Vector3(0, 0, 1);
            effect.DirectionalLight2.SpecularColor = new Vector3(0, 0, 1);
            effect.DirectionalLight2.Enabled = true;

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;
        }
        mesh.Draw();
    }
 
    base.Draw(gameTime);
}

The following picture shows the result:

Another cool thing with BasicEffect is the Fog effect. This might be used, for example, in scenes in an open space where objects which are far enough from the camera should be a little shaded. Fog in BasicEffect is controlled by four parameters: FogColor, FogEnabled, FogStart, and FogEnd. FogEnabled enables or disables fog, and FogColor is just the color of the fog. The meaning of FogStart and FogEnd parameters might be better understood by looking at the following picture:

To see the fog effect in a scene with multiple models, first we will extract a method which will render a single teapot. We also set the parameters of the fog there.

private void DrawTeapot(Matrix world, Matrix view, Matrix projection, Matrix[] transforms)
{
    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.PreferPerPixelLighting = true;

            effect.LightingEnabled = true;
            effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);

            effect.SpecularColor = new Vector3(1, 1, 1);
            effect.SpecularPower = 24;

            effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
            effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
            effect.DirectionalLight0.SpecularColor = new Vector3(1, 0, 0);
            effect.DirectionalLight0.Enabled = true;

            effect.DirectionalLight1.Direction = new Vector3(0, -1, -1);
            effect.DirectionalLight1.DiffuseColor = new Vector3(0, 1, 0);
            effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
            effect.DirectionalLight1.Enabled = true;

            effect.DirectionalLight2.Direction = new Vector3(-1, -1, 0);
            effect.DirectionalLight2.DiffuseColor = new Vector3(0, 0, 1);
            effect.DirectionalLight2.SpecularColor = new Vector3(0, 0, 1);
            effect.DirectionalLight2.Enabled = true;

            effect.FogEnabled = true;
            effect.FogColor = new Vector3(0.1f, 0.1f, 0.1f); // Dark grey
            effect.FogStart = 2;
            effect.FogEnd = 5;

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;
        }
        mesh.Draw();
    }
}

Now we can render a few teapots with a different distance from the camera.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
        * Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    DrawTeapot(world, view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -1), view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -2), view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -3), view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -4), view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -5), view, projection, transforms);
    DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -6), view, projection, transforms);

    base.Draw(gameTime);
}

In the following picture, you can see that the fog intensity grows with the distance from the camera:

DualTextureEffect

DualTextureEffect is a simple multi-texturing effect. It allows you to apply two textures to the model. It also supports fog like BasicEffect. DualTextureEffect is a good effect because of its "cheapness" for hardware (compared to other effects), and it might be used for applying light maps, masks, decals, tiling, and so on.

DualTextureEffect uses 2X modulation where the color of each pixel is calculated with such a formula:

Color = Texture1.rgb * Texture2.rgb * 2

This means that the grey color in Texture2 will not change the base color, any darker color will make the pixel darker, and so on.

Let's look at simple example of DualTextureEffect usage; here we will do a nice global illumination effect. Creating shadows in real-time is always a hard operation for hardware; also, real-time shadows usually have some visual artifacts, which is not good either. What can we do is calculate light maps during the modeling stage and use those nice light maps in our game.

Here is an image of scene lighting from my modeling tool:

There is one point light in front of two adjacent boxes and a directional light to the left. DualTextureEffect requires two texture coordinate channels for the model. I exported the light map and created a diffuse texture:

First of all, we will render our model with BasicEffect just to make sure everything is fine.

Model model;
Texture2D lightMap;
Texture2D diffuseMap;

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    model = Content.Load<Model<("boxes");
    lightMap = Content.Load<Texture2D>("light");
    diffuseMap = Content.Load<Texture2D>("diffuse");
}

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY(
          MathHelper.ToRadians(30)) * Matrix.CreateScale(0.003f)
        * Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.TextureEnabled = true;
            effect.Texture = diffuseMap;

            // uncomment this to render with lightmap
            //effect.Texture = lightMap;

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;
        }
         mesh.Draw();
    }

    base.Draw(gameTime);
}

Rendering a model with two different textures will give us the following pictures:

Let's start using DualTextureEffect. First, we will need to set the correct Content Processor parameters. Go to the parameters of the Model in the Content Project and set Content Processor -> Default Effect to DualTextureEffect.

Another thing which we will do prior to rendering is creating a special 1x1 pixel texture with grey color. We will use it temporarily to disable one of the textures.

Texture2D grey;
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    model = Content.Load<Model>("boxes");
    lightMap = Content.Load<Texture2D>("light");
    diffuseMap = Content.Load<Texture2D>("diffuse");

    grey = new Texture2D(GraphicsDevice, 1, 1);
    grey.SetData(new Color[] { new Color(128, 128, 128, 255) });
}

In the Draw method, we will use DualTextureEffect instead of BasicEffect.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY(
          MathHelper.ToRadians(30)) * Matrix.CreateScale(0.003f) * 
          Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(
          new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
           MathHelper.ToRadians(45), 
           GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);


    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (DualTextureEffect effect in mesh.Effects)
        {
            effect.Texture = diffuseMap;
            effect.Texture2 = lightMap;

            // uncomment this to hide diffuse map
            //effect.Texture = grey;
            // uncomment this to hide light map
            //effect.Texture2 = grey;

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;

        }
        mesh.Draw();
    }
    base.Draw(gameTime);
}

Here is the result:

AlphaTestEffect

AlphaTestEffect might be hard to understand if you are not familiar with computer graphics; on the other hand, it might be very helpful in certain situations like using billboards. AlphaTestEffect simply draws a texture but skips pixels which don't pass the alpha test.

Let's look at the example. Imagine we need to render a lot of instances of the same model on the screen. And it is usually a hard task for the hardware.

First of all, we will create a simple frame per second counter to watch the performance.

SpriteFont font;
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    font = Content.Load<SpriteFont>("defaultFont");

}

protected override void Draw(GameTime gameTime)
{
    // TODO: Add your drawing code here

    GraphicsDevice.Clear(Color.Black);
    
    float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
    if (seconds > 0)
    {
        spriteBatch.Begin();
        spriteBatch.DrawString(font, Window.Title = (1f / seconds).ToString(), 
                               Vector2.Zero, Color.White);
        spriteBatch.End();

        // set GraphicsDevice parameters to default after spritebatch work
        GraphicsDevice.BlendState = BlendState.Opaque;
        GraphicsDevice.DepthStencilState = DepthStencilState.Default;
        GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
        GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
    }

    base.Draw(gameTime);
}

We should see something around 30 because that's what we have set in the constructor of the Game1 class. This is not the best way to make a FPS counter. It's much better to count frames which were actually rendered during the last second.

The following method will render 900 models:

private void SlowDraw(ref Matrix world, ref Matrix view, 
                      ref Matrix projection, Matrix[] transforms)
{
    for (float i = -2; i <= 2; i += 0.5f)
    {
        for (float j = 0; j < 100; j++)
        {
                DrawTeapot(world * Matrix.CreateTranslation(i, 0, -j), 
                           view, projection, transforms);
        }
    }
}

You should see a significant performance decrease.

An effective trick might be used in such situations:

  • Render the model only once to a separate texture
  • Copy this texture everywhere you need

But in the second step, we will face the problem of blending: Only a part of the texture which represents our model should be drawn while other pixels should be skipped. That's where AlphaTestEffect can be helpful. All transparent pixels from the texture will not appear on the actual screen (they also should not appear in the depth buffer).

The following source code creates a separate render target and renders the teapot model there:

RenderTarget2D renderTarget;
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    model = Content.Load<Model>("teapot");
    font = Content.Load<SpriteFont>("defaultFont");

    renderTarget = new RenderTarget2D(GraphicsDevice, 512, 512, 
                       false, SurfaceFormat.Color, DepthFormat.Depth24);
}

private void DrawToRenderTarget(ref Matrix world, ref Matrix view, 
             ref Matrix projection, Matrix[] transforms)
{
    // save main render target
    RenderTargetBinding[] previousRenderTargets = GraphicsDevice.GetRenderTargets();

    GraphicsDevice.SetRenderTarget(renderTarget);
    
    // fill with transparent color before rendering model
    GraphicsDevice.Clear(Color.Transparent);
    DrawTeapot(world, view, projection, transforms);

    // restore render target
    GraphicsDevice.SetRenderTargets(previousRenderTargets);
}

Now when we have a texture with a single teapot in renderTarget, we will create a geometry for billboards which will be cloned all over the screen. We will also create an AlphaTestEffect variable and set its parameters so that only pixels with alpha greater than some value (let's say, 128) will be shown.

AlphaTestEffect alphaTestEffect;
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    model = Content.Load<Model>("teapot");
    font = Content.Load<SpriteFont>("defaultFont");

    renderTarget = new RenderTarget2D(GraphicsDevice, 512, 512, 
                       false, SurfaceFormat.Color, DepthFormat.Depth24);
    alphaTestEffect = new AlphaTestEffect(GraphicsDevice);
    alphaTestEffect.AlphaFunction = CompareFunction.Greater;
    alphaTestEffect.ReferenceAlpha = 128;

}
private void DrawBillboards(Matrix world, Vector3 cameraPosition, 
             Vector3 cameraTarget, Matrix view, Matrix projection)
{
    int count = 900;

    float width = 0.3f;
    float height1 = 0.9f;
    float height2 = -0.1f;

    // Create billboard vertices.
    VertexPositionTexture[] vertices = new VertexPositionTexture[count * 4];
    int index = 0;

    for (float i = -2; i <= 2; i += 0.5f)
    {
        for (float j = 0; j < 100; j++)
        {
            Matrix worldMatrix = world * Matrix.CreateTranslation(i, 0, -j);

            Matrix billboard = Matrix.CreateConstrainedBillboard(
              worldMatrix.Translation, cameraPosition, Vector3.Up, 
              cameraTarget - cameraPosition, null);

            vertices[index].Position = 
              Vector3.Transform(new Vector3(width, height1, 0), billboard);
            vertices[index++].TextureCoordinate = new Vector2(0, 0);

            vertices[index].Position = 
              Vector3.Transform(new Vector3(-width, height1, 0), billboard);
            vertices[index++].TextureCoordinate = new Vector2(1, 0);

            vertices[index].Position = 
              Vector3.Transform(new Vector3(-width, height2, 0), billboard);
            vertices[index++].TextureCoordinate = new Vector2(1, 1);

            vertices[index].Position = 
              Vector3.Transform(new Vector3(width, height2, 0), billboard);
            vertices[index++].TextureCoordinate = new Vector2(0, 1);
        }
    }
           
    // Create billboard indices.
    short[] indices = new short[count * 6];
    short currentVertex = 0;
    index = 0;

    while (index < indices.Length)
    {
        indices[index++] = currentVertex;
        indices[index++] = (short)(currentVertex + 1);
        indices[index++] = (short)(currentVertex + 2);

        indices[index++] = currentVertex;
        indices[index++] = (short)(currentVertex + 2);
        indices[index++] = (short)(currentVertex + 3);

        currentVertex += 4;
    }
 
    // Draw the billboard sprites.
    alphaTestEffect.World = Matrix.Identity;
    alphaTestEffect.View = view;
    alphaTestEffect.Projection = projection;
    alphaTestEffect.Texture = renderTarget;

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

    GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionTexture>(
       PrimitiveType.TriangleList, vertices, 0, 
       count * 4, indices, 0, count * 2);
}
protected override void Draw(GameTime gameTime)
{
    // TODO: Add your drawing code here

    Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
        * Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
       MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    DrawToRenderTarget(ref world, ref view, ref projection, transforms);

    GraphicsDevice.Clear(Color.Black);
    DrawBillboards(world, new Vector3(0, 0, 1.2f), Vector3.Zero, view, projection);


    float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
    if (seconds > 0)
    {
        spriteBatch.Begin();
        spriteBatch.DrawString(font, Window.Title = (1f / seconds).ToString(), 
                               Vector2.Zero, Color.White);
        spriteBatch.End();

        // set GraphicsDevice parameters to default after spritebatch work
        GraphicsDevice.BlendState = BlendState.Opaque;
        GraphicsDevice.DepthStencilState = DepthStencilState.Default;
        GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
        GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
    }

    base.Draw(gameTime);
}

As you can see, the frame rate returned to 30.

SkinnedEffect

SkinnedEffect is an effect which is used with animated 3D models. It supports all visual effects from BasicEffect like up to three lights, fog, and texture mapping. Moreover, it supports skinning data for models.

The bad thing about SkinnedEffect is that it doesn't support model animation out of the box. To achieve this goal, we need to use a special Content Processor. It might be found here: http://create.msdn.com/en-US/education/catalog/sample/skinned_model.

Now we will create a simple example based on the application from the App Hub.

  • Download SkinnedSample_4_0
  • Add the SkinnedModel and SkinnedModelPipeline projects to the new solution in Visual Studio
  • Add dude.fbx and all the textures from the Content folder of the SkinningSample project to your project
  • Add dude.fbx to the ContentProject in Visual Studio (don't add textures, just add them to the same folder as the model)
  • Set SkinnedModelProcessor as the ContentProcessor for dude.fbx

Now everything is set up and we are ready to write some code.

Actually, it will be almost similar to the BasicEffect example except the fact that here we will have an AnimationPlayer which will animate our model.

Model currentModel;
AnimationPlayer animationPlayer;
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    currentModel = Content.Load<Model>("dude");

    // Look up our custom skinning information.
    SkinningData skinningData = currentModel.Tag as SkinningData;

    if (skinningData == null)
        throw new InvalidOperationException
            ("This model does not contain a SkinningData tag.");

    // Create an animation player, and start decoding an animation clip.
    animationPlayer = new AnimationPlayer(skinningData);

    AnimationClip clip = skinningData.AnimationClips["Take 001"];

    animationPlayer.StartClip(clip);
}
protected override void Update(GameTime gameTime)
{
    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();

    // TODO: Add your update logic here
    animationPlayer.Update(gameTime.ElapsedGameTime, true, Matrix.Identity);

    base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    // TODO: Add your drawing code here
    Matrix[] bones = animationPlayer.GetSkinTransforms();

    Matrix world = Matrix.CreateScale(0.007f) * 
      Matrix.CreateRotationY(MathHelper.Pi) * Matrix.CreateTranslation(0,-0.2f,0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1), Vector3.Zero, Vector3.Up);

    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);

    // Render the skinned mesh.
    foreach (ModelMesh mesh in currentModel.Meshes)
    {
        foreach (SkinnedEffect effect in mesh.Effects)
        {
            effect.SetBoneTransforms(bones);

            effect.World = world;
            effect.View = view;
            effect.Projection = projection;

            effect.EnableDefaultLighting();

            effect.SpecularColor = new Vector3(0.25f);
            effect.SpecularPower = 16;

        }

        mesh.Draw();
    }

    base.Draw(gameTime);
}

SkinnedEffect can not do animation for all your models. The animation must be prepared in a 3D modeling tool for each model.

EnvironmentMapEffect

EnvironmentMapEffect is another great effect supported by the XNA Framework. It allows you to easily apply an environment map onto the model.

An environment map should be represented by a cube map which might be created dynamically at run time or prepared in some external tool (for example, DirectX Texture Tool from DirectX SDK) and saved into DDS format prior to loading into the game.

An environment cube map stores six separate textures each representing the projection of the environment from one side of the object which should be used for environment mapping. A cube map should look like this:

To apply EnvironmentMapEffect to the model, set the Default Effect property of the Content Processor of the model to EnvironmentMapEffect.

Our code for rendering will be almost the same as earlier for BasicEffect. Moreover, EnvironmentMapEffect supports almost all effects from BasicEffect like fog, directional lights, etc.

Model model;
TextureCube envMap;

Texture2D background;
Texture2D bunnyTexture;
 protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    model = Content.Load<Model>("bunny");
    envMap = Content.Load<TextureCube>("env");
    background = Content.Load<Texture2D>("back");

    bunnyTexture = Content.Load<Texture2D>("metal1");
}
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    // Draw background
     spriteBatch.Begin();
    spriteBatch.Draw(background, new Rectangle(0, 0, 480, 800), Color.White);
    spriteBatch.End();

    // Restore default parameters for GraphicsDevice
    GraphicsDevice.BlendState = BlendState.Opaque;
    GraphicsDevice.DepthStencilState = DepthStencilState.Default;
    GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
    GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

    Matrix world = Matrix.CreateRotationY(MathHelper.PiOver4)
                * Matrix.CreateTranslation(0, -0.4f, 0);
    Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (EnvironmentMapEffect effect in mesh.Effects)
        {
            effect.EnableDefaultLighting();


            effect.EnvironmentMap = envMap;
            effect.Texture = bunnyTexture;

            effect.View = view;
            effect.Projection = projection;
            effect.World = transforms[mesh.ParentBone.Index] * world;
        }
        mesh.Draw();
    }

    base.Draw(gameTime);
}

Our bunny might look a little too shiny. The amount of reflection may be changed through the EnvironmentMapAmount parameter of the effect.

For example, 0.5 means that the base texture and the environment map will be blended 50/50.

EnvironmentMapEffect also supports the Fresnel reflectivity effect which make things more photorealistic in some cases.

Another nice trick with EnvironmentMapEffect is its ability to simulate complex lighting of the scene using the alpha channel of the environment cube map (described in detail at http://blogs.msdn.com/b/shawnhar/archive/2010/08/09/environmentmapeffect.aspx).

Summary

The XNA Framework is a powerful tool for the creation of great 3D games for Windows Phone 7, Windows, and Xbox 360. In this article, we reviewed the basics of 3D graphics programming for Windows Phone 7, and I hope it will help you in your future projects.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here