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.
February 24th, 2010 - 14:32
This is extremely useful, thank you.