3D file formats

I’m going to be doing a lot with programatically manipulated models. Therefore there’s a benefit to me in using a file format which is human-readable, and also easily parsed. Compactness is also a virtue, but it’s a virtue I can achieve by compressing human readable files, I think.

Loading time is an issue, but I will be loading relatively few models and manipulating them in memory to produce large numbers of variants, rather than loading a new model for every asset I wish to place. So both from the point of view of total disk space and from the point of view of performance, I think that formats which are optimised more towards my needs as a developer than to raw performance or storage size are probably justified.

Obviously jMonkeyEngine’s own .j3o format is something I can parse – because I have the native jMonkeyEngine libraries to do it – but it’s not something I can inspect or hand edit, so I’m unwilling at this stage to use it. As I understand it, it’s a file of serialised Java objects, which, if so, would make it relatively efficient to load.

XML formats

XML formats are easy to parse; they’re also reasonably easy to edit both as text files and as structured files. So my preference is to use them as a starting point for search. The obvious XML formats are

  1. 3DXML, a proprietary format owned by Dassault Systèmes;
  2. Collada, an open format apparently seen primarily as an unlossy interchange format between 3d applications;
  3. X3D, an open format seen as the successor to (and much more prolix than) VRML;
  4. Ogre 3D, a format designed for the Ogre 3D game engine and now widely used in the jMonkeyEngine community, but for which I can find no user-oriented documentation.

JSON-based formats

JSON is also easy to parse and to edit; the glTF 2.0 standard, in its JSON/ASCII representation, is such a format.

Other text formats

VRML isn’t either XML or JSON, but it looks reasonably easy to parse.

Features

Name XML JSON Blender MakeHuman jME3 Notes
3DXML Unsupported No No Proprietary; apparently, engineering oriented
Collada Built in importer/exporter Exporter No Prolix, but sort-of interpretable and manageable.
X3D Built in importer/exporter No No
Ogre 3D Unsupported Exporter built in importer
glTF 2.0 Built in importer/exporter No built in importer On inspection, the glTF 2.0 files generated by Blender are thoroughly inscrutable. Although they are technically text files, they’re essentially very thin wrappers around ascii-coded binary blobs. This doesn’t look usable to me.
VRML Apparently accepted by the X3D importer; no exporter No No I couldn’t make import of a sample file into Blender work. The import completed with no errors, but no objects appeared in the scene.

Discussion

I did not investigate 3DXML, because I’m not going to sign up to even a free licence from an armaments company.

Of the rest: I’m pretty disappointed.

glTF

glTF might as well be a binary format; it’s unusable for my purposes.

X3D

X3D export of the simple cube from Blender results in a file which is readable, but which does not have explicit vertices or edges. Instead it has an attribute whose value is a string representation of a sequence of 24 numbers:

<Coordinate DEF="coords_ME_Cube"
			point="1.000000 1.000000 1.000000 1.000000 1.000000 -1.000000 1.000000 -1.000000 1.000000 1.000000 -1.000000 -1.000000 -1.000000 1.000000 1.000000 -1.000000 1.000000 -1.000000 -1.000000 -1.000000 1.000000 -1.000000 -1.000000 -1.000000 "/>

I’m confidently assuming that that sequence is expected to be read as a set of six triples, each triple representing a vertex with x, y, z coordinates; but it’s not at all clear to me how edges and faces are encoded.

Furthermore, when you reimport an X3D file exported by Blender back into Blender you get nothing visible (and no error message). This is exactly the same as what you get when you import a VRML file, which does not raise confidence.

Collada

Collada is a little better in that it explicitly says how many numbers are in the array, and does not present them as an attribute but as data; additionally, it has information on how to decode that data into tuples:

        <source id="Cube-mesh-positions">
          <float_array id="Cube-mesh-positions-array" count="24">1 1 1 1 1 -1 1 -1 1 1 -1 -1 -1 1 1 -1 1 -1 -1 -1 1 -1 -1 -1</float_array>
          <technique_common>
            <accessor source="#Cube-mesh-positions-array" count="8" stride="3">
              <param name="X" type="float"/>
              <param name="Y" type="float"/>
              <param name="Z" type="float"/>
            </accessor>
          </technique_common>
        </source>

and the vertices are declared as being the points in this array:

        <vertices id="Cube-mesh-vertices">
          <input semantic="POSITION" source="#Cube-mesh-positions"/>
        </vertices>

but again there’s nothing obvious to indicate which vertices are joined by edges. There is an array of six normals, which presumably imply the faces of the cube, but that’s a little sketchy. There are also triangles, declared as follows:

        <triangles material="Material-material" count="12">
          <input semantic="VERTEX" source="#Cube-mesh-vertices" offset="0"/>
          <input semantic="NORMAL" source="#Cube-mesh-normals" offset="1"/>
          <input semantic="TEXCOORD" source="#Cube-mesh-map-0" offset="2" set="0"/>
          <p>4 0 0 2 0 1 0 0 2 2 1 3 7 1 4 3 1 5 6 2 6 5 2 7 7 2 8 1 3 9 7 3 10 5 3 11 0 4 12 3 4 13 1 4 14 4 5 15 1 5 16 5 5 17 4 0 18 6 0 19 2 0 20 2 1 21 6 1 22 7 1 23 6 2 24 4 2 25 5 2 26 1 3 27 3 3 28 7 3 29 0 4 30 2 4 31 3 4 32 4 5 33 0 5 34 1 5 35</p>
        </triangles>

H’mmm… Twelve triangles makes sense for six square faces, but there are 108 numbers in that <p> element, which is eighteen numbers per triangle; and since the highest number is 35, they’re not indices into either the Cube-mesh-vertices (8) or the Cube-mesh-normals (6) arrays, so that implies they are indices into Cube-mesh-map-0, which does have 36 entries…

        <source id="Cube-mesh-map-0">
          <float_array id="Cube-mesh-map-0-array" count="72">0.875 0.5 0.625 0.75 0.625 0.5 0.625 0.75 0.375 1 0.375 0.75 0.625 0 0.375 0.25 0.375 0 0.375 0.5 0.125 0.75 0.125 0.5 0.625 0.5 0.375 0.75 0.375 0.5 0.625 0.25 0.375 0.5 0.375 0.25 0.875 0.5 0.875 0.75 0.625 0.75 0.625 0.75 0.625 1 0.375 1 0.625 0 0.625 0.25 0.375 0.25 0.375 0.5 0.375 0.75 0.125 0.75 0.625 0.5 0.625 0.75 0.375 0.75 0.625 0.25 0.625 0.5 0.375 0.5</float_array>
          <technique_common>
            <accessor source="#Cube-mesh-map-0-array" count="36" stride="2">
              <param name="S" type="float"/>
              <param name="T" type="float"/>
            </accessor>
          </technique_common>
        </source>

which is six entries per face. Which sort of makes sense. But the numbers make no obvious sense to me. There are nine distinct values:

user=> (def s (set [0.875 0.5 0.625 0.75 0.625 0.5 0.625 0.75 0.375 1 0.375 0.75 0.625 0 0.375 0.25 0.375 0 0.375 0.5 0.125 0.75 0.125 0.5 0.625 0.5 0.375 0.75 0.375 0.5 0.625 0.25 0.375 0.5 0.375 0.25 0.875 0.5 0.875 0.75 0.625 0.75 0.625 0.75 0.625 1 0.375 1 0.625 0 0.625 0.25 0.375 0.25 0.375 0.5 0.375 0.75 0.125 0.75 0.625 0.5 0.625 0.75 0.375 0.75 0.625 0.25 0.625 0.5 0.375 0.5]))
#'user/s
user=> s
#{0 0.125 0.25 0.5 0.625 0.375 0.75 0.875 1}
user=> (sort s)
(0 0.125 0.25 0.375 0.5 0.625 0.75 0.875 1)

But there’s no obvious pattern to how frequently these values occur:

user=> (map (fn [i] (count (filter (fn [j] (= i j)) all-values))) s)
(3 3 6 12 15 15 12 3 3)

So… it’s not obvious to me how I extract a piece of solid geometry, namely a cube with sides of length 2, centred on the origin, from this data. I know it can be done, because when Blender reimports the file it not only renders a cube but recognises it as a cube; but it isn’t trivial.

Ogre 3D

Ogre 3D is a lot clearer. Here is (part of) its attempt at saving the cube:

<mesh>
    <sharedgeometry vertexcount="24">
        <vertexbuffer colours_diffuse="False" normals="true" positions="true" tangent_dimensions="0" tangents="False" texture_coords="1">
            <vertex>
                <position x="1.000000" y="1.000000" z="-1.000000"/>
                <normal x="0.000000" y="1.000000" z="-0.000000"/>
                <texcoord u="0.625000" v="0.500000"/>
            </vertex>
            <vertex>
                <position x="-1.000000" y="1.000000" z="1.000000"/>
                <normal x="0.000000" y="1.000000" z="-0.000000"/>
                <texcoord u="0.875000" v="0.250000"/>
            </vertex>
			...
        </vertexbuffer>
    </sharedgeometry>
    <submeshes>
        <submesh material="Material" operationtype="triangle_list" use32bitindexes="False" usesharedvertices="true">
            <faces count="12">
                <face v1="0" v2="1" v3="2"/>
                <face v1="3" v2="4" v3="5"/>
                ...
            </faces>
        </submesh>
    </submeshes>
    <submeshnames>
        <submesh index="0" name="Material"/>
    </submeshnames>
</mesh>

Nothing in this explicitly says it’s a cube. But where we have two triangles with a common edge and a common normal, we know they form part of the same face; and, given that each of the distinct normals are either at right angles to, or in opposition to, each of the others, the boxiness is easily inferred. Finally, given all the edges are of the same length, we have a cube.

Although the Ogre import/export add on is not supported by Blender, an exported cube can be successfully reimported into Blender. However, although Blender can export the meshes of a model to Ogre format successfully, it fails to export the skeleton. However the MakeHuman Ogre exporter does successfully export the skeleton.

The only documentation for Ogre 3D XML that I can find is two DTDs here, but, in fact, that is what I most need.

Summary

Generally, this is disappointing. I may need to rethink my approach. But, although I was initially prejudiced against Ogre 3D because of its relative obscurity and lack of user-oriented documentation, it is probably the best of the bunch.