3D World Hierarchy

3D World Hierarchy

Related Topics:
Data Structures and Recursion
Object Oriented Fundamentals
Depth Cues
Quad Property

This demo shows how a 3D hierarchy is built using a tree data structure. It shows how world transforms are calculated from relative transforms, some object-oriented ideas, and how to generate XML from a tree. It has one directional light and a fixed camera.

Drag on object to scale, drag off objects to rotate group
3D World Hierarchy – source movie

Trees are used to model 3D worlds because they reflect how real-world objects are related to each other—objects are composed of smaller objects, which are composed of still smaller objects, etc. In tree terms, an object is the parent of the objects it is made of, which are its children. Each object is programmed as a node in the tree data structure:

In addition, moving an object moves all the parts it is made of. This is accomplished in the 3D model by storing the orientation of each object as an offset of its parent’s orientation. Moving the parent will then move all its children. The absolute orientation of any object in the 3D world is thus determined by a combination of the relative orientations of all its ancestors up to the root of the 3D hierarchy.

In Lingo, the transform data type can be used to store the orientation of each node relative to its parent. It also supports operations that simplify the combining of orientations.

For example, multiplying the transforms of the nodes from the root node to a descendant node gives the world orientation of the descendant node. This 3Dnode method shows how it is done recursively, where trans is the node’s parent-relative transform:

on getWorldTransform()
if parent = void then return trans –root
return parent.getWorldTransform() * trans

Calculating a node’s world transform recursively

For more on the transform data type, see Director’s Help.

Object Geometry
In Shockwave 3D, the geometry of a visible object is determined by what is called a “resource” which can be used at any number of nodes in the hierarchy.

In this demo there is one type of geometry, the plane, which is rendered using the quad property. Instead of using separate resource objects, resource information is included in each node. Each node is either rendered as a plane or is not visible.

Keeping resource information in the tree has limitations (ie can’t reuse), but simplifies the code for this introductory demo. 3D Resource Objects shows how to program separate resource geometry.

Overview of the code
All the code necessary for creating and rendering the 3D world is contained in the two scripts “3Dnode” and “3Drender”. The 3D hierarchy is first built with nodes created from the “3Dnode” script. The root node of the tree is then passed to a new 3Drender object which starts rendering the tree.

This set up process is shown in the setup() handler of the movie script, and is similar to how a Shockwave 3D world is built:

–build 3D tree
root = script(“3dnode”).new(“root”)
cube1 = script(“3dcube”).new(“cube1″,”txtr”,vector(40,40,40))
cube2 = script(“3dcube”).new(“cube2″,”txtr”,vector(40,40,40))
cube3 = script(“3dcube”).new(“cube3″,”txtr”,vector(40,40,40))root.addChild(cube1)

–adjust transforms
root.trans.rotation = vector(75,0,0)
cube1.trans.position = vector(0,150,0)
cube2.trans.position = vector(100,-50,0)
cube3.trans.position = vector(-100,-50,0)
cube2.trans.scale = vector(1.5,1.5,1.5)
cube3.trans.scale = vector(2,2,2)

–start rendering
objRender = script(“3Drender”).new(root)

Setting up the 3D hierarchy

The “3Dcube” script shows how a 3Dnode can be extended to make geometry creation easier. 3Dcube isn’t a resource, it is a 3Dnode with a few extra properties and methods added to automate cube creation (notice 3Dcube’s ancestor is 3Dnode).

Most of the properties and methods of 3Dnode are tree-related methods for adding, finding, and removing nodes. In addition it has four vectors, a transform, and a sprite which store the node’s 3D-related properties.

If a cast member is passed when creating a new 3Dnode, the node will be rendered as a quad (sprite with quad property set). If no member is passed, the node won’t be rendered and only acts to group the nodes beneath it in the tree.

The getWorldTransform() method shows how world transforms are calculated. The world transform of each node is the product of the parent-relative transforms of the nodes in the path from the root to the node.

The toXml() method recursively generates an XML representation of the tree. This is a simple version that lists the names of the nodes, useful for testing and debugging. The method can be modified to write out enough of the 3D world information that the world can be saved as XML and recreated later.

on toXML(me, _tab)
if _tab = void then _tab = “”
str = return & _tab & “<” & name
if child.count = 0 then
str = str & “/>”
str = str & “>”
repeat with c = 1 to child.count
str = str & child[c].toXML(_tab & ” “)
end repeat
str = str & return & _tab & “</” & name & “>”
end if
return str

Encoding the tree as XML

3Dcube extends 3Dnode to make creation and manipulation of cubes easier. Since 3Dcube’s ancestor is 3Dnode, it inherits the properties and methods of 3Dnode. It can be considered a 3Dnode with some extra properties and methods for making cubes.

When a new 3Dcube is created, it automatically creates the 3Dnodes for the cube faces and adds them as its children. The dimensions of the cube are determined by the vector passed to the new() method.

The way it is programmed in the demo shows how object-oriented techniques can be used to make cube manipulation easier. Notice that changing a cube vertex (cuVert) will alter all three faces that share that vertex, because those faces all point to the same vector object for that cube vertex.

One note about altering a cube vertex. The statement

cuNode.cuVert.LTF = cuNode.cuVert.LTF * 2

looks like it should stretch the left-top-front corner of the cube, but doesn’t. What happens is it creates a new vector object (vector() * 2) and stores it in cuNode.cuVert.LTF. This doesn’t affect the vector object that the faces point to. To do that, operate on the x, y, and z values of the vector object separately:

cuNode.cuVert.LTF.x = cuNode.cuVert.LTF.x * 2

After the cube is created, calling setCorners() will resize the cube to the dimensions of the passed vector.

This object provides the render method, and stores the camera and light information. The render method traverses the tree and sets the properties of each node’s sprite. The sprite rendering is taken from The Quad Property, with lighting, z-axis blocking, and clipping added.

Instead of using each node’s getWorldTransform() method, the world transforms are calculated as the tree is traversed. If getWorldTransform() were used each node’s world transform would be recalculated for each node in its sub-tree, which is less efficient.

Things To Try
New geometries
Extend 3Dnode (a la 3Dcube) to make different geometries.

Separate resources from 3Dnodes
To reuse resources, separate the resource information from the 3D hierarchy. Each resource would be a separate tree, and each 3Dnode would have a resource property that would point to the root of a resource tree (or void).

This would allow the same resource to be used more than once in the 3D hierarchy, and modifying the resource would modify all appearances of it in the 3D world. 3D Resource Objects shows how to program separate resource objects.

Mobile camera
This can be done by performing one more transform in rendering, based on the camera’s transform. For an example see 3D Camera Movement.

Tagged on: ,