Sgt. Conker We are "absolutely fine"

28Oct/094

Article : Creating A Scene-Graph in XNA

Putting it all together

With all the basic elements now created, its time to tie them all together into a scene graph.

  public class SceneGraph
    {
        // Fields

        SceneNode rootNode = new SceneNode();
        GraphicsDevice device;
        Camera camera;
        GameTime gameTime;
        int nodesCulled;

        // Properties

        public SceneNode RootNode
        {
            get { return rootNode; }
            set { rootNode = value; }
        }

        public Camera Camera
        {
            get { return camera; }
            set { camera = value; }
        }

        public GameTime GameTime
        {
            get { return gameTime; }
        }

        public GraphicsDevice GraphicsDevice
        {
            get { return device; }
        }

        public int NodesCulled
        {
            get { return nodesCulled; }
        }

        // Constructor
        public SceneGraph(GraphicsDevice device)
        {
            this.device = device;
        }

        // Methods
        void CalculateTransformsRecursive(SceneNode node)
        {
            node.AbsoluteTransform = Matrix.CreateTranslation(node.Offset) *
                Matrix.CreateFromQuaternion(node.Rotation) *
                node.AbsoluteTransform *
                Matrix.CreateTranslation(node.Position);

            //Update children recursively
            foreach (SceneNode childNode in node.Children)
            {
                childNode.AbsoluteTransform = node.AbsoluteTransform;
                CalculateTransformsRecursive(childNode);
            }
        }

        void UpdateRecursive(SceneNode node)
        {
            //Update node
            node.Update(this);

            //Update children recursively
            foreach (SceneNode childNode in node.Children)
            {
                UpdateRecursive(childNode);
            }
        }

        void DrawRecursive(SceneNode node)
        {
            //Draw
            if (node.Visible)
            {
                BoundingSphere transformedSphere = new BoundingSphere();

                transformedSphere.Center = Vector3.Transform(node.BoundingSphere.Center,
                                                             node.AbsoluteTransform);
                transformedSphere.Radius = node.BoundingSphere.Radius;

                if (camera.Frustum.Intersects(transformedSphere))
                {
                    node.Draw(this);
                }
                else
                {
                    nodesCulled++;
                }
            }

            foreach (SceneNode childNode in node.Children)
            {
                DrawRecursive(childNode);
            }
        }

        void CalculateTransforms()
        {
            CalculateTransformsRecursive(rootNode);
        }

        public void Update(GameTime time)
        {
            gameTime = time;

            if (camera != null)
                camera.Update(this);

            UpdateRecursive(rootNode);
            CalculateTransformsRecursive(rootNode);
        }

        public void Draw()
        {
            nodesCulled = 0;
            rootNode.AbsoluteTransform = Matrix.Identity;

            DrawRecursive(rootNode);
        }
    }

The scene-graph encompasses the entire scene tree by maintaining a root node. This node in turn maintains a list of children which in turn maintain a list of children and so forth. Traversal of the tree is accomplished through recursion. The scene-graph performs an operation on a node and calls itself recursively for each child in that node. The process repeats until all nodes have been processed.

During the recursion for calculating a node's AbsoluteTransform, the parent node sets its children to have the same transform as itself. In turn, when the children are called, they modify this transform adding their own position and rotation and propagate it to their children.

node.AbsoluteTransform = Matrix.CreateTranslation(node.Offset) *
                             Matrix.CreateFromQuaternion(node.Rotation) *
                             node.AbsoluteTransform *
                             Matrix.CreateTranslation(node.Position);

A node first moves (translates) to its offset. It then rotates the amount specified and concatenates the transform given to it by its parent. Finally, it moves to its desired position which is now relative to the parent.

View frustum culling occurs during the draw traversal of the graph. The bounding sphere for a node is placed in the world via its world matrix (AbsoluteTransform). It is then compared against the view frustum of the camera. If the sphere is determined to be in the frustum, the node is drawn; otherwise, it is skipped.

Example Usage

The following represents the minimal amount of code required to display a model using the scene-graph in XNA.

   public class SceneGraphSampleGame : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        ContentManager content;
        SceneGraph sceneGraph;

        // Constructor

        public SceneGraphSampleGame()
        {
            graphics = new GraphicsDeviceManager(this);
            content = new ContentManager(Services);
        }

        // Methods

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            sceneGraph = new SceneGraph(graphics.GraphicsDevice);

            // Create a node to display a model
            ModelSceneNode modelNode = new ModelSceneNode(
                                            content.Load<model>("Content/Ship"));

            // Attach the node to the graph
            sceneGraph.RootNode.Children.Add(modelNode);

            // Create a camera to view the node.
            sceneGraph.Camera = new Camera();
            sceneGraph.Camera.Position = new Vector3(0,0,200);
        }

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

            // Update the scene-graph
            if (sceneGraph != null)
                sceneGraph.Update(gameTime);

            // Update other components
            base.Update(gameTime);
        }

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

            // Draw the scene-graph
            if (sceneGraph != null)
                sceneGraph.Draw();

            // Draw other components
            base.Draw(gameTime);
        }
    }

As shown, a member of type SceneGraph is added to the default XNA render loop. The scene-graph is constructed in the LoadGraphicsContent method by creating a ModelSceneNode and a Camera class. The scene-graph is updated in the Update method and displayed in the Draw method.

Controllers

With the scene-graph now built, we can start making controllers that move the nodes. A controller is a class that implements IController and will be called when the node is updating.

 public abstract class ControllerBase : IController
    {
        public abstract void UpdateSceneNode(SceneNode node, GameTime gameTime);
    }

    public class XRotationController : ControllerBase
    {
        // Fields

        float radiansPerSecond;

        // Properties

        public float RadiansPerSecond
        {
            get { return radiansPerSecond; }
            set { radiansPerSecond = value; }
        }

        // Default Constructor
        public XRotationController()
        {
        }

        // Methods

        public override void UpdateSceneNode(SceneNode node, GameTime gameTime)
        {
            if (radiansPerSecond != 0.0f)
            {
                float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
                float rotation = radiansPerSecond * elapsedTime;

                node.Rotation *= Quaternion.CreateFromYawPitchRoll(0, rotation, 0);
            }
        }
    }

    public class YRotationController : ControllerBase
    {
        // Private Fields

        float radiansPerSecond;

        // Public Properties

        public float RadiansPerSecond
        {
            get { return radiansPerSecond; }
            set { radiansPerSecond = value; }
        }

        // Default Constructor

        public YRotationController()
        {
        }

        // Methods

        public override void UpdateSceneNode(SceneNode node, GameTime gameTime)
        {
            if (radiansPerSecond != 0.0f)
            {
                float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
                float rotation = radiansPerSecond * elapsedTime;

                node.Rotation *= Quaternion.CreateFromYawPitchRoll(rotation, 0, 0);
            }
        }
    }

   public class ZRotationController : ControllerBase
    {
        // Fields

        float radiansPerSecond;

        // Properties

        public float RadiansPerSecond
        {
            get { return radiansPerSecond; }
            set { radiansPerSecond = value; }
        }

        // Default Constructor

        public ZRotationController()
        {
        }

        // Methods

        public override void UpdateSceneNode(SceneNode node, GameTime gameTime)
        {
            if (radiansPerSecond != 0.0f)
            {
                float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
                float rotation = radiansPerSecond * elapsedTime;

                node.Rotation *= Quaternion.CreateFromYawPitchRoll(0, 0, rotation);
            }
        }
    }

The ControllerBase class is an abstract class that implements the IController interface. This class is used for convenience and is not required since a class may directly implement IController.

The XRotationController, YRotationController, and ZRotationController all perform the same function of rotating a scene node albeit on different axes. A rotation rate is specified using the RadiansPerSecond property and the required calculations are performed in the Update method. The Update method uses quaternion concatenation to add each additional amount of rotation.

The following code shows the changes to the LoadGraphicContent method to use a controller:


    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        sceneGraph = new SceneGraph(graphics.GraphicsDevice);

        ModelSceneNode modelNode = new ModelSceneNode(content.Load<model>("Content/Ship"));
        sceneGraph.RootNode.Children.Add(modelNode);

        sceneGraph.Camera = new Camera();
        sceneGraph.Camera.Position = new Vector3(0,0,200);

        // Code added to use a controller
        YRotationController rotationController = new YRotationController();
        rotationController.RadiansPerSecond = (float)Math.PI;

        modelNode.Controller = rotationController;
    }

Once the controller is attached to the scene node, it updates the scene node without the need for further code. The sample will now rotate the model one revolution (PI) per second.

Next Page

About Sgt. Conker

The Sergeant!
Comments (4) Trackbacks (0)
  1. This is extremely useful, thank you.

  2. Cheers! This is excellent

  3. great share, great article, very usefull for me…thank

    you

  4. great article!


Leave a comment


*

No trackbacks yet.