Chapter Objectives
After reading this chapter, you'll be able to do the following:
Write a scene graph to a file in ASCII or binary format
Read a file into the Inventor database
Use the Inventor file format as an alternative to creating scene graphs programmatically
Read a scene graph from a buffer in memory
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.
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 |
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()
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.
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.
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 } |
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 |
| ||
floats | integer or floating point number. For example:
| ||
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:
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:
| ||
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:
| ||
vectors | n floats separated by white space:
| ||
colors | 3 floats (RGB) separated by white space:
| ||
rotation | a 3-vector for the axis, followed by a float for the angle (in radians), separated by white space:
| ||
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]
|
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. |
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.
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 } |
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”.
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.)
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.
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.
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 } |
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.
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 |
| |
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 } } |
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.
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”).
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.)
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).
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 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.
// 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; } |