How 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:
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.
// 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:
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.
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.
// 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.
// 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:
// 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;