Article : Creating A Scene-Graph in XNA
The Basics
Constructing the Tree
As previously mentioned, a scene-graph is a tree structure. This tree structure will be represented by two classes: SceneNodeCollection and SceneNode. A third class, IController, will be used later to animate the nodes.
public class SceneNodeCollection : ICollection<scenenode>
{
[This code simply implements a standard collection and is omitted]
}
public interface IController
{
void UpdateSceneNode(SceneNode node, GameTime gameTime);
}
public class SceneNode
{
// Fields
SceneNodeCollection children = new SceneNodeCollection();
Vector3 position = Vector3.Zero;
Vector3 offset = Vector3.Zero;
Quaternion rotation = Quaternion.Identity;
bool visible = true;
Matrix absoluteTransform = Matrix.Identity;
IController controller;
// Properties
public SceneNodeCollection Children
{
get { return children; }
}
public Vector3 Position
{
get { return position; }
set { position = value; }
}
public Vector3 Offset
{
get { return offset; }
set { offset = value; }
}
public Quaternion Rotation
{
get { return rotation; }
set { rotation = value; }
}
public bool Visible
{
get { return visible; }
set { visible = value; }
}
public BoundingSphere BoundingSphere
{
get { return GetBoundingSphere(); }
}
public Matrix AbsoluteTransform
{
get { return absoluteTransform; }
set { absoluteTransform = value; }
}
public IController Controller
{
get { return controller; }
set { controller = value; }
}
// Default Constructor
public SceneNode()
{
}
//Methods
protected virtual BoundingSphere GetBoundingSphere()
{
return new BoundingSphere(center, 0);
}
public virtual void Update(SceneGraph sceneGraph)
{
if (controller != null)
controller.UpdateSceneNode(this, sceneGraph.GameTime);
}
public virtual void Draw(SceneGraph sceneGraph)
{
}
}
The SceneNodeCollection class implements a standard collection. This class could be replaced by a List class; however, coding standards discourage the use of List as a publicly exposed item.
The SceneNode class implements the leafs of the tree. Each SceneNode maintains a list of children as a SceneNodeCollection. This allows the hiearchial nature of the scene graph to be expressed. Position and Rotation properties on the class allow the location and orientation of the leaf to be expressed in coordinates relative to the parent. The world space coordinates are then calculated from these values by the scene graph. The BoundingSphere property allows the scene graph to determine if the node should be draw by testing it against the view frustum. Finally, the Update method allows the scene graph to change its internal state, and the Draw method allows it to display itself.
Creating visuals
Now that the basis for the scene graph is constructed, a scene node that displays an XNA model will be added.
public class ModelSceneNode : SceneNode
{
// Fields
Model model;
BoundingSphere modelSphere;
// Properties
public Model Model
{
get { return model; }
set
{
model = value;
if (model == null)
modelSphere = new BoundingSphere(Center, 0);
else
CalculateBoundingSphere();
}
}
// Constructors
public ModelSceneNode()
{
}
public ModelSceneNode(Model model)
{
this.model = model;
CalculateBoundingSphere();
}
// Methods
private void CalculateBoundingSphere()
{
//Calculate the bounding sphere for the entire model
modelSphere = new BoundingSphere();
foreach (ModelMesh mesh in model.Meshes)
{
modelSphere = Microsoft.Xna.Framework.BoundingSphere.CreateMerged(
modelSphere,
model.Meshes[0].BoundingSphere);
}
}
protected override BoundingSphere GetBoundingSphere()
{
return modelSphere;
}
public override void Draw(SceneGraph sceneGraph)
{
if (sceneGraph.Camera != null)
{
if (model != null)
{
// Copy the model hierarchy transforms
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
// Render each mesh in the model
foreach (ModelMesh mesh in model.Meshes)
{
foreach (Effect effect in mesh.Effects)
{
BasicEffect basicEffect = effect as BasicEffect;
if (basicEffect == null)
{
throw new NotSupportedException(
"Only the BasicEffect is supported in a model.");
}
//Set the matrices
basicEffect.World = transforms[mesh.ParentBone.Index] *
AbsoluteTransform;
basicEffect.View = sceneGraph.Camera.View;
basicEffect.Projection = sceneGraph.Camera.Projection;
basicEffect.EnableDefaultLighting();
}
mesh.Draw(SaveStateMode.SaveState);
}
}
}
}
}
The ModelSceneNode class inherits from the SceneNode class created earlier. It adds a Model property to allow changing the model it displays. It overrides the CalculateBoundingSphere method to return a custom bounding sphere that encloses the entire model. The Draw method is also overriden to display the model by multiplying (concatenating) each mesh's matrix with the matrix for the scene node (AbsoulteTransform) calculated by the scene graph. This has the effect of allowing the model to be placed anywhere in the scene and follow its parent.
As mentioned previously, a camera allows the scene to be viewed from any angle without the need to reposition the model.
public class Camera
{
// Fields
Vector3 position = Vector3.Zero;
Quaternion rotation = Quaternion.Identity;
float fieldOfView = (float)(System.Math.PI / 4.0);
float aspectRatio = 4f / 3f; // Width/Height
float nearPlane = 1.0f;
float farPlane = 10500.0f;
Matrix view;
Matrix projection;
BoundingFrustum frustum;
// Properties
public Vector3 Position
{
get { return position; }
set { position = value; }
}
public Quaternion Rotation
{
get { return rotation; }
set { rotation = value; }
}
public float FieldOfView
{
get { return fieldOfView; }
set { fieldOfView = value; }
}
public float AspectRatio
{
get { return aspectRatio; }
set { aspectRatio = value; }
}
public float NearPlane
{
get { return nearPlane; }
set { nearPlane = value; }
}
public float FarPlane
{
get { return farPlane; }
set { farPlane = value; }
}
public Matrix View
{
get { return view; }
}
public Matrix Projection
{
get { return projection; }
}
public BoundingFrustum Frustum
{
get { return frustum; }
}
// Default Constructor
public Camera()
{
}
// Methods
public void LookAt(Vector3 target, Vector3 up)
{
view = Matrix.CreateLookAt(position, target, up);
rotation = Quaternion.CreateFromRotationMatrix(Matrix.Invert(view));
}
public virtual void Update(SceneGraph sceneGraph)
{
projection = Matrix.CreatePerspectiveFieldOfView(fieldOfView,
aspectRatio,
nearPlane,
farPlane);
Matrix matrix = Matrix.CreateFromQuaternion(rotation);
matrix.Translation = position;
view = Matrix.Invert(matrix);
frustum = new BoundingFrustum( Matrix.Multiply(view, projection));
}
}
The camera class provides Position and Rotation properties that describe the location of the camera in the world and its orientation. The LookAt method provides a simple means to aim the camera. The Update method performs the necessary calculations to create a view and projection matrix and calculates a new frustum.
February 24th, 2010 - 14:32
This is extremely useful, thank you.