Tecate and Interactive 3-D

Dr. Dobb's Journal July 1997

Combining networking and 3-D objects

By Peter D. Kochevar

Peter, a software engineer with Digital Equipment Corp. is in residence at the San Diego Supercomputer Center. He can be contacted at kochevar@sdsc.edu.

Browsers such as Netscape's Navigator or Microsoft's Internet Explorer use a text-based "document" metaphor to interface computers and the Internet. An alternative metaphor is to provide 3-D virtual worlds that let you conduct experiments in virtual laboratories, see how to replace a virtual part from the same vantage point a real part would have to be replaced, plan itineraries by previsiting 3-D virtual destinations before actually embarking on journeys, and more.

The Tecate system provides an infrastructure in which application-specific browsers can be quickly crafted to meet the demands of end users. Tecate provides a set of tools for describing animated 3-D scenes and their interaction with end users, which enables the description of interfaces to existing applications and data sources such as the World Wide Web, enables remote collaboration, and fosters software reuse through object orientation. Tecate's networking capabilities allow these worlds to interface with the Web, external databases, other worlds, and existing applications. The modular, object-oriented nature of Tecate worlds allows their components to be reused.

Tecate is publicly available at ftp:// ftp.sdsc.edu/pub/sdsc/graphics/tecate/ tecate.tar.gz. It runs on most UNIX platforms that support OpenGL 3-D. The Mesa OpenGL emulation library can be used for those systems that don't have OpenGL support. Tecate has been successfully installed on Digital Alpha and SGI Irix systems.

Tecate and Other Technologies

Tecate is not a browser -- it is a collection of powerful components that can be combined to create application-specific browsers. Traditional browsers are really "black boxes" that offer limited customization through the setting of parameters. (One exception to this architecture is Sun's HotJava browser, which adopted a component architecture similar to Tecate.)

Tecate uses virtual worlds as an underlying browsing metaphor instead of documents. A Tecate world can share information with other remote worlds. That is, a single world can be built in which multiple users can enter and independently navigate. This is something that is just beginning to appear in products such as Black Sun's CyberHub, a VRML-based multiuser server. The Tecate MOO example demonstrates this capability.

Tecate's API is the Abstract Visualization Language (AVL), a typeless scripting language based on Tcl. AVL extends Tcl by adding object-oriented programming support, 3-D animation, and a sophisticated event-handling mechanism. The growing use of scripting languages such as AVL is evidenced by the recent proliferation of JavaScript, VRMLscript, NeXT's WebScript, VBScript, and the like. AVL differs from many of these languages in that it has 3-D graphics incorporated within it, something that is just beginning to happen with the other languages. If interpreted AVL is too slow, Tecate allows time-critical behavior to be implemented in a compilable language such as C or C++.

AVL has taken a different approach than other 3-D graphics languages. In VRML, for instance, the stress is on creating 3-D graphics and not on specifying the behaviors of objects. VRML's behavior system is loosely coupled to a graphics system based on OpenInventor. In AVL, graphics and behaviors are more tightly coupled. AVL stresses object-object interactions, with graphics appearing as a side effect. In general, VRML is still rooted in classical computer graphics methodology -- the hierarchical scene graph -- while AVL takes a more modern, object-oriented approach.

Although both Tecate and VRML support interactive 3-D graphics, the two systems are very different. VRML uses a traditional "scene graph" to define the graphics, with behavior defined separately. Tecate provides a modern, object-oriented environment in which the graphics are a side effect of object-object interactions. Tecate's full-featured programming language supports networking and database queries, making it easier to dynamically access and visualize information from within Tecate worlds than from within their VRML counterparts.

Tecate's Architecture

Tecate is comprised of a run-time system and API (see Figure 1). The run-time system is an extensible, object-oriented system whose major components are objects as well as entities that appear within interactive virtual worlds. All objects possess four classes of properties:

Tecate's run-time system has a layered architecture consisting of a kernel, a complement of system services, and a toolkit of visualization and user-interface components (see Figure 2). The kernel consists of the Object Manager, the Rendering Engine, and the Communications Stub. The Object Manager manages the creation, deletion, modification, and communication of objects. The Rendering Engine is responsible for creating visual and aural renditions of virtual worlds that represent data. The Communications Stub provides the means for multiple Tecate sessions to run simultaneously while sharing a single virtual environment, thus fostering collaboration among end users.

Tecate's system services consist of interfaces to the Web and to databases managed by the Illustra database management system. The web interface allows any Tecate object to specify a URL from which information is to be drawn, and then mapped into an appropriate virtual world (Figure 3). The database interface functions similarly except that Tecate objects can pose queries written in SQL and the query results are then mapped into an animated, 3-D scene that represents the informational content of the queried data (Figure 4).

You can add new system services to Tecate by providing a set of functions that can be invoked by other Tecate objects. These functions correspond to the behaviors that are called when the service receives a message from other objects. Tools are provided to register the behaviors with Tecate and to manage the communication between the object providing the service and other Tecate objects.

Tecate's toolkit is a set of predefined objects that can be used for developing applications. The toolkit contains windows, lights, and cameras used to illuminate and render virtual worlds on a computer display. The toolkit also contains a collection of 3-D UI widgets -- sliders, menus, icons, legends, coordinate axes, and the like -- that can be used within virtual worlds. Finally, the toolkit contains a clock that is used to pace animations and trigger events. As with the system services, Tecate's base toolkit can be augmented by application programmers.

Describing 3-D Animation

AVL applications are descriptions of active objects. Tecate focuses on creating these objects and defining how they interact with one another over time. Although these objects can have geometry and appearance properties, the handling of graphics is not a primary concern of Tecate. Rather, 3-D animation is a side effect of general object-to-object interactions.

A collection of objects defined within Tecate constitutes a virtual world. Objects in a virtual world are "active" because each object functions independently from other objects under a separate thread of control. Objects interact with one another by passing messages between them. When objects receive messages, handlers are invoked to respond to the messages. These handlers are methods, called "behaviors," that are encapsulated within the objects. Within behaviors, objects can send new messages to other objects; create new objects or destroy old ones; and objects can alter the state, appearance, behavior, and subobject properties of either themselves or other existing objects.

Once all of the objects have been defined, a virtual world is set in motion by sending messages to those objects. These objects process the messages, usually resulting in a new set of messages being sent to yet other objects and so on. Often messages are sent to specific objects by users via input devices such as a mouse or keyboard. In Tecate, input devices are objects that send messages to other objects whenever input is provided. Objects wishing to be informed of input events register themselves with the associated input device objects.

Not all input devices trigger solely upon direct user interaction. For instance, within Tecate, clocks can be set under program control. At every clock tick, all objects that have registered with a clock are sent a message informing objects of the tick. These clocks can be used to time events or trigger actions. More importantly, the clocks are used to pace animations, allowing objects to periodically update their state and appearance.

Aside from object-object relationships via inheritance, AVL also supports object-subobject relationships, which bind one object to another. Currently, there are two types of constraints supported within Tecate:

An object can be logically constrained to any number of objects while at the same time being transformationally constrained to at most one other object. Also, an object can have any number of subobjects of either constraint type provided that all the subobjects are distinct.

In Tecate, there is a single object-creation operation called "cloning," and any object in the system can serve as a prototype from which a copy can be made through the clone operation. A clone inherits properties by maintaining a link to its prototype. When a reference to a property is made within an object, the system looks for the property value locally within the object. If no property value is found, then the object's prototype is searched in order to associate a value with the reference. If the prototype is itself a clone from some other object, then the prototype's prototype is recursively searched to resolve the reference. This type of "lazy" evaluation of property references is known as "delegation."

Note that subobject relationships are not inherited, that is, Tecate only does shallow cloning. Deep cloning, in which the subobjects are recursively cloned, must be done programatically within AVL. Subobject properties are not "intrinsic properties" of an object, as are state variables, appearance attributes, and behaviors. Only intrinsic properties are inherited.

With Tecate's delegation model, a change in a property value can affect all other objects cloned from the original object. This type of semantics is useful for establishing class-instance-like relationships between objects. For instance, one object may represent a particular class of automobile tire while all clones of the object would represent class instances. If a class-level change is needed that affects all instances (for example, a new tread pattern is to be introduced), only the object representing the tire class needs to change rather than having to change all instances individually.

The clone-prototype chaining implied by delegation can be overridden by changing the property values locally. For example, if one particular tire instance is to have a new tread pattern, then the pattern is altered in that instance only. References to the tread pattern for that object will use the local tread value rather than chain back to the tire class object. All other instances will continue to reference the value present in the tire class object.

Since AVL is a superset of Tcl, AVL possesses the standard program-control constructs such as if-then-else, switch, for-loops, and so on. In addition, AVL has a rich set of list and string manipulation commands that it inherits from Tcl.

AVL adds several additional commands to the standard Tcl instruction set. The clone command is the object-creation command within AVL, and the delete command is the complementary operation to delete objects from the system. Object properties are specified and manipulated using the add command and deleted using the remove command. Behaviors in one object are initiated by another object using the send command, which specifies the behavior to invoke and the arguments to be passed. Queries about object properties can be made using the inquire command. The which command is used to determine where an object's properties are actually defined in light of Tecate's use of delegation to resolve property references. Finally, AVL provides a rich set of matrix and vector operators that are useful when positioning objects within 3-D scenes.

A 3-D MOO Example

To illustrate some of the capabilities of Tecate, I'll use AVL to build a framework for a 3-D Multi-User Dungeon Object-Oriented (MOO). A MOO is a shared virtual world that individuals can enter and act within, independent of other users. Prior to entering a shared world, each user defines an avatar -- a 3-D model that will represent his or her presence in the world. As users move about the world, their avatars also move, allowing others to see who is present in the world and what they are looking at.

A 3-D MOO framework can be implemented as an AVL program. A copy of this program is run on each user's computer, creating a local copy of the shared world. This copy interacts with its counterparts to ensure that the copies remain consistent over time.

The program also allows users to enter and leave the shared world. When new users enter a world, their copies of the program request a snapshot of the world's current state from some other copy. The new user's program then builds its local copy of the world based on the information that it is sent. In addition, the presence of new users is broadcast to all others in the MOO, so that their programs can begin sending updates to new users.

In the program, the object named moo (see Listing One,s responsible for creating the local copy of the shared world and for maintaining state consistency. To simplify the example, the specification of the shared world is hardwired into the program. Furthermore, I assume that the shared world does not change over time save for the movement of participants' avatars. Therefore, the state of the MOO at a particular time can be described simply by listing the position and orientation of each avatar.

The moo object is cloned from the WorldViewer object, part of Tecate's basic toolkit. WorldViewer manages the display of the MOO world, and it provides a participant with helicopter-like flight controls for navigating the world. WorldViewer is an "abstract" object; it is not intended to be used directly but rather serves as a template from which other objects can be cloned.

To join a MOO session, new users send a bootstrap message to the moo object on their local host computer. The name of a remote host computer that is already running is passed as an argument, as is a specification of the shape of the avatar new users wish to assume. The moo object on the remote host is then informed that a new user wishes to join the MOO via the enter message. The remote host's moo object responds by sending the new user's local moo object the current state of the MOO using the startUp message. The remote host's moo object also informs all current MOO participants that new users are joining the session by broadcasting a newPlayer message. Note that in AVL, the send command can be used to send a message to an object residing on a remote machine by prepending a host name to the name of the object that is to receive the message.

For simplicity, the name of a distinguished host computer is hardwired into the single program that all MOO participants run. That host must be the first to run the program, thus providing all other participants' programs with a source for acquiring the current state of the MOO.

The move behavior in moo (inherited from WorldViewer) is responsible for altering a user's position within the shared world of the MOO. This behavior is linked automatically to certain mouse and keyboard events when the init message that is inherited from WorldViewer is sent to the moo object. To the default move behavior, moo adds a broadcast of a user's position and orientation to all other MOO participants.

In this example, the shared world of the MOO consists of a collection of rooms. Each room is a clone of the Room abstract prototype object; see Listing Two. Unlike rooms in a building, the rooms within the implemented MOO are not spatially adjacent to one another. Instead, each room serves as a virtual world unto itself. At any given time, a user is located in a single room, and since all rooms in the MOO have no windows, a user can only see the contents of the room in which he or she currently resides. Therefore, only one room needs to be displayed at any given time. When users are in a room, that room is considered to be "realized," and all other rooms are "unrealized."

MOO participants can move from one room to another by crossing a "bridge." A bridge consists of two "portals," each of which appears as a disk in the rooms spanned by the bridge. Figure 5 is a typical room in the MOO containing a few portals, and two avatars signifying the presence of other users in the room. Listing Three presents code for the Bridge abstract prototype object (code for a Portal abstract prototype object is available electronically; see "Availability," page 3).

To travel from one room to another (a process called "wormholing"), a MOO participant can perform one of two actions -- fly into a disk representing a portal, or select a disk with the mouse and be drawn automatically into a portal. Both mechanisms rely on the proximity behavior within a Portal object. If a user moves within a certain predefined distance from the center of a portal, the user is then wormholed into another room by the wormhole behavior of Portal.

Proximity testing is performed each time users move within a realized room. In the body of the init behavior, portal objects notify the room objects in which they reside that they wish to be informed of "realize" events. Then, when a room is first redisplayed, a portal's enableProximity behavior is invoked. This behavior registers with the viewer object that a portal is to be informed of "move" events. When so registered, user movement automatically causes a portal's proximity behavior to be invoked.

When users pick a disk representing a portal, they are automatically flown into the portal. The process by which this happens illustrates how animation occurs within Tecate: Objects wishing to animate inform a clock object that they want to receive clock tick events. In the MOO program, a mouse click on a portal causes the portal to register itself with the predefined clock object called SystemClock. At each clock pulse, a portal's tractor method is called, which moves the user a tenth of the distance from their current position to the center of the portal.

Listing Four (available electronically) is the code segment where the MOO's shared world is defined, and all object initializations take place. The shared world consists of three rooms: a, b, and c. Bridges are created to allow users to wormhole from any room to any other room. Users are initially placed into room a when they first enter the shared world of the MOO. The name of a currently running host and a user's avatar shape description are assumed to be defined in the AVL variables bootstrapHost and shape, respectively. If the program is run on the host named in bootstrapHost, then the moo object there allows the user to move about the three-room world while waiting for entry requests from other users at remote sites.

Acknowledgments

This work was supported by the Digital Equipment Corp. and the San Diego Supercomputer Center. Further support was provided by a grant from NASA's Earth Observing System, Distributed Information System (EOSDIS) project. Thanks to Santi Becerra and Ryan Camoras for their help in constructing the latest Tecate prototype.


Listing One

clone moo WorldVieweradd moo {
   behavior {
      bootstrap {remoteHost shape} {
      # Alert a running session on remoteHost that we want to play
         if {$remoteHost != [gethost]} {
            send $remoteHost:[getself] enter "[gethost] {$shape}"
         } else {
            addstate shape $shape
         }
      }
      enter {host shape} {
      # Register the given host as a player, and let everyone else know
         set hostlist [getstate hosts]
         foreach i $hostlist {
            send [lindex $i 0]:[getself] newPlayer "$host {$shape}"
         }
         send [getsender] startUp "{[lappend hostlist 
                                          "[gethost] {[getstate shape]}"]}"
         send [getself] newPlayer "$host {$shape}"
      }
      newPlayer {host shape} {
      # Add the given host as a new player
         set hostlist [getstate hosts]
         addstate hosts [lappend hostlist "$host {$shape}"]
         puts "([gethost]) newPlayer: creating object $host"
         clone $host Entity
         add $host "appearance {$shape}"

set position [get [getstate camera] state position] set orientation [get [getstate camera] state orientation] send $host:[getself] updateState "[gethost] [getstate room] {$position} {$orientation}" } startUp {hosts} { # Initialize play addstate hosts $hosts foreach i $hosts { set host [lindex $i 0] set shape [lindex $i 1] puts "([gethost]) startUp: creating object $host" clone $host Entity add $host "appearance {$shape}" } send [getself] broadcast {} } move {} { # Move self if flying inherited move {} send [getself] broadcast {} } requestUpdates {} { # Request an update from every other player foreach i [getstate hosts] { set host [lindex $i 0] send $host:[getself] updateMe } } updateMe {} { # Sender wishes to receive update of state set position [get [getstate camera] state position] set orientation [get [getstate camera] state orientation] send [getsender] updateState "[gethost] [getstate room] {$position} {$orientation}" } broadcast {} { # Broadcast current position and orientation to everyone else set position [get [getstate camera] state position] set orientation [get [getstate camera] state orientation] foreach i [getstate hosts] { set host [lindex $i 0] send $host:[getself] updateState "[gethost] [getstate room] {$position} {$orientation}" } } updateState {host room position orientation} { # Update the position and orientation of the player on the given host if {$room == [getstate room]} { set transform [mconcat [mtranslate $position] "$orientation"] add [getstate scene] "subobject {$host {$transform}}" } elseif {[lsearch [inquire [getstate scene]subobject] $host] != -1} { remove [getstate scene] "subobject {$host}" } } } }

Back to Article

Listing Two

clone Room Entityadd Room {
   state {
      scene ""
      window ""
      realized 0
   }
   behavior {
      init {scene window size color} {
      # Initialize the room
         addstate scene $scene
         addstate window $window
         addstate size $size
         add [getself] "appearance {
            shape {box [getstate size]}
            diffusecolor {$color}
         }"
      }
      inLimits {x y z} {
      # Check if given point is inside or outside the room
         set width [lindex [getstate size] 0]
         set height [lindex [getstate size] 1]
         set depth [lindex [getstate size] 2]
         set min $width
         if {$height < $min} {set min $height}
         if {$depth < $min} {set min $depth}
         set pad [expr 0.05*$min]
         if {$pad < $x && $x < [expr $width - $pad]} {
            if {$pad < $y && $y < [expr $height - $pad]} {
               if {$pad < $z && $z < [expr $depth - $pad]} {
                  return 1
               }
            }
         }
         return 0
      }
      realize {target position up} {
      # Display the room and its contents
         addstate realized 1
         add [getstate scene] "subobject {[getself] {[midentity]}}"
         set x [expr 0.5*[lindex [getstate size] 0]]
         set y [lindex [getstate size] 1]
         set z [expr 0.5*[lindex [getstate size] 2]]
         send [getstate window]-light(2) setView "{$x [expr 0.5*$y] 0.0} 
                                                       {$x $y $z} {0 1 0}"
         send [getstate window]-light(3) setView "{$x [expr 0.5*$y] 
                                        [expr $z+$z]} {$x 0.0 $z} {0 1 0}"
         set camera [get [getstate window] state camera]
         send $camera setView "{$target} {$position} {$up}"
         add [getstate window] "state {defaultPos {$position} 
                                                    defaultTar {$target}}"
         send [getstate window] setRoom "[getself]"
         send [getself] callback realize
      }
      unrealize {} {
      # Disable the room's display
         addstate realized 0
         remove [getstate scene] "subobject {[getself]}"
         send [getself] callback unrealize
      }

Back to Article

Listing Three

clone Bridge Entityadd Bridge {
   state {
      portals {}
   }
   behavior {
      init {color room0 target0 position0 up0 room1 target1 position1 up1} {
         clone [getself]-0 Portal
         send [getself]-0 init "[getself] $room0 {$target0} {$position0} 
                                                          {$up0} {$color}"
         clone [getself]-1 Portal
         send [getself]-1 init "[getself] $room1 {$target1} {$position1} 
                                                         {$up1} {$color}"
         addstate portals "[getself]-0 [getself]-1"
         add [getself] "subobject {[getself]-0 [getself]-1}"
      }
      crossBridge {portal} {
         set i [lsearch [getstate portals] $portal]
         if {$i == -1} {
            puts "([getself]:oppositePortal) Unrecognized portal '$portal'"
         } else {
        set i [expr 1 - $i]
        set portal [lindex [getstate portals] $i]
        set position [get $portal state position]
        set target [get $portal state normal]
        set position [vadd $position [vscale 1.1 $target]]
        set target [vadd $position $target]
        send [get $portal state room] realize"{$target}{$position} {0 1 0}"
         }
      }
   }
}

Back to Article

Listing Four

clone Portal Entity
add Portal {
   state {
      bridge   ""
      room    ""
      position ""
      normal   ""
      auto     0
   }
   behavior {
      init {bridge room target position up color} {
         addstate bridge $bridge
         addstate room $room
         addstate position $position
         addstate color $color

         add [getself] "appearance {
            shape {cylinder 0.2 1}
            diffusecolor {$color}
         }"
         set zvec [vnormalize [vadd $target [vscale -1.0 $position]]]
         addstate normal $zvec
         set xvec [vnormalize [vcross $zvec $up]]
         set yvec [vcross $xvec $zvec]
         set orientation [mtranspose "$xvec 0 $yvec 0 $zvec 0 0 0 0 1"]
         add $room "subobject {[getself] {[mconcat [mtranslate $position] $orientation]}}"
         
         send $room register "[getself] realize enableProximity"
         send $room register "[getself] unrealize disableProximity"
         set window [get $room state window]
         send $window register "[getself] Pick-Button-1 beginTractor"
      }

      enableProximity {} {
         set room [getstate room]
         set window [get $room state window]
         send $window register "[getself] move proximity"
      }

      disableProximity {} {
         set room [getstate room]
         set window [get $room state window]
         send $window unregister "[getself] move proximity"
      }

      proximity {} {
         set room [getstate room]
         set window [get $room state window]
         set camera [get $window state camera]
         set cameraPos [get $camera state position]
         set pos [getstate position]

         set d [vlength [vadd $pos [vscale -1.0 $cameraPos]]]
         if {$d &lt; 1.0} {
            if {[getstate auto]} {
               addstate auto 0
               send SystemClock unregister "[getself] tractor"
            }
            send [getself] wormhole
         }
      }

      wormhole {} {
         set room [getstate room]
         send $room unrealize

         set bridge [getstate bridge]
         send $bridge crossBridge "[getself]"

         send [get $room state window] broadcast {}
         send [get $room state window] requestUpdates
      }

      beginTractor {} {
         addstate auto 1

         send SystemClock register "[getself] tractor"
      }

      tractor {} {
         set room [getstate room]
         set window [get $room state window]
         set camera [get $window state camera]
         set cameraPos [get $camera state position]
         set pos [getstate position]

         set disp [vadd $pos [vscale -1.0 $cameraPos]]
         set pos [vadd $cameraPos [vscale 0.1 $disp]]

         set zvec [get $camera state zvec]

         send $camera setView "{[vadd $zvec $pos]} {$pos} {0 1 0}"
         send [getself] proximity

         send $window broadcast {}
         send $window callback move
      }
   }
}

Back to Article

DDJ


Copyright © 1997, Dr. Dobb's Journal