Click or drag to resize
DigitalRuneHow To: Add a Custom Scene Node

DigitalRune Graphics contains an extensible scene graph. This section demonstrates how to add a new type of scene node.

This topic contains the following sections:

Creating a custom scene node

Let's assume we want to render text labels at 3D position within the scene. To define the text labels we can implement a new type of SceneNode.

The following code defines a new scene node that stores a color and a text.

C#
// The TextNode contains a text (string) and a color.
public class TextNode : SceneNode
{
  // ----- The properties of the new scene node.
  public Color Color { get; set; }
  public string Text { get; set; }

  // ----- The constructor sets relevant scene node properties.
  public TextNode()
  {
    // The IsRenderable flag needs to be set to indicate that the scene node should 
    // be handled during rendering.
    IsRenderable = true;

    // The CastsShadows flag needs to be set if the scene node needs to be rendered 
    // into the shadow maps. But in this case the scene node should be ignored.
    CastsShadows = false;

    // A bounding shape needs to be set for frustum culling.
    Shape = new PointShape();
  }

  // ----- The following methods are required by the cloning mechanism:
  // CreateInstanceCore() is called when a clone needs to be created.
  protected override SceneNode CreateInstanceCore()
  {
    return new TextNode();
  }

  // CloneCore() is called to initialize the clone.
  protected override void CloneCore(SceneNode source)
  {
    // Copy the SceneNode properties (base class).
    base.CloneCore(source);

    // Copy the TextNode properties.
    var sourceTextNode = (TextNode)source;
    Color = sourceTextNode.Color;
    Text = sourceTextNode.Text;
  }
}

The newly created scene node can be used to define a label within a scene. For example:

C#
var textNode = new TextNode
{
  PoseLocal = new Pose(new Vector3F(1, 2, 3),
  Color = Color.Red,
  Text = "Label123",
};
myScene.Children.Add(textNode);

But DigitalRune Graphics does not yet know how to render a TextNode. A new type of SceneNodeRenderer needs to be implemented.

Creating a new scene node renderer

A SceneNodeRenderer handles one or more types of scene nodes. For example, the MeshRenderer handles MeshNodes, the BillboardRenderer handles BillboardNodes and ParticleSystemNodes, etc.

Let's implement a scene node renderer that draws text labels.

C#
// The TextRenderer is a custom scene node renderer which draws TextNodes using a 
// SpriteBatch and a SpriteFont.
public class TextRenderer : SceneNodeRenderer
{
  private SpriteBatch _spriteBatch;
  private SpriteFont _spriteFont;

  public TextRenderer(IGraphicsService graphicsService, SpriteFont font)
  {
    if (graphicsService == null)
      throw new ArgumentNullException("graphicsService");
    if (font == null)
      throw new ArgumentNullException("font");

    _spriteBatch = new SpriteBatch(graphicsService.GraphicsDevice);
    _spriteFont = font;

    // The TextRenderer should be called after all other scene node renderers.
    // This is only relevant if different types of scene nodes (e.g. MeshNodes, 
    // TextNodes, ...) are rendered at the same time.
    Order = 100;
  }

  // CanRender() checks whether a given scene node can be rendered with this
  // scene node renderer.
  public override bool CanRender(SceneNode node, RenderContext context)
  {
    return node is TextNode;
  }

  // Render() draws a list of scene nodes.
  public override void Render(IList<SceneNode> nodes, RenderContext context, RenderOrder order)
  {
    // For simplicity we ignore the 'order' parameter and do not sort the TextNodes
    // by distance.
    var graphicsDevice = context.GraphicsService.GraphicsDevice;
    var cameraNode = context.CameraNode;
    if (cameraNode == null)
      return; // No camera set.

    Matrix view = (Matrix)cameraNode.View;
    Matrix projection = cameraNode.Camera.Projection;
    var viewport = graphicsDevice.Viewport;

    // Use the SpriteBatch for rendering text.
    _spriteBatch.Begin();

    for (int i = 0; i < nodes.Count; i++)
    {
      var node = nodes[i] as TextNode;
      if (node != null)
      {
        // Draw text centered at position of TextNode.
        Vector3 positionWorld = (Vector3)node.PoseWorld.Position;
        Vector3 positionScreen = viewport.Project(positionWorld, projection, view, Matrix.Identity);
        Vector2 position2D = new Vector2(positionScreen.X, positionScreen.Y);
        Vector2 size = _spriteFont.MeasureString(node.Text);
        _spriteBatch.DrawString(_spriteFont, node.Text, position2D - size / 2, node.Color);
      }
    }

    _spriteBatch.End();
  }
}

A SceneRenderer can be used to create a renderer which combines the TextRenderer with other scene node renderers.

C#
// Let's create a renderer that supports meshes, and text nodes.
var sceneRenderer = new SceneRenderer();
sceneRenderer.Renderers.Add(new MeshRenderer());
sceneRenderer.Renderers.Add(new TextRenderer(graphicsService, spriteFont)

This renderer can be used to render a scene:

C#
// Set render context info.
context.CameraNode = Player.CameraNode;
context.RenderPass = "Default"; // Needs to be set for MeshNodes.
context.Scene = myScene;

var graphicsDevice = context.GraphicsService.GraphicsDevice;
graphicsDevice.DepthStencilState = DepthStencilState.Default;
graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
graphicsDevice.BlendState = BlendState.Opaque;

// Render all scene nodes within camera frustum.
var query = myScene.Query<CameraFrustumQuery>(context.CameraNode);
sceneRenderer.Render(query.RenderableNodes, context);

// Clean up.
context.Scene = null;
context.RenderPass = null;
context.CameraNode = null;