Skip to content

Prototypes

Andreas Plesch edited this page Apr 26, 2022 · 40 revisions

Prototypes

X3D Prototypes allow defining and using your own fully functional custom nodes. x3dom has experimental support for X3D Prototypes and adds x3dom custom capabilities.

Table of Contents

Introduction

An advanced feature of X3D is the ability to define new nodes as a scene author, without coding. Once defined, the new node can be used in a scene just like a regular X3D node. An example would be a new <teapot> node which could be used just like a <box> node.

The new node is based on a combination of existing nodes, and a definition of what kind of fields it accepts. For example, a <teapot> node would be based on a <IndexedFaceSet> node and could sport a solid field which controls display of the inside of the teapot.

Two main element tags are provided - to first define a new node, and then use the node in the scene. The <ProtoDeclare> tag provides the definition by employing a child <ProtoInterface> tag for what fields it accepts, and a child <ProtoBody> tag for how exising X3D nodes are combined. Then a <ProtoInstance> tag will insert the new node identified by a name at the desired location in the scene graph. All of these tags make up what is referred to as X3D Prototypes or sometimes just as protos.

X3D documentation

There is extensive documentation on X3D Prototypes, generally available from http://web3d.org. A good starting point is this slideset from the book "X3D for Web Authors": http://x3dgraphics.com/slidesets/X3dForWebAuthors/Chapter14Prototypes.pdf

The chapter is accompanied by examples available here: https://x3dgraphics.com/examples/X3dForWebAuthors/Chapter14Prototypes/ . The page contains links to node summaries such as https://www.web3d.org/x3d/content/X3dTooltips.html#ProtoDeclare.

Please note that many Prototype examples use a <Script> node in the <ProtoBody>. X3D <Script> nodes are currently not supported in x3dom since there are hard to resolve conflicts with the native HTML <script> element.

The relevant standard specification sections are

https://www.web3d.org/documents/specifications/19775-1/V3.3/Part01/concepts.html#PROTOdefinitionsemantics

https://www.web3d.org/documents/specifications/19775-1/V3.3/Part01/components/core.html#PROTOStatement

Minimal example

As a minimal example, let's create a new node named <DiffuseApp> which can replace a regular <Appearance> node in a more convenient but restricted way:

    <ProtoDeclare name='DiffuseApp'>
      <ProtoInterface>
        <field accessType='inputOutput' name='diffuseColor' type='SFColor' value='.8 .4 .7'></field>
      </ProtoInterface>
      <ProtoBody>
        <Appearance>
            <Material>
                <IS>
                    <connect nodeField='diffuseColor' protoField='diffuseColor'></connect>
                </IS>
            </Material>
        </Appearance>
      </ProtoBody>
    </ProtoDeclare>
    <!-- End of prototype -->
    <!-- The prototype is now defined. -->

    <Shape>
      <!-- The prototype is used here. -->
      <ProtoInstance name='DiffuseApp'>
        <fieldValue name='diffuseColor' value='1 0 0'></fieldValue>
      </ProtoInstance>
      <Cone></Cone>
    </Shape>

It would be straightforward to connect more or all <Material> fields to make the proto more useful.

In x3dom, it is possible to be more concise (see below) and use the new node this way:

    <Shape>
      <DiffuseApp diffuseColor='1 0 0'></DiffuseApp>
      <Cone></Cone>
    </Shape>

Annotated example

The example is augmented from https://www.web3d.org/x3d/content/examples/Basic/X3dSpecifications/PrototypeIndex.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.3//EN" "http://www.web3d.org/specifications/x3d-3.3.dtd">
<X3D profile='Immersive' version='3.3' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-3.3.xsd'>
  <head>
    <meta content='Prototype_InRoute.x3d' name='title'/>
    <meta content='X3D encodings example: defining a Prototype, demonstration of IS/connect definitions.' name='description'/>
    <meta content='Don Brutzman and Joe Williams' name='creator'/>
    <meta content='1 June 2002' name='created'/>
    <meta content='28 October 2019' name='modified'/>
    <meta content='X3D encodings, ISO/IEC 19776-1.3, Part 1: XML encoding, Annex C.4 Prototype example' name='specificationSection'/>
    <meta content='https://www.web3d.org/documents/specifications/19776-1/V3.3/Part01/examples.html#PrototypeExample' name='specificationUrl'/>
    <meta content='https://www.web3d.org/x3d/content/examples/Basic/X3dSpecifications/Prototype.x3d' name='identifier'/>
    <meta content='X3D-Edit 3.3, https://savage.nps.edu/X3D-Edit' name='generator'/>
    <meta content='../license.html' name='license'/>
  </head>
  <Scene>
    <WorldInfo title='Prototype.x3d'/>

Before we can use our new node, we need to define it with a new prototype declaration using the <ProtoDeclare> element. The name attribute identifies the new node, both here and for ProtoInstance tags.

    <ProtoDeclare name='TwoColorTable'>

The ProtoInterface element defines the fields of the new node. legColor and topColor will be available for ROUTEs and are of type SFColor. Input fields should be assigned a default value, with the value attribute.

      <ProtoInterface>
        <field accessType='inputOutput' name='legColor' type='SFColor' value='.8 .4 .7'/>
        <field accessType='inputOutput' name='topColor' type='SFColor' value='.6 .6 .1'/>
      </ProtoInterface>

The ProtoBody element is the meat of the prototype and defines its function in terms of other existing nodes. Apart from native nodes, these can include instances of other Prototypes, even ones declared within the ProtoBody, but cannot include instances of itself.

      <ProtoBody>

The first node in the ProtoBody is special. It determines the type of the new node, here <Transform>, a <X3DChildNode>. The new node can be inserted into a scene graph whereever its type node, the first node, could be inserted. The new node in this example could be be inserted as a child in grouping nodes.

        <Transform>
          <Transform translation='0.0 0.6 0.0'>
            <!-- table top -->
            <Shape>
              <Appearance>
                <Material DEF='TableTopMaterial'>

The <IS> <connect> construct links the fields defined in the <ProtoInterface> above - the protoField - with a field of the parent element, the nodeField. Here the <Material> diffuseColor field is linked with the topColor field of the new node. A protoField can be linked to nodeFields of multiple nodes.

                  <IS>
                    <connect nodeField='diffuseColor' protoField='topColor'/>
                  </IS>
                </Material>
              </Appearance>
              <Box size='1.2 0.2 1.2'/>
            </Shape>
          </Transform>
          <Transform translation='-0.5 0.0 -0.5'>
            <!-- first table leg -->

DEF/USE names inside a ProtoBody have their own name scope. That means the naming does not conflict with names outside of the ProtoBody and that the named nodes cannot be referenced or accessed from outside the ProtoBody.

            <Shape DEF='Leg'>
              <Appearance>
                <Material DEF='LegMaterial' diffuseColor='1.0 0.0 0.0'>
                  <IS>
                    <connect nodeField='diffuseColor' protoField='legColor'/>
                  </IS>
                </Material>
              </Appearance>
              <Cylinder height='1.0' radius='0.1'/>
            </Shape>
          </Transform>
          <Transform translation='0.5 0.0 -0.5'>
            <!-- another table leg -->
            <Shape USE='Leg'/>
          </Transform>
          <Transform translation='-0.5 0.0 0.5'>
            <Shape USE='Leg'/>
            <!-- another table leg -->
          </Transform>
          <Transform translation='0.5 0.0 0.5'>
            <Shape USE='Leg'/>
            <!-- another table leg -->
          </Transform>
          <!-- End of root Transform's children -->
        </Transform>
        <!-- End of root Transform -->
      </ProtoBody>
    </ProtoDeclare>
    <!-- End of prototype -->
    <!-- The prototype is now defined. Although it contains a number of nodes, only the legColor and topColor fields are public. Instead of using the default legColor and topColor, this instance of the table has red legs and a green top: -->

The new node is defined and can now be instanced with the ProtoInstance element. The name attribute references the ProtoDeclare element of the same name, to create a node of this new node type. The new node gets a DEF name so we can route field events to it later.

    <ProtoInstance DEF='animatedTable' name='TwoColorTable'>

The child fieldValue elements are the (verbose) way to provide values for the fields of the new node.

      <fieldValue name='legColor' value='1 0 0'/>
      <fieldValue name='topColor' value='0 1 0'/>
    </ProtoInstance>
    
	<TimeSensor DEF='Timer' cycleInterval='5' loop='true'/>
	<ColorInterpolator DEF='ColorChanger' key='0 0.3333 0.6666 1' keyValue='1 0 0 0 1 0 0 0 1 1 0 0'/>
	<ROUTE fromField='fraction_changed' fromNode='Timer' toField='set_fraction' toNode='ColorChanger'/>

We can simply route colors generated by the <ColorInterpolator> to a SFColor field of the new node.

	<ROUTE fromField='value_changed' fromNode='ColorChanger' toField='set_legColor' toNode='animatedTable'/>
    
	<NavigationInfo type='"EXAMINE"'/>        
    <!-- Use the Examine viewer -->
  </Scene>
</X3D>

This is fairly basic example. It only uses the first node of a ProtoBody. It is possible to use additional helper nodes to add logic or animation. These additional nodes will never be rendered.

Please find a live version of the example here:

https://5f0d1e604fd3920007d1b07b--x3dom.netlify.app/examples/functional/proto/inline.html

under xml example: Prototype_InRoute

DEF/USE vs. Prototypes

Prototypes enable multiple reuse of nodes defined in a <ProtoDeclare> statement. Similarly, the DEF/USE mechanism also allows multiple reuse of nodes. In fact, sometimes a <ProtoDeclare/> <ProtoInstance/> sequence can be recast as a DEF/USE sequence. If that is possible, DEF/USE is less resource costly and may be preferred as a more concise solution. But often this is not possible. Prototypes can be parametrized, eg. can generate different outcomes based on input fields which is not possible by simple DEF/USE. However, it is possible that a scene only uses one or a few different parameter values for its ProtoInstances which makes DEF/USE a potentially interesting alternative. Prototypes can also contain internal event routing and logic which is not possible with DEF/USE.

The DEF/USE mechanism means that a single node is referenced multiple times throughout the scene. This is very efficient. The X3D browser needs to maintain only one node. Changing anything inside this node will be reflected in all places where this node was used. For example, changing a <Material> diffuseColor field value in a DEF node, will recolor all shapes where that <Material> node is USEd.

In contrast, each instance of a prototype is its own independent copy. Changing something in one <ProtoInstance> is independent from other instances.

X3DOM specific capabilities

The x3dom prototype implementation attempts to fully support the X3D standard specification. In addition, it adds x3dom specific capabilities. These consist of a much more concise and natural syntax for ProtoInstances, and DOM manipulation and DOM event support for elements written in the concise syntax.

Concise ProtoInstance syntax

In place of the standard <ProtoInstance name='myNode'> followed by <fieldValue name='field1' value='value1'> syntax, one can simply provide the more natural syntax <myNode field1=value1> following the syntax for all built-in nodes. An example would be <twocolortable legColor='1 0 0'/>. In fact, x3dom translates the standard syntax to the concise syntax and appends the concise element in the DOM first before it processes the instance further. That means that a DOM node with the concise syntax is always availabe, either directly provided by the scene author or generated from the standard syntax.

Concise syntax example

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.3//EN" "http://www.web3d.org/specifications/x3d-3.3.dtd">
<X3D profile='Immersive' version='3.3' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-3.3.xsd'>
  ...
  <Scene>
    <WorldInfo title='Prototype.x3d'/>
    <ProtoDeclare name='TwoColorTable'>
      ... see above
    </ProtoDeclare>
    <!-- End of prototype -->
    <!-- The prototype is now defined. Although it contains a number of nodes, only the legColor and topColor fields are public. Instead of using the default legColor and topColor, this instance of the table has red legs and a green top: -->

Above is unchanged from the annotated example. The instance is then created using the concise syntax which looks exactly like the syntax of native nodes.

    <TwoColorTable DEF='animatedTable'
      legColor='1 0 0'
      topColor='0 1 0'/>

The remaining scene is unchanged,

	<TimeSensor DEF='Timer' cycleInterval='5' loop='true'/>
	<ColorInterpolator DEF='ColorChanger' key='0 0.3333 0.6666 1' keyValue='1 0 0 0 1 0 0 0 1 1 0 0'/>
	<ROUTE fromField='fraction_changed' fromNode='Timer' toField='set_fraction' toNode='ColorChanger'/>
	<ROUTE fromField='value_changed' fromNode='ColorChanger' toField='set_legColor' toNode='animatedTable'/>
    
	<NavigationInfo type='"EXAMINE"'/>        
    <!-- Use the Examine viewer -->
  </Scene>
</X3D>

DOM attribute and node mutation

Just like built-in nodes, instances of new Proto nodes can be modified, removed or created using DOM attribute methods and DOM node methods. This capability only applies to elements using the concise syntax. Continueing the example,

table = document.querySelector('twocolortable');
table.setAttribute('legColor', '0 0 1');

would change the legColor to blue.

Annotated example with DOM scripting

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="chrome=1,IE=edge" />
    <title>Proto test</title>
    <script type="text/javascript" src="../../../../x3dom-include.js?full"></script>
    <style>
        x3d {
        	height: 50%
        }
    </style>
</head>
<body>

The X3D is the same as above but we need full closing tags for HTML.

<X3D profile='Immersive' version='3.3' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-3.3.xsd'>
  <Scene>
    <!--WorldInfo title='Prototype.x3d'-->
    <ProtoDeclare name='TwoColorTable'>
      ...see above
    </ProtoDeclare>
    <!-- End of prototype -->
    <!-- The prototype is now defined. Although it contains a number of nodes, only the legColor and topColor fields are public. Instead of using the default legColor and topColor, this instance of the table has red legs and a green top: -->

We can attach a click event handler to the new node.

    <TwoColorTable onclick='logClick(event)' DEF='animatedTable'
      legColor='1 0 0'
      topColor='0 1 0'></TwoColorTable>

	<!--TimeSensor DEF='Timer' cycleInterval='5' loop='true'/>
	<ColorInterpolator DEF='ColorChanger' key='0 0.3333 0.6666 1' keyValue='1 0 0 0 1 0 0 0 1 1 0 0'/>
	<ROUTE fromField='fraction_changed' fromNode='Timer' toField='set_fraction' toNode='ColorChanger'/>
	<ROUTE fromField='value_changed' fromNode='ColorChanger' toField='set_legColor' toNode='animatedTable'/-->
    
	<NavigationInfo type='"EXAMINE"'/>     
    <!-- Use the Examine viewer -->
  </Scene>
</X3D>

After the X3D scene a <button> click invokes a function. The <textarea> is used for output.

<button onclick='randomizeColor()'>random colors</button>
<div >
<textarea style="height: 100%; width: 100%;" id='logArea'></textarea>
</div>
</body>
<script type="text/javascript">

The button click invokes this function which gets the DOM node of the new node from the document, and then assigns new values to attributes. x3dom then observes these element mutations in the background and modifies the x3d node and scene accordingly.

   function randomizeColor()
   {
		var table = document.querySelector('TwoColorTable');
		table.setAttribute('legColor', randomColor());
		table.setAttribute('topColor', randomColor());
   }
   function randomColor()
   {
        return [Math.random(),Math.random(),Math.random()].join();
   }

This function is called when the table is clicked as it is its click event handler. It is passed an event object which contains details about the click event.

   function logClick(e)
   {
        var l = document.querySelector('#logArea');
   	    for ( var k in e )
   	    {
   	        l.textContent = k + ":" + e[k] + "\n" +
                                l.textContent;
   	    }
   }
</script>
</html>

Here is the live version of the example: https://andreasplesch.github.io/x3dom/test/functional/proto/x3dom/dom/Prototype_InShort.html

Implementation notes

Overview

The x3dom implementation works by looking for the proto* tags in child elements of scene elements setting up the tree of scene graph or subgraph, eg. during parsing.

For <ProtoDeclare> elements, a new ProtoDeclaration object is constructed from the fields defined in the <ProtoInterface> element, and from the <ProtoBody> element. The <ProtoBody> DOM elements gets augmented by an object which represents the <IS> connections as routes after taking care of a few requirements. The new ProtoDeclaration object has then the ability to register the new node type to the x3dom registry of known X3D nodes. It is added to an array of available protos for the current name scope.

For <ExternProtoDeclare> elements, a provisional ProtoDeclaration is added to the array of available protos. The external file will be loaded only when an instance of the proto requires it.

For <ProtoInstance> elements, there is a check for a corresponding ProtoDeclaration and the element is converted to the concise syntax in the form of a DOM node which then appended after the <ProtoInstance> element. The regular parsing during scene graph setup then picks up the appended element and treats it like any other registered node.

If the corresponding ProtoDeclaration is marked provisional from an <ExternProtoDeclare>, the external file is fetched and loaded, and the ProtoDeclaration converted to a full declaration. Since the loading occurs asynchronously, out of sequence with the overall processing, it is necessary to make sure that then instances are added in the correct sequence, and to explicitly trigger parsing since the regular tree parsing likely has concluded. For the same reason, it is necessary to explicitly establish scene routes in which the now available instances participate.

Generalized node functionality

During registration of the new node, its name, its type and its fields are defined. When the node is then instanced, the new node is initialized by setting up a subgraph of its <ProtoBody> clone, and a way to transfer events in to and out from nodes in this subgraph.

Files

The implementation of Prototype functionality is mainly provided in two files:

https://github.com/x3dom/x3dom/blob/master/src/NodeNameSpaceProtos.js

  • processes proto* elements, loads extern urls

https://github.com/x3dom/x3dom/blob/master/src/util/protos/ProtoDeclaration.js

  • generalized node registration, node initialization, node setup, and event handling.

In addition, there are smaller changes in

https://github.com/x3dom/x3dom/blob/master/src/NodeNameSpace.js

  • in .setupTree(): parse proto* tags, store broken routes as not yet available for later setup

https://github.com/x3dom/x3dom/blob/master/src/nodes/Core/X3DNode.js

  • in .addChild(): inserts both the type node and an internal switch node which includes hidden helper nodes of proto instances.

Open questions

Inheriting prototypes from parents

Prototypes have their own execution context. Each execution context has an array of prototype declaration which are defined in the execution context. It turns out that some/most(?) x3d browsers inherit defined prototypes from parent contexts. These prototypes are then additionally available for instancing. The spec. seems to require this:

https://www.web3d.org/documents/specifications/19775-1/V4.0/Part01/concepts.html#Prototypescopingrules

"A prototype may be instantiated in a file anywhere after the completion of the prototype definition."

If the specs meant what you are suggesting --protos are self contained including proto definitions-- then it would have said "context" rather than "file"

"Prototype definitions appearing inside a prototype definition ( i.e., nested) are local to the enclosing prototype. "

  • refinement of the above file order rule.

Here is a test scene by Doug Sanden:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.0//EN" "http://www.web3d.org/specifications/x3d-3.0.dtd">
<X3D profile='Immersive' >
<Scene>

<ProtoDeclare name='Thing'>
 <ProtoInterface/>
 <ProtoBody>
  <Shape>
   <Appearance>
    <Material diffuseColor='1 0 0' />
   </Appearance>
   <Sphere/>
  </Shape>
 </ProtoBody>
</ProtoDeclare>

<ProtoDeclare name='NestedDuo'>
 <ProtoInterface/>
 <ProtoBody>
 <Group>
  <Transform translation='-1 0 0'>
   <ProtoInstance name='Thing' />
  </Transform>

  <ProtoDeclare name='Thing'>
   <ProtoInterface/>
   <ProtoBody>
   <Shape>
   <Appearance>
    <Material diffuseColor='0 0 1' />
   </Appearance>
   <Box/>
  </Shape>
  </ProtoBody>
  </ProtoDeclare>

  <Transform translation='1 0 0'>
    <ProtoInstance name='Thing' />
  </Transform>
  </Group>
  </ProtoBody>
 </ProtoDeclare>

 <ProtoInstance name='NestedDuo' />

<!-- experiment below -->
<Transform translation='0 -2 0'>
 <ProtoInstance name='Thing' />
</Transform>

</Scene>
</X3D>

Rendering by titania:

image

Rendering by freewrl:

image

Rendering by view3dscene:

image

Rendering by InstantPlayer:

image