Chapter 11. File Format

Chapter Objectives

After reading this chapter, you'll be able to do the following:

This chapter describes the Inventor ASCII file format. Whenever you apply a write action to a node, path, or path list, the output file is written in this format. You can read files that use this format into the Inventor scene database by using the read method on the database. The file format is also used for transferring 3D copy and paste data between processes.

Writing a Scene Graph

As described in Chapter 9, you can apply a write action to a node, path, or path list. When the write action is applied to a node, it writes the entire subgraph rooted at that node.

SoWriteAction 	writeAction;

writeAction.apply(root); //writes the entire scene graph to stdout

Reading a File into the Database

You can read a scene graph from a file into the scene database using the readAll() method on the Inventor database. This example reads a file with the given filename and returns a separator containing the file. It returns NULL if there is an error reading the file.

SoSeparator *
readFile(const char *filename)
{
   // Open the input file
   SoInput mySceneInput;
   if (!mySceneInput.openFile(filename)) {
      fprintf(stderr, "Cannot open file %s\n", filename);
      return NULL;
   }

   // Read the whole file into the database
   SoSeparator *myGraph = SoDB::readAll(&mySceneInput);
   if (myGraph == NULL) {
      fprintf(stderr, "Problem reading file\n");
      return NULL;
   } 
   mySceneInput.closeFile();
   return myGraph;
}

There are two read() methods. One method reads a graph rooted by a node, returning a pointer to that node. The other reads a graph defined by a path. You must call the correct method, based on the contents of the input. When you read in a model, you usually read a node. If you are cutting and pasting with paths, you will need to read a path.

SoDB uses the class when reading Inventor data files. This class can also be used to read from a buffer in memory. By default, SoInput looks for a specified file in the current directory (unless the specification begins with /). You can add directories to the search path with the addDirectory-
First()
and addDirectoryLast() methods (see the Open Inventor C++ Reference Manual on SoInput). Use the clearDirectories() method to clear the directory list.

You can also add a list of directories that is specified as the value of an environment variable. Use the following methods on SoInput:

addEnvDirectoriesFirst()

addEnvDirectoriesLast()

File Format Syntax

The following sections outline the syntax for the Inventor ASCII file format. In this file format, extra white space created by spaces, tabs, and new lines is ignored. Comments begin with a number sign (#) anywhere on a line and continue to the end of the line:

# this is a comment in the Inventor file format

For simplicity, this discussion focuses on writing a scene graph to a file. This same format applies to files you create that will be read into the Inventor database.

See the Open Inventor C++ Reference Manual for descriptions of the file format for each Inventor class.

File Header

Every Inventor data file must have a standard header to identify it. This header is the first line of the file and has the following form:

#Inventor V2.0 ascii

or

#Inventor V2.0 binary

To determine whether a random file is an Inventor file, use the SoDB::isValidHeader() method and pass in the beginning of the file in question. Although the header may change from version to version (V2.0 is the current version), it is guaranteed to begin with a # sign, be no more than 80 characters, and end at a newline. Therefore, the C () routine can be used. The isValidHeader() method returns TRUE if the file contains an Inventor header. Inventor also reads older (V1.0) files and converts them.

Writing a Node

A node is written with the following elements:

  • Name of the node (without the So prefix)

  • Open brace ( { )

  • Fields within the node (if any), followed by children of the node (if any)

  • Close brace ( } )

For example:

DrawStyle {
   style	        LINES
   lineWidth	    3
   linePattern	  255
}

Writing Values within a Field

Fields within a node are written as the name of the field, followed by the value or values contained in the field. If the field value has not been changed from its default value, that field is not written out. Fields within a node can be written in any order. An example of writing field values is as follows:

Transform {
	   translation	     0 -4 0.2
}

LightModel {
   model	           BASE_COLOR
}

Material {
   ambientColor   .3 .1 .1
   diffuseColor  [.8 .7 .2,
                   1 .2 .2,
                  .2  1 .2,
                  .2 .2  1]
   specularColor  .4 .3 .1
   emissiveColor  .1  0 .1
}

Brackets surround multiple-value fields, with commas separating the values, as shown for the diffuseColor field in the preceding example. It's all right to have a comma after the last value as well:

[value1, value2, value3,]

Single-value () fields do not contain any brackets or commas. Multiple-value () fields usually have brackets, but they are not necessary if only one value is present:

specularColor    .4 .3 .1

or

specularColor   [.4 .3 .1]

The value that is written depends on the type of the field, as described in the following list.

Type of Field  

Acceptable Formats

longs, shorts, unsigned shorts 


integers, in decimal, hexadecimal, or octal;
For example:

255
0xff
0177

floats 

integer or floating point number. For example:

		13
	13.0
	13.123
	1.3e-2

names, strings 

double quotation marks ( " " ) around the name if it is more than one word, or just the name (with no white space) if it is a single word (quotation marks are optional). For example:

label   " front left leg " 
label   car

You can have any ASCII character in the string, including newlines and backslashes, except for double quotation marks. To include a double quotation mark in the string, precede it with a backslash (\").

enums 

either the mnemonic form of the enum or the integer form. (The mnemonic form is recommended, both for portability and readability of code.) For example:

MaterialBinding {
   value   PER_FACE
	}

bit mask 

one or more mnemonic flags, separated by a vertical bar (|) if there are multiple flags. When more than one flag is used, parentheses are required:

	Cylinder {
	   parts   SIDES
	}

	Cylinder {
	   parts   (SIDES | TOP)
	}

vectors  

n floats separated by white space:
(SbVecnf, where n is
the number of

PerspectiveCamera {
components of the	   position   0 0 9.5
vector)	}

colors  

3 floats (RGB) separated by white space:

	BaseColor {
	   rgb   0.3 0.2 0.6
	}

rotation 

a 3-vector for the axis, followed by a float for the angle (in radians), separated by white space:

	Transform {
	   rotation   0 1 0 1.5708
	   # y axis ... pi\xb9 /2 radians
	}

matrix 

16 floats, separated by white space

path 

an SFPath has one value, a pointer to a path. To write this value, write the path (see “Writing a Path”). An MFPath has multiple values, which are all pointers to paths. To write this value, enclose the path list in brackets, and use commas to separate each path:

[first_path, second_path, ... nth_path]

node 

an SFNode has one value, a pointer to a node. To write this value, write the node. An MFNode has multiple values, which are all pointers to nodes. To write this value, enclose the node list in brackets, and use commas to separate each node:

[node1, node2, ... noden]

Boolean	TRUE, FALSE or 0, 1:
	SoFile {
	   isWriteBack   FALSE
	}

Ignore Flag

The Ignore flag for a field (see Chapter 3) is written as a tilde ( ~ ), either after or in place of the field value or values. For example:

transparency [ .9, .1 ] ~

or

transparency ~

The first case preserves the values even though the field is ignored. The second case uses the default value but ignores the field.


Tip: The Ignore flag applies only to properties. It is not used for cameras, lights, and shapes.


Field Connections

Connections are written as the object containing the field or output connected to the field, followed by a period (.) and then the name of the field or output. For example:

Separator {
	    DEF Trans Translation { translation  1 2 3 }
	    Cube {}
	}
	Separator {
	    Translation { translation 0 0 0 = USE Trans.translation }
	    Cone {}
	}

The value of a connected field (0 0 0 in this case) is optional, so the second Translation node could be written as

Translation { translation = USE Trans.translation }

If an ignored field is connected, the connection specification follows the Ignore flag:

translation 000 ~ = USE Trans.translation
#or
translation ~ = USE Trans.translation

If a value is given as well as a connection, the value is used for the field. If a value is sent along the connection later, it will override the value.

Global Fields

A global field needs to have at least one connection in order for it to be written out. It is written out in this format:

GlobalField {
type
value
}

The braces contain the type and value of the field. The name of the global field is stored as the name of the value field. For example, the Text3 node could be connected to a global field (here, currentFile) that stores the current file name an application is working on. The Text3 node would then always display that current file name. Here is the ASCII file format for that connection:

Text3 {
   string "" = GlobalField {
      type SFString
      currentFile "aircar.iv"
   } . currentFile
}

Writing an Engine

The syntax for an engine definition is the same as that of a nongroup node:

EngineType {
input_fields
}

Engines can't be written on their own; they must be connected to at least one part of the scene graph. A field-to-engine connection is specified as follows:

fieldname value = engine . outputname

Here is an example of changing a sphere's radius using an SoOneShot engine:

Sphere {
   radius 0.5 = OneShot { duration 3.0 } . ramp
}

For a more complex example, see “Defining and Using Shared Instances of Nodes”.

Writing a Path

A path (see Chapter 3) is written with the following elements:

  • The word Path

  • Open brace ({ )

  • The entire subgraph that is rooted on the head node for the path

  • Number of indices in the rest of the path chain

  • The indices themselves

  • Close brace (})

When Inventor encounters separator groups within the subgraph, it ignores them if they do not affect the nodes in the path chain. Written indices for the children within a group are adjusted to account for the skipped separator groups. For example, in Figure 11-1, node N is counted as child index 1 when written, since the two previous children are separator groups that do not affect this node at all. (The indices in the path itself remain unchanged.)

Figure 11-1. Adjusting Path Indices to Account for Separator Groups



Note: If a path contains connections to nodes that are not part of the path, these nodes are not written out. For example, if a path contains an engine connected to a node outside the path, the engine will be written, but the node will not be.

Example 11-1 illustrates the process of writing a path to a file. First, here is the file for the scene graph, which creates chartreuse, rust, and violet spheres.

Separator {
   PerspectiveCamera {
      position 0 0 9.53374
      aspectRatio 1.09446
      nearDistance 0.0953375
      farDistance 19.0675
      focalDistance 9.53374
   }

   DirectionalLight {
   }

   Transform {
      rotation -0.189479 0.981839 -0.00950093 0.102051
      center 0 0 0 
   }

   DrawStyle {
   }

   Separator {
      LightModel {
         model   BASE_COLOR
      }
      Separator {
         Transform {
            translation -2.2 0 0
         }
         BaseColor {
            rgb .2 .6 .3   # chartreuse
         }
         Sphere { }
      }
      Separator {
         BaseColor {
            rgb .6 .3 .2   # rust
         }
         Sphere { }
      }
      Separator {
         Transform {
            translation 2.2 0 0 
         }
         BaseColor {
            rgb .3 .2 .6   # violet
         }
         Sphere { }
      }
}

Figure 11-2 shows the scene graph for this file.

Figure 11-2. Scene Graph for a Scene with Three Spheres


If you pick the third sphere (the violet one), the pick path could be written to a file as shown in Example 11-1. First, the subgraph under the root node is written. This description is followed by the number of indices in the path (3), and the indices themselves (4, 1, 2), as shown in Figure 11-3.

Example 11-1. Writing a Path


Path {
   Separator {
      PerspectiveCamera {
         position 0 0 9.53374
         aspectRatio 1.09446
         nearDistance 0.0953375
         farDistance 19.0675
         focalDistance 9.53374
      }
      DirectionalLight {
      }
      Transform {
         rotation -0.189479 0.981839 -0.00950093 0.102051
      }
      DrawStyle {
      }
      Separator {
         LightModel {
            model BASE_COLOR
         }
         Separator {
            Transform {
               translation 2.2 0 0
            }
            BaseColor {
               rgb 0.3 0.2 0.6
            }
            Sphere {
            }
         }
      }
   }
   3
   4
   1
   2
}

Figure 11-3. Pick Path for Violet Sphere


Defining and Using Shared Instances of Nodes

In the file format, the keyword DEF introduces a named instance of a node, path, or engine. It can be referenced later with the USE keyword. For example:

// This example shows keeping a cone between two cubes using an
// InterpolateVec3f engine.

Separator {
   DEF A Translation { translation -4 0 0 }
   Cube { }
}
Separator {
   DEF B Translation { translation 4 5 6 }
   Cube { }
}
Separator {
   Translation { translation 0 0 0 =
                 InterpolateVec3f {
                    input0 0 0 0 = USE A.translation
                    input1 0 0 0 = USE B.translation
                    alpha 0.5
                 } . output
               }
   Cone { }
}

The name can be any valid SbName. In certain cases, Inventor adds some extra characters to the name when the file is written out. For example, consider the somewhat unusual scene graph shown in Figure 11-4. To indicate which instance of the beachball node is used by node B, the scene graph is written out as follows:

Separator{
   Separator{
      DEF beachball+0
      DEF beachball+1
   }
   Separator{
      USE beachball+0
      USE beachball+1
   }
}

When the scene graph is read back in, the original names are preserved, but the +n notations are discarded.

Figure 11-4. Shared Instances of Nodes


Writing a Node Kit

When a node kit is written, it includes one field for each part. For example:

AppearanceKit {
   lightModel			    LightModel { model PHONG }
   drawStyle			     DrawStyle  { style LINES }
   material			      Material   { diffuseColor .5 .5 .5 }
   complexity	    Complexity { value .5 }
}

In this format, the name of the field (lightModel) is followed by the name of the node (LightModel), and then the node's fields and values (each part is contained in an SoSFNode field). If the part has not been created, or if it is NULL, it is not written out. However, if a part is created by default (such as the shape part in the SoShapeKit), and if the part is explicitly set to NULL, it is written out.

This example shows nesting node kits. Here, the appearance kit is the value for the appearance field. The appearance kit, in turn, has a material field.

SeparatorKit {
   appearance			    AppearanceKit {
      material   				Material { diffuseColor	   1 1 1 }
   }
}

When Inventor writes out a node kit, it writes out the intermediate parts. When you enter the information yourself, you can use a shorthand method and omit the intermediate parts. For example, if you omit the AppearanceKit, the SeparatorKit knows to add an AppearanceKit and put the Material node inside. So, you could simply enter this:

SeparatorKit {
   material   				Material { diffuseColor	   1 1 1 }
}

The file format for list parts within node kits is a bit more specialized. Each list part has three standard fields:

containerTypeName 


for example, separator or switch; in string format

childTypeName  

an SoMFString that lists the types of children that this node is allowed to contain

containerNode  

the node that contains the children

For example, here is the childList part of an instance of SoSeparatorKit:

SeparatorKit {
   childList			    NodeKitListPart {
      containerTypeName	   "Separator"
      childTypeNames	      "SeparatorKit"
      containerNode	        			Separator {
         SeparatorKit {
            transform	      Transform { translation 	1 0 0 }
         }
         SeparatorKit {
            transform	      Transform { translation 	0 1 0 }
         }
         SeparatorKit {
            transform	      Transform { translation 	0 0 1 }
         }
      }
   }
}

By default, Inventor does not write out the internal parts, such as separators and groups, or fields with default values. But if the node kit is in a path, everything is written out, as the following example shows. Generally, it writes out the parts in the reverse order they are defined in the catalog, with the leaf nodes first:

#Inventor V2.0 ascii

SeparatorKit {
   appearance	    DEF +0 AppearanceKit {
      material   		DEF +1 Material {
         diffuseColor	   1 0 1
      }
   }
   childList	    DEF +2 NodeKitListPart {
      containerTypeName   "Separator"
      childTypeNames	       "SeparatorKit"
      containerNode	   	     DEF +3 Separator {
         ShapeKit {
            appearance	   		  DEF +4 AppearanceKit {
               material			    DEF +5 Material {}
		            }
            transform			   DEF +6 Transform {}
            shape			   DEF +7 Cube {}
		            topSeparator			  Separator {
               USE +4
               USE +6
               DEF +8 Separator {
                  USE +7
               }
            }

            shapeSeparator  			USE +8
        }
   }
}
topSeparator	    Separator {
   USE +0
   USE +2
   }
}

Including Other Files

To include a file within another file, use an SoFile node. This node is useful when you are building scene graphs that include many objects. The objects can reside in their own files, and SoFile nodes can be used to refer to them without copying them directly into the new file. The SoFile node is written as

File {
   name "myFile.iv"
}

where the name field is the name of the file to be included. On read, the contents of the file myFile.iv are added as hidden children of SoFile. On write, Inventor just writes the filename (but not the children).

The objects within an SoFile node are not editable. You can copy the contents of an SoFile node using the method

SoFile::copyChildren()

or you can modify the name field of the SoFile node. Whenever the value of the name field changes, the new file is read in. If the name is not an absolute path name, the list of directories set on SoInput is used to search for the file (see “Reading a File into the Database”). automatically adds the directory of the file being read to 's list of directories to search.

For example, suppose you have myFile.iv, which contains windmill.iv.

Contents of /usr/tmp/myFile.iv:

#Inventor V2.0 ascii
File { name "myObjects/windmill.iv" }

Contents of /usr/tmp/myObjects/windmill.iv:

#Inventor V2.0 ascii
//format to make the windmill

When /usr/tmp/myFile.iv is read in, /usr/tmp is added to the directory search list. When the SoFile node in myFile.iv calls SoDB::read, SoInput will find /usr/tmp/myObjects/windmill.iv, and it will be read (the directory /usr/tmp/myObjects will also be added to the list of search directories). When reading finishes, /usr/tmp/myObjects and /usr/tmp will be removed from the search directories list.

ASCII and Binary Versions

The SoOutput object in an SoWriteAction has a setBinary() method, which sets whether the output should be in ASCII (default) or binary format (see Chapter 9). The getOutput() method returns a pointer to the SoOutput. When a file is written, Inventor inserts a header line that indicates whether the file is in ASCII or binary format, as well as the Inventor version number (see “File Header”).

Reading in Extender Nodes and Engines

As described in The Inventor Toolmaker, developers can create their own nodes or engines and use them in new applications. This section describes what happens if you read in a file with references to extender nodes and engines whose code may or may not be accessible to your program. (In most cases, nodes and engines are interchangeable, so the discussion refers only to nodes for simplicity. In cases where engines differ slightly from nodes, those differences are called out explicitly.)

When an Inventor node is read from a file, Inventor first checks to see if the node's type has been registered. If the name is found, an instance of that node is created and the fields for the node are read in. This situation occurs if your program is linked with the code for the new node.

However, if your program is not linked with the code for the new node, things become slightly more interesting. If your system supports dynamic loading of compiled objects, Inventor can find the compiled object and recognize the new node. In this case, the author of the new node supplies the compiled object for the node and places it in a directory that can be found by the system. (Check your release notes for information on whether your system supports dynamic loading of shared objects and how it implements searching directories for the objects.)

File Format for Unknown Nodes and Engines

If Inventor is unable to locate the code for the new node or engine, it creates an instance of the class SoUnknownNode or SoUnknownEngine. The first keyword in the file format for all new nodes is named fields, and it is followed by the field type and name of all fields defined within the node. For example:

WeirdNode {
   fields [ SFFloat length, SFLong data ]
   length 5.3
   Material {}
   Cube {}
}

This unknown node has two fields, length and data. Because the data field uses its default value, it is not written out. The node also has two children, an SoMaterial and an SoCube, which are listed after the fields of WeirdNode. These nodes are treated as hidden children and are not used for rendering, picking, or searching. They are only used by the write action.

The file format for new engines contains descriptions of both the inputs and outputs for the engine, as follows:

WeirdEngine {
   inputs [ SFFloat factor, SFFloat seed ]
   factor 100
   seed 0.5
   outputs [ SFFloat result ]
}

Since no code accompanies the node, most operations on the unknown node will not function. However, reading, writing, and searching can still be performed on this node (but not on its children).

Alternate Representation

The author of a new node class may also provide an alternate representation for the node, to be used in cases where the node is treated as an unknown node. This representation is specified in the alternateRep field for the node, which contains the complete scene graph for the alternate representation. This scene graph will be used in place of the actual node for picking, rendering, and bounding-box actions.

The following node kit provides an alternate representation:

Airplane {
   fields [ SFNode wing, SFNode fuselage, SFNode alternateRep ]
   wing Separator { ... the wing scene graph ... }
   fuselage Separator { ... the fuselage scene graph ... }
   alternateRep Separator {
      Cube {}
      Transform { translation 10 0 0 }
      Cone {}
   }
}

Reading from a String

“Reading a File into the Database” showed you how to read from a file. Example 11-2 shows how you can read from a string stored in memory. Note that when a graph is read from a buffer, you do not need the file-header string. This example creates a dodecahedron from an indexed face set.

Example 11-2. Reading from a String


// Reads a dodecahedron from the following string: 
// (Note: ANSI compilers automatically concatenate 
// adjacent string literals together, so the compiler sees 
// this as one big string)

static char *dodecahedron =
   "Separator { "
      "   Material { "
   "      diffuseColor [ "
   "         1  0  0,   0 1  0,   0  0 1,   0  1  1, "
   "         1  0  1,  .5 1  0,  .5  0 1,  .5  1  1, "
   "         1 .3 .7,  .3 1 .7,  .3 .7 1,  .5 .5 .8 "
   "      ] "
   "   } "
   "   MaterialBinding { value PER_FACE } "
   "   Coordinate3 { "
   "      point [ "
   "         1.7265 0 0.618,    1 1 1, "
   "         0 0.618 1.7265,    0 -0.618 1.7265, "
   "         1 -1 1,    -1 -1 1, "
   "         -0.618 -1.7265 0,    0.618 -1.7265 0, "
   "         1 -1 -1,    1.7265 0 -0.618, "
   "         1 1 -1,    0.618 1.7265 0, "
   "         -0.618 1.7265 0,    -1 1 1, "
   "         -1.7265 0 0.618,    -1.7265 0 -0.618, "
   "         -1 -1 -1,    0 -0.618 -1.7265, "
   "         0 0.618 -1.7265,    -1 1 -1 "
   "      ] "
   "   } "
   "   IndexedFaceSet { "
   "      coordIndex [ "
   "         1, 2, 3, 4, 0, -1,  0, 9, 10, 11, 1, -1, "
   "         4, 7, 8, 9, 0, -1,  3, 5, 6, 7, 4, -1, "
   "         2, 13, 14, 5, 3, -1,  1, 11, 12, 13, 2, -1, "
   "         10, 18, 19, 12, 11, -1,  19, 15, 14, 13, 12, -1, "
   "         15, 16, 6, 5, 14, -1,  8, 7, 6, 16, 17, -1, "
   "         9, 8, 17, 18, 10, -1,  18, 17, 16, 15, 19, -1, "
   "      ] "
   "   } "
   "}";

// Routine to create a scene graph representing a dodecahedron
SoNode *
makeDodecahedron()
{
   // Read from the string.
   SoInput in;
   in.setBuffer(dodecahedron, strlen(dodecahedron));

   SoNode *result;
   SoDB::read(&in, result);

   return result;
}