EDM Specification

This document defines what is known about the structure of the EDM files and the data contained therein. Not everything is known, and some of it is guesswork, which will usually be noted. Luckily, the structure is rather simple - almost everything has a count prepended, object names are stored as character strings, and there are no pointer-references, so everything can be read sequentially.

Reading Type definitions

C-like struct notation is relatively common for defining binary files, but can be misleading by e.g. giving the impression that it describes a fixed-size length of data, which would be incorrect in the case of this file format. Therefore, in this document we will use a list-based description with roughly c-style names; in order to define a type, we present it’s name and then a list of fields to be read in order - which may be simple or complex - of fixed size or no. For example, the definition of the prefixed string type used is duplicated here as:

uint_string :=
  uint  count;
  char  data[count];

indicating that an unsigned int is to be read, followed by a sequence of characters - of length determined by the previously read number. The size of the array could be simple or an expression based on calculation - as in the vertex counts of the render nodes.

Additionally, there may occasionally require reading of exact constants; these are indicated by specifying const followed by either literal python bytestrings (in ASCII) e.g. const b'EDM', or typed declarations, without names e.g. const uint = -1, const uint = [0,0] which would require reading one (two) unsigned integers and comparing them.

It should also be noted that C++ style template syntax will also be used - both to represent general instances (in which case T will usually be used), and specific instances - to match exact type names. Improvements for clarity of notation are welcome.

Although progress was initially made describing this as a formal language (in the history of the repository the EDM file was actually parsed as such) there are exceptions where it may be easier just to describe what is going on. This should be reasonably obvious from the context.

Basic types and Structures

The .EDM files are all written little-endian, and use the common sizes for the basic types in both signed and unsigned variants; indicated by a prefix of u. The most common type in the files are probably unsigned integers, or ‘uint‘. The lengths are listed here for completeness:

Type Name Size (bytes)
char 1
short 2
int 4
float 4
double 8

In addition to the basic types, there are several fundamental structures that are repeated throughout the file. One of these were strings, however newer versions of the .edm format have made strings more complicated, so they are described below. Strings are particularly important in understanding one of the most common patterns in EDM files, the named_type:

named_type :=
  string    typeName;
  typeName  value;

e.g. a string should be read, which tells you the type of the object that needs to be read next. Usually there is some subset of types that this can be, but that is entirely dependent on the context.

Lists of some kind are also very common which, here using C++ template syntax can be seen to be length-prefixed:

list<T> :=
  uint count;
  T    data[count];

and can be used with named_type to represent a generic list of any named object - and each entry in the list could potentially be a different type. Related, is the mapping/dictionary type, which is written in the file as a list of paired keys and values:

map<T,Q> :=
  List<Pair<T,Q>>

pair<T,Q> :=
  T key
  Q value

 Strings

Whilst simple in version 8 files, strings become a little more complicated in version 10. Let’s start with version 8. All strings are length-prefixed, without trailing null character:

// Strings in file format v8
string := uint_string

uint_string :=
  unsigned_int count
  char         data[count];

And the character data is encoded in windows-1251 encoding.

For version 10 files, they are a little different. Most of the string data is encoded in a lookup table at the beginning of the file (see EDMFile). So a string now looks like:

// Strings in file format v10
string :=
  uint index;

and the actual value is subsequently found in lookupTable[index]. There are also instances of uint_string, used for the node base names. (these will mostly be unique, so not much point in moving them to a lookup table).

In absence of further evidence, the character data is assumed to also be encoded in windows-1251 encoding.

Math Types

Before moving on to the primary structure of the file, it’s helpful to look at some of the compound math types that are used. The EDM math types are based on the OpenSceneGraph library, and named accordingly.

Vector types encode both the count and type of the data into their name:

osg::Vec2f :=
  float a, b;

osg::Vec2d :=
  double a, b;

osg::Vec3f :=
  float x, y, z;

osg::Vec3d :=
  double x, y, z;

And matrix types also used in the file:

osg:Matrixf :=
  float data[16]

osg::Matrixd :=
  double data[16]

Matrices are written in column-major order, for OpenGL, so may need to be transposed if desired in row-major. Finally, Quaternions might need to be read, and the components are in this order:

osg:Quaternion :=
  float x, y, z, w;

Properties

Finally, another relatively common meta-pattern is that of properties:

model::Property<T> :=
  string name
  T      value

They function in a similar way to the map structure pair, except that being named types can hold values for anything. Related, as a parent structure very similar in purpose to a map is the PropertiesSet:

model::PropertiesSet := List<named_type>

Where the named_type is restricted to instances of model::Property<T> (and that includes subtypes e.g. model::AnimatedProperty). This type is tracked separately as a ‘named’ type in the main file index.

Animated properties are similar, but hold keyframe data for a specific animation number (argument):

model::AnimatedProperty<T> :=
  string        name;
  uint          argument;
  uint          count;
  model::Key<T> keyFrames[count];

model::Key<T> :=
  double   frame;
  T        value;

The keyframe type, as appears in the main file index, does not directly correspond to the exact type name. The translation is relatively simple, however:

Animated Property Type Keyframe Type
float key::FLOAT
osg::Vec2f key::VEC2F
osc::Vec3f key::VEC3F

So e.g. an model::AnimatedProperty<osg::Vec2f> contains a type of model::Key<key::VEC2F>.

Finally, there is another kind of animated property, the ArgumentProperty:

model::ArgumentProperty :=
  string  name;
  uint    argument;

The interpretation appears to be: Use the argument animation value as the value for this property.

File-level Structure

We now know enough to parse the EDM file, following type definitions. Let’s look at what the structure is:

EDMFile :=
  const b'EDM'
  ushort              version;    # 8 or 10 in all current EDM files
  // v10 ONLY
  uint                lookupSize;
  char                lookup[lookupSize];
  // End of v10 only
  map<string, uint>   indexA;
  map<string, uint>   indexB;
  named_type          rootNode;   # Always model::RootNode
  uint                nodeCount;
  named_type          nodes[nodeCount];
  uint                nodeParents[nodeCount];
  map<string,list<named_type>>   renderItems;

followed by an EOF.

After the file signature and version, If the file version is 8, the indexes follow. If version 10, however, the string lookup tables are placed immediately. The lookup is in a big block of character data, consisting of a number of null-terminated strings, one after another. Once split and decoded, this data forms the string lookup table described earlier in the definition for strings. This is then immediately used by the file indexes...

They translate as a lookup table of (almost entirely) typename-to-count values and seem to act as a crosscheck for the file. indexA seems to function as a tracking index exclusively for direct children of the rootNode and render items (e.g. types that only appear as members of the model::RootNode, or in the renderItems map). Here is an example of the contents of this index, in python dictionary form:

{
  'model::ArgAnimationNode': 216,
  'model::RenderNode':       200,
  'model::RootNode':           1,
  'model::ArgVisibilityNode':168,
  'model::Connector':         28,
  'model::TransformNode':     28,
  'model::Node':               1
}

The second index seems to function in a similar way, except listing named types that appear elsewhere, as members of the node objects:

{'__gi_bytes': 906501,
 '__gv_bytes': 5599044,
 'model::AnimatedProperty<float>': 6,
 'model::ArgAnimationNode::Position': 111,
 'model::ArgAnimationNode::Rotation': 118,
 'model::ArgVisibilityNode::Arg': 169,
 'model::ArgVisibilityNode::Range': 169,
 'model::Key<key::FLOAT>': 12,
 'model::Key<key::POSITION>': 322,
 'model::Key<key::ROTATION>': 601,
 'model::PropertiesSet': 23,
 'model::Property<float>': 107,
 'model::Property<osg::Vec2f>': 23,
 'model::Property<osg::Vec3f>': 6,
 'model::Property<unsigned int>': 1,
 'model::RNControlNode': 203}

With the addition of the first two fields; __gi_bytes, which is a count of the number of raw bytes of vertex indexing data, and __gv_bytes which is the number of raw vertex data bytes. Presumably this is used as a quick evaluation of how much video memory is required to load the model.

These can be useful whilst parsing the data, because they provide a cross- check that you have properly read objects, and also provided clues as to how certain objects were broken down. Presumably building them is critically important for writing new EDM files.

The Root Node

The next entry is a named_type - but is always a named instance of model::RootNode.

Transformation Nodes

There is then a list of named transformation and animation nodes, always starting with an empty model::Node. The types of node that appear in this list are:

  • model::Node
  • model::TransformNode
  • model::Bone
  • model::LodNode
  • model::BillboardNode
  • model::ArgAnimationNode
  • model::ArgScaleNode
  • model::ArgRotationNode
  • model::ArgPositionNode
  • model::ArgAnimatedBone
  • model::ArgVisibilityNode

Following this list is a rather opaque block of data - on the surface it appears to be a -1 followed by a large block of mostly zeros, with only occasional data. This block is simply an array of transformation node references - each uint holds the transformation node index of the parent in it’s transformation chain, with -1 indicating that the node has no parent - the reason most of the data is zero is that mostly there is no need for a complex chain of parent transformation.

This tree of parenting allows for complex animations and skeletal structures that allow multiple animations to be applied to each render item.

Render Items

Finally, after the unknown data block comes the world-placed objects - a string identifier followed by a typed list. This can have up to four entries, though any (or all) may be missing:

String Node types
CONNECTORS model::Connector
RENDER_NODES model::RenderNode, model::SkinNode, model::FakeOmniLightsNode, model::FakeSpotLightsNode, model::FakeALSNode
SHELL_NODES model::ShellNode, model::SegmentsNode
LIGHT_NODES model::LightNode

with the contents described in the sections for those types.

It is at this point that the file should have reached it’s end - with 0 bytes left to read, and being fully completed all final cross-checks and cross-links can be made, ready for interpretation however one wishes.

Named Types

At this point ‘all that is left’ is to define specific types, and allow the chain to be followed. Let’s start with the first major node we are interested in reading, the root node.

 model::RootNode

The RootNode object holds information about all of the materials in the scene, and a chunk of vector data that is not well understood.

model::RootNode :=
  uint_string       name;
  uint              version;      # Asssumed
  model::PropertiesSet properties;
  uchar             unknownA;     # Either 0, 1 or 2
  osg::Vec3d        boundingBoxMin;
  osg::Vec3d        boundingBoxMax;
  osg::Vec3d        unknownB[4];
  list<Material>    materials;
  uint              unknownC[2];

The object begins like all other Node-derived objects, with the name (although in v10 files, this is explicitly a uint_string, not in the lookup table), class version and properties dictionary. Although the properties are often empty for Node-derived objects, in the RootNode this always has the contents {"__VERSION__": 2} - a value which appears to be important when writing (it changes the layout of the unknown areas of the class?)

After a single char which is not understood, We then have two Vector3d objects. These define the bounding box of the model - the first being the lower corner of the box, the second the upper corner. In the model viewer, this defines the range that the axes are displayed (e.g. the X axis is shown from boundingBoxMin.x to boundingBoxMax.x.

After this is another chunk of unknown double data, unknownB.

The list of materials contains the bulk of the contents of the class - in an easy-to-read list format. After these, there is a small unknown block - that seems to always consist of a single uint = 0, followed by another number.

Material

Material objects are entirely constructed as a single map:

Material :=
  map<string, X>

However, the type, labelled X of the value corresponding to the string depends on that string e.g. if the string is UNIFORMS then X is a model::PropertiesSet. If the string is BLENDING then X is a single uchar.

The possible entries in the material map are:

Key string Entry type Interpretation
BLENDING uchar Opacity mode enum
CULLING uchar Unknown (0 or 1)
DEPTH_BIAS uint Unknown (0 or 1)
MATERIAL_NAME string The base edm Material
NAME string Material name
SHADOWS uchar Unknown (0, 2 or 3)
TEXTURES See Below  
TEXTURE_COORDINATE_ CHANNELS uint count + count ints Unknown (10, 11 or 12 counts)
UNIFORMS model::PropertiesSet Shader uniform parameters
ANIMATED_UNIFORMS model::PropertiesSet Animated shader parameters
VERTEX_FORMAT uint count + count bytes Layout of vertex data

 Blending

This describes the opacity mode.

Value Mode setting
0 None
1 Blend
2 Alpha Test
3 Sum. Blending
4 Z Written Blending (Unverified, and unused in any .edm file)

 Material Name

This is the internal renderer material that should be used, and modified by the material settings saved in the file. It corresponds to the 3DS edm tools ‘Material’ option, and should expect a unique value for each of those settings; the observed values are:

value 3DS Material Meaning
additive_self_illum_material    
bano_material    
building_material    
chrome_material    
color_material    
def_material Default Basic, diffuse, textured material
fake_als_lights    
fake_omni_lights    
fake_spot_lights    
forest_material    
glass_material Glass ?(mostly transparent material with gloss)
lines_material    
mirror_material    
self_illum_material Self-illuminated Things like panels that need to be lit when there is no light
transparent_self_illum_material Transparent Self-illuminated Used for e.g. indicator bulbs

Uniforms

Literally the shader uniform values, these values effectively control the parameters of the material. Two sets are present in the file; UNIFORMS are the basic, fixed properties, and ANIMATED_UNIFORMS are AnimatedProperty types including argument and keyframe information - but applied to the same uniform names. A list of all materials/uniforms observed in all .edm files included in DCS World:

Base Material Name Uniforms
additive_self_illum_material diffuseShift, multiplyDiffuse, phosphor, reflectionValue, selfIlluminationColor, selfIlluminationValue, specFactor, specMapValue, specPower
bano_material banoDistCoefs, diffuseValue
building_material diffuseValue, reflectionValue, selfIlluminationValue, specFactor, specPower
chrome_material diffuseShift, diffuseValue, normalMapValue, reflectionValue, specFactor, specMapValue, specPower
color_material color, diffuseValue, reflectionValue, selfIlluminationValue, specPower
def_material diffuseShift, diffuseValue, reflectionValue, specFactor, specMapValue, specPower
fake_omni_lights shiftToCamera, sizeFactors
fake_spot_lights coneSetup, sizeFactors
forest_material diffuseValue, reflectionValue, selfIlluminationValue, specFactor, specPower
glass_material diffuseValue, reflectionValue, specFactor, specPower
lines_material color, selfIlluminationValue
mirror_material diffuseShift, diffuseValue, reflectionValue, specFactor, specPower
self_illum_material diffuseShift, multiplyDiffuse, phosphor, reflectionValue, selfIlluminationColor, selfIlluminationValue, specFactor, specPower
transparent_self_illum_materia l diffuseShift, selfIlluminationValue

 Textures and Texture Coordinate Channels

The TEXTURES entry holds a list of actual texture files used in the model as a simple uint-prefixed list. The full interpretation needs more work to be understood. The structure is:

TEXTURES := list<textureDEF>

textureDEF :=
  int           index;
  int           unknown;     # ALWAYS -1
  string        filename;
  uint          unknown2[4]  # Some data - ALWAYS [2, 2, 10, 6]
  osg::Matrixf  unknown3;    # Assume is a texture transformation matrix.
                             # Almost always identity - very rare to not

Index seems to indicate the type of texture; this is derived from observation of the associated filenames - this possibly affects which UV map/set of vertex data is used to map the texture:

Index Role Typical texture name examples
0 Diffuse Wide variety, as would be expected
1 Normals Names tend to include _normal or _nm, or sometimes _bump
2 Specular _spec, _specular makes this also relatively obvious
3 Numerals bf-109k-4_bort_number, mi_8_bort_number, su-27_numbers
4 Glass Dirt tf51d-cpt_glassdirt, mig-29_cpt_glassdirt (only two exist)
5 Damage bulle_dam, f86f_damage, tu22m3_damage_konsol_l
8 ? Lots of Flame_*, BANO, _light - possible emittance?
9 ? mi_8_tex_1_ao (only example in all .edm files)
10 Damage Normals mi_8_damage_normal, f-86f_glass_damage_nm
11 ? tu-22m3_glass_color_spec, kab_glass_spec_color (only two)
12 ? f-86f_chrom, sa342_int_cpit_glass_reflect, chromic_blur (only three)

(Note: A forum post claims 3=Decal, 4=Dirt, 5=Damage, 6=Puddles, 7=Snow, 8=Self-Illumination, 9=Ambient Occlusion. Also seems to imply exact definitions are dependent on accompanying lua files)

The TEXTURE_COORDINATE_CHANNELS field is defined as:

TEXTURE_COORDINATE_CHANNELS :=
  uint count;
  uint channels[count];

And remains a mystery for now. There is usually 10, 11 or 12 channels, and most of the channels are filled with -1 (e.g. 0xFFFFFFFF). Best guess is that it is some kind of mask - an error in writing resulted in one of the channels being written with value 1, which led to the model viewer error Empty Channel. Writing zero for the first channel (by guesswork and inspection of other .edm files) seems to work for the simple one-texture case.

 Vertex Format

Specifies the format of the vertex data; The render nodes store the total count and stride, but is otherwise an opaque block of float values. This defines how those floats are used:

VERTEX_FORMAT :=
  uint    count;
  uchar   channels[count];

Most entries have a count of 26 - however a few (possibly older?) models have an entry of 24 - so it is not always safe to assume the length. Each of the channels counts has a fixed meaning, and observed lengths:

Channel Length Represents
0 4 Position data. The last of these appears to relate to vertex group for parenting purposes
1 3 Normals data
2 3  
3 3  
4 2 Texture UV
5 2  
6 2  
7 2  
8 2  
20 3  
21 4 Bone data related - number of bone references? (appears to be x2 entries in vertex data)
24 3  
25 3  

Nodes

model::Node

The Node node is used both as an empty node, and also is the basis for many of the other nodes - which all share the identical starting layout:

model::Node :=
  uint_string   name;
  uint          version;
  propertiesset props;

Noting that the name is explicitly a non-lookup string, regardless of file version. As with RootNode (which we can also see matches this exact layout) we have assumed that the uint field is representative of class version - this seems to have no other meaning, and makes a lot of sense in terms of allowing the schema to evolve over time.

model::TransformNode

model::TransformNode :=
  model::Node   base;
  osg::Matrixd  transform;

model::LodNode

The LodNode object is a transform-tree root object - it controls the LOD appearance of the node graph underneath it. It appears that model::Node objects always act as ‘fake’ roots underneath a LodNode (untested).

model::LodNode :=
  model::Node             base;
  uint                    count;
  model::LodNode::Level   levels[count];

model::LodNode::Level :=
  double start_sq;
  double end_sq;

It appears that the count of LodNode::Level should always match the number of child nodes (also untested globally), and so it seems that the association between level to node is purely based on the ordering of the children.

A model::LodNode::Level object contains a start and end value, stored as the square of the actual LOD distance desired (e.g. an LOD of 850m would be stored as 722500).

model::Bone

model::Bone :=
  model::Node   base;
  osg::Matrixd  m1;
  osg::Matrixd  m2;

model::BillboardNode

Not much is understood about this node other than the size:

model::BillboardNode :=
  model::Node   base;
  uchar         unknown[154];

 Animation Nodes

model::ArgAnimationNode, Position, Rotation and Scale

This is a special node, as the nodes model::ArgPositionNode, model::ArgRotationNode, and model::ArgScaleNode are all parsed exactly the same way - but just appear to be written when the animation only has a single (position, rotation, scale) channel of animation:

model::ArgRotationNode := model::ArgAnimationNode
model::ArgPositionNode := model::ArgAnimationNode
model::ArgScaleNode    := model::ArgAnimationNode

The actual ArgAnimationNode contains quite a lot of data:

model::ArgAnimationNode :=
  model::Node       base;
  osg::Matrixd      tf_Matrix;
  osg::Vec3d        tf_Position;
  osg::Quaternion   tf_Quat1;
  osg::Quaternion   tf_Quat2;
  osg::Vec3d        tf_Scale;

  list<model::ArgAnimationNode::Position>  positionData;
  list<model::ArgAnimationNode::Rotation>  rotationData;
  list<model::ArgAnimationNode::Scale>     scaleData;

The set of transformation tf_ values are assumed to describe the chain of transformation in order to properly process the vertex data (in a RenderNode object referencing this node as it’s parent). The exact application is currently unknown; at the moment a working best-guess is something along the lines of:

Transform = tf_Matrix * tf_Position * tf_Quat1 * keyRotation * tf_Scale

Where each of the objects has obviously been transformed to be compatible with the other (e.g. so you can apply a matrix to a quaternion...). The entry keyRotation is the current best guess for where the keyframe rotation value is applied.

In addition, the matrix tf_Matrix seems to have the extra role of swapping axis - animation vertex data seems to be kept in the original 3DS coordinate system. This is an area of active research.

The position and rotation entries are relatively similar; just an animation argument value followed by a list of keys:

model::ArgAnimationNode::Position :=
  uint                              argument;
  list<model::Key<key::POSITION>>   keys;

model::ArgAnimationNode::Rotation :=
  uint                              argument;
  list<model::Key<key::ROTATION>>   keys;

model::Key<key::POSITION> :=
  double          frame;
  osg::Vector3d   value;

model::Key<key::ROTATION> :=
  double            frame;
  osg::Quaternion   value;

However, scale appears to be handled slightly differently, and appears to contain two sets of keys, of currently unknown interpretation - one set of four doubles, and one of three:

model::ArgAnimationNode::Scale :=
  uint argument;
  list<ScaleKeyA>   keys;
  list<ScaleKeyB>   keys2;

ScaleKeyA :=
  double      frame;
  osg::Vec4f  value;

ScaleKeyB :=
  double      frame;
  osg::Vec3f  value;

model::ArgAnimatedBone

The bone animation node is the same as the ArgAnimationNode, but with the addition of an extra transfomation matrix:

model::ArgAnimatedBone :=
  model::ArgAnimationNode   base;
  osg::Matrixd              xf_Bone;

The application of this extra transformation is also currently unknown, because at time-of-writing skeletal animation/skinning in the EDM files has not been investigated.

model::ArgVisibilityNode

Not containing a transformation - only a list of toggles for on/off visibility, this is a much simpler node:

model::ArgVisibilityNode :=
  model::Node   base;
  list<model::ArgVisibilityNode::Arg>   visibilityData;

model::ArgVisibilityNode::Arg :=
  uint                                    argument;
  list<model::ArgVisibilityNode::Range>   keys;

model::ArgVisibilityNode::Range :=
  double frameStart;
  double frameEnd;

Where the two key entries are the start and end ranges of visibility. Note that in cases where the object ‘becomes visible’ and stays that way, over the range of the animation argument, the frameEnd value will often be very high - values of 1e300 are not uncommon.

 Object Nodes

 Connector

Connectors are a very simple named connection to a parent transformation node:

model::Connector :=
  model::Node       base;
  uint              parent;
  uint              unknown;

And will always have a name entry in the base node reading. The parent field is the index of the parent transformation node - that is, the index in the RootNode.nodes list that was read earlier in the file. The last entry remains unknown - all known examples of .edm files have this field zero, so does not appear to be important.

Properties have been observed in the base.props field e.g. {“Type”: “bounding_box”}. It is unknown if this is merely commentary or holds some significance.

Render Nodes

RenderNode objects generally contain very large amounts of vertex and index data, the actual renderable geometry of the edm file:

model::RenderNode :=
  model::Node       base;
  uint              unknown;   # Always zero in known files
  uint              materialId;
  PARENTDATA        parentData;
  VERTEXDATA        vertexData;
  INDEXDATA         indexData;

The materialId is the index of the material to be applied to this data - the index in the RootNode.materials list. The VERTEXDATA and INDEXDATA types are shared with the model::ShellNode and model::SkinNode types.

Let’s start with the parent data, which is slightly unusual - the exact layout depends on the value of the first count entry. If there is only one parent entry:

PARENTDATA :=
  const uint count = 1;
  uint  parent;
  int   damageArgument;

Or, if count > 1:

PARENTDATA :=
  uint          count;
  PARENT_ENTRY  parents[count];

PARENT_ENTRY :=
  uint  parent;
  int   indexStart;
  int   damageArgument;

This multiple-parent structure allows objects with identical materials to be merged into a single rendernode, presumably allowing some render-time optimisation in DCS. For nodes with multiple entries, the node can be effectively split into multiple objects, the indexStart field determines the entry in the index table which starts defining the faces of the objects.

In addition, each object entry has a damageArgument field. This is used to determine part visibility as the damage to the object in the game progresses. For objects unrelated to damage modelling, this is set to -1.

One unknown, however, is that the vertex data itself has four entries for it’s ‘position’ field - and the fourth entry seems to refer to the index in this PARENTDATA.parents array. Thus, it appears that this information is duplicated, so some uncertainty remains.

This structure also appears to be related to the index count of model::RNControlNode. In particular: there appears to be one RNControlNode in the index for each additional parent data entry - that is, the sum of model::RNControlNode = PARENTDATA.count - 1 for every model::RenderNode in the .edm file.

This link was derived by observing numeric correlation and testing the hypothesis on every existing .edm file. It appears to be correct.

Let’s examine the vertex data:

VERTEXDATA :=
  uint    count;
  uint    stride;
  float   data[count*stride];

Where the data array could also be interpreted identically as:

VERTEXDATA :=
  uint    count;
  uint    stride;
  VERTEX   data[count];

VERTEX :=
  float   data[stride];

e.g. an array of float vertex data, where each set of stride values corresponds to a single vertex. The exact value of stride, and the meaning of each of the vertex entries - corresponds to the vertex format specified in the associated material. The amount of data in this array - e.g. count * stride * sizeof(float) is counted towards the index bytes counter - __gv_bytes for model::RenderNode.

With vertex data we also need a way to represent faces; that is where the index array comes in:

INDEXDATA :=
  uchar         data_type;
  uint          entries;
  uint          unknown;
  INDEXVALUE    data[entries];

Where the INDEXVALUE type depends on the value of the data_type field:

data_type Type of each entry
0 uchar
1 ushort
2 uint

and each entry refers to the index of a single vertex in the vertexData array. Allowing the data type to be varied allows saving of space because the number of vertices in a single object can - sometimes - run up to the hundreds of thousands, but would be a complete waste of space for the majority of models with, say, less than 60k vertices.

The unknown field is either 0, 1 or 5 - most commonly 5. What is known is that the count of the index data is not always a multiple of three - but this does not appear to correlate with the value of the unknown field (which is what would be expected if, say, the unknown field represented face type).

The physical data read in the index array - entries * sizeof(data_type) is counted towards the index bytes counter __gi_bytes, for model::RenderNode.

model::SkinNode

SkinNode describes a set of vertex data designed to be layered over Bone nodes. It is relatively similar to RenderNode except instead of parent transforms it explicitly lists a set of bones:

model::SkinNode :=
  model::Node   base;
  uint          material;
  list<uint>    bones;
  uint          unknown;

  VERTEXDATA    vertexData;
  INDEXDATA     indexData;

Where the bones refer to offset indexes in the RootNode.nodes array. The vertex data for such nodes will have bone index/weight data in - the indices for which can be extracted by inspecting the material vertex format. Because it is rendered data, the vertex and index data for these nodes contribute to the __gv_bytes and __gi_bytes index counts.

model::FakeOmniLightsNode

model::FakeOmniLightsNode :=
  model::Node   base;
  uint          unknown[5]
  uint          count;
  model::FakeOmniLight        data[count];

mode::FakeOmniLight :=
  double  data[6];

 model::FakeSpotLightsNode

This has a relatively similar structure to model::RenderNode. The counts and data types have been inferred from index and inspection, but the interpretation is currently unknown.

model::FakeSpotLightsNode :=
  model::Node   base;
  uint          unknown;
  uint          materialId;   // Assumed - same as renderNode
  uint          controlNodeCount;
  FSLNPARENT    parentData[controlNodeCount];
  uint          lightCount;
  model::FakeSpotLight  lights[lightCount];

FSLNPARENT :=
  uint    nodeId;
  uint    unknownA;
  float   unknownB[3];

model::FakeSpotLight :=
  uchar data[64];
  uchar final_byte;

And, similarly to RenderNode - the count of model::FSLNControlNode is equal to the number of these parent entries minus one. The actual light entries appear to be a big mix of float-like data, but definitely have a separate uchar at the end of them. The meaning remains unknown.

 model::FakeALSNode

model::FakeALSNode := uint unknown[3]; uint count; model::FakeALSLight lights[count];

model::FakeALSLight := uchar data[80];

 Light Nodes

The parent reference along with a properties set of light properties is known - but the other values (which are assumed to be flags of some sort) have unknown interpretation. The light properties do not count towards the general index count of propertiesset:

model::LightNode :=
  model::Node     base;
  uint            parent;
  uchar           unknownB;
  propertiesset   lightProperties;
  uchar           unknownC;

Shell Nodes

Shell nodes appear to define the collision shells for models; as such, they do not appear to have a material reference. They do, however, embed their own vertex format:

model::ShellNode :=
  model::Node     base;
  uint            unknown;
  VERTEX_FORMAT   vertex_format;
  VERTEXDATA      vertexData;
  INDEXDATA       indexData;

Where the vertex and index raw data read for these nodes contribute to the __cv_bytes and __ci_bytes index counters.

model:SegmentsNode

Only the layout is known for these nodes:

model::SegmentsNode :=
  model::Node     base;
  uint            unknown;
  list<model::SegmentsNode::Segments>   segments;

model::SegmentsNode::Segments :=
  float   data[6];