Prototyping

Gavin Bell, <gavin@sgi.com>, Silicon Graphics
Mitra <mitra@mitra.biz>

This document can be found at http://reality.sgi.com/employees/gavin/vrml/Prototypes.html. It was last updated by Gavin on Jan 7, 1996.


Prototyping is a mechanism that allows the set of node types to be extended from within a VRML file. It allows the encapsulation and parameterization of geometry, behaviors, or both.

PROTO

A prototype is defined using the PROTO keyword, as follows:

PROTO typename [ eventIn eventtypename name,
                 eventOut eventtypename name,
                 field fieldtypename name defaultValue,
                 ... ]
      { node { ... } Script and/or ROUTES }

A prototype is NOT a node; it merely defines a prototype (named 'typename') that can be used later in the same file as if it were a built-in node. The implementation of the prototype is contained in the scene graph rooted by node. That node may be followed by Script and/or ROUTE declarations, as necessary to implement the prototype.

The eventIn and eventOut declarations export events inside the scene graph given by node. Specifying the type of each event in the prototype is intended to prevent errors when the implementation of prototypes are changed, and to provide consistency with EXTERNPROTO. Events generated or received by nodes in the prototype's implementation are associated with the prototype using the new keyword IS.

Fields hold the persistent state of VRML objects. Allowing a prototype to export fields allows the initial state of a prototyped object to be specified by prototype instances. The fields of the prototype are associated with fields in the implementation using the IS keyword.

Prototype definitions may be nested. Prototype or DEF names declared inside the prototype are not visible outside the prototype.

A prototype is instantiated as if typename were a built-in node. A prototype instance may be DEF'ed or USE'ed. For example, a simple chair with variable colors for the leg and seat might be prototyped as:

PROTO TwoColorChair [ field MFColor legColor .8 .4 .7,
        field MFColor seatColor .6 .6 .1 ] {
     Separator {
        Separator {
            DEF seat Material { diffuseColor IS seatColor }
            Cube { ... }
        }
        Separator {
            Transform { ... }
            DEF leg Material { diffuseColor IS legColor }
            Cylinder { ... }
        }
    } # End of root Separator
} # End of prototype
# Prototype is now defined.  Can be used like:
TwoColorChair { legColor 1 0 0  seatColor 0 1 0 }

Note: PROTO gives people their non-instantiating DEF:
PROTO foo [] { Cube { } } foo { }
... is equivalent to DEF foo Cube { }
USE foo
... is equivalent to foo { }
Should DEF/USE be kept as convenient shorthand?

NodeReference

What if we wanted a prototype that could be instantiated with arbitrary geometry? For example, we might want to define a prototype chair that allowed the geometry for the legs to be defined, with the default (perhaps) being a simple cylinder.

VRML 1.1 will include the SFNode field type-- a field that contains a pointer to a node. Using SFNode, it is easy to write the first part of the PROTO definition:

PROTO Chair [ field SFNode legGeometry ]

... but we'd get stuck trying to insert it into the implementation's scene graph using IS. This can be accomplished with a new node, the NodeReference node:

NodeReference {
    field SFNode node NULL  # (NULL is valid syntax for SFNode)
    # eventIn SFNode setNode
    # eventOut SFNode nodeChanged
}

Functionally, NodeReference is a "do-nothing" node-- it just behaves exactly like whatever 'nodeToUse' points to (unless nodeToUse is NULL, of course, in which case NodeReference does nothing). For example, this would be a verbose way to add a Sphere to the scene:

NodeReference { nodeToUse Sphere { } }

NodeReference is only interesting if its nodeToUse field is exposed in a prototype (or it receives a nodeToUse event). So, for example, our Chair with arbitrary leg geometry (with a Cylinder default if none is specified) can be filled out as:

PROTO Chair [ field SFNode legGeometry Cylinder { } ] {
     Separator {
        Separator {
            Transform { ... }
            DEF NR NodeReference { nodeToUse IS legGeometry }
        }
        Separator {
            Transform { ... }
            USE NR
        }
        ... would re-use leg with a USE NR, would have
        geometry for seat/back/etc...
    }
}

Using the Chair prototype would look like:

Chair {
    legGeometry Separator { Shape/Coordinate3/IndexedFaceSet/etc }
}

It might also make sense to share the same geometry between several prototype instances; for example, you might do:

Chair {
    legGeometry DEF LEG Separator { Coordinate3/IndexedFaceSet/etc }
}
... somewhere later in scene...
Chair {
    legGeometry USE LEG
}

Note that SFNode fields follow the regular DEF/USE rules, and that SFNode fields contain a pointer to a node; using DEF/USE an SFNode field may contain a pointer to a node that is also a child of some node in the scene graph, is pointed to by some other SFNode field, etc.

The NodeReference node has nice, clean semantics, and allows a lot of flexibility and power for defining prototypes. It also has some nice implementation side effects:

Browsers that want to maintain a different internal representation for the scene graph can implement NodeReference so that nodeToUse is read and the different internal representation is generated. Optimizations might also be performed at the same time:

Browsers that optimize scene graphs can implement NodeReference such that whenever nodeToUse changes an optimized scene is created. When rendering, the optimized scene will be used instead of the unotimized scene.

A really smart browser will figure out that nobody is using the unoptimized scene and may free it from memory.

EXTERNPROTO

A second form of the prototype syntax allows prototypes to be defined in external files:

EXTERNPROTO typename [ eventIn eventtypename name,
                       eventOut eventtypename name,
                       field fieldtypename,
                       ... ]
            URL or [ URL, URL, ... ]

In this case, the implementation of the prototype is found in the given URL. The file pointed to by that URL must contain ONLY a single prototype implementation (using PROTO). That prototype is then given the name typename in this file's scope (allowing possible naming clashes to be avoided). It is an error if the eventIn/eventOut declaration in the EXTERNPROTO is not a subset of the eventIn/eventOut declaration specified in URL.

Note: The rules about allowing exporting only from files that contain a single PROTO declaration are consistent with the WWWInline rules; until we have VRML-aware protocols that can send just one object or prototype declaration across the wire, I don't think we should encourage people to put multiple objects or prototype declarations in a single file. When we DO have VRML-aware protocols, the best way to refer to one of many PROTO's in a file is probably to add the PROTO's name to the end of the URL for the file (e.g http://machine/directory/file/protoname).

We need to think about scalability when using nested EXTERNPROTO's. EXTERNPROTOS don't have bounding boxes specified like WWWInlines, and they might need them. I'm starting to think that we might need to add bboxCenter/Size fields to Separator instead of having them only on WWWInline; with animations possible, pre-specifying maximum-possible bounding boxes could save a lot of work recalculating bounding boxes as things move.

Note to myself: Desireable to allow reporting of "fraction fetched" for objects than can be loaded across the web. EXTERNPROTO is just syntax; implies that ANY node class may need to report on how much fetching has been done (there's no helper-node like WWWInline).