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 = voidthen return trans --root return parent.getWorldTransform()
* trans end
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))
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).
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 = voidthen _tab = ""
str = return & _tab &
"<" & name if child.count
= 0then
str = str & "/>" else
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 end
Encoding the tree as XML
3Dcube
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
etc.
After the cube is created, calling setCorners()
will resize the cube to the dimensions of the passed vector.
3Drender
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.