• entries
155
100
• views
94025

# My preciousss scene

97 views

I thought I'd explain the Spineless scene graph a bit, both because it's somewhat unusual and because feedback would be nice. Though I don't know how many are reading this... Warning: general ranting about scene graphs included. :)

As usual, the scene graph in Spineless is actually a tree of nodes (though some scene graphs are acyclic graphs, this would prevent a lot of optimizations at least with my approach). There are basically two approaches to implementing interesting behaviour in a scene graph: inheriting and composition.

Inheriting means you have a base Node class from which you inherit special node classes, such as LightNode, CameraNode and ModelNode. This is often the first approach you'd think of, and very early incarnations of Spineless used this approach. The problem with this approach is that inheritance is a static relationship: you can't (at least easily, though I'm sure this would be possible with some metaclass hackery) change the superclass of a class at runtime. If you can't comprehend what this means, let me give an example:

Let's assume you have a basic CameraNode which you can position around the world, and which allows you to render the world from its viewpoint. Now, you realize you need a camera which can track objects by always rotating to look at them. No problem, you just inherit from CameraNode and write a TrackingCameraNode. For some other situation, you need a camera that can follow another node, such as a character. Again, no problem, you'll just create a FollowingCameraNode inheriting from CameraNode. And then you need a camera that can follow a node while tracking another node... No problem, just inherit from both FollowingCameraNode and TrackingCameraNode, creating a FollowingTrackingCameraNode? Welcome the dreaded Diamond of Inheritance. Both of them inherit from CameraNode, so you are inheriting from it twice, and everyone knows this is a Bad Thing. Not to mention it's really ugly having to create new subclasses just to implement new functionality.

Now, using composition, this would be much cleaner. One way of handling the above camera problem would be having a CameraNode, a FollowerNode and a TrackerNode. Now, TrackerNode would be the parent of CameraNode, and FollowerNode would be a parent of TrackerNode, like this: root -> follower -> tracker -> camera. Now you can dynamically make any type of node a follower, a tracker or a combination of these. You connect nodes together to create new functionality, not write new classes.

The Spineless scene graph doesn't stop here though. In Spineless, there are no subclasses of Node whatsoever, and nodes are just dumb containers for arbitraty attributes: they have no idea about what their attributes are for. Contrary to most scene graphs, even the transformation of a node is no special attribute. All the intelligence is contained in the World class and various "workers" for different subsystems, such as renderer, audio and physics. From the developer's point of view, all you have to do is create nodes, assign their attributes and connect them to each other. Then assign the root of your tree as the root node of a World. Something like this:

rootNode = Node()rootNode.projection = PerspectiveProjection(45.0, 1.0, 100.0)rootNode.lighting = Lighting(enable=True)lightNode = Node(rootNode)lightNode.light = DirectionalLight()lightNode.rotation = quaternion.lookAt(Vector(0, -1, 0))tinyBox = Box(0.1, 0.1, 0.1)boxNode = Node(rootNode)boxNode.render = tinyBox.createPrimitive().renderboxNode.renderMaterial = Material()boxNode.renderMaterial.diffuse = color.REDboxNode.collisionGeometry = tinyBox.createCollisionGeometry()cameraNode = Node(rootNode)cameraNode.position = Vector(0, 5, 5)transform.lookAt(cameraNode, boxNode)myWorld = World()myWorld.rootNode = rootNodemyView = View()myView.viewNode = cameraNodemyWorld.addView(myView)

Now for example, the projection and lighting attributes are recognized by the renderer (inside the view) and collisionGeometry is recognized by the Space class, which keeps track of spatial relationships between nodes: collisions, sorting nodes by distance etc. You'll probably notice that all of this is quite low level. Also, details of the interface are not yet set in the stone, especially concerning geometry. I'm yet to create a higher level interface, but I already have a plan for it. The plan is called entities.

Entities manage subtrees of nodes: they always have a rootNode, and might have other named nodes. For example, a third person camera entity might have a focusNode which you can attach to a node you want to follow and a viewNode which is the actual camera location. A ModelNode might contain a list of meshNodes and helperNodes. These are of course implemented using the low-level nodes. Also, they will usually be loaded from data files instead of being specified manually.

Whew, that was a long post. I hope I get at least one useful comment for this. :) Constructive criticism and questions are very welcome. I'm probably going to explain the inner workings of scene management in a later post, especially if there's interest (which you can express by commenting).

EDIT: Before anyone comments on a tree structure not being the best option for all purposes: that's just the public interface visible for the developer. Internally, nodes are stored in various data structures best suited for each task. But more on this in a later post.

Very interesting. I would love to hear more about the nuts-and-bolts of the system.

I second that. Very good illustration of how composition should be preferred over inheritance. Nice entry!

I agree with your main point. Inheritance is limiting and may make things messy, composition is cleaner and more versatile.

As for the details, if you want the camera to follow some object, you can just put it in the same node as the object. If you want it to follow at a fixed distance, orbit around it, etc., you do that as a child transform. So to do the following of a Tomb-raideresque camera:

root
node1 (trans1, rot1)
node2 (trans2, rot2)
Lara
node3 (trans3, rot3)
camera

In the implementation, you find the camera (in your case, you have a reference to it in View), and apply the relevant transforms inverted and in reverse order:

-trans3 * -rot3 * -trans2 * -rot2 * -trans1 * -rot1

Maybe the order is rot -> trans, I always get confused by this; the point is it should apply the rotations and translations in reverse order than you'd normally use when visiting a node.

So there you have the viewing matrix. Now you walk the tree and render the stuff normally. All this is is transparent to the user, who places the camera in the scene just like any object.

As for the tracking, I would have a special LookAt object from which you can build an orientation at runtime.

cameranode.rotation = LookAt(cameranode, Lara, up=vector.UNIT_Y)

Which would take care of adjusting the rotation from the absolute coordinates of this node and Lara's.

LookAt is just a delayed value, and as such it can be any callable really.

def lookat(me, ob, up):
return lambda: quaternion.lookAt(me.absolutePosition, ob.absolutePosition, up)

But a callable class instance may be nicer for debugging and instrumenting.

Just some random ideas..

Sorry, indentation got fudged up in that example.. I hope you get the idea.