Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 20, Plug-in Manager)

Introduction

Welcome to Part 20 of the XNA Framework GameEngine Development series.  This is the first article of my new game engine series Roe3. 

This article will focus on creating a plug-in architecture for game components and managers.

Creating the plug-in socket

While writing this series I found a few instances when I wanted to be able to turn off a component or manager in order to test or even to create a game that didn’t use all of bells and whistles that the engine had to offer.  I normally would go into the base engine code and remove the components rebuild and move on.  Of course this works, but it is not exactly a very elegant solution.

So I created a configuration management system that would allow me to instantiate managers and components using an xml file that looks something like the following.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <components>
    <component value="RoeEngine3.GameComponents.Cursor"/>
    <component value="RoeEngine3.GameComponents.FpsCounter"/>
    <component value="RoeEngine3.GameComponents.DebugOverlay"/>
  </components>
  <managers>
    <manager value="RoeEngine3.Managers.AudioManager"/>
    <manager value="RoeEngine3.Managers.ScreenManager"/>
  </managers>
</configuration>

Much nicer!  In this example there are 3 components and 2 managers that will be created as the engine starts up… without the need to recompile the engine.  Don’t want to show a cursor or fpscounter?  Simply remove the component from the config.

So all we need to do is create an xml reader for this file, something like the following.

namespace RoeEngine3.Managers
{
    [XmlType("configuration")]
    public sealed class ConfigurationManager
    {
        public static ConfigurationManager Instance { get; private set; }

        static ConfigurationManager()
        {
            if(Instance == null)
            {
                string file = @"Managers.config";

                if(!File.Exists(file))
                {
                    throw new FileNotFoundException();
                }

                using(StreamReader reader = new StreamReader(file))
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationManager));
                    Instance = (ConfigurationManager)serializer.Deserialize(reader);
                }
            }
        }

        [XmlArray("managers"), XmlArrayItem("manager")]
        public Managers Managers { get; set; }

        [XmlArray("components"), XmlArrayItem("component")]
        public Components Components { get; set; }
    }

    public sealed class Components : KeyedCollection<string, Component>
    {
        protected override string GetKeyForItem(Component item)
        {
            return item.Value;
        }
    }

    public sealed class Managers : KeyedCollection<string, Manager>
    {
        protected override string GetKeyForItem(Manager item)
        {
            return item.Value;
        }
    }

    [XmlType("component")]
    public sealed class Component
    {
        [XmlAttribute("value")]
        public string Value { get; set; }
    }

    [XmlType("manager")]
    public sealed class Manager
    {
        [XmlAttribute("value")]
        public string Value { get; set; }
    }
}

Now we have a list of Managers and Components that we want to activate in the engine.  So in our base engine class we need to write some prep code for our managers and components.

        private void PrepareComponents()
        {
            foreach (Component component in ConfigurationManager.Instance.Components)
            {
                GameComponent baseComponent = ComponentActivator.CreateInstance(Type.GetType(component.Value)) as GameComponent;
                EngineManager.Game.Components.Add(baseComponent);
            }
        }
       
        private void PrepareManagers()
        {    
            foreach (Manager manager in ConfigurationManager.Instance.Managers)
            {
                GameComponent baseManager = ManagerActivator.CreateInstance(Type.GetType(manager.Value)) as GameComponent;              
                Components.Add(baseManager);
            }
        }
        protected override void Initialize()
        {
            PrepareManagers();
            PrepareComponents();

     ... // OTHER INITIALIZE STUFF GOES HERE
 }

Turn on the Power

All that is left for us to do is activate the managers and components.  Just need to create some reflection magic to invoke them.

    public sealed class ManagerActivator
    {
        public static object CreateInstance(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("Manager Type");
            }

            ConstructorInfo info = type.GetConstructor(new Type[] { typeof(Game) });

            if (info == null)
            {
                return null;
            }

            object instance;

            try
            {
                instance = info.Invoke(new object[] { EngineManager.Game });
            }
            catch (Exception ex)
            {
                if (ex is SystemException)
                {
                    throw;
                }
                instance = null;
            }
            return instance;
        }
    }

    public sealed class ComponentActivator
    {
        public static object CreateInstance(Type type)
        {
            return ManagerActivator.CreateInstance(type);
        }
    }

A few side notes here, if you notice the GetConstructor object passes in a Type array of type Game.  This is because all GameComponents have a constructor parameter of the Game.  Your components will obviously need to subscribe to the correct GetConstructor types.

Conclusion

In this article I outlined a simple method of creating a pluggable interface for components and managers.  This interface will allow us to turn on and off components and managers as we need them without writing additional code.

December 17, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 22 Comments

XNA Framework GameEngine Development. (Part 19, Hardware Instancing PC Only)

Introduction

Welcome to Part19 of the XNA FrameWork GameEngine Development series.  In this article I will discuss the magic that is Hardware instancing.  Recent comments have expressed some concerns about poor performance in the engine.  One way to improve performance in the engine is to support instancing.

part19.jpg

Hardware instancing

This is by far the easiest method of instancing, I will discuss shader and vertex fetch instancing in later articles.  Essentially hardware instancing tells the graphics card that you want to draw an object multiple times in the same draw call.  Obviously, this will improve performance since one of the slowest calls you will ever make to a graphics device is draw.  We should always look to draw the most geometry in the fewest draw calls possible.

hardwareinstancing.png

Image courtesy MS XNA Team Site

Since shader model 3.0 we have at our disposal the concept of hardware instancing.  This method of instancing allows us to set up a vertex and index buffer to be drawn multiple times using an array of matrices.  This means pretty much any piece of complex geometry that uses the same effect settings can and should be drawn at the same time.

Starting the Magic

We need a way to identify a normal Model from an instanced model.  I do this by adding a few properties to the RoeModel class.  The ModelParts list will contain information about what else… model parts, and the Instanced proeprty will define if the model was created for instancing.  Finally, using the constructor parameter instanced will allow us to load the content and set up the model parts for instance rendering.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Managers;

namespace RoeEngine2.Models
{
    public class RoeModel : IRoeModel
    {
        public List<RoeModelPart> ModelParts = new List<RoeModelPart>();

        private bool _instanced;
        public bool Instanced
        {
            get { return _instanced; }
        }
 
        private string _fileName;
        /// <summary>
        /// The file name of the asset.
        /// </summary>
        public string FileName
        {
            get { return _fileName; }
            set { _fileName = value; }
        }

        private Model _baseModel;
        public Model BaseModel
        {
            get { return _baseModel; }
        }

        private bool _readyToRender = false;
        ///<summary>
        ///Is the texture ready to be rendered.
        ///</summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
        }

        public RoeModel(string fileName, bool instanced)
        {
            _instanced = instanced;
            _fileName = fileName;
        }

        /// <summary>
        /// Construct a new RoeModel.
        /// </summary>
        /// <param name="fileName">The asset file name.</param>
        public RoeModel(string fileName)
        {
            _fileName = fileName;
        }

        public void LoadContent()
        {
            if (!String.IsNullOrEmpty(_fileName))
            {
                _baseModel = EngineManager.ContentManager.Load<Model>(_fileName);
                if (_instanced)
                {
                    foreach (ModelMesh mesh in _baseModel.Meshes)
                    {
                        foreach (ModelMeshPart part in mesh.MeshParts)
                        {
                            ModelParts.Add(new RoeModelPart(part, mesh.VertexBuffer, mesh.IndexBuffer));
                        }
                    }
                }
                _readyToRender = true;
            }
        }
    }
}

Most of the work will happen in the model parts clss, but here you can see how a model is loaded as normal, then if it is instanced we do additional work to load the specific parts.  Please note, we do not have to use any custom processors to create the model parts.  For me this is a huge improvement over the example given on the team development site.

Backstage pass

This is were all the work takes place, here in the RoeModelPart class.  Each mesh part will be set up for prime GPU processing here, plus we will extend the vertex declaration to include the instancing magic.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Managers;

namespace RoeEngine2.Models
{
    public class RoeModelPart
    {
        private int _primitiveCount;
        public int PrimitiveCount
        {
            get { return _primitiveCount; }
        }

        private int _vertexCount;
        public int VertexCount
        {
            get { return _vertexCount; }
        }

        private int _vertexStride;
        public int VertexStride
        {
            get { return _vertexStride; }
        }

        private VertexDeclaration _vertexDeclaration;
        public VertexDeclaration VertexDeclartion
        {
            get { return _vertexDeclaration; }
        }

        private VertexBuffer _vertexBuffer;
        public VertexBuffer VertexBuffer
        {
            get { return _vertexBuffer; }
        }

        private IndexBuffer _indexBuffer;
        public IndexBuffer IndexBuffer
        {
            get { return _indexBuffer; }
        }

        VertexElement[] originalVertexDeclaration;
 
        internal RoeModelPart(ModelMeshPart part, VertexBuffer vertexBuffer, IndexBuffer indexBuffer)
        {
            _primitiveCount = part.PrimitiveCount;
            _vertexCount = part.NumVertices;
            _vertexStride = part.VertexStride;
            _vertexDeclaration = part.VertexDeclaration;

            _vertexBuffer = vertexBuffer;
            _indexBuffer = indexBuffer;

            originalVertexDeclaration = part.VertexDeclaration.GetVertexElements();

            InitializeHardwareInstancing();
        }

        private void InitializeHardwareInstancing()
        {
            // When using hardware instancing, the instance transform matrix is
            // specified using a second vertex stream that provides 4x4 matrices
            // in texture coordinate channels 1 to 4. We must modify our vertex
            // declaration to include these channels.
            VertexElement[] extraElements = new VertexElement[4];

            short offset = 0;
            byte usageIndex = 1;
            short stream = 1;

            const int sizeOfVector4 = sizeof(float) * 4;

            for (int i = 0; i < extraElements.Length; i++)
            {
                extraElements[i] = new VertexElement(stream, offset,
                                                VertexElementFormat.Vector4,
                                                VertexElementMethod.Default,
                                                VertexElementUsage.TextureCoordinate,
                                                usageIndex);

                offset += sizeOfVector4;
                usageIndex++;
            }

            ExtendVertexDeclaration(extraElements);
        }

        private void ExtendVertexDeclaration(VertexElement[] extraElements)
        {
            // Get rid of the existing vertex declaration.
            _vertexDeclaration.Dispose();

            // Append the new elements to the original format.
            int length = originalVertexDeclaration.Length + extraElements.Length;

            VertexElement[] elements = new VertexElement[length];

            originalVertexDeclaration.CopyTo(elements, 0);

            extraElements.CopyTo(elements, originalVertexDeclaration.Length);

            // Create a new vertex declaration.
            _vertexDeclaration = new VertexDeclaration(EngineManager.Device, elements);
        }
    }
}

The Beautiful Assistant

Now that we have a mesh broken up into its basic parts for fast GPU rendering, we must find a way to store matrices in a simple and easy to manage location.  I accomplish this using the SceneGraphManager.  This class will now support a dictionary keyed by string and support a list of matrices.  In addition, we also need a way to load up the dictionary and finally draw the instances.

        private static Dictionary<string, List<Matrix>> instanceMatrices;

        private static void DrawInstances(GameTime gameTime)
        {
            instancedshaderEffect effect = ShaderManager.GetShader("instance") as instancedshaderEffect;
            if (effect.ReadyToRender)
            {
                effect.View = CameraManager.ActiveCamera.View;
                effect.Projection = CameraManager.ActiveCamera.Projection;

                foreach (string key in instanceMatrices.Keys)
                {
                    RoeModel model = ModelManager.GetModel(key) as RoeModel;
                    if (model.ReadyToRender)
                    {
                        Model meshModel = model.BaseModel;
                        
                        foreach (RoeModelPart part in model.ModelParts)
                        {
                            EngineManager.Device.VertexDeclaration = part.VertexDeclartion;
                            EngineManager.Device.Vertices[0].SetSource(part.VertexBuffer, 0, part.VertexStride);
                            EngineManager.Device.Indices = part.IndexBuffer;
                            effect.BaseEffect.Begin();
                            foreach (EffectPass pass in effect.BaseEffect.CurrentTechnique.Passes)
                            {
                                pass.Begin();
                                DrawHardwareInstancing(instanceMatrices[key].ToArray(), part.VertexCount, part.PrimitiveCount);
                                pass.End();
                            }
                            effect.BaseEffect.End();
                        }
                    }
                }
            }
        }

        private static void DrawHardwareInstancing(Matrix[] matrix, int vertexCount, int primitiveCount)
        {
            const int sizeofMatrix = sizeof(float) * 16;
            int instanceDataSize = sizeofMatrix * matrix.Length;

            DynamicVertexBuffer instanceDataStream = new DynamicVertexBuffer(EngineManager.Device,
                                                                             instanceDataSize,
                                                                             BufferUsage.WriteOnly);

            instanceDataStream.SetData(matrix, 0, matrix.Length, SetDataOptions.Discard);

            VertexStreamCollection vertices = EngineManager.Device.Vertices;

            vertices[0].SetFrequencyOfIndexData(matrix.Length);

            vertices[1].SetSource(instanceDataStream, 0, sizeofMatrix);
            vertices[1].SetFrequencyOfInstanceData(1);

            EngineManager.Device.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                                       0, 0, vertexCount, 0, primitiveCount);

            // Reset the instancing streams.
            vertices[0].SetSource(null, 0, 0);
            vertices[1].SetSource(null, 0, 0);
        }

MisDirection

Now that we have a simple way to store instance objects and render them, we need to load up the dictionary.  This is done in the SceneObjectNode class.  As usual, we do not want to render objects that are culled.  In addition, if we are using instancing we need to bypass all of the occlusion work.  This is a tradeoff that we have to make, either we use occlusion or we use instancing.

        public override void DrawCulling(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable)
            {
                ((IRoeCullable)SceneObject).Culled = false;
                if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint)
                {
                    ((IRoeCullable)SceneObject).Culled = true;
                }
                else if (ModelManager.GetModel(SceneObject.ModelName).Instanced)
                {
                    SceneGraphManager.AddInstance(SceneObject.ModelName, SceneObject.World);
                }
                else
                {
                    SceneObject.DrawCulling(gameTime);
                }
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject.ModelName != null && ModelManager.GetModel(SceneObject.ModelName).Instanced)
            {
                return;
            }
            else if (SceneObject is IRoeCullable && ((IRoeCullable)SceneObject).Culled)
            {
                SceneGraphManager.Culled++;
            }
            else if (SceneObject is IRoeOcclusion && ((IRoeOcclusion)SceneObject).Occluded)
            {
                SceneGraphManager.Occluded++;
            }
            else
            {
                SceneObject.Draw(gameTime);
            }
        }

The Grand Finale

Finally, our dictionary is loaded and we are ready to draw, time for some shader work.

#define MAX_SHADER_MATRICES 60

// Array of instance transforms used by the VFetch and ShaderInstancing techniques.
float4x4 instanceTransforms[MAX_SHADER_MATRICES];

// Camera settings.
float4x4 view;
float4x4 projection;

// This sample uses a simple Lambert lighting model.
float3 lightDirection = normalize(float3(-1, -1, -1));
float3 diffuseLight = 1.25;
float3 ambientLight = 0.25;

struct VS_INPUT
{
 float4 Position : POSITION0;
 float3 Normal : NORMAL;
 float2 TexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
 float4 Position     : POSITION;
 float4 Color  : COLOR0;
 float2 TexCoord : TEXCOORD0;
};

VS_OUTPUT VertexShaderCommon(VS_INPUT input, float4x4 instanceTransform)
{
    VS_OUTPUT output;

    // Apply the world and camera matrices to compute the output position.
    float4 worldPosition = mul(input.Position, instanceTransform);
    float4 viewPosition = mul(worldPosition, view);
    output.Position = mul(viewPosition, projection);

    // Compute lighting, using a simple Lambert model.
    float3 worldNormal = mul(input.Normal, instanceTransform);
    
    float diffuseAmount = max(-dot(worldNormal, lightDirection), 0);
    
    float3 lightingResult = saturate(diffuseAmount * diffuseLight + ambientLight);
    
    output.Color = float4(lightingResult, 1);

    // Copy across the input texture coordinate.
    output.TexCoord = input.TexCoord;

    return output;
};

// On Windows shader 3.0 cards, we can use hardware instancing, reading
// the per-instance world transform directly from a secondary vertex stream.
VS_OUTPUT HardwareInstancingVertexShader(VS_INPUT input,
                                         float4x4 instanceTransform : TEXCOORD1)
{
    return VertexShaderCommon(input, transpose(instanceTransform));
}

// All the different instancing techniques share this same pixel shader.
float4 PixelShaderFunction(VS_OUTPUT input) : COLOR0
{
    return input.Color;
}

// Windows instancing technique for shader 3.0 cards.
technique HardwareInstancing
{
    pass Pass1
    {
        VertexShader = compile vs_3_0 HardwareInstancingVertexShader();
        PixelShader = compile ps_3_0 PixelShaderFunction();
    }
}

 

March 17, 2008 Posted by | C#, XNA | 26 Comments

XNA Framework GameEngine Development. (Part 18, Brute Force Terrain with Physics)

Introduction

Welcome to Part18 of the XNA Framework GameEngine Development series.  In this article I will introduce terrain rendering concepts.  All good outdoor games need terrain as a primary building block, so it is essential to render terrain as realistically and as performant as possible.  There are dozens of algorithmic approaches to terrain rendering, I could literally spend an entire series discussing terrain.  Here I will discuss some of my favorite terrain rendering methods and maybe a few of your own if you leave suggestions.

part18.jpg

Release

Here is the sourcecode.

Content Pipeline

The XNA content pipeline allows us to create custom processors for data.  I use the content pipeline to convert a 2D heightmap into a 3D model.  This is a well known technique for creating simple terrain, but I will describe it here briefly.

  • Create a greyscale image with black being low laying areas and white being high altitude areas.
  • Assume the world is a square lattice with vertices at precise intervals in the lattice.
  • The height at each point in the lattice is set as the pixel in the greyscale image.
  • Connect the vertices as a triangle strip.

Create a mesh from any greyscale image that is a power of 2 + 1 and voila you have a simple brute force heightmap.

BruteForceTerrainProcessor

Create a new Content Pipeline project.  I will be expanding on this project later, add the following content processor code.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.Texture2DContent;
using TOutput = Microsoft.Xna.Framework.Content.Pipeline.Processors.ModelContent;

namespace RoeEngine2ContentPipeline
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// TODO: change the ContentProcessor attribute to specify the correct
    /// display name for this processor.
    /// </summary>
    [ContentProcessor(DisplayName = "BruteForceTerrainProcessor")]
    public class BruteForceTerrainProcessor : ContentProcessor<TInput, TOutput>
    {
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            MeshBuilder builder = MeshBuilder.StartMesh("Terrain");

            input.ConvertBitmapType(typeof(PixelBitmapContent<float>));

            PixelBitmapContent<float> heightfield;
            heightfield = (PixelBitmapContent<float>)input.Mipmaps[0];

            for (int y = 0; y < heightfield.Height; y++)
            {
                for (int x = 0; x < heightfield.Width; x++)
                {
                    Vector3 position;

                    position.X = (x - heightfield.Width / 2);
                    position.Z = (y - heightfield.Height / 2);
                    position.Y = (heightfield.GetPixel(x, y) - 1);

                    builder.CreatePosition(position);
                }
            }

            int texCoordId = builder.CreateVertexChannel<Vector2>(
                            VertexChannelNames.TextureCoordinate(0));

            for (int y = 0; y < heightfield.Height - 1; y++)
            {
                for (int x = 0; x < heightfield.Width - 1; x++)
                {
                    AddVertex(builder, texCoordId, heightfield.Width, x, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y + 1);

                    AddVertex(builder, texCoordId, heightfield.Width, x, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y + 1);
                    AddVertex(builder, texCoordId, heightfield.Width, x, y + 1);
                }
            }

            MeshContent terrain = builder.FinishMesh();

            ModelContent model = context.Convert<MeshContent, ModelContent>(terrain, "ModelProcessor");

            model.Tag = new HeightMapContent(heightfield);

            return model;
        }

        /// <summary>
        /// Helper for adding a new triangle vertex to a MeshBuilder,
        /// along with an associated texture coordinate value.
        /// </summary>
        static void AddVertex(MeshBuilder builder, int texCoordId, int w, int x, int y)
        {
            builder.SetVertexChannelData(texCoordId, new Vector2(x, y) / w);

            builder.AddTriangleVertex(x + y * w);
        }
    }
}

Now we need a way to write and read data that we want to store in the model.Tag field.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace RoeEngine2ContentPipeline
{
    public class HeightMapContent
    {
        private float[,] _height;

        public float[,] Height
        {
            get { return _height; }
            set { _height = value; }
        }

        public HeightMapContent(PixelBitmapContent<float> bitmap)
        {
            _height = new float[bitmap.Width, bitmap.Height];
            for (int y = 0; y < bitmap.Height; y++)
            {
                for (int x = 0; x < bitmap.Width; x++)
                {
                    // the pixels will vary from 0 (black) to 1 (white).
                    // by subtracting 1, our heights vary from -1 to 0, which we then
                    // multiply by the "bumpiness" to get our final height.
                    _height[x, y] = (bitmap.GetPixel(x, y) - 1);
                }
            }
        } 
    }

    /// <summary>
    /// A TypeWriter for HeightMapInfo, which tells the content pipeline how to save the
    /// data in HeightMapInfo. This class should match HeightMapInfoReader: whatever the
    /// writer writes, the reader should read.
    /// </summary>
    [ContentTypeWriter]
    public class HeightMapInfoWriter : ContentTypeWriter<HeightMapContent>
    {
        protected override void Write(ContentWriter output, HeightMapContent value)
        {
            output.Write(value.Height.GetLength(0));
            output.Write(value.Height.GetLength(1));
            foreach (float height in value.Height)
            {
                output.Write(height);
            }
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(HeightMapReader).AssemblyQualifiedName;
        }

        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(HeightMap).AssemblyQualifiedName;
        }
    }

    public class HeightMap
    {
        public float[,] Heights;

        public HeightMap(float[,] heights)
        {
            Heights = heights;
        }
    }

    public class HeightMapReader : ContentTypeReader<HeightMap>
    {
        protected override HeightMap Read(ContentReader input, HeightMap existingInstance)
        {
            int width = input.ReadInt32();
            int height = input.ReadInt32();
            float[,] heights = new float[width, height];

            for (int x = 0; x < width; x++)
            {
                for (int z = 0; z < height; z++)
                {
                    heights[x, z] = input.ReadSingle();
                }
            }
            return new HeightMap(heights);
        }
    }
}

I chose to create the reader and writer in the same namespace, this makes getting the runtime reader and type much easier.

Creating Terrain SceneObjects

Now that we have a content processor, we can now import the model and render it in our game.  Include a reference to the content pipeline in both the content reference and in the reference section of the main engine project.

Create a BruteForceTerrain SceneObject

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.SceneObject.BaseObjects;
using RoeEngine2.Interfaces;
using JigLibX.Physics;
using JigLibX.Collision;
using Microsoft.Xna.Framework;
using RoeEngine2.Models;
using RoeEngine2.Managers;
using JigLibX.Utils;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2ContentPipeline;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class BruteForceTerrain: OccluderSceneObject, IRoePhysics
    {
        private Body _body;
        public Body Body
        {
            get { return _body; }
        }

        private CollisionSkin _collisionSkin;
        public CollisionSkin CollisionSkin
        {
            get { return _collisionSkin; }
        } 

        public BruteForceTerrain()
        {
            _body = new Body();
            _collisionSkin = new CollisionSkin(null);

            RoeModel terrainModel = new RoeModel("Content/Models/TerrainRoE");
            ModelManager.AddModel(terrainModel, "terrainModel");

            this.ModelName = "terrainModel";
            this.OcclusionModelName = "terrainModel";
        }

        public BruteForceTerrain(Vector3 newPosition)
        {
            Position = newPosition;

            _body = new Body();
            _collisionSkin = new CollisionSkin(null);

            RoeModel boxmodel = new RoeModel("Content/Models/Box");

            ModelManager.AddModel(boxmodel, "boxmodel");

            this.ModelName = "boxmodel";
            this.OcclusionModelName = "boxmodel";
        }

        public Vector3 SetMass(float mass)
        {
            return Vector3.Zero;
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                HeightMap heightMap = model.BaseModel.Tag as HeightMap;

                Array2D field = new Array2D(heightMap.Heights.GetUpperBound(0), heightMap.Heights.GetUpperBound(1));

                for (int x = 0; x < heightMap.Heights.GetUpperBound(0); x++)
                {
                    for (int z = 0; z < heightMap.Heights.GetUpperBound(1); z++)
                    {
                        field.SetAt(x, z, heightMap.Heights[x, z] * Scale.Y + Position.Y );
                    }
                }

                _body.MoveTo(Position, Matrix.Identity);

                _collisionSkin.AddPrimitive(new JigLibX.Geometry.Heightmap(field, Position.X, Position.Z, Scale.X, Scale.Z),
                                            (int)MaterialTable.MaterialID.UserDefined,
                                            new MaterialProperties(0.7f, 0.7f, 0.6f));

                PhysicsSystem.CurrentPhysicsSystem.CollisionSystem.AddCollisionSkin(_collisionSkin);

                ReadyToRender = true;
            }
        }

        public override void UnloadContent()
        {
            PhysicsManager.RemoveObject(this);
        }

        public override string ToString()
        {
            return "Brute Force Terrain";
        }
    }
}

Simple, now we can use this new class to render the terrain object.

            BruteForceTerrain terrain = new BruteForceTerrain();
            terrain.Position = new Vector3(0f, -15f, 0f);
            terrain.Scale = new Vector3(10, 10, 10);
            SceneGraphManager.AddObject(terrain);

Conclusion

In this article I introduced the content pipeline and simple brute force terrain.  This article will be the kickoff for a terrain rendering sub series.  I look forward to your comments and suggestions.

March 12, 2008 Posted by | C#, XBOX360, XNA | 9 Comments

XNA Framework GameEngine Development. (Part 17, Hoffman Atmosphere)

Introduction

Welcome to Part17 of the XNA Framework GameEngine Development series.  In this article I will be introducing non-renderable scene objects to the engine.   Non-renderable scene objects are a unique subset of the SceneObject class that contain no geometry.  This is not necessarily interesting, accept that they are loaded into the Scene Management system as a normal renderable scene object and do take their turn with the renderer.  Mainly, I use this type of scene object to set up special shader commands, enter the Hoffman scattering algorithm.

part17.jpg

Release

Here is the code, I really want to hear your ideas to make the engine better, please leave a comment.

Hoffman Light Scattering

Hoffman/Preetham light scattering is an algorithmic approach to model how light will deflect off gas particles in the atmosphere.  The main goal of the algorithm is to define two angles of approach for light particles entering the eye.  In the Hoffman simulation these angles are determined analytically based on some pretty intense research that is beyond the scope of this series.  What this article will focus on is making it look good and render fast in a gameing situation.

Non-Geometric SceneObjects

This is a new concept in the engine, essentially a Non-Geometric SceneObject is exactly that, a scene object that does not have geometry to render.  This type of scene object will give us access to use all of the events that a normal scene object would have, but without actually sending polygons to the graphics card.  As a side effect, which I will show in an upcoming article, this class of sceneobject will allow us to share shader information between geometric sceneobjects.
Now that we have this new concept available to use we can create Atmosphere objects.  An atmoshpere object will be used to set up outdoor lighting and fogging, almost every object in an outdoor environment can use the atmosphere to render major light reflections.

Enter the HoffmanAtmosphere, this Non-Geometric Sceneobject will set up all of the maths necessary to compute the two attenuation angles required in the Hoffman light scattering algorithm, as well as store some important variables.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.Shaders;
using RoeEngine2.Managers;

#if !XBOX360
using System.ComponentModel;
using RoeEngine2.Helpers;
#endif

namespace RoeEngine2.SceneObject.NonGeometricObjects
{
    public class HoffmanAtmosphere : RoeSceneObject
    {
        private float _sunDirection = 0;
        [PropertyAttribute(typeof(float), 0, 180, 1)]
        public float SunDirection
        {
            get { return _sunDirection; }
            set
            {
                _sunDirection = value;
                if (_sunDirection > 180.0f)
                    _sunDirection = 0;
                else if (_sunDirection < 0.0f)
                    _sunDirection = 0;
                Position = new Vector3(0.0f, (float)Math.Sin(MathHelper.ToRadians(value)), (float)Math.Cos(MathHelper.ToRadians(value)));
            }
        }

        private float _sunIntensity = 1.0f;
        [PropertyAttribute(typeof(float), 0, 10, 1)]
        public float SunIntensity
        {
            get { return _sunIntensity; }
            set { _sunIntensity = value; }
        }

        private float _turbitity = 1.0f;
        [PropertyAttribute(typeof(float), 1, 10, 1)]
        public float Turbitity
        {
            get { return _turbitity; }
            set { _turbitity = value; }
        }

        private Vector3 _hGg = new Vector3(0.9f, 0.9f, 0.9f);
        public Vector3 HGg
        {
            get { return _hGg; }
            set { _hGg = value; }
        }

        private float _inscatteringMultiplier = 1.0f;
        [PropertyAttribute(typeof(float), 0, 10, 1)]
        public float InscatteringMultiplier
        {
            get { return _inscatteringMultiplier; }
            set { _inscatteringMultiplier = value; }
        }

        private float _betaRayMultiplier = 8.0f;
        [PropertyAttribute(typeof(float), 0, 20, 1)]
        public float BetaRayMultiplier
        {
            get { return _betaRayMultiplier; }
            set { _betaRayMultiplier = value; }
        }

        private float _betaMieMultiplier = 0.00005f;
        [PropertyAttribute(typeof(float), 0, 10, 100000)]
        public float BetaMieMultiplier
        {
            get { return _betaMieMultiplier; }
            set { _betaMieMultiplier = value; }
        }

        private Vector3 _betaRPlusBetaM;
        private Vector3 _betaDashR;
        private Vector3 _betaDashM;
        private Vector3 _oneOverBetaRPlusBetaM;
        private Vector4 _multipliers;
        private Vector4 _sunColorAndIntensity;

        private Vector3 _betaRay;
        private Vector3 _betaDashRay;
        private Vector3 _betaMie;
        private Vector3 _betaDashMie;

        public override string ToString()
        {
            return "Atmosphere";
        }

        public HoffmanAtmosphere()
        {
            hoffmanshaderEffect effect = new hoffmanshaderEffect();

            ShaderManager.AddShader(effect, "hoffman");
            Material.Shader = "hoffman";

            ReadyToRender = true;

            SunDirection = 0;

            const float n = 1.0003f;
            const float N = 2.545e25f;
            const float pn = 0.035f;

            float[] lambda = new float[3];
            float[] lambda2 = new float[3];
            float[] lambda4 = new float[3];

            lambda[0] = 1.0f / 650e-9f;   // red
            lambda[1] = 1.0f / 570e-9f;   // green
            lambda[2] = 1.0f / 475e-9f;   // blue

            for (int i = 0; i < 3; ++i)
            {
                lambda2[i] = lambda[i] * lambda[i];
                lambda4[i] = lambda2[i] * lambda2[i];
            }

            Vector3 vLambda2 = new Vector3(lambda2[0], lambda2[1], lambda2[2]);
            Vector3 vLambda4 = new Vector3(lambda4[0], lambda4[1], lambda4[2]);

            // Rayleigh scattering constants

            const float temp = (float)(Math.PI * Math.PI * (n * n - 1.0f) * (n * n - 1.0f) * (6.0f + 3.0f * pn) / (6.0f - 7.0f * pn) / N);
            const float beta = (float)(8.0f * temp * Math.PI / 3.0f);

            _betaRay = beta * vLambda4;

            const float betaDash = temp / 2.0f;

            _betaDashRay = betaDash * vLambda4;

            // Mie scattering constants

            const float T = 2.0f;
            const float c = (6.544f * T - 6.51f) * 1e-17f;
            const float temp2 = (float)(0.434f * c * (2.0f * Math.PI) * (2.0f * Math.PI) * 0.5f);

            _betaDashMie = temp2 * vLambda2;

            float[] K = new float[3] { 0.685f, 0.679f, 0.670f };
            const float temp3 = (float)(0.434f * c * Math.PI * (2.0f * Math.PI) * (2.0f * Math.PI));

            Vector3 vBetaMieTemp = new Vector3(K[0] * lambda2[0], K[1] * lambda2[1], K[2] * lambda2[2]);

            _betaMie = temp3 * vBetaMieTemp;
        }

        public override void Draw(GameTime gameTime)
        {
            if (!ReadyToRender)
            {
                ReadyToRender = ShaderManager.GetShader("hoffman").ReadyToRender;
            }
            else
            {
                Vector3 vZenith = new Vector3(0.0f, 1.0f, 0.0f);

                float thetaS = Vector3.Dot(Position, vZenith);
                thetaS = (float)(Math.Acos(thetaS));

                ComputeAttenuation(thetaS);
                SetMaterialProperties();
            }
        }

        private void ComputeAttenuation(float thetaS)
        {
            float beta = 0.04608365822050f * _turbitity - 0.04586025928522f;
            float tauR, tauA;
            float[] fTau = new float[3];
            float m = (float)(1.0f / (Math.Cos(thetaS) + 0.15f * Math.Pow(93.885f - thetaS / Math.PI * 180.0f, -1.253f)));  // Relative Optical Mass
            float[] lambda = new float[3] { 0.65f, 0.57f, 0.475f };

            for (int i = 0; i < 3; ++i)
            {
                // Rayleigh Scattering
                // lambda in um.
                tauR = (float)(Math.Exp(-m * 0.008735f * Math.Pow(lambda[i], -4.08f)));

                // Aerosal (water + dust) attenuation
                // beta - amount of aerosols present
                // alpha - ratio of small to large particle sizes. (0:4,usually 1.3)
                const float alpha = 1.3f;
                tauA = (float)(Math.Exp(-m * beta * Math.Pow(lambda[i], -alpha)));  // lambda should be in um

                fTau[i] = tauR * tauA;
            }

            _sunColorAndIntensity = new Vector4(fTau[0], fTau[1], fTau[2], _sunIntensity * 100.0f);

        }

        private void SetMaterialProperties()
        {
            float reflectance = 0.1f;

            Vector3 vecBetaR = _betaRay * _betaRayMultiplier;
            _betaDashR = _betaDashRay * _betaRayMultiplier;
            Vector3 vecBetaM = _betaMie * _betaMieMultiplier;
            _betaDashM = _betaDashMie * _betaMieMultiplier;
            _betaRPlusBetaM = vecBetaR + vecBetaM;
            _oneOverBetaRPlusBetaM = new Vector3(1.0f / _betaRPlusBetaM.X, 1.0f / _betaRPlusBetaM.Y, 1.0f / _betaRPlusBetaM.Z);
            Vector3 vecG = new Vector3(1.0f - _hGg.X * _hGg.X, 1.0f + _hGg.X * _hGg.X, 2.0f * _hGg.X);
            _multipliers = new Vector4(_inscatteringMultiplier, 0.138f * reflectance, 0.113f * reflectance, 0.08f * reflectance);

            hoffmanshaderEffect hoffmanShader = ShaderManager.GetShader(Material.Shader) as hoffmanshaderEffect;

            hoffmanShader.SunDirection = Position;
            hoffmanShader.BetaRPlusBetaM = _betaRPlusBetaM;
            hoffmanShader.HGg = HGg;
            hoffmanShader.BetaDashR = _betaDashR;
            hoffmanShader.BetaDashM = _betaDashM;
            hoffmanShader.OneOverBetaRPlusBetaM = _oneOverBetaRPlusBetaM;
            hoffmanShader.Multipliers = _multipliers;
            hoffmanShader.SunColorAndIntensity = _sunColorAndIntensity;
        }
    }
}

Introduction to HLSL – YEAH RIGHT!

I feel pretty bad about using this shader as the first example of High Level Shader Language (HLSL), I will most likely write a sub-series of articles to help introduce shaders when I am finished with the major pieces of the engine.  This shader has been discussed at length on the GameDev.net site so I will not bore you with the details here, ignore the terrain stuff for now, I will be demonstrating that soon.

float4x4 worldViewProject;
float4x4 worldView;

float3 sunDirection;
float3 betaRPlusBetaM;
float3 hGg;
float3 betaDashR;
float3 betaDashM;
float3 oneOverBetaRPlusBetaM;
float4 multipliers;
float4 sunColorAndIntensity;

float3 groundCursorPosition;
bool showGroundCursor;

texture terrainAlpha, terrainBreakup, terrainOne, terrainTwo, terrainThree, terrainFour, groundCursor;

sampler samplergroundCursor = sampler_state
{
   Texture = <groundCursor>;
   ADDRESSU = CLAMP;
   ADDRESSV = CLAMP;
   MAGFILTER = LINEAR;
   MINFILTER = LINEAR;
   MIPFILTER = LINEAR;
};

sampler samplerAlpha = sampler_state
{
 texture = <terrainAlpha>;
 MINFILTER = ANISOTROPIC;
 MAGFILTER = ANISOTROPIC;
 MIPFILTER = ANISOTROPIC;
};

sampler samplerOne = sampler_state
{
 texture = <terrainOne>;
 MINFILTER = ANISOTROPIC;
 MAGFILTER = ANISOTROPIC;
 MIPFILTER = ANISOTROPIC;
 ADDRESSU = WRAP;
 ADDRESSV = WRAP;
};

sampler samplerTwo = sampler_state
{
 texture = <terrainTwo>;
 MINFILTER = ANISOTROPIC;
 MAGFILTER = ANISOTROPIC;
 MIPFILTER = ANISOTROPIC;
 ADDRESSU = WRAP;
 ADDRESSV = WRAP;
};

sampler samplerThree = sampler_state
{
 texture = <terrainThree>;
 MINFILTER = ANISOTROPIC;
 MAGFILTER = ANISOTROPIC;
 MIPFILTER = ANISOTROPIC;
 ADDRESSU = WRAP;
 ADDRESSV = WRAP;
};

sampler samplerFour = sampler_state
{
 texture = <terrainFour>;
 MINFILTER = ANISOTROPIC;
 MAGFILTER = ANISOTROPIC;
 MIPFILTER = ANISOTROPIC;
 ADDRESSU = WRAP;
 ADDRESSV = WRAP;
};

float4 constants = { 0.25f, 1.4426950f, 0.5f, 0.0f };

struct VS_INPUT
{
 float4 Position : POSITION0;
 float3 Normal : NORMAL;
 float2 TexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
 float4 Position     : POSITION;
 float2 TerrainCoord : TEXCOORD0;
 float3 Normal  : TEXCOORD1;
 float3 Lin          : COLOR0;
 float3 Fex          : COLOR1;
};

VS_OUTPUT HoffmanShader(VS_INPUT Input)
{ 
 float4 worldPos = mul(Input.Position, worldView);
 float3 viewDir = normalize(worldPos.xyz);
 float distance = length(worldPos.xyz);
 
 float3 sunDir= normalize(mul(float4(sunDirection, 0.0), worldView ).xyz);
 
 float theta = dot(sunDir, viewDir);
 
 // 
 // Phase1 and Phase2
 //

 float phase1 = 1.0 + theta * theta;
 float phase2 = pow( rsqrt( hGg.y - hGg.z * theta ), 3 ) * hGg.x;

 //
 // Extinction term
 //

     float3 extinction      = exp( -betaRPlusBetaM * distance * constants.x );
     float3 totalExtinction = extinction * multipliers.yzw;
    
 //
 // Inscattering term
 //

 float3 betaRay = betaDashR * phase1;
 float3 betaMie = betaDashM * phase2;

 float3 inscatter = (betaRay + betaMie) * oneOverBetaRPlusBetaM * (1.0 - extinction);

 //
 // Apply inscattering contribution factors
 //

 inscatter *= multipliers.x;
 //
 // Scale with sun color & intensity
 //

 inscatter       *= sunColorAndIntensity.xyz * sunColorAndIntensity.w;
 totalExtinction *= sunColorAndIntensity.xyz * sunColorAndIntensity.w;

 VS_OUTPUT Output;
 Output.Position = mul(Input.Position, worldViewProject);
  Output.TerrainCoord = Input.TexCoord.xy;
  Output.Normal = Input.Normal;
 Output.Lin = inscatter;
 Output.Fex = totalExtinction;

 return Output;
};

struct PS_INPUT
{
 float4 Position : POSITION;
 float2 TerrainCoord : TEXCOORD0;
 float3 Normal : TEXCOORD1;
 float3 Lin : COLOR0;
 float3 Fex : COLOR1;
};

float4 SkyShader(PS_INPUT Input) : COLOR0
{
 return float4(Input.Lin, 1.0f);
};

float4 TerrainShader(PS_INPUT Input) : COLOR0
{
 Input.Normal = normalize(Input.Normal);

 vector alphaSamp = tex2D(samplerAlpha, Input.TerrainCoord);
 vector oneSamp = tex2D(samplerOne, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector twoSamp = tex2D(samplerTwo, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector threeSamp = tex2D(samplerThree, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector fourSamp = tex2D(samplerFour, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 
 float4 tester1 = 1.0 - alphaSamp.a;
 float4 tester2 = 1.0 - alphaSamp.b;
 float4 tester3 = 1.0 - alphaSamp.g;
 float4 tester4 = 1.0 - alphaSamp.r;
 
 float4 tester = lerp(threeSamp, oneSamp, saturate(dot(float3(0, 1, 0), Input.Normal) * 2));
 
 vector l = alphaSamp.a * oneSamp + tester1 * tester;
 vector m = alphaSamp.b * twoSamp + tester2 * l;
 vector o = alphaSamp.g * threeSamp + tester3 * m;
 vector p = alphaSamp.r * fourSamp + tester4 * o;
  
 float4 albedo = saturate((dot(normalize(sunDirection), Input.Normal) + .9f)) * p;
 
 albedo *= float4(Input.Fex, 1.0f);
 albedo += float4(Input.Lin, 1.0f);
 
 return albedo;
};

float4 TerrainShaderWithCursor(PS_INPUT Input) : COLOR0
{
 Input.Normal = normalize(Input.Normal);

 vector alphaSamp = tex2D(samplerAlpha, Input.TerrainCoord);
 vector oneSamp = tex2D(samplerOne, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector twoSamp = tex2D(samplerTwo, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector threeSamp = tex2D(samplerThree, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 vector fourSamp = tex2D(samplerFour, float2(Input.TerrainCoord.x * 2048, Input.TerrainCoord.y * 2048));
 
 float4 tester1 = 1.0 - alphaSamp.a;
 float4 tester2 = 1.0 - alphaSamp.b;
 float4 tester3 = 1.0 - alphaSamp.g;
 float4 tester4 = 1.0 - alphaSamp.r;
 
 float4 tester0 = lerp(threeSamp, oneSamp, saturate(dot(float3(0, 1, 0), Input.Normal) * 2));
 
 vector l = alphaSamp.a * oneSamp + tester1 * tester0;
 vector m = alphaSamp.b * twoSamp + tester2 * l;
 vector o = alphaSamp.g * threeSamp + tester3 * m;
 vector p = alphaSamp.r * fourSamp + tester4 * o;
  
 float4 albedo = saturate((dot(normalize(sunDirection), Input.Normal) + .9f)) * p;
 
 albedo *= float4(Input.Fex, 1.0f);
 albedo += float4(Input.Lin, 1.0f);
 
 if(showGroundCursor)
 {
  float cursorScale = 40.0f;
  albedo += tex2D(samplergroundCursor,
   (Input.TerrainCoord * (cursorScale)) -
   (groundCursorPosition.xz * (cursorScale)) + 0.5f);
 }
 
 return albedo;
};

float4 Wireframe( PS_INPUT Input ) : COLOR0
{
    return float4( 1, 1, 1, 1 );
};

technique Sky
{
 pass P0
 {
  CullMode = CCW;
  FillMode = Solid;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_3_0 SkyShader(); 
 }
};

technique SkyWireframe
{
 pass P0
 {
  CullMode = CCW;
  FillMode = Solid;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_3_0 SkyShader(); 
 }
 pass P1
 {
  CullMode = CCW;
  FillMode = Wireframe;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_1_1 Wireframe();
 }
};

technique Terrain
{
 pass P0
 {
  CullMode = CCW;
  FillMode = Solid;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_3_0 TerrainShader();  
 }
};

technique TerrainWithCursor
{
 pass P0
 {
  CullMode = CCW;
  FillMode = Solid;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_3_0 TerrainShaderWithCursor();  
 }
};

technique TerrainWireframe
{
 pass P0
 {
  CullMode = CCW;
  FillMode = Solid;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_3_0 TerrainShader(); 
  
 }
  pass P1
 {
  CullMode = CCW;
  FillMode = Wireframe;
  VertexShader = compile vs_3_0 HoffmanShader();
  PixelShader = compile ps_1_1 Wireframe();
 }
};

Now that we have a working shader, it is time to create our first shader proxy.  Remember way back to Part4 of this series, we created an effect code generator.  Pretty much that is an executable that will create a shader file for us when we drag a .fx file onto the executable.  You will get something like the following.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Interfaces;

namespace RoeEngine2.Shaders
{
 public class hoffmanshaderEffect : IRoeShader
 {
  public enum Techniques
  {
   Sky,
   SkyWireframe,
   Terrain,
   TerrainWithCursor,
   TerrainWireframe,
  }

  private Effect _baseEffect;
  ///<summary>
  ///Gets the underlying Effect.
  ///</summary>
  public Effect BaseEffect
  {
   get { return _baseEffect; }
  }

  private bool _readyToRender = false;
  ///<summary>
  ///Is the shader ready to be rendered.
  ///</summary>
  public bool ReadyToRender
  {
   get { return _readyToRender; }
  }

  #region Effect Parameters

  private EffectParameter _worldViewProjectParam;
  public Matrix WorldViewProject
  {
   get
   {
    if (_worldViewProjectParam == null)
     throw new Exception("Cannot get value of WorldViewProject; WorldViewProject EffectParameter is null.");
    return _worldViewProjectParam.GetValueMatrix();
   }
   set
   {
    if (_worldViewProjectParam == null)
     throw new Exception("Cannot set value of WorldViewProject; WorldViewProject EffectParameter is null.");
    _worldViewProjectParam.SetValue(value);
   }
  }

  private EffectParameter _worldViewParam;
  public Matrix WorldView
  {
   get
   {
    if (_worldViewParam == null)
     throw new Exception("Cannot get value of WorldView; WorldView EffectParameter is null.");
    return _worldViewParam.GetValueMatrix();
   }
   set
   {
    if (_worldViewParam == null)
     throw new Exception("Cannot set value of WorldView; WorldView EffectParameter is null.");
    _worldViewParam.SetValue(value);
   }
  }

  private EffectParameter _sunDirectionParam;
  public Vector3 SunDirection
  {
   get
   {
    if (_sunDirectionParam == null)
     throw new Exception("Cannot get value of SunDirection; SunDirection EffectParameter is null.");
    return _sunDirectionParam.GetValueVector3();
   }
   set
   {
    if (_sunDirectionParam == null)
     throw new Exception("Cannot set value of SunDirection; SunDirection EffectParameter is null.");
    _sunDirectionParam.SetValue(value);
   }
  }

  private EffectParameter _betaRPlusBetaMParam;
  public Vector3 BetaRPlusBetaM
  {
   get
   {
    if (_betaRPlusBetaMParam == null)
     throw new Exception("Cannot get value of BetaRPlusBetaM; BetaRPlusBetaM EffectParameter is null.");
    return _betaRPlusBetaMParam.GetValueVector3();
   }
   set
   {
    if (_betaRPlusBetaMParam == null)
     throw new Exception("Cannot set value of BetaRPlusBetaM; BetaRPlusBetaM EffectParameter is null.");
    _betaRPlusBetaMParam.SetValue(value);
   }
  }

  private EffectParameter _hGgParam;
  public Vector3 HGg
  {
   get
   {
    if (_hGgParam == null)
     throw new Exception("Cannot get value of HGg; HGg EffectParameter is null.");
    return _hGgParam.GetValueVector3();
   }
   set
   {
    if (_hGgParam == null)
     throw new Exception("Cannot set value of HGg; HGg EffectParameter is null.");
    _hGgParam.SetValue(value);
   }
  }

  private EffectParameter _betaDashRParam;
  public Vector3 BetaDashR
  {
   get
   {
    if (_betaDashRParam == null)
     throw new Exception("Cannot get value of BetaDashR; BetaDashR EffectParameter is null.");
    return _betaDashRParam.GetValueVector3();
   }
   set
   {
    if (_betaDashRParam == null)
     throw new Exception("Cannot set value of BetaDashR; BetaDashR EffectParameter is null.");
    _betaDashRParam.SetValue(value);
   }
  }

  private EffectParameter _betaDashMParam;
  public Vector3 BetaDashM
  {
   get
   {
    if (_betaDashMParam == null)
     throw new Exception("Cannot get value of BetaDashM; BetaDashM EffectParameter is null.");
    return _betaDashMParam.GetValueVector3();
   }
   set
   {
    if (_betaDashMParam == null)
     throw new Exception("Cannot set value of BetaDashM; BetaDashM EffectParameter is null.");
    _betaDashMParam.SetValue(value);
   }
  }

  private EffectParameter _oneOverBetaRPlusBetaMParam;
  public Vector3 OneOverBetaRPlusBetaM
  {
   get
   {
    if (_oneOverBetaRPlusBetaMParam == null)
     throw new Exception("Cannot get value of OneOverBetaRPlusBetaM; OneOverBetaRPlusBetaM EffectParameter is null.");
    return _oneOverBetaRPlusBetaMParam.GetValueVector3();
   }
   set
   {
    if (_oneOverBetaRPlusBetaMParam == null)
     throw new Exception("Cannot set value of OneOverBetaRPlusBetaM; OneOverBetaRPlusBetaM EffectParameter is null.");
    _oneOverBetaRPlusBetaMParam.SetValue(value);
   }
  }

  private EffectParameter _multipliersParam;
  public Vector4 Multipliers
  {
   get
   {
    if (_multipliersParam == null)
     throw new Exception("Cannot get value of Multipliers; Multipliers EffectParameter is null.");
    return _multipliersParam.GetValueVector4();
   }
   set
   {
    if (_multipliersParam == null)
     throw new Exception("Cannot set value of Multipliers; Multipliers EffectParameter is null.");
    _multipliersParam.SetValue(value);
   }
  }

  private EffectParameter _sunColorAndIntensityParam;
  public Vector4 SunColorAndIntensity
  {
   get
   {
    if (_sunColorAndIntensityParam == null)
     throw new Exception("Cannot get value of SunColorAndIntensity; SunColorAndIntensity EffectParameter is null.");
    return _sunColorAndIntensityParam.GetValueVector4();
   }
   set
   {
    if (_sunColorAndIntensityParam == null)
     throw new Exception("Cannot set value of SunColorAndIntensity; SunColorAndIntensity EffectParameter is null.");
    _sunColorAndIntensityParam.SetValue(value);
   }
  }

  private EffectParameter _groundCursorPositionParam;
  public Vector3 GroundCursorPosition
  {
   get
   {
    if (_groundCursorPositionParam == null)
     throw new Exception("Cannot get value of GroundCursorPosition; GroundCursorPosition EffectParameter is null.");
    return _groundCursorPositionParam.GetValueVector3();
   }
   set
   {
    if (_groundCursorPositionParam == null)
     throw new Exception("Cannot set value of GroundCursorPosition; GroundCursorPosition EffectParameter is null.");
    _groundCursorPositionParam.SetValue(value);
   }
  }

  private EffectParameter _showGroundCursorParam;
  public bool ShowGroundCursor
  {
   get
   {
    if (_showGroundCursorParam == null)
     throw new Exception("Cannot get value of ShowGroundCursor; ShowGroundCursor EffectParameter is null.");
    return _showGroundCursorParam.GetValueBoolean();
   }
   set
   {
    if (_showGroundCursorParam == null)
     throw new Exception("Cannot set value of ShowGroundCursor; ShowGroundCursor EffectParameter is null.");
    _showGroundCursorParam.SetValue(value);
   }
  }

  private EffectParameter _terrainAlphaParam;
  public Texture2D TerrainAlpha
  {
   get
   {
    if (_terrainAlphaParam == null)
     throw new Exception("Cannot get value of TerrainAlpha; TerrainAlpha EffectParameter is null.");
    return _terrainAlphaParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainAlphaParam == null)
     throw new Exception("Cannot set value of TerrainAlpha; TerrainAlpha EffectParameter is null.");
    _terrainAlphaParam.SetValue(value);
   }
  }

  private EffectParameter _terrainBreakupParam;
  public Texture2D TerrainBreakup
  {
   get
   {
    if (_terrainBreakupParam == null)
     throw new Exception("Cannot get value of TerrainBreakup; TerrainBreakup EffectParameter is null.");
    return _terrainBreakupParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainBreakupParam == null)
     throw new Exception("Cannot set value of TerrainBreakup; TerrainBreakup EffectParameter is null.");
    _terrainBreakupParam.SetValue(value);
   }
  }

  private EffectParameter _terrainOneParam;
  public Texture2D TerrainOne
  {
   get
   {
    if (_terrainOneParam == null)
     throw new Exception("Cannot get value of TerrainOne; TerrainOne EffectParameter is null.");
    return _terrainOneParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainOneParam == null)
     throw new Exception("Cannot set value of TerrainOne; TerrainOne EffectParameter is null.");
    _terrainOneParam.SetValue(value);
   }
  }

  private EffectParameter _terrainTwoParam;
  public Texture2D TerrainTwo
  {
   get
   {
    if (_terrainTwoParam == null)
     throw new Exception("Cannot get value of TerrainTwo; TerrainTwo EffectParameter is null.");
    return _terrainTwoParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainTwoParam == null)
     throw new Exception("Cannot set value of TerrainTwo; TerrainTwo EffectParameter is null.");
    _terrainTwoParam.SetValue(value);
   }
  }

  private EffectParameter _terrainThreeParam;
  public Texture2D TerrainThree
  {
   get
   {
    if (_terrainThreeParam == null)
     throw new Exception("Cannot get value of TerrainThree; TerrainThree EffectParameter is null.");
    return _terrainThreeParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainThreeParam == null)
     throw new Exception("Cannot set value of TerrainThree; TerrainThree EffectParameter is null.");
    _terrainThreeParam.SetValue(value);
   }
  }

  private EffectParameter _terrainFourParam;
  public Texture2D TerrainFour
  {
   get
   {
    if (_terrainFourParam == null)
     throw new Exception("Cannot get value of TerrainFour; TerrainFour EffectParameter is null.");
    return _terrainFourParam.GetValueTexture2D();
   }
   set
   {
    if (_terrainFourParam == null)
     throw new Exception("Cannot set value of TerrainFour; TerrainFour EffectParameter is null.");
    _terrainFourParam.SetValue(value);
   }
  }

  private EffectParameter _groundCursorParam;
  public Texture2D GroundCursor
  {
   get
   {
    if (_groundCursorParam == null)
     throw new Exception("Cannot get value of GroundCursor; GroundCursor EffectParameter is null.");
    return _groundCursorParam.GetValueTexture2D();
   }
   set
   {
    if (_groundCursorParam == null)
     throw new Exception("Cannot set value of GroundCursor; GroundCursor EffectParameter is null.");
    _groundCursorParam.SetValue(value);
   }
  }

  private EffectParameter _constantsParam;
  public Vector4 Constants
  {
   get
   {
    if (_constantsParam == null)
     throw new Exception("Cannot get value of Constants; Constants EffectParameter is null.");
    return _constantsParam.GetValueVector4();
   }
   set
   {
    if (_constantsParam == null)
     throw new Exception("Cannot set value of Constants; Constants EffectParameter is null.");
    _constantsParam.SetValue(value);
   }
  }

  #endregion

  #region Effect Techniques

  private EffectTechnique _SkyTechnique;

  private EffectTechnique _SkyWireframeTechnique;

  private EffectTechnique _TerrainTechnique;

  private EffectTechnique _TerrainWithCursorTechnique;

  private EffectTechnique _TerrainWireframeTechnique;

  #endregion

  #region Initialize Methods

  ///<summary>
  ///Initializes the Effect from byte code for the given GraphicsDevice.
  ///</summary
  ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
  public void Initialize(GraphicsDevice graphicsDevice)
  {
   Initialize(graphicsDevice, CompilerOptions.None, null);
  }

  ///<summary>
  ///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.
  ///</summary
  ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
  ///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
  public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)
  {
   Initialize(graphicsDevice, compilerOptions, null);
  }

  ///<summary>
  ///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.
  ///</summary
  ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
  ///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
  ///<param name="effectPools">The EffectPool to use with the effect.</param>
  public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)
  {
   _baseEffect = new Effect(graphicsDevice, byteCode, compilerOptions, effectPool);
   _readyToRender = true;

   _worldViewProjectParam = _baseEffect.Parameters["worldViewProject"];
   _worldViewParam = _baseEffect.Parameters["worldView"];
   _sunDirectionParam = _baseEffect.Parameters["sunDirection"];
   _betaRPlusBetaMParam = _baseEffect.Parameters["betaRPlusBetaM"];
   _hGgParam = _baseEffect.Parameters["hGg"];
   _betaDashRParam = _baseEffect.Parameters["betaDashR"];
   _betaDashMParam = _baseEffect.Parameters["betaDashM"];
   _oneOverBetaRPlusBetaMParam = _baseEffect.Parameters["oneOverBetaRPlusBetaM"];
   _multipliersParam = _baseEffect.Parameters["multipliers"];
   _sunColorAndIntensityParam = _baseEffect.Parameters["sunColorAndIntensity"];
   _groundCursorPositionParam = _baseEffect.Parameters["groundCursorPosition"];
   _showGroundCursorParam = _baseEffect.Parameters["showGroundCursor"];
   _terrainAlphaParam = _baseEffect.Parameters["terrainAlpha"];
   _terrainBreakupParam = _baseEffect.Parameters["terrainBreakup"];
   _terrainOneParam = _baseEffect.Parameters["terrainOne"];
   _terrainTwoParam = _baseEffect.Parameters["terrainTwo"];
   _terrainThreeParam = _baseEffect.Parameters["terrainThree"];
   _terrainFourParam = _baseEffect.Parameters["terrainFour"];
   _groundCursorParam = _baseEffect.Parameters["groundCursor"];
   _constantsParam = _baseEffect.Parameters["constants"];

   _SkyTechnique = _baseEffect.Techniques["Sky"];
   _SkyWireframeTechnique = _baseEffect.Techniques["SkyWireframe"];
   _TerrainTechnique = _baseEffect.Techniques["Terrain"];
   _TerrainWithCursorTechnique = _baseEffect.Techniques["TerrainWithCursor"];
   _TerrainWireframeTechnique = _baseEffect.Techniques["TerrainWireframe"];
  }

  #endregion

  ///<summary>
  ///Sets the current technique for the effect.
  ///</summary>
  ///<param name="technique">The technique to use for the current technique.</param>
  public void SetCurrentTechnique(hoffmanshaderEffect.Techniques technique)
  {
   switch (technique)
   {
    case hoffmanshaderEffect.Techniques.Sky:
     _baseEffect.CurrentTechnique = _SkyTechnique;
     break;

    case hoffmanshaderEffect.Techniques.SkyWireframe:
     _baseEffect.CurrentTechnique = _SkyWireframeTechnique;
     break;

    case hoffmanshaderEffect.Techniques.Terrain:
     _baseEffect.CurrentTechnique = _TerrainTechnique;
     break;

    case hoffmanshaderEffect.Techniques.TerrainWithCursor:
     _baseEffect.CurrentTechnique = _TerrainWithCursorTechnique;
     break;

    case hoffmanshaderEffect.Techniques.TerrainWireframe:
     _baseEffect.CurrentTechnique = _TerrainWireframeTechnique;
     break;

   }
  }
//omitted compiled byte code, takes up too much space!
 }
}

Conclusion

In this article I demonstrated the Hoffman light scattering algorithm.  In the released code, by pressing the up/down arrow, you can adjust the time of day and watch the sun go through a 12 hour cycle.

Please leave comments or suggestions, I would very much enjoy hearing ideas on how to improve on the shader here.

March 11, 2008 Posted by | C#, XBOX360, XNA | 11 Comments

XNA Framework GameEngine Development. (Part 16, Skydome)

Introduction

Welcome to Part16 of the XNA Framework GameEngine Development series.  In this article I will get back on track toward my original plan.  I am sorry if any readers felt misled by my deviation from the series plan.  From now on I will treat Physics updates and WorldBuilder updates as their own sub-series.

Now that we have created a reasonably decent rendering wrapper, we should start creating some standard objects that will be used most often in games.  I normally start my engine development off with a good skydome, mostly because the math is simple and well known (more on this silly statement soon).  Essentially, we are going to draw half a sphere on the screen, but with criteria.

  • Would prefer to use TriangleStrips over TriangleLists.
  • Would like to have control over the Skydome vertex/index complexity.
  • Would like to have the skydome centered on the camera.

part16.jpg

Release

Here is the source code, as a side note thanks to feedback from readers I implemented the JigLibX physics library into this latest release.  This library has a cleaner feel than BulletX and will be the physics engine for this game engine… well until next month when I change my mind again.

Where to begin?

At this point, I grabbed my old geometry book and found… well nothing helpful to me.  I wish I was able to say, “yup I used my highschool geometry books and wrote a skydome tutorial”, but I did not.  At this point, I did what any developer would do, I googled until I found something helpful. 

After reading a few dozen articles I realized, there are literally thousands of people making skydomes, skyplanes, skyboxes, and skyspheres.  As a bi-product, there are thousands of ways to create a skydome and by no means am I saying the way I chose is best.  In fact, while writing this article, I looked up at least 3 other ways to create a skydome and will probably write up tutorials on those subjects as well.

Skydome

A skydome is a simple half sphere that will allow us to surround the camera with a beautifully shaded sky.

First, we create a scene object that is loadable.  This skydome is constructed based on four parameters, the radius of the circle, the height (this implies we can make the sphere more elliptical in the Y direction), the number of rings (latitude), and the number of segments (longitude).

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using RoeEngine2.Managers;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class Skydome : RoeSceneObject, IRoeLoadable
    {
        int _numberOfVertices;
        int _numberOfIndices;
        float _radius;
        float _height;
        int _rings;
        int _segments;
        VertexBuffer _vb;
        IndexBuffer _ib;

        private bool _attachToCamera = true;
        public bool AttachToCamera
        {
            get { return _attachToCamera; }
            set { _attachToCamera = value; }
        }

        public Skydome(float radius, float height, float rings, float segments)
        {
            _radius = (float)Math.Max(radius, 1.0e-05);
            _height = (float)Math.Max(height, 1.0e-05);

            _rings = (int)Math.Max(rings, 1) + 1;
            _segments = (int)Math.Max(segments, 4) + 1;

            _numberOfVertices = _rings * (_segments + 1);
            _numberOfIndices = _segments * _rings * 2;
        }  

        public virtual void LoadContent()
        {
            CreateVB();
            CreateIB();
            ReadyToRender = true;
        }

        public virtual void UnloadContent()
        {
           
        }

        BasicEffect effect = new BasicEffect(EngineManager.Device, null);

        public override void Draw(GameTime gameTime)
        {
            if (_attachToCamera)
            {
                Position = new Vector3(CameraManager.ActiveCamera.Position.X, Position.Y, CameraManager.ActiveCamera.Position.Z);
            }

            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = true;

            effect.World = World;
            effect.View = CameraManager.ActiveCamera.View;
            effect.Projection = CameraManager.ActiveCamera.Projection;

            effect.DiffuseColor = new Vector3(255, 0, 0);
            effect.DirectionalLight0.Enabled = true;

            effect.Begin();
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Begin();
                using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionTexture.VertexElements))
                {
                    EngineManager.Device.VertexDeclaration = declaration;
                    EngineManager.Device.Vertices[0].SetSource(_vb, 0, VertexPositionTexture.SizeInBytes);
                    EngineManager.Device.Indices = _ib;
                    EngineManager.Device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, _numberOfVertices, 0, _numberOfIndices - 2);
                }
                pass.End();
            }
            effect.End();
        }

        public override string ToString()
        {
            return "Skydome";
        }

        private void CreateIB()
        {
            _ib = new IndexBuffer(
                EngineManager.Device,
                typeof(int),
                _numberOfIndices,
                BufferUsage.WriteOnly);

            int[] indices = new int[_numberOfIndices];

            bool leftToRight = false;
            int n = 0;
            for (int y = -0; y < _segments; y++)
            {
                if (leftToRight == true)
                {
                    for (int x = 0; x = 0; x--)
                    {
                        indices[n++] = y * _rings + x;
                        indices[n++] = (y + 1) * _rings + x;
                    }
                }
            }
            _ib.SetData(indices);
        }

        private void CreateVB()
        {

            _vb = new VertexBuffer(
                EngineManager.Device,
                typeof(VertexPositionTexture),
                _numberOfVertices,
                BufferUsage.WriteOnly);

            VertexPositionTexture[] vertices = new VertexPositionTexture[_numberOfVertices];
            int n = 0;
            for (int y = 0; y = 0; x--)
                {
                    float phi = (float)x / _rings * MathHelper.ToRadians(90.0f);

                    VertexPositionTexture vert = new VertexPositionTexture();
                    vert.Position.X = (float)(Math.Sin(phi) * Math.Cos(theta) * _radius);
                    vert.Position.Z = (float)(Math.Sin(phi) * Math.Sin(theta) * _radius);
                    vert.Position.Y = (float)(Math.Cos(phi) * _height);
                    vertices[n++] = vert;
                }
            }
            _vb.SetData(vertices);
        }
    }
}

I would like to point out a few items that might interest readers.  I am using a BasicEffect at this point, this will be replaced by a more sophisticated shader in the next article.  Also, I am using a user defined vertex and index buffer rather than a model.  Hopefully, readers will realize the flexibility of not always using a model from the content pipeline for rendering.Adding a Skydome to the SceneGraph

Lines of code like the following are the reason why we write game engines.  We want to make game developers lives easier so they can focus on gameplay.  Personally, this is the reason why I write this series, to make it possible for game developers to worry less about graphics and more about gameplay.

            Skydome dome = new Skydome(20, 20, 32, 32);
            SceneGraphManager.AddObject(dome);

Conclusion

In this article, I introduced the first user defined SceneObject in the engine.  A simple and useful Skydome will be the backdrop of any outdoor game.  In the next article, I will show how to paint this dome with a true to life lightscattering shader.

Please feel free to leave comments or suggestions, I thoroughly enjoy the feedback.

March 5, 2008 Posted by | C#, XBOX360, XNA | 15 Comments

XNA Framework GameEngine Development. (Part 15, Adding WinForm Support, step 1 to World Builder)

Introduction

Welcome to Part15 of the XNA Framework GameEngine Development series.  This article will introduce using XNA 2.0 in the WinForms environment.  This is the first obvious step to creating a World Builder application.  I know the XNA team site has an example of how to do this, but I simply did not like how truely difficult it was to get an XNA Game into WinForms using their example. 
So I turned again to the open source community and was not disappointed.  Pedro Güida was kind enough to show an example of doing exactly what I want to do on the codeproject resource site.  This article is my implementation of his work, with a few minor tweeks.

part15.jpg

Release

Here is the sourcecode, if you like this series or have suggestions please leave a comment.  Your feedback is very important and is fuel for new ideas.

Simple steps to WinForms

  • Create a Game Project (DONE) – this is what we have been doing all along.
  • Create a Windows Form in our Engine project.
  • Avoid using any references to Windows Forms on the XBOX360.
  • Hide the game window – use the window form handle as the presentation device handle.

Adding a WinForm

Add a new winform to the RoeEngine2 project.  Add a panel to the project window and add a few properties for use later.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace RoeEngine2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public Panel XNAPanel
        {
            get { return panel1; }
        }

        public int PanelWidth
        {
            get { return panel1.Width; }
        }

        public int PanelHeight
        {
            get { return panel1.Height; }
        }

        public IntPtr PanelHandle
        {
            get
            {
                return this.panel1.IsHandleCreated ? this.panel1.Handle : IntPtr.Zero;
            }
        }
    }
}

Updates to base engine

In the RoeEngine class we need to do some work.  Our main goal here is to hide the game form, handle window resize, window close, and Show the WinForm.  Then copy everything we would have drawn in the game window to the new WinForm window.

In the Initialize method add the following code.

#if !XBOX360
            if (_useWinForm)
            {
                SysWinForms.Form gameWindowForm = (SysWinForms.Form)SysWinForms.Form.FromHandle(this.Window.Handle);
                gameWindowForm.Shown += new EventHandler(gameWindowForm_Shown);

                myForm = new Form1();
                myForm.HandleDestroyed += new EventHandler(myForm_HandleDestroyed);
                myForm.Resize += new EventHandler(myForm_Resize);
                myForm.XNAPanel.Resize += new EventHandler(XNAPanel_Resize);
                myForm.Show();
            }
#endif

Wire up some event handlers for resizing and destroying windows.

#if !XBOX360
        void XNAPanel_Resize(object sender, EventArgs e)
        {
            ApplyResolutionChange(((SysWinForms.Panel)sender).Width, ((SysWinForms.Panel)sender).Height);
        }

        void myForm_Resize(object sender, EventArgs e)
        {
            ApplyResolutionChange(((Form1)sender).PanelWidth, ((Form1)sender).PanelHeight);
        }

        void myForm_HandleDestroyed(object sender, EventArgs e)
        {
            this.Exit();
        }

        void gameWindowForm_Shown(object sender, EventArgs e)
        {
            ((SysWinForms.Form)sender).Hide();
        }
#endif

Finally, show something to the user.  Add the following code to the Draw method.

#if !XBOX360
            if (_useWinForm)
            {
                GraphicsDevice.Present(myForm.PanelHandle);
            }
#endif

Yes, it is that easy, I do not quite understand why the XNA team site went through so much trouble in their sample.  Could be my inexperience, I would like to discuss possible changes to this solution if some smart folks have anything to offer.  For now, this seems simple enough for what we need to do.

World Builder project

For completeness, I like to have a seperate project that will be my world builder startup project.  This is pretty much the exact same thing as the “normal” game project.  Just this time, we pass true as the useWinForms parameter when creating the game.

                using (EngineManager game = new EngineManager(true))
                {
                    EngineManager.Game = game;
                    SetupScene();
                    game.Run();
                }

Conclusion

In this article I introduced using XNA in a WinForm project.  This will be the beginning of our World Builder project and gives us a reasonably useful way to easily manipulate the engine.

I look forward to your suggestions and feedback.  I know the XNA team site does this in a very different way from what I am doing.  I would very much like to hear your thoughts and opinions about this solution.

February 29, 2008 Posted by | C#, XNA | 16 Comments

XNA Framework GameEngine Development. (Part 14, Adding Physics)

Introduction

Welcome to Part 14 of the XNA Framework GameEngine Development series.  This is the first article in the Physics sub-section and will deal mainly on integrating physics into the exisiting engine.  I am not a physics expert and will not go into the details of how dynamic objects interact.  Rather my goal is to integrate an existing physics engine into up until now can only be described as a wrapper for the XNA rendering engine.

part14.jpg

Reader Feedback

I had intended to use the Newton Physics Wrapper written by TamedTornado, but thanks to feedback from (kt) I moved over to the BulletX physics engine.  I do read your comments and want them, take the time to make a suggestion and I will work to make it a part of the engine.  I do want to give everybody an oppurtunity to get the best start for making games, I don’t know all the answers, so I find your insights very valuable.  We all get better games in return.

Release

Here is the source code for this article.  Please feel free to use this code, make improvements if you like, but share what you do.  Drop me a line or comment if you do make something with this engine, I would love to see it.

Shapes

The only major new concept that we have to pay attention to for physics is the concept of shapes.  Every object that we want to have in the physics engine must define a shape.  The BulletX engine goes out of its way to provide many useful shapes for our game objects.  In this example I have solely used the Box shape, but in future demos I will use others.

PhysicsManager

The PhysicsManager is the first internal class GameComponent of the engine.  That means the “game” will never be able to use its classes.  This is an encapsulation technique that will protect a very busy class from being misused.  As a result, the PhysicsManager is tied very closly to the SceneGraphManager (and will be tied to other scene managers in the future).  So what does the physics manager do for us?

First, it defines the World using properties such as gravity, the axis aligned bounding box (aabb) algorithm, as well as collision data.  Quite a lot of information here, most of which is just intializing the stuff that makes BulletX work.

Second, it contains the CreateRigidBody method.  This is the single most important method in the class and the one you will use most often as a game developer.  This method takes an IRoeBulletPhysics object as a parameter adds it to the world and returns a world index to the object passed in as an out parameter.  This method does all the setup work for getting objects into the BulletX engine, not that it is hard, it just isn’t code I want to write all the time.

Finally, the GetObject method allows you to grab the CollisionObject from the world by index.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;
using RoeEngine2.Interfaces;
using XnaDevRu.BulletX;
using XnaDevRu.BulletX.Dynamics;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    internal class PhysicsManager : GameComponent
    {
        private static int _collisionIndex = 0;
        private static CollisionDispatcher _collisionDispatcher;
        private static OverlappingPairCache _broadPhase;
        private static SequentialImpulseConstraintSolver _solver;
        private static DiscreteDynamicsWorld _world;
        public static DiscreteDynamicsWorld World
        {
            get { return _world; }
        }

        private static bool _useSweepAndPrune;
        public static bool UseSweepAndPrune
        {
            get { return _useSweepAndPrune; }
            set { _useSweepAndPrune = value; }
        }

        private static bool _drawDebugger;
        public static bool DrawDebugger
        {
            get { return _drawDebugger; }
            set { _drawDebugger = value; }
        }

        private static bool _initialized = false;
        /// 
        /// Is the PhysicsManagers Initialized, used for test cases and setup of Effects.
        /// 
        public static bool Initialized
        {
            get { return _initialized; }
        }

        /// 
        /// Create the Physics Managers.
        /// 
        /// 
        public PhysicsManager(Game game)
            : base(game)
        {
            Enabled = true;
            ResetPhysics();
        }

        public static void ResetPhysics()
        {
            _collisionDispatcher = new CollisionDispatcher();

            if (_useSweepAndPrune)
            {
                Vector3 worldAabbMin = new Vector3(-10000, -10000, -10000);
                Vector3 worldAabbMax = new Vector3(10000, 10000, 10000);
                const int maxProxies = 32766;
                _broadPhase = new AxisSweep3(worldAabbMin, worldAabbMax, maxProxies);
            }
            else
            {
                _broadPhase = new SimpleBroadphase();
            }
            _solver = new SequentialImpulseConstraintSolver();
            _world = new DiscreteDynamicsWorld(_collisionDispatcher, _broadPhase, _solver);

            _world.Gravity = new Vector3(0, -10.0f, 0);

            GC.Collect();
        }

        public override void Initialize()
        {
            base.Initialize();

            XnaDebugDraw debugDrawer = new XnaDebugDraw(EngineManager.Device);
            debugDrawer.DebugMode = DebugDrawModes.DrawAabb | DebugDrawModes.DrawContactPoints;
            _world.DebugDrawer = debugDrawer;

            _initialized = true;
        }

        public static void Draw(GameTime gameTime)
        {
            if (_drawDebugger)
                ((XnaDebugDraw)_world.DebugDrawer).Update(CameraManager.ActiveCamera.View, CameraManager.ActiveCamera.Projection);
            _world.StepSimulation(1.0f / 60.0f, 1);
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        /// Starting body matrix transform.
        /// Body shape
        /// Index into the Physics World collection
        /// Created Rigid Body
        private static void CreateRigidBody(float mass, Matrix startMatrix,
                                                 CollisionShape newShape, CollisionOptions options,
                                                 out int worldIndex)
        {
            bool isDynamic = (mass > 0.0f);

            Vector3 localInertia = new Vector3();
            if (isDynamic)
                newShape.CalculateLocalInertia(mass, out localInertia);

            //using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
            DefaultMotionState myMotionState = new DefaultMotionState(startMatrix, Matrix.Identity);
            RigidBody body = new RigidBody(mass, myMotionState, newShape, localInertia, 0.0f, 0.0f, 0.5f, 0.0f);

            _world.AddRigidBody(body);

            worldIndex = _collisionIndex;
            _collisionIndex++;

            body.CollisionFlags |= options;
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        public static void CreateRigidBody(IRoeBulletPhysics iRoeBulletPhysics)
        {
            int index;
            RoeSceneObject obj = (RoeSceneObject)iRoeBulletPhysics;
            CreateRigidBody(iRoeBulletPhysics.Mass, Matrix.CreateTranslation(obj.Position) * Matrix.CreateFromQuaternion(obj.Rotation),
                            iRoeBulletPhysics.CollisionShape, iRoeBulletPhysics.CollisionOptions,
                            out index);
            iRoeBulletPhysics.WorldIndex = index;
        }

        public static CollisionObject GetObject(int index)
        {
            if (_world.CollisionObjectsCount > index)
            {
                return _world.CollisionObjects[index];
            }
            return null;
        }
    }
}

Interface

As usual I like to use interfaces to define base classes for game objects.  Here is the new IRoeBulletPhysics interface.

using System;
using System.Collections.Generic;
using System.Text;
using XnaDevRu.BulletX.Dynamics;
using XnaDevRu.BulletX;

namespace RoeEngine2.Interfaces
{
    /// 
    /// Every RoeSceneObject that implements this interface will be added to the physics manager
    /// 
    public interface IRoeBulletPhysics : IRoeSceneObject
    {
        CollisionOptions CollisionOptions
        {
            get;
            set;
        }

        int WorldIndex
        {
            get;
            set;
        }
       
        CollisionShape CollisionShape
        {
            get;
            set;
        }

        float Mass
        {
            get;
            set;
        }
    }
}

RoeSceneObject

Just some small changes here, mainly to do with the World matrix.  We pretty much need to give control over world transform to the physics engine, here is how to do it.

        public virtual Matrix World
        {
            get
            {
                if (ReadyToRender)
                {
                    if (this is IRoeBulletPhysics)
                    {
                        Matrix world = Matrix.Identity;
                        CollisionObject obj = PhysicsManager.World.CollisionObjects[((IRoeBulletPhysics)this).WorldIndex];
                        RigidBody body = RigidBody.Upcast(obj);

                        if (body != null && body.MotionState != null)
                        {
                            DefaultMotionState myMotionState = (DefaultMotionState)body.MotionState;
                            return Matrix.CreateScale(Scale) * myMotionState.GraphicsWorldTransform;
                        }
                        else
                        {
                            return Matrix.CreateScale(Scale) * obj.WorldTransform;
                        }
                    }
                }
                return Matrix.CreateScale(_scale) * Matrix.CreateFromQuaternion(_rotation) * Matrix.CreateTranslation(_position);
            }
        }

Box.cs

I know this isn’t very interesting, but it is a start.  In any good physics demo, you get to see spheres or boxes falling and colliding.  So we need to make a Box class.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaDevRu.BulletX;
using RoeEngine2.Managers;
using RoeEngine2.Models;
using XnaDevRu.BulletX.Dynamics;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class Box : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeOcclusion, IRoeBulletPhysics
    {
        public VertexPositionColor[] points = new VertexPositionColor[8];
        public int[] index = new int[24];

        private string _occlusionModelName;
        public string OcclusionModelName
        {
            get { return _occlusionModelName; }
            set { _occlusionModelName = value; }
        }

        private OcclusionQuery _query = new OcclusionQuery(EngineManager.Device);
        public OcclusionQuery Query
        {
            get { return _query; }
        }

        private bool _occluded = false;
        public bool Occluded
        {
            get { return _occluded; }
            set { _occluded = value; }
        }

        private bool _culled = false;
        public bool Culled
        {
            get { return _culled; }
            set { _culled = value; }
        }

        private CollisionOptions _collisionOptions;
        public CollisionOptions CollisionOptions
        {
            get { return _collisionOptions; }
            set { _collisionOptions = value; }
        }

        private int _worldIndex;
        public int WorldIndex
        {
            get { return _worldIndex; }
            set { _worldIndex = value; }
        }

        private CollisionShape _collisionShape;
        public CollisionShape CollisionShape
        {
            get { return _collisionShape; }
            set { _collisionShape = value; }
        }

        private float _mass = 0.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public Box()
        {
           
        }

        public Box(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;
           
            min = Vector3.Transform(_boundingBox.Min, World);
            max = Vector3.Transform(_boundingBox.Max, World);

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = BoundingBox.Min * new Vector3(0.6f, 0.6f, 0.6f);
                max = BoundingBox.Max * new Vector3(0.6f, 0.6f, 0.6f);

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                _collisionShape = new BoxShape(Scale / 2.0f);
                SceneGraphManager.AddPhysics(this);
               
                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Box");

            ModelManager.AddModel(model, "boxmodel");
            this.ModelName = "boxmodel";
            this.OcclusionModelName = "boxmodel";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;

        }

        public void UnloadContent()
        {
           
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionColor.VertexElements))
            {
                EngineManager.Device.RenderState.PointSize = 1.0f;
                EngineManager.Device.VertexDeclaration = declaration;

                IRoeShader shader = ShaderManager.GetShader("basic");

                if (shader.ReadyToRender)
                {
                    BasicEffect effect = shader.BaseEffect as BasicEffect;
                    effect.DiffuseColor = Color.Red.ToVector3();
                    effect.View = CameraManager.ActiveCamera.View;
                    effect.Projection = CameraManager.ActiveCamera.Projection;
                    effect.World = World;

                    effect.Begin();

                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        EngineManager.Device.DrawUserIndexedPrimitives(
                                            PrimitiveType.LineList, points,
                                            0,
                                            8,
                                            index,
                                            0,
                                            12);
                        pass.End();
                    }
                    effect.End();
                }
            }
        }

        public override void DrawCulling(GameTime gameTime)
        {
            Occluded = false;
            if (ReadyToRender && !Culled)
            {
                _query.Begin();
                IRoeModel model = ModelManager.GetModel(_occlusionModelName);
                if (model != null && model.ReadyToRender)
                {
                    Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                    model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                    foreach (ModelMesh mesh in model.BaseModel.Meshes)
                    {
                        foreach (BasicEffect effect in mesh.Effects)
                        {
                            effect.EnableDefaultLighting();
                            effect.PreferPerPixelLighting = true;
                            effect.World = World;
                            effect.View = CameraManager.ActiveCamera.View;
                            effect.Projection = CameraManager.ActiveCamera.Projection;
                        }
                        mesh.Draw();
                    }
                }
                _query.End();

                while (!_query.IsComplete)
                {

                }

                if (_query.IsComplete && _query.PixelCount == 0)
                {
                    Occluded = true;
                }
            }
        }
    }
}

The Rest

Pretty much now you have the basics of physics in the engine.  Now all you need to do is add some logic to the game to add some boxes and objects and watch them collide.

Here is my gameclass Initalizer to get you started.

        /// 
        /// Load graphics content for the game.
        /// 
        public override void LoadContent()
        {
            SceneGraphManager.DrawDebugger = true;

            Box box = new Box();
            box.Position = new Vector3(0, -10, 0);
            box.Scale = new Vector3(10, 10, 10);
            box.Mass = 0.0f;
            SceneGraphManager.AddObject(box);

            for (int j = -2; j < 3; j++)
            {
                box = new Box(new Vector3(j * 2, 0, 0));
                box.Scale = new Vector3(2, 2, 2);
                box.Mass = 1.0f;
                SceneGraphManager.AddObject(box);

                for (int i = 1; i < 10; i++)
                {
                    box = new Box(new Vector3(j * 2f, i * 3.0f, 0));
                    box.Scale = new Vector3(0.5f, 0.5f, 0.5f);
                    box.Mass = 10.0f;
                    SceneGraphManager.AddObject(box);
                }
            }
            SceneGraphManager.LoadContent();

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            EngineManager.Game.ResetElapsedTime();
        }

Conclusion

In this article I introduced physics into the engine.  This is a point of demarcation between a rendering engine and a true to life game engine, by including multiple core components working in unison.  From now on this series can truely be called a game engine.

Please leave a suggestion or comment.  I value what you have to say and read every comment, and sometimes make improvements based on what you have to say.

February 26, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 6 Comments

XNA Framework GameEngine Development. (Part 13, Occlusion Culling and Frustum Culling)

Introduction

Welcome to Part 13 of the XNA Framework GameEngine Development series.  Yet another slight departure from my planned series.  This article will focus on culling, both using the Frustum and OcclusionQuery.  I know I covered Culling in Part 12, but going back to look at that article I realized I was falling sadly short on explaination.  So rather than confuse readers by going back to make changes in previous articles, I will simply explain sections of code and release code with each sample.  Hopefully, this new format will be beneficial to readers.

part13.jpg

Release

If you enjoy these articles please leave a comment, here is the sourcecode.  Eventually, I am going to run out of things to talk about, your feedback is very important to me and helps formulate future articles.

Built-in Frustum Culling

The frustum is an imaginary trapezoid that defines the area that a camera can view.  In general this trapezoid can be defined using a near plane, the far plane, and the field of view.  There are plenty of articles that you can find online that describe the frustum in far more elegant ways than I can, but I think of the frustum as the following.

Imagine you are looking out of a window in your room, in real physical terms, the distance you are sitting from that window is the same as the near plane distance.  If you moved closer to the window your near plane distance would be smaller, easy enough. 

The far plane is a little harder to describe, it is the farthest viewable distance that you can see.  It literally is a wall that sits out in space and covers up everything beyond.  Finally, there is the field of View, this is how wide your periferal vision extends.  Anybody who has looked through a telescope, camera lens, or binoculars should notice this.

So what does this mean to a game engine?  Quite simply if we dont see it in our frustum, we dont need to draw it.  Modern video cards do this very well, they simply do not render pixels that lay outside of the frustum, you should know however, the draw call is sent to the video card though. 

This is a very important distiction to make, depending on the video card to cull objects outside of the view frustum is not enough.  Why send instructions to the video card if it is just going to not draw an object?  So, we cull the items that never are going to be drawn before they go to the video card.

part13-frustum-culling.jpg

Frustum Culling

So the whole point of frustum culling is to reduce the number of objects we send to the video card to be drawn.  This is usually done by testing simple geometry for inclusion in the scene, enter the BoundingBox and BoundingSphere.  A BoundingBox or BoundingSphere is a very simple mathematical shape that we can use to test for inclusion in the frustum.  These shapes are just large enough to completely enclose the entire object.  See image below (Notice, the bounding box is a perfect square, even though the top and bottom sides of the random object do not touch the edge).

 part13-bounding-box.jpg

Now that we have a bounding box or sphere, we can get to some serious work.  See the image below for the new comparison.

part13-frustum-culling-bounding-box.jpg

Wow!  If you are thinking we just reduced the number of objects sent to the video card in this scene by half, you are correct. 

Obviously, this sort of culling is very important for scene optimization, so how do we get this magic to work in the Engine?

First, create an interface so that we can identify Cullable SceneObjects

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Every RoeSceneObject that implements this interface will be culled using its bounding object.
    /// </summary>
    public interface IRoeCullable : IRoeSceneObject
    {
        bool DrawBoundingBox
        {
            get;
            set;
        }

        bool Culled
        {
            get;
            set;
        }

        bool BoundingBoxCreated
        {
            get;
        }

        BoundingBox BoundingBox
        {
            get;
        }

        BoundingBox GetBoundingBoxTransformed();
    }
}

Next, implement DrawCulling in the Node.cs class.

        public virtual void DrawCulling(GameTime gameTime)
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.DrawCulling(gameTime);
               });
        }

Do the same in the SceneObjectNode.cs class, but this time do some work if the sceneobject is cullable.

        public override void DrawCulling(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable)
            {
                ((IRoeCullable)SceneObject).Culled = false;
                if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint)
                {
                    ((IRoeCullable)SceneObject).Culled = true;
                }
                else
                {
                    SceneObject.DrawCulling(gameTime);
                }
            }
        }

Implement DrawCulling in the SceneGraphManager.cs as well.  I am also going to implement a static int so we can indicate if anything has been culled.

 public static int culled = 0;
        
 public static void DrawCulling(GameTime gameTime)
        {
            culled = 0;
            occluded = 0;
            _root.DrawCulling(gameTime);
        }

Finally in RoeSceneObject add a virtual so we can override it with cullable objects and draw culling objects.

        public virtual void DrawCulling(GameTime gameTime)
        {

        }

That is pretty much all there is to do for culling.  The real work is done by the SceneObjectNode class.  The line CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint essentially is testing to see if the cullable object is intersecting/within the frustum and simply marking the object as culled if it is not.  Now we can test for culled when we draw.

Add the following code to SceneObjectNode.cs

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable && ((IRoeCullable)SceneObject).Culled)
            {
                SceneGraphManager.culled++;
            }         
            else
            {
                SceneObject.Draw(gameTime);
            }
        }

Excellent, that is all there is to do for frustum culling.

Occlusion Culling

Now this is a little bit more complex form of culling, but it is a very important concept to learn for good culling and portal scene management.  Occlusion is what happens when one object passes in front of another object.  In reality the object that was occluded still exists and the same is true in our engine, accept in our engine we no longer have to draw the complex geometry of the occluded object.

This is usually accomplished by drawing less geometrically complex and somewhat larger scale object.  We are obviously drawing more geometry per object, but hopefully less geometry overall by doing this simple test.  In this engine, I will accomplish this task in two “passes”.  First a pass of simple geometry to test for occlusion, and second the final geometry for the scene.

I start by creating a simple interface to identify Occlusion sceneobjects

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Test if an object is occluded in the scene.
    /// </summary>
    public interface IRoeOcclusion : IRoeCullable, IRoeSceneObject
    {
        string OcclusionModelName
        {
            get;
            set;
        }

        OcclusionQuery Query
        {
            get;
        }

        bool Occluded
        {
            get;
            set;
        }

        void DrawCulling(GameTime gameTime);
    }
}

Notice IRoeOcclusion also implements IRoeCullable, this is very important, you NEVER EVER want to Occlusion cull something that is already Frustum culled.  A simple rule to live by.

Since occlusion culling is a geometry based technique, we also need to implement an object modle for the occluder.  Finally, we want to force the object to implement the DrawCulling override.

IT IS VERY IMPORTANT TO NOTE, when doing occlusion culling your objects must be sorted by their distance to the camera.  You want to draw objects nearer to the camera first, this will reduce overdraw in the scene and make your graphics card very happy.  Also, occlusion culling just doesn’t work if you dont.  So we have to make our Node class an IComparable, I wont explain all of this, if you have questions about what is going on feel free to use MSN.

Node.cs should implement IComparable and add the following code

        int IComparable.CompareTo(object obj)
        {
            SceneObjectNode node1 = (SceneObjectNode)this; 
            SceneObjectNode node2 = (SceneObjectNode)obj;

            if (node1.SceneObject.Distance < node2.SceneObject.Distance)
                return -1;
            else if (node1.SceneObject.Distance > node2.SceneObject.Distance)
                return 1;
            else
                return 0;
        }

        public void Sort()
        {
            _nodes.Sort();
        }

In the SceneGraphManager.DrawCulling method add the following code

        public static void DrawCulling(GameTime gameTime)
        {
            _root.Sort();
            culled = 0;
            occluded = 0;
            _root.DrawCulling(gameTime);
        }

That is bascially all we really have to do to implement Occlusion culling, well all accept the real work.  Now we have to test for occlusion.  The very basic implementation looks something like this.

      OcclusionQuery query = new OcclusionQuery(EngineManager.Device);

      query.Begin();
      // Drawing simple objects, bounding areas, etc.
      query.End();

      // Do additional work here to provide enough time for the GPU to complete query execution.
      
      // Draw additional models
      if (query.IsComplete == true && query.PixelCount > )
      {
      // A non-zero pixel count means some of the low res model is visible
      // so let's draw the real version of it
      }

That isn’t so bad.  so now we can test occluders and simply not draw an object if it is occluded.  We can accomplish that task like this.

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable && ((IRoeCullable)SceneObject).Culled)
            {
                SceneGraphManager.culled++;
            }
            else if (SceneObject is IRoeOcclusion && ((IRoeOcclusion)SceneObject).Occluded)
            {
                SceneGraphManager.occluded++;
            }            
            else
            {
                SceneObject.Draw(gameTime);
            }
        }

Please feel free to look at the code linked at the top of this page.  I will no longer add every line of code I write to the blog.

Conclusion

In this article I introduced Frustum and Occlusion Query culling.  These techniques are used to reduce the number of complex geomotry draw calls that we need to make to the graphics card.

I very much enjoy your comments, if you found something here that was helpful or interesting please leave some feedback.  Hope you are enjoying reading these articles as much as I am creating them.

February 18, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 10 Comments

XNA Framework GameEngine Development. (Part 12, Culling and Chase Camera)

Introduction

Welcome to Part 12 of the XNA Framework GameEngine Development series.  This article is a slight departure from my original plan of showing a SkyDome game object.  Rather, I decided to go back and clean up several of the classes I have already created as well as include some culling in the SceneGraph.

part12.jpg

Interfaces

I removed the IRoeDrawable Interface from the project.  I figure all RoeSceneObjects should be drawable.

IRoeSimplePhysics.cs – allows an object to maintain some simple physical updates.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Adds simple physics and reset to an RoeSceneObject
    /// </summary>
    public interface IRoeSimplePhysics : IRoeSceneObject
    {
        Vector3 Up
        {
            get;
        }

        Vector3 Right
        {
            get;
        }

        float RotationRate
        {
            get;
            set;
        }

        float Mass
        {
            get;
            set;
        }

        float ThrustForce
        {
            get;
            set;
        }

        float DragFactor
        {
            get;
            set;
        }

        Vector3 Velocity
        {
            get;
            set;
        }

        void Reset();
    }
}

IRoeAcceptInput.cs – each object that inherits this interface will be able to Handle game input.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Allows a RoeSceneObject to handle input.
    /// </summary>
    public interface IRoeAcceptInput : IRoeSceneObject
    {
        void HandleInput(GameTime gameTime, Input input);
    }
}

IRoeCullable.cs – allows an object to be culled by the SceneManager.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    public interface IRoeCullable : IRoeSceneObject
    {
        bool BoundingBoxCreated
        {
            get;
        }

        BoundingBox BoundingBox
        {
            get;
        }

        BoundingBox GetBoundingBoxTransformed();
    }
}

IRoeUpdateable.cs – Added GameTime to the update method

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Allows an RoeSceneObject to be Updated
    /// </summary>
    public interface IRoeUpdateable : IRoeSceneObject
    {
        void Update(GameTime gameTime);
    }
}

Class changesNode.cs – Added HandleInputMethod also clar nodes after they are unloaded

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;

namespace RoeEngine2.SceneObject.SceneGraph
{
    public class Node
    {
        protected NodeList _nodes;
        public NodeList Nodes
        {
            get { return _nodes; }
        }

        public Node()
        {
            _nodes = new NodeList();
        }

        public void AddNode(Node newNode)
        {
            _nodes.Add(newNode);
        }

        public virtual void HandleInput(GameTime gameTime, Input input)
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.HandleInput(gameTime, input);
               });
        }

        public virtual void Update(GameTime gameTime)
        {
            _nodes.ForEach(
                delegate(Node node)
                {
                    node.Update(gameTime);
                });
        }

        public virtual void UnloadContent()
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.UnloadContent();
               });
            _nodes.Clear();
        }

        public virtual void LoadContent()
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.LoadContent();
               });
        }

        public virtual void Draw(GameTime gameTime)
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.Draw(gameTime);
               });
        }
    }
}

SceneObjectNode.cs – Added HandleInput and Culling

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.SceneObject.SceneGraph
{
    public class SceneObjectNode : Node
    {
        private RoeSceneObject _sceneObject;
        public RoeSceneObject SceneObject
        {
            get { return _sceneObject; }
            set { _sceneObject = value; }
        }

        public SceneObjectNode(RoeSceneObject newObject)
        {
            _sceneObject = newObject;
        }

        public override void HandleInput(GameTime gameTime, Input input)
        {
            if (SceneObject is IRoeAcceptInput)
                ((IRoeAcceptInput)SceneObject).HandleInput(gameTime, input);
        }

        public override void Update(GameTime gameTime)
        {
            if (SceneObject is IRoeUpdateable)
                ((IRoeUpdateable)SceneObject).Update(gameTime);
        }

        public override void UnloadContent()
        {
            if (SceneObject is IRoeLoadable)
                ((IRoeLoadable)SceneObject).UnloadContent();
        }

        public override void LoadContent()
        {
            if (SceneObject is IRoeLoadable)
                ((IRoeLoadable)SceneObject).LoadContent();
        }

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable)
            {
                if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint)
                {
                    SceneGraphManager.culled++;
                }
                else
                {
                    SceneObject.Draw(gameTime);
                    SceneObject.DrawNodesBoundingBox(gameTime);
                }
            }
            else
                SceneObject.Draw(gameTime);
        }
    }
}

RoeSceneObject.cs – added bounding box drawing and World matrix

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.Interfaces;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.SceneObject
{
    public class RoeSceneObject : IRoeSceneObject
    {
        public VertexPositionColor[] points = new VertexPositionColor[8];
        public int[] index = new int[24];

        private bool _readyToRender = false;
        /// <summary>
        /// Is this object ready to render?
        /// </summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
            set { _readyToRender = value; }
        }

        private Material _material;
        public Material Material
        {
            get { return _material; }
            set { _material = value; }
        }

        private string _modelName;
        public string ModelName
        {
            get { return _modelName; }
            set { _modelName = value; }
        }    

        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// The position of this object in 3d space.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
            set { _position = value; }
        }

        private Vector3 _scale = Vector3.One;
        /// <summary>
        /// Scale of the object.
        /// </summary>
        public Vector3 Scale
        {
            get { return _scale; }
            set { _scale = value; }
        }

        private Quaternion _rotation = Quaternion.Identity;
        /// <summary>
        /// Yaw, pitch and roll of the object.
        /// </summary>
        public Quaternion Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }
       
        public virtual Matrix World
        {
            get
            {
                return Matrix.CreateScale(this.Scale) *
                       Matrix.CreateFromQuaternion(this.Rotation) *
                       Matrix.CreateTranslation(this.Position);
            }
        } 

        public virtual void Draw(GameTime gameTime)
        {
            if (ReadyToRender)
            {
                IRoeModel model = ModelManager.GetModel(_modelName);
                if (model != null && model.ReadyToRender)
                {
                    Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                    model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                    foreach (ModelMesh mesh in model.BaseModel.Meshes)
                    {
                        foreach (BasicEffect effect in mesh.Effects)
                        {
                            effect.EnableDefaultLighting();
                            effect.PreferPerPixelLighting = true;
                            effect.World = World;
                            effect.View = CameraManager.ActiveCamera.View;
                            effect.Projection = CameraManager.ActiveCamera.Projection;
                        }
                        mesh.Draw();
                    }
                }
            }
        }

        public void DrawNodesBoundingBox(GameTime gameTime)
        {
            if(ReadyToRender)
            {
                using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionColor.VertexElements))
                {
                    EngineManager.Device.RenderState.PointSize = 1.0f;
                    EngineManager.Device.VertexDeclaration = declaration;

                    IRoeShader shader = ShaderManager.GetShader("basic");

                    if (shader.ReadyToRender)
                    {
                        BasicEffect effect = shader.BaseEffect as BasicEffect;
                        effect.DiffuseColor = Color.Red.ToVector3();
                        effect.View = CameraManager.ActiveCamera.View;
                        effect.Projection = CameraManager.ActiveCamera.Projection;
                        effect.World = World;

                        effect.Begin();

                        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                        {
                            pass.Begin();
                            EngineManager.Device.DrawUserIndexedPrimitives<VertexPositionColor>(
                                                PrimitiveType.LineList, points,
                                                0,
                                                8,
                                                index,
                                                0,
                                                12);
                            pass.End();
                        }
                        effect.End();
                    }
                }
            }
        }
    }  
}

CameraManager.cs – Now supports FirstPersonCamera specifically.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections;
using RoeEngine2.GameComponents;

namespace RoeEngine2.Managers
{
    public class CameraManager : GameComponent
    {
        private static Hashtable _cameras = new Hashtable();

        public enum CameraNumber
        {
            _default = 1,
            _dolly = 2,
            _3 = 3,
            _4 = 4,
            _5 = 5,
            _6 = 6,
            _7 = 7,
            _8 = 8,
            _9 = 9,
            _10 = 10
        }

        private static bool _initialized = false;
        /// <summary>
        /// Is the CameraManagers Initialized, used for test cases and setup of Effects.
        /// </summary>
        public static bool Initialized
        {
            get { return _initialized; }
        }

        private static Camera _activeCamera;
        /// <summary>
        /// The camera where all the action takes place.
        /// </summary>
        public static Camera ActiveCamera
        {
            get { return _activeCamera; }
        }

        /// <summary>
        /// Create the camera Managers.
        /// </summary>
        /// <param name="game"></param>
        public CameraManager(Game game)
            : base(game)
        {
            Enabled = true;
        }
       
        /// <summary>
        /// Create the cameras.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();

            AddCamera(new FirstPersonCamera(), CameraNumber._default);
            SetActiveCamera(CameraNumber._default);
            AddCamera(new FirstPersonCamera(), CameraNumber._dolly);

            _initialized = true;
        }

        /// <summary>
        /// Update the active camera.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            _activeCamera.Update(gameTime);
        }

        /// <summary>
        /// Adds a new camera to the CameraManagers
        /// </summary>
        /// <param name="newCamera"></param>
        /// <param name="cameraLabel"></param>
        public static void AddCamera(Camera newCamera, CameraNumber cameraNumber)
        {
            if (!_cameras.Contains(cameraNumber))
            {
                _cameras.Add(cameraNumber, newCamera);
            }
        }

        /// <summary>
        /// Change the projection matrix of all cameras.
        /// </summary>
        /// <param name="aspectRatio"></param>
        public static void SetAllCamerasProjectionMatrix(float aspectRatio)
        {
            foreach (Camera camera in _cameras.Values)
            {
                camera.Projection = Matrix.CreatePerspectiveFieldOfView(
                                        camera.FieldOfView, aspectRatio, camera.NearPlane, camera.FarPlane);
            }
        }

        /// <summary>
        /// Changes the active camera by label
        /// </summary>
        /// <param name="cameraLabel"></param>
        public static void SetActiveCamera(CameraNumber cameraNumber)
        {
            if (_cameras.ContainsKey(cameraNumber))
            {
                _activeCamera = _cameras[cameraNumber] as Camera;
            }
        }    
    }
}

SceneGraphManager.cs – Added support for HandleInput

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.SceneObject.SceneGraph;
using RoeEngine2.SceneObject;
using RoeEngine2.GameComponents;

namespace RoeEngine2.Managers
{
    public class SceneGraphManager : GameComponent
    {
        public static int culled = 0;

        private static Node _root;
        /// <summary>
        /// The root of the scene graph
        /// </summary>
        public static Node Root
        {
            get { return _root; }
        }
    
        /// <summary>
        /// Create the scenegraph Managers.
        /// </summary>
        /// <param name="game"></param>
        public SceneGraphManager(Game game)
            : base(game)
        {
            Enabled = true;
            _root = new Node();
        }

        /// <summary>
        /// Draw objects
        /// </summary>
        /// <param name="gameTime"></param>
        public static void Draw(GameTime gameTime)
        {
            culled = 0;
            _root.Draw(gameTime);
        }

        public static void HandleInput(GameTime gameTime, Input input)
        {
            _root.HandleInput(gameTime, input);
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            _root.Update(gameTime);
        }

        /// <summary>
        /// Load the content of all the objects in the scenegraph
        /// </summary>
        public static void LoadContent()
        {
            _root.LoadContent();
        }

        /// <summary>
        /// Unload the content of all the objects in the scenegraph
        /// </summary>
        public static void UnloadContent()
        {
            _root.UnloadContent();
        }

        /// <summary>
        /// Add an object to the scenegraph.
        /// </summary>
        /// <param name="newObject"></param>
        public static void AddObject(RoeSceneObject newObject)
        {
            SceneObjectNode node = new SceneObjectNode(newObject);
            _root.AddNode(node);
        }
    }
}

New ClassesShip.cs – This is just for the test case in the example

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.SceneObject;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using RoeEngine2.Models;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;

namespace Kynskavion.GameObjects.Ship
{
    public class Ship : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeCullable
    {
        public Vector3 Up
        {
            get { return Vector3.Up; }
        }

        public Vector3 Right
        {
            get { return Vector3.Right; }
        }

        private float _rotationRate = 1.0f;
        public float RotationRate
        {
            get { return _rotationRate; }
            set { _rotationRate = value; }
        }

        public float Mass
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public float ThrustForce
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public float DragFactor
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public Vector3 Velocity
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public Ship()
        {

        }

        public Ship(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;

            min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position));
            max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position));

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            Rotation = Quaternion.Multiply(Rotation, Quaternion.CreateFromAxisAngle(Vector3.UnitZ, _rotationRate * elapsed));

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = BoundingBox.Min;
                max = BoundingBox.Max;

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            this.ModelName = "ship";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;
        }

        public void UnloadContent()
        {
           
        }

        public void Reset()
        {
            RotationRate = 0.0f;
        }
    }
}

ShipMain.cs – Same Thing as Ship, but this also accepts input

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.SceneObject;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using RoeEngine2.Models;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.GameComponents;
using Microsoft.Xna.Framework.Input;

namespace Kynskavion.GameObjects.ShipMain
{
    public class ShipMain : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeAcceptInput, IRoeCullable
    {
        public Vector3 Direction;

        private Vector3 _up = Vector3.Up;
        public Vector3 Up
        {
            get { return _up; }
        }

        private Vector3 _right;
        public Vector3 Right
        {
            get { return _right; }
        }

        private float _rotationRate = 1.0f;
        public float RotationRate
        {
            get { return _rotationRate; }
            set { _rotationRate = value; }
        }

        private float _mass = 1.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private float _thrustForce = 24000.0f;
        public float ThrustForce
        {
            get { return _thrustForce; }
            set { _thrustForce = value; }
        }

        private float _dragFactor = 0.97f;
        public float DragFactor
        {
            get { return _dragFactor; }
            set { _dragFactor = value; }
        }

        private Vector3 _velocity;
        public Vector3 Velocity
        {
            get { return _velocity; }
            set { _velocity = value; }
        }

        private Matrix _world;
        public override Matrix World
        {
            get { return _world; }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public ShipMain()
        {
            Reset();
        }

        public ShipMain(Vector3 newPosition)
        {
            Position = newPosition;
            Reset();
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;

            min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position));
            max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position));

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = Vector3.Transform(BoundingBox.Min, Matrix.CreateTranslation(Position));
                max = Vector3.Transform(BoundingBox.Max, Matrix.CreateTranslation(Position));

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            this.ModelName = "ship";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;
        }

        public void UnloadContent()
        {
           
        }

        public void Reset()
        {
            Position = new Vector3(0, 0, 0);
            Direction = Vector3.Forward;
            _up = Vector3.Up;
            _right = Vector3.Right;
            Velocity = Vector3.Zero;
        }

        public void HandleInput(GameTime gameTime, Input input)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            if (ReadyToRender)
            {
                Vector2 rotationAmount = -input.CurrentGamePadState.ThumbSticks.Left;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.W))
                    rotationAmount.Y = -1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.S))
                    rotationAmount.Y = 1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.D))
                    rotationAmount.X = -1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.A))
                    rotationAmount.X = 1.0f;
                // Determine thrust amount from input
                float thrustAmount = input.CurrentGamePadState.Triggers.Right;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Space))
                    thrustAmount = 1.0f;

                // Scale rotation amount to radians per second
                rotationAmount = rotationAmount * RotationRate * elapsed;

                // Correct the X axis steering when the ship is upside down
                if (Up.Y < 0)
                    rotationAmount.X = -rotationAmount.X;

                // Create rotation matrix from rotation amount
                Matrix rotationMatrix =
                    Matrix.CreateFromAxisAngle(Right, rotationAmount.Y) *
                    Matrix.CreateRotationY(rotationAmount.X);

                // Rotate orientation vectors
                Direction = Vector3.TransformNormal(Direction, rotationMatrix);
                _up = Vector3.TransformNormal(Up, rotationMatrix);

                // Re-normalize orientation vectors
                // Without this, the matrix transformations may introduce small rounding
                // errors which add up over time and could destabilize the ship.
                Direction.Normalize();
                Up.Normalize();

                // Re-calculate Right
                _right = Vector3.Cross(Direction, Up);

                // The same instability may cause the 3 orientation vectors may
                // also diverge. Either the Up or Direction vector needs to be
                // re-computed with a cross product to ensure orthagonality
                _up = Vector3.Cross(Right, Direction);

                // Calculate force from thrust amount
                Vector3 force = Direction * thrustAmount * ThrustForce;

                // Apply acceleration
                Vector3 acceleration = force / Mass;
                Velocity += acceleration * elapsed;

                // Apply psuedo drag
                Velocity *= DragFactor;

                // Apply velocity
                Position += Velocity * elapsed;

                // Reconstruct the ship's world matrix
                _world = Matrix.Identity;
                _world.Forward = Direction;
                _world.Up = Up;
                _world.Right = Right;
                _world.Translation = Position;
            }
        }
    }
}

FirstPersonCamera.cs – The original camera, just now encapsulated in its own class

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;

namespace RoeEngine2.GameComponents
{
    public class FirstPersonCamera : Camera
    {
        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// Postition of the camera.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
        }

        private Vector3 _cameraReference = new Vector3(0, 0, 10);
        /// <summary>
        /// The spot in 3d space where the camera is looking.
        /// </summary>
        public Vector3 CameraReference
        {
            get { return _cameraReference; }
        }

        private float _yaw = 0.0f;      

        private float _pitch = 0.0f;       

        /// <summary>
        /// Set the position in 3d space.
        /// </summary>
        /// <param name="newPosition"></param>
        public override void SetPosition(Vector3 newPosition)
        {
            _position = newPosition;
        }

        /// <summary>
        /// Set the point in 3d space where the camera is looking.
        /// </summary>
        /// <param name="newReference"></param>
        public void SetCameraReference(Vector3 newReference)
        {
            _cameraReference = newReference;
        }

        /// <summary>
        /// Move the camera in 3d space.
        /// </summary>
        /// <param name="move"></param>
        public override void Translate(Vector3 move)
        {
            Matrix forwardMovement = Matrix.CreateRotationY(_yaw);
            Vector3 v = new Vector3(0, 0, 0);
            v = Vector3.Transform(move, forwardMovement);
            _position.Z += v.Z;
            _position.X += v.X;
            _position.Y += v.Y;
        }

        /// <summary>
        /// Rotate around the Y, Default Up axis.  Usually called Yaw.
        /// </summary>
        /// <param name="angle">Angle in degrees to rotate the camera.</param>
        public override void RotateY(float angle)
        {
            angle = MathHelper.ToRadians(angle);
            if (_yaw >= MathHelper.Pi * 2)
                _yaw = MathHelper.ToRadians(0.0f);
            else if (_yaw <= -MathHelper.Pi * 2)
                _yaw = MathHelper.ToRadians(0.0f);
            _yaw += angle;
        }

        /// <summary>
        /// Rotate the camera around the X axis.  Usually called pitch.
        /// </summary>
        /// <param name="angle">Angle in degrees to rotate the camera.</param>
        public override void RotateX(float angle)
        {
            angle = MathHelper.ToRadians(angle);
            _pitch += angle;
            if (_pitch >= MathHelper.ToRadians(75))
                _pitch = MathHelper.ToRadians(75);
            else if (_pitch <= MathHelper.ToRadians(-75))
                _pitch = MathHelper.ToRadians(-75);
        }

        public override void Update(GameTime gameTime)
        {
            Vector3 cameraPosition = _position;
            Matrix rotationMatrix = Matrix.CreateRotationY(_yaw);
            Matrix pitchMatrix = Matrix.Multiply(Matrix.CreateRotationX(_pitch), rotationMatrix);
            Vector3 transformedReference = Vector3.Transform(_cameraReference, pitchMatrix);
            Vector3 cameraLookat = cameraPosition + transformedReference;

            View = Matrix.CreateLookAt(cameraPosition, cameraLookat, Vector3.Up);

            Frustum = new BoundingFrustum(Matrix.Multiply(View, Projection));
            ReflectedFrustum = new BoundingFrustum(Matrix.Multiply(ReflectedView, Projection));
        }
    }
}

ChaseCamera.cs – A camera that chases an object in an over the should view.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;

namespace RoeEngine2.GameComponents
{
    public class ChaseCamera : Camera
    {
        private bool _springEnabled = true;
        public bool SpringEnabled
        {
            get { return _springEnabled; }
            set { _springEnabled = value; }
        }
    
        private Vector3 _position;
        public Vector3 Position
        {
            get { return _position; }
        }

        private Vector3 _velocity;
        public Vector3 Velocity
        {
            get { return _velocity; }
        }

        private float _stiffness = 1800.0f;
        public float Stiffness
        {
            get { return _stiffness; }
            set { _stiffness = value; }
        }

        private float _damping = 600.0f;
        public float Damping
        {
            get { return _damping; }
            set { _damping = value; }
        }

        private float _mass = 50.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private Vector3 _chasePosition;
        public Vector3 ChasePosition
        {
            get { return _chasePosition; }
            set { _chasePosition = value; }
        }

        private Vector3 _chaseDirection;
        public Vector3 ChaseDirection
        {
            get { return _chaseDirection; }
            set { _chaseDirection = value; }
        }

        private Vector3 _up = Vector3.Up;
        public Vector3 Up
        {
            get { return _up; }
            set { _up = value; }
        }    

        private Vector3 _desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);
        public Vector3 DesiredPositionOffset
        {
            get { return _desiredPositionOffset; }
            set { _desiredPositionOffset = value; }
        }

        private Vector3 _desiredPosition;
        public Vector3 DesiredPosition
        {
            get { return _desiredPosition; }
            set { _desiredPosition = value; }
        }

        private Vector3 _lookAtOffset = new Vector3(0, 2.8f, 0);
        public Vector3 LookAtOffset
        {
            get { return _lookAtOffset; }
            set { _lookAtOffset = value; }
        }

        private Vector3 _lookAt;
        public Vector3 LookAt
        {
            get { return _lookAt; }
        }

        public override void Update(GameTime gameTime)
        {
            UpdatePosition();

            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            Vector3 stretch = _position - _desiredPosition;
            Vector3 force = -_stiffness * stretch - _damping * _velocity;

            Vector3 acceleration = force / _mass;
            _velocity += acceleration * elapsed;

            _position += _velocity * elapsed;

            UpdateMatrices();
        }

        private void UpdatePosition()
        {
            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Forward = ChaseDirection;
            transform.Up = Up;
            transform.Right = Vector3.Cross(Up, ChaseDirection);

            // Calculate desired camera properties in world space
            _desiredPosition = ChasePosition +
                Vector3.TransformNormal(DesiredPositionOffset, transform);
            _lookAt = ChasePosition +
                Vector3.TransformNormal(LookAtOffset, transform);
        }

        private void UpdateMatrices()
        {
            View = Matrix.CreateLookAt(Position, LookAt, Vector3.Up);

            Frustum = new BoundingFrustum(Matrix.Multiply(View, Projection));
            ReflectedFrustum = new BoundingFrustum(Matrix.Multiply(ReflectedView, Projection));
        }

        public override void Reset()
        {
            UpdatePosition();

            _velocity = Vector3.Zero;

            _position = _desiredPosition;

            UpdateMatrices();
        }
    }
}

Updates to GameScreensGameplayScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;
using System.Threading;
using RoeEngine2.Models;
using RoeEngine2.SceneObject;
using Microsoft.Xna.Framework.Input;
using RoeEngine2.Shaders;
using RoeEngine2.Texures;
using Kynskavion.GameObjects.Ship;
using Microsoft.Xna.Framework.Graphics;
using Kynskavion.GameObjects.ShipMain;

namespace Kynskavion.GameScreens
{
    class GameplayScreen : GameScreen
    {
        private static double delta = 0.0;
        private static ShipMain shipMain = new ShipMain();

        public GameplayScreen()
        {
            TransitionOnTime = TimeSpan.FromSeconds(1.5);
            TransitionOffTime = TimeSpan.FromSeconds(0.5);

            ChaseCamera camera = new ChaseCamera();
            camera.DesiredPositionOffset = new Vector3(0.0f, 2000.0f, 3500.0f);
            camera.LookAtOffset = new Vector3(0.0f, 150.0f, 0.0f);

            camera.NearPlane = 10.0f;
            camera.FarPlane = 1000000.0f;

            CameraManager.AddCamera(camera, CameraManager.CameraNumber._3);
            CameraManager.SetActiveCamera(CameraManager.CameraNumber._3);
            CameraManager.SetAllCamerasProjectionMatrix((float)EngineManager.Device.Viewport.Width / EngineManager.Device.Viewport.Height);
        }

        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            Random rnd = new Random();
            for (int i = -10000; i <= 10000; i += 5000)
            {
                for (int j = -10000; j <= 10000; j += 5000)
                {
                    for (int k = -10000; k <= 10000; k += 5000)
                    {
                        Ship ship = new Ship(new Vector3(i, j, k));

                        ship.RotationRate = rnd.Next(-10, 10);
                        SceneGraphManager.AddObject(ship);
                    }
                }
            }

            SceneGraphManager.AddObject(shipMain);

            SceneGraphManager.LoadContent();

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            EngineManager.Game.ResetElapsedTime();
        }

        /// <summary>
        /// Unload graphics content used by the game.
        /// </summary>
        public override void UnloadContent()
        {
            SceneGraphManager.UnloadContent();
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
            delta = gameTime.ElapsedGameTime.TotalSeconds;

            ChaseCamera camera = (ChaseCamera)CameraManager.ActiveCamera;

            camera.ChasePosition = shipMain.Position;
            camera.ChaseDirection = shipMain.Direction;
            camera.Up = shipMain.Up;
        }

        public override void HandleInput(GameTime gameTime, Input input)
        {
           
            if (input.PauseGame)
            {
                ScreenManager.AddScreen(new PauseMenuScreen());
            }
            else
            {
                SceneGraphManager.HandleInput(gameTime, input);
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Q))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(0, 3000f * (float)delta, 0.0f));
                }
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Z))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(0, -3000f * (float)delta, 0.0f));
                }
                if (input.CurrentKeyboardState.IsKeyDown(Keys.W))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(0, 0, 1000f * (float)delta));
                }
                if (input.CurrentKeyboardState.IsKeyDown(Keys.S))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(0, 0, -1000f * (float)delta));
                }
                if (input.CurrentKeyboardState.IsKeyDown(Keys.D))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(-1000f * (float)delta, 0, 0));
                }
                if (input.CurrentKeyboardState.IsKeyDown(Keys.A))
                {
                    CameraManager.ActiveCamera.Translate(new Vector3(1000f * (float)delta, 0, 0));
                }
                if (input.CurrentMouseState.RightButton == ButtonState.Pressed)
                {
                    CameraManager.ActiveCamera.RotateX(input.MouseMoved.Y);
                    CameraManager.ActiveCamera.RotateY(input.MouseMoved.X);
                }
            }
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            SceneGraphManager.Draw(gameTime);
        }

        public override void PostUIDraw(GameTime gameTime)
        {
            base.PostUIDraw(gameTime);

            string message = "FPS:  " + EngineManager.FpsCounter.FPS.ToString() + "  Culled:  " + SceneGraphManager.culled.ToString();

            // Center the text in the viewport.
            Vector2 textPosition = new Vector2(10, 20);

            Color color = new Color(255, 255, 255, TransitionAlpha);

            ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
                                            SaveStateMode.SaveState);
            ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, message, textPosition, color);
            ScreenManager.SpriteBatch.End();
        }
    }
}

ConclusionIn this article I introduced many new concepts.  First, I introduced the chase camera.  This camera option is an over the shoulder view camera, it will follow an object around at a nice viewing distance. Next, I demonstrated culling in the SceneGraphManager.  Culling is accomplished view bounds checking with the camera frustum and the bounding box of an object.  Finally, I demonstrated all of these concepts in a simple fly through game.

Looking forward to your comments and suggestions.

February 15, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 7 Comments

XNA Framework GameEngine Development. (Part 11, Static Meshes)

Introduction

Welcome to Part 11 of the XNA Framework GameEngine Development series.  This article will be a huge update to the engine and will introduce some of the first static meshes to the game engine as well as movement within the game.

Part 11

Release

I finally put together a release of the code up until now.  You will see the sample game, the engine, the shader generator and test cases.  Hope you enjoy.  Link to Part 11.

Add Content

For this article you will need to add content items from the XNA team blog.

  • Ship.fbx (from several team site examples)
  • ShipDiffuse.tga (from several team site examples)

Changes to SceneGraphManager GameComponent

I had some issues with the way the SceneGraphManager worked when it was a DrawableGameComponent.  I had very little control over when the SceneGraphManager actually began the draw cycle.

So as a minor improvement I made it a GameComponent instead.  This means, I will have to call Draw, LoadContent and UnloadContent on my own rather than allow the game loop to do so.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.SceneObject.SceneGraph;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    public class SceneGraphManager : GameComponent
    {
        private static Node _root;
        /// <summary>
        /// The root of the scene graph
        /// </summary>
        public static Node Root
        {
            get { return _root; }
        }
    
        /// <summary>
        /// Create the scenegraph Managers.
        /// </summary>
        /// <param name="game"></param>
        public SceneGraphManager(Game game)
            : base(game)
        {
            _root = new Node();
        }

        /// <summary>
        /// Draw objects
        /// </summary>
        /// <param name="gameTime"></param>
        public static void Draw(GameTime gameTime)
        {
            _root.Draw(gameTime);
        }

        /// <summary>
        /// Load the content of all the objects in the scenegraph
        /// </summary>
        public static void LoadContent()
        {
            _root.LoadContent();
        }

        /// <summary>
        /// Unload the content of all the objects in the scenegraph
        /// </summary>
        public static void UnloadContent()
        {
            _root.UnloadContent();
        }

        /// <summary>
        /// Add an object to the scenegraph.
        /// </summary>
        /// <param name="newObject"></param>
        public static void AddObject(RoeSceneObject newObject)
        {
            SceneObjectNode node = new SceneObjectNode(newObject);
            _root.AddNode(node);
        }
    }
}

Material.cs

Material will be used to contain information about the shader used by an object and a list of all the textures used by that object.  I will use this later.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.SceneObject
{
    public class Material
    {
        private string _shader;
        public string Shader
        {
            get { return _shader; }
            set { _shader = value; }
        }

        private List<string> _textureList;
        public List<string> TextureList
        {
            get { return _textureList; }
            set { _textureList = value; }
        }
    }
}

RoeSceneObject.cs

Some updates to this class from last time.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.Interfaces;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.SceneObject
{
    public class RoeSceneObject : IRoeSceneObject
    {
        private bool _readyToRender = false;
        /// <summary>
        /// Is this object ready to render?
        /// </summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
            set { _readyToRender = value; }
        }

        private BoundingBox _boundingBox;
        /// <summary>
        /// The bounding box of this object, used for culling.
        /// </summary>
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
            set { _boundingBox = value; }
        }

        private Material _material;
        public Material Material
        {
            get { return _material; }
            set { _material = value; }
        }

        private string _modelName;
        public string ModelName
        {
            get { return _modelName; }
            set { _modelName = value; }
        }    

        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// The position of this object in 3d space.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
            set { _position = value; }
        }

        private Vector3 _scale = Vector3.One;
        /// <summary>
        /// Scale of the object.
        /// </summary>
        public Vector3 Scale
        {
            get { return _scale; }
            set { _scale = value; }
        }

        private Quaternion _rotation = Quaternion.Identity;
        /// <summary>
        /// Yaw, pitch and roll of the object.
        /// </summary>
        public Quaternion Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }

        public virtual void Draw(GameTime gameTime)
        {
            IRoeModel model = ModelManager.GetModel(_modelName);
            if (model != null && model.ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    foreach (BasicEffect effect in mesh.Effects)
                    {
                        effect.EnableDefaultLighting();
                        effect.PreferPerPixelLighting = true;
                        effect.World = transforms[mesh.ParentBone.Index] * Matrix.Identity;

                        effect.View = CameraManager.ActiveCamera.View;
                        effect.Projection = CameraManager.ActiveCamera.Projection;
                    }
                    mesh.Draw();
                }
            }           
        }
    }  
}

LoadingScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Kynskavion.GameScreens
{
    class LoadingScreen : GameScreen
    {
        bool _loadingIsSlow;
        bool _otherScreensAreGone;

        GameScreen[] _screensToLoad;

        private LoadingScreen(bool loadingIsSlow, GameScreen[] screensToLoad)
        {
            _loadingIsSlow = loadingIsSlow;
            _screensToLoad = screensToLoad;

            TransitionOnTime = TimeSpan.FromSeconds(0.5);
        }

        public static void Load(bool loadingIsSlow, params GameScreen[] screensToLoad)
        {
            foreach (GameScreen screen in ScreenManager.GetScreens())
            {
                screen.ExitScreen();
            }

            LoadingScreen loadingScreen = new LoadingScreen(loadingIsSlow, screensToLoad);

            ScreenManager.AddScreen(loadingScreen);
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

            if (_otherScreensAreGone)
            {
                ScreenManager.RemoveScreen(this);

                foreach (GameScreen screen in _screensToLoad)
                {
                    if (screen != null)
                    {
                        ScreenManager.AddScreen(screen);
                    }
                }

                EngineManager.Game.ResetElapsedTime();
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if ((ScreenState == ScreenState.Active) &&
                (ScreenManager.GetScreens().Length == 1))
            {
                _otherScreensAreGone = true;
            }

            if (_loadingIsSlow)
            {
                const string message = "Loading...";

                // Center the text in the viewport.
                Viewport viewport = EngineManager.Device.Viewport;
                Vector2 viewportSize = new Vector2(viewport.Width, viewport.Height);
                Vector2 textSize = ScreenManager.Font.MeasureString(message);
                Vector2 textPosition = (viewportSize - textSize) / 2;

                Color color = new Color(255, 255, 255, TransitionAlpha);

                ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
                                                SaveStateMode.SaveState);
                ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, message, textPosition, color);
                ScreenManager.SpriteBatch.End();
            }
        }
    }
}

PauseMenuScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;

namespace Kynskavion.GameScreens
{
    class PauseMenuScreen : MenuScreen
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        public PauseMenuScreen()
            : base("Paused")
        {
            // Flag that there is no need for the game to transition
            // off when the pause menu is on top of it.
            IsPopup = true;

            // Create our menu entries.
            MenuEntry resumeGameMenuEntry = new MenuEntry("Resume Game");
            MenuEntry quitGameMenuEntry = new MenuEntry("Quit Game");

            // Hook up menu event handlers.
            resumeGameMenuEntry.Selected += OnCancel;
            quitGameMenuEntry.Selected += QuitGameMenuEntrySelected;

            // Add entries to the menu.
            MenuEntries.Add(resumeGameMenuEntry);
            MenuEntries.Add(quitGameMenuEntry);
        }

        /// <summary>
        /// Event handler for when the Quit Game menu entry is selected.
        /// </summary>
        void QuitGameMenuEntrySelected(object sender, EventArgs e)
        {
            const string message = "Are you sure you want to quit this game?";

            MessageBoxScreen confirmQuitMessageBox = new MessageBoxScreen(message);

            confirmQuitMessageBox.Accepted += ConfirmQuitMessageBoxAccepted;

            ScreenManager.AddScreen(confirmQuitMessageBox);
        }

        /// <summary>
        /// Event handler for when the user selects ok on the "are you sure
        /// you want to quit" message box. This uses the loading screen to
        /// transition from the game back to the main menu screen.
        /// </summary>
        void ConfirmQuitMessageBoxAccepted(object sender, EventArgs e)
        {
            LoadingScreen.Load(false, new BackgroundScreen(),
                                      new MainMenuScreen());
        }

        /// <summary>
        /// Draws the pause menu screen. This darkens down the gameplay screen
        /// that is underneath us, and then chains to the base MenuScreen.Draw.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            ScreenManager.FadeBackBufferToBlack(TransitionAlpha * 2 / 3);

            base.Draw(gameTime);
        }
    }
}

GameplayScreen.cs

This is where all the magic happens.  In this class we will load all of the game content, Update cameras, draw game content, and handle input from the user.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;
using System.Threading;
using RoeEngine2.Models;
using RoeEngine2.SceneObject;
using Microsoft.Xna.Framework.Input;

namespace Kynskavion.GameScreens
{
    class GameplayScreen : GameScreen
    {
        private static double delta = 0.0;

        public GameplayScreen()
        {
            TransitionOnTime = TimeSpan.FromSeconds(1.5);
            TransitionOffTime = TimeSpan.FromSeconds(0.5);

            CameraManager.ActiveCamera.CameraType = Camera.CameraEnumType.FirstPerson;
            CameraManager.ActiveCamera.NearPlane = 10.0f;
            CameraManager.ActiveCamera.FarPlane = 100000.0f;
            CameraManager.ActiveCamera.SetPosition(new Vector3(0, 0, -10000));
            CameraManager.SetAllCamerasProjectionMatrix((float)EngineManager.Device.Viewport.Width / EngineManager.Device.Viewport.Height); 
        }

        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            RoeSceneObject obj = new RoeSceneObject();
            obj.ModelName = "ship";
            SceneGraphManager.AddObject(obj);

            SceneGraphManager.LoadContent();

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            EngineManager.Game.ResetElapsedTime();
        }

        /// <summary>
        /// Unload graphics content used by the game.
        /// </summary>
        public override void UnloadContent()
        {
            SceneGraphManager.UnloadContent();
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
            delta = gameTime.ElapsedGameTime.TotalSeconds;
        }

        public override void HandleInput(Input input)
        {           
            if (input.PauseGame)
            {
                ScreenManager.AddScreen(new PauseMenuScreen());
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.Q))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 3000f * (float)delta, 0.0f));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.Z))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, -3000f * (float)delta, 0.0f));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.W))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 0, 1000f * (float)delta));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.S))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 0, -1000f * (float)delta));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.D))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(-1000f * (float)delta, 0, 0));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.A))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(1000f * (float)delta, 0, 0));
            }
            if (input.CurrentMouseState.RightButton == ButtonState.Pressed)
            {
                CameraManager.ActiveCamera.RotateX(input.MouseMoved.Y);
                CameraManager.ActiveCamera.RotateY(input.MouseMoved.X);
            }
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            SceneGraphManager.Draw(gameTime);
        }

        public override void PostUIDraw(GameTime gameTime)
        {
            base.PostUIDraw(gameTime);
        }
    }
}

Conclusion

In this article, I finally demonstrated some objects in the engine using the basic effect and model loading.  I also included a link to the first release of the engine code.  I hope you enjoy.

I am very interested in your thoughts and suggestions, your feedback is very important.

February 12, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 5 Comments

Follow

Get every new post delivered to your Inbox.