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.
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.
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).
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:
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.
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:
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.
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:
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.
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.
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).
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:
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);
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:
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.
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)
{
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);
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, 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.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); 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);
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()
{
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);
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;
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()
{
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);
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;
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()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("defaultFont");
}
protected override void Draw(GameTime gameTime)
{
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();
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()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
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)
{
RenderTargetBinding[] previousRenderTargets = GraphicsDevice.GetRenderTargets();
GraphicsDevice.SetRenderTarget(renderTarget);
GraphicsDevice.Clear(Color.Transparent);
DrawTeapot(world, view, projection, transforms);
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()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
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;
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);
}
}
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;
}
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)
{
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();
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()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
currentModel = Content.Load<Model>("dude");
SkinningData skinningData = currentModel.Tag as SkinningData;
if (skinningData == null)
throw new InvalidOperationException
("This model does not contain a SkinningData tag.");
animationPlayer = new AnimationPlayer(skinningData);
AnimationClip clip = skinningData.AnimationClips["Take 001"];
animationPlayer.StartClip(clip);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
animationPlayer.Update(gameTime.ElapsedGameTime, true, Matrix.Identity);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
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);
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()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
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);
spriteBatch.Begin();
spriteBatch.Draw(background, new Rectangle(0, 0, 480, 800), Color.White);
spriteBatch.End();
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.