Actor-, Scene-, and L3D? Nodes

To be or not to be (drawn) ... or why actor- and scenenodes?

One important method how to speed up an graphics engine is the way how to decide whether an object is to be drawn or not to be drawn. If something is not visible, for example if it lies behind the camera, it is not important at all and does not need further processing. This is also called frustum culling.

(A): Culled objects (rejected for drawing)
(B): Accepted objects for drawing
(C): Camera
(D): Backplane, everything behind is cut off
(E): Frontplane, anything before that plane is also ignored

The frustum culling is using the axis aligned box-boundaries (aabox) of the objects that should be drawn to estimate how large the object is.

Objects and their axis-aligned box boundaries (aabox)

This datastructures for the frustum culling are computed by the actor- and scenenode system (spatialnodes) which is also called scenemanagement. There is only a small difference between both systems:

Scenenodes should be used for static geometry. It precomputes as much information as possible to speed up the frustum culling. The data structures are only updated, when the scene is changed (i.e. if a scenenode was moved, created or deleted).

Actornodes are similar to scenenodes, except that it is assumed that they are constantly changing and thus, the data structures for the frustum culling are computed once each frame. So actornodes are slightly less efficient, however, if scenenodes would be used for dynamic scenes, the overhead for the precomputation of the datastructures would be less efficient than the approach of the actornodes.

Actornodes can also be attached at particlesystems or physical simulated bodies.

In contrary, scenenodes can also have hierarchic structures of children and parents.

Actornodes vs scenenodes: the bounding boxes of the actornodes are not optimized like in case of the scenenode system, which makes the scenenode system especially for huge stretched objects much more efficient

The script for creating the scene from above looks like this:

function buildscene (adder) -- constructs a scene, 
    -- independent from the used scenegraphmodel
    adder(l3dprimitive.newsphere("sp",1,1,1),0,0,0,0,0,0)
    adder(l3dprimitive.newbox("sp",5,1.5,.5),6,0,0,0,0,0)
    adder(l3dprimitive.newcylinder("sp",1,1.,5),-6,0,0,20,40,0)
end


nodes = {} -- a list of all nodes, prevents garbage collection

function scenenodeadder (l3d,x,y,z,rx,ry,rz)
    -- provides functionality to use the scenenode system
    local node = scenenode.new("node")

    node:localpos(x,y,z)
    node:localrotdeg(rx,ry,rz)
    node.l3d = l3d
    l3d:linkinterface(node)
    l3d:rfLitSun(true)

    nodes[#nodes+1] = node
end

function actornodeadder(l3d,x,y,z,rx,ry,rz)
    -- provides functionality to use the actornode system
    local node = actornode.new("node")

    node:pos(x,y,z)
    node:rotdeg(rx,ry,rz)
    node.l3d = l3d
    l3d:linkinterface(node)
    l3d:rfLitSun(true)

    nodes[#nodes+1] = node
end 

-- use either one or another system to create the same scene:
buildscene(actornodeadder)
--buildscene(scenenodeadder) 

--- some scene setup for our camera / lighting
cam = actornode.new("cam")
l3dcamera.default():linkinterface(cam)
cam:pos(10,10,35)
cam:lookat(0,0,0,0,0,1)

sun = actornode.new("sun",100,40,200)
sun.light = l3dlight.new("sun")
sun.light:makesun()
sun.light:linkinterface(sun)

For dynamic lighting the spatialnodes' bounding box is turned into a transformed bounding capsule. Which means its not axis aligned, but resembles the rotation of an object better. The closest 3 lights, whose range touches the capsule, are used for fxlighting if activated.

How something is drawn

After culling all scene/actornodes put their linked l3dnodes in a drawlist. There is no frustum culling done on these individually, however each l3dnode might contain additional rendering tests such as visibility to a camera (l3dnode.visflag), ranged visibility (l3dnode.cambox / camsphere) or a manual renderflag to disable drawing (l3dnode.rfNodraw). l3dnodes represent an object visually and manage rendering styles like if it is lit, if it is drawn in wireframe, blend state, material assignments, animations... They also allow positional and rotational offsets to their linked spatialnode (l3dnode.uselocal). Finally those drawing informations are put in the l3dlayers and drawn in the l3dviews.

Combining both

Actor- and scenenodes are used to determine if and where something is drawn while l3dnodes will define how and what it is going to be drawn. Because of this, the l3dnodes can be attached at actor- or scenenodes (l3dnode.linkinterface), but they can also be attached at other l3dnodes (l3dnode.parent). Attaching l3dnodes to each other is mostly done for detail objects, and make culling more efficient. For example you have a spacestation with small detail objects. It is very likely that once the station is visible the detailobjects, like radar dishes, turrets are as well. Therefore you would link the l3dmodel of the station to a single spatialnode, and then link the detailobjects to that l3dmodel. You could animate them, link them to bones and so on, yet they would not create any extra culling costs. Also when linking them to the same spatialnode as the station and using local offsets, no additional costs are created.

A small drawback is now, that the scenemanagement is not aware how large an object might be. The size is estimated by the boundaries of the triangles of the object. If this changes (due to vertexshaders or boneanimation), an object may be culled (not drawn) even though it may be visible. Thus it is important to be aware of this information if the size of an object cannot be estimated easily like it is automatically done for static mesh geometry. You can always manually change the boundingbox information of actor- and scenenodes however (actornode / scenenode.vistestbbox)

As an example, this code constructs a composite l3dnode which is attached at the root scenenode. After doing so, the bounding box of the scenenode is fitted to the boundaries of the l3d object.

view = UtilFunctions.simplerenderqueue()
view.rClear:colorvalue(1.0,1.0,1,0) 

snode = scenenode.getroot() -- just get the root node

function l3d(l3d,x,y,z)
    l3d:rfLitSun(true)
    l3d:localpos(x or 0, y or 0, z or 0)
    return l3d
end

snode.l3d = l3d(l3dprimitive.newsphere("s1",1))
snode.l3d:linkinterface(snode)

snode.l3d.box = l3d(l3dprimitive.newbox("b1",.5,3,1),0,1.5,0)
snode.l3d.box:parent(snode.l3d)

snode.l3d.box.cyl = l3d(l3dprimitive.newcylinder("c1",.5,.5,5),0,1.5,0)
snode.l3d.box.cyl:parent(snode.l3d.box)

snode.l3d.box.box = l3d(l3dprimitive.newbox("b2",3,.5,.5),1.5,0,0)
snode.l3d.box.box:parent(snode.l3d.box)

snode:vistestbbox(-1,-1,-2.5,3,3.5,2.5)

--- some scene setup for our camera / lighting
cam = actornode.new("cam")
l3dcamera.default():linkinterface(cam)
cam:pos(10,10,35)
cam:lookat(0,0,0,0,0,1)

sun = actornode.new("sun",100,40,200)
sun.light = l3dlight.new("sun")
sun.light:makesun()
sun.light:linkinterface(sun)