GLTF Format

Recap assets/simple.gltf

The Tutorial [1] explained the test/html/asset/simple.gltf.

nodes & meshes

The logic (virtual) object represented in gltf scene.

{ "nodes" : [ { "mesh" : 0, "name": "simple-0" } ],
  "meshes" : [
    { "primitives" : [ { "attributes" : { "POSITION" : 1 }, "indices" : 0 } ] }
  ],
}

Mesh using data through accessor s.

accessors & bufferViews

The bufferViews are actually the raw data’s logical view.

{ "buffers" : [
    { "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength" : 44 }
  ],

  "bufferViews" : [
    { "buffer" : 0, "byteOffset" : 0, "byteLength" : 6, "target" : 34963 },
    { "buffer" : 0, "byteOffset" : 8, "byteLength" : 36, "target" : 34962 }
  ]
}

In buffers[0].uri, a vertices array is provided. The buffer view split it into 2 sections, vertex indexes and positions.

Image from Buffers, BufferViews, and Accessors, Totorial [1].

../_images/001-buffer-view.png

Image 5b: The buffer views, referring to parts of the buffer,

The way to use data array is exactly specified by accessors.

"accessors" : [
    { "bufferView" : 0, "byteOffset" : 0, "componentType" : 5123, "count" : 3,
      "type" : "SCALAR",
      "max" : [ 2 ], "min" : [ 0 ] },
    { "bufferView" : 1, "byteOffset" : 0, "componentType" : 5126, "count" : 3,
      "type" : "VEC3",
      "max" : [ 1.0, 1.0, 0.0 ], "min" : [ 0.0, 0.0, 0.0 ] }
],

Now it’s natural to share mesh for different nodes:

"nodes" : [
    { "mesh" : 0 },
    { "mesh" : 0,
      "translation" : [ 1.0, 0.0, 0.0 ] }
  ],

Lowpoly City Example

city/scene.gltf representation

Some parts of low poly city gltf assets:

{ "accessors": [
  { "bufferView": 2, "componentType": 5126, "count": 1762,
    "max": [ 25.192995071411133, 10.835280418395996, 27.863927841186523 ],
    "min": [ -18.667209625244141, -29.31907844543457, -72.12615966796875 ],
    "type": "VEC3" },
  { "bufferView": 2, "byteOffset": 21144, "componentType": 5126, "count": 1762,
    "max": [ 1, 1, 0.98781126737594604 ], "min": [ -1, -1, -1 ],
    "type": "VEC3" }, ],
  "asset": {
    "extras": { "author": "antonmoek (https://sketchfab.com/antonmoek)",
      "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
      "source": "https://sketchfab.com/models/edd1c604e1e045a0a2a552ddd9a293e6",
      "title": "Cartoon Lowpoly Small City Free Pack" },
      "generator": "Sketchfab-3.25.5", "version": "2.0" },
    "bufferViews": [
      { "buffer": 0, "byteLength": 443160, "byteOffset": 0, "name": "floatBufferViews", "target": 34963 },
      { "buffer": 0, "byteLength": 557656, "byteOffset": 443160, "byteStride": 8, "name": "floatBufferViews", "target": 34962 },
      { "buffer": 0, "byteLength": 1672968, "byteOffset": 1000816, "byteStride": 12, "name": "floatBufferViews", "target": 34962 },
      { "buffer": 0, "byteLength": 1115312, "byteOffset": 2673784, "byteStride": 16, "name": "floatBufferViews", "target": 34962 } ],
    "buffers": [ { "byteLength": 3789096, "uri": "scene.bin" } ],
    "images": [
      { "uri": "textures/World_ap.16_baseColor.jpeg" },
      ...
      { "uri": "textures/World_ap.11_baseColor.jpeg" } ],
    "materials": [
      { "doubleSided": true, "emissiveFactor": [ 0, 0, 0 ],
        "name": "World_ap",
        "pbrMetallicRoughness": { "baseColorFactor": [ 1, 1, 1, 1 ], "baseColorTexture": { "index": 4, "texCoord": 0 },
        "metallicFactor": 0, "roughnessFactor": 1 } },
      { "doubleSided": true, "emissiveFactor": [ 0, 0, 0 ], "name":
        "World_ap.8",
        "pbrMetallicRoughness": { "baseColorFactor": [ 1, 1, 1, 1 ], "baseColorTexture": { "index": 6, "texCoord": 0 },
        "metallicFactor": 0, "roughnessFactor": 0.59999999999999998 } },
      ...
  ],
  "meshes": [
      { "name": "CAR_03_1_World ap_0",
        "primitives": [
          { "attributes": { "NORMAL": 1, "POSITION": 0, "TANGENT": 2, "TEXCOORD_0": 3 },
            "indices": 4, "material": 0, "mode": 4 }
        ] },
      { "name": "CAR_03_World ap_0",
        "primitives": [
          { "attributes": { "NORMAL": 6, "POSITION": 5, "TANGENT": 7, "TEXCOORD_0": 8 },
            "indices": 9, "material": 0, "mode": 4 }
        ] },
      ...
  ],
  "nodes": [
      { "children": [ 1 ], "name": "RootNode (gltf orientation matrix)", "rotation": [ -0.70710678118654746, -0, -0, 0.70710678118654757 ] },
      { "children": [ 2 ], "name": "RootNode (model correction matrix)" },
      { "children": [ 3 ], "matrix": [ 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1 ], "name": "4d4100bcb1c640e69699a87140df79d7.fbx" },
      { "children": [ 4, 6, 22, 65, 98, 134, 178, 227, 237 ], "name": "RootNode" },
      ...
      { "children": [ 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63 ], "matrix": [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -369.06906127929688, -90.703544616699219, -920.1591796875, 1 ],
        "name": "Cars" },
      { "children": [ 24 ], "matrix": [ -1.1161040868103447, 1.3668332938134597e-16, -1.002153514889434, 0, -1.8070770596253361e-08, 1.4999999999999998, 2.0125520511237016e-08, 0, 1.002153514889434, 2.7047907974381987e-08, -1.1161040868103445, 0, 22.131305694580078, 14.663174629211426, -475.07095336914062, 1 ],
        "name": "CAR_03_1" },
      { "mesh": 0, "name": "CAR_03_1_World ap_0" },
      { "children": [ 26 ], "matrix": [ -0.039509975088762972, 4.8385761910227429e-18, -1.4994795636715044, 0, 1.6096576513098873e-09, 1.5, -4.2413066498289683e-11, 0, 1.4994795636715044, -1.610216327898289e-09, -0.039509975088762972, 0, -281.15509033203125, 14.663183212280273, 108.45243835449219, 1 ],
        "name": "CAR_03" },
      { "mesh": 1, "name": "CAR_03_World ap_0" },
      ...
  ],
  "samplers": [ { "magFilter": 9729, "minFilter": 9987, "wrapS": 10497, "wrapT": 10497 } ],
  "scene": 0,
  "scenes": [ { "name": "OSG_Scene", "nodes": [ 0 ] } ],
  "textures": [
      { "sampler": 0, "source": 0 },
      ...
  ]

Note: 21144 = 1762 x 12

Node Example

The loaded node example (name = ‘Tree-1-3’)

for city/scene.gltf, paras.nodes = ['Tree-1-3'],
nodes[0].children[0].type == 'Mesh',
nodes[0].children[0].geometry is a BufferGeometry, with array of
BufferAttributes as 'attributes'.
nodes[0].children[0].geometry.attributes['position'] ==
   length: 2772
   dynamic: false
   name: ""
   array: Float32Array(2772) [135.61163330078125, 31.193208694458008, -2.098475694656372, …]
   itemSize: 3
   count: 924
   normalized: false
   usage: 35044
   updateRange: {offset: 0, count: -1}
   version: 0

Three.js GLTFLoader

The gltf loader processing can be simplify and clarified if with some basic gltf knowledge.

function GLTFLoader( manager ) {
    parse: function ( data, path, onLoad, onError ) {
        var parser = new GLTFParser( json, extensions, { manager: this.manager } );
        parser.parse( onLoad, onError );
    }
}

function GLTFParser( json, extensions, options ) {
    this.json = json || {};
    this.extensions = extensions || {};
    this.options = options || {};

    this.parse = function ( onLoad, onError ) {
        var parser = this;
        var json = this.json;
        var extensions = this.extensions;
        Promise.all( [
            this.getDependencies( 'scene' ),
            this.getDependencies( 'animation' ),
            this.getDependencies( 'camera' ),
        ] ).then( function ( dependencies ) {
            var result = {
                scene: dependencies[ 0 ][ json.scene || 0 ],
                asset: json.asset,
                ...
            };
            ...
            onLoad( result );
        } ).catch( onError );
    };

This loading and parsing is finished after multiple dependency like mesh, nodes, etc. been parsed.

/**Ody: Load mesh with vertices accessing via accessors.
 * For a primitive.mode == WEBGL_CONSTANTS.TRIANGLES, it's
 * new Mesh( geometry, material )
 *
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
 * @param {number} meshIndex
 * @return {Promise<Group|Mesh|SkinnedMesh>}
 */
GLTFParser.prototype.loadMesh = function ( meshIndex ) {
    var parser = this;
    var json = this.json;
    var meshDef = json.meshes[ meshIndex ];
    var primitives = meshDef.primitives;
    var pending = [];

    for ( var i = 0, il = primitives.length; i < il; i ++ ) {
        var material = primitives[ i ].material === undefined
            ? createDefaultMaterial()
            : this.getDependency( 'material', primitives[ i ].material );
        pending.push( material );
    }

    return Promise.all( pending ).then( function ( originalMaterials ) {
        return parser.loadGeometries( primitives )
          // Ody:
          // geometries must be BufferGeometry. See GLTFParser.loadGeometries()
          .then( function ( geometries ) {
            var meshes = [];
            for ( var i = 0, il = geometries.length; i < il; i ++ ) {
                var geometry = geometries[ i ];
                var primitive = primitives[ i ];
                // 1. create Mesh
                var mesh;
                var material = originalMaterials[ i ];
                if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ) {
                    mesh = meshDef.isSkinnedMesh === true
                        ? new SkinnedMesh( geometry, material )
                        : new Mesh( geometry, material );
                } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {
                    mesh = new LineSegments( geometry, material );
                }
                else ...
                mesh.name = meshDef.name || ( 'mesh_' + meshIndex );
                if ( geometries.length > 1 ) mesh.name += '_' + i;
                ...
                meshes.push( mesh );
            }
            return meshes[ 0 ];
        } );
    } );
};

/**Requests the specified dependency asynchronously, with caching.
 * Ody:
 * Dependency means scene, node, mesh, material etc., except scenes.
 * Anything that can be dependend by others.
 * @param {string} type
 * @param {number} index
 * @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>}
 */
GLTFParser.prototype.getDependency = function ( type, index ) {
    var cacheKey = type + ':' + index;
    var dependency = this.cache.get( cacheKey );

    if ( ! dependency ) {
        switch ( type ) {
            case 'scene':
                dependency = this.loadScene( index );
                break;
            case 'camera':
                dependency = this.loadCamera( index );
                break;
            ...
            default:
                throw new Error( 'Unknown type: ' + type );
        }
        this.cache.add( cacheKey, dependency );
    }
    return dependency;
};

The Material Instancing

Three.js GLTFLoader will create then use a cache for meshes’ material:

GLTFParser.prototype.assignFinalMaterial = function ( mesh ) {

    if ( ! cachedMaterial ) {
        cachedMaterial = material.isGLTFSpecularGlossinessMaterial
            ? extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].cloneMaterial( material )
            : material.clone();
        ...
    }
}

This depends on Three.js/Material’s copy() method, which will ignoring property for MRT suport. (GLTFLoader now depends on MRT Suport)

The X-visual Loader

Which is an x-visual vision of GLTF loader modified from There.js GLTFLoader.

Source: x-visual/packages/three/GLTFLoader

The modification includes:

Exposing Raw Nodes/Geometry Buffer

1. Add the scope (GLTFLoader function call stack) as the argument of GLTFParser constructor, which makes the GLTFLoader instance can be accessed while parsing nodes.

function GLTFLoader( manager ) {
    this.nodeMap = {};

    load: function ( url, onLoad, onProgress, onError ) {
        var scope = this;
        var loader = new FileLoader( scope.manager );
        loader.load( url, function ( data ) {
            try {
                scope.parse( data, resourcePath, function ( gltf ) {
                    onLoad( gltf, scope.nodeMap );
                }, _onError, scope );
            } catch ( e ) {
                _onError( e );
            }
        }, onProgress, _onError );
    },

    parse: function ( data, path, onLoad, onError, loaderScope ) {
        var parser = new GLTFParser( json, extensions,
            { ... },
            loaderScope );

        parser.parse( onLoad, onError );
}

function GLTFParser( json, extensions, options, scope ) {
    this.loaderScope = scope;
    this.json = json || {};
    ...
}

The ‘node’ dependency will get return and returned by parser.parse():

GLTFParser.prototype.parse = function ( onLoad, onError ) {
    var parser = this;
    var json = this.json;
    var extensions = this.extensions;

    Promise.all( [
        this.getDependencies( 'scene' ),
        this.getDependencies( 'animation' ),
        this.getDependencies( 'camera' ),
        // modification
        // nodes[ix].children.geometry.attributes.position is a BufferAttribute
        // nodes[ix].children.geometry.attributes.position.array is a Float32Array
        this.getDependencies( 'node' ),
    ] ).then( function ( dependencies ) {
            ...
        }
}

2. When parsing nodes, update a map in ‘scope’ so nodes name - index can be find out.

GLTFParser.prototype.loadNode = function ( nodeIndex ) {
        ...
    }()
    // then build node (Object3D etc.) with the objects
    .then( function ( objects ) {
        return ( function () {
            ...

            if ( nodeDef.name !== undefined ) {
                node.userData.name = nodeDef.name;
                node.name = PropertyBinding.sanitizeNodeName( nodeDef.name );
            }

            if (!node.name) {
                node.name = String(nodeDef.idx);
            }
            scope.nodeMap[node.name] = nodeDef.idx;
            return node;
        } );
    });

Note

Debug Notes: The node name is been sanitized. Which means you can not use it like “node one”, it’s been replaced with “node_one”.

See similar issue at github & tips in test/html/gltf-car.html.

Also, the AssetKeepr.loadGltfNodes() also access it using sanitized names.

  1. After every thing done, the nodes array also been taken out in gltf results.

For promise returning ‘ndoes’, see Parse Promise.

GLTFParser.prototype.parse = function ( onLoad, onError ) {
    Promise.all(
        ...
    ).then( function ( dependencies ) {
        var result = {
            scene: dependencies[ 0 ][ json.scene || 0 ],
            scenes: dependencies[ 0 ],
            animations: dependencies[ 1 ],
            cameras: dependencies[ 2 ],
            // odys-z
            nodes: dependencies[3],
            asset: json.asset,
            parser: parser,
            userData: {}
        };
        onLoad( result ); // callback reporting results to caller
    };

Enable MRT

The material (currently only MeshStandardMaterial) is created by default supporting MRT.

GLTFParser.prototype.loadMaterial = function ( materialIndex ) {
    var materialParams = {isMrt: true, glslVersion: GLSL3};
    ...
}

This created MRT material template, with support of Three.js MRTSupport version, will be cloned for GLTF nodes’ materail with additional properties, i.e. isMrt & glslVersion.

Exporting GLTF

X-visual created object in scene are intended to be exportable by Three.js GLTFExporter.

In v0.3.80, geo-objects created according to geo-json can be partly exported. ( The texture and uv is still to be done.)

Test:

Asynchronous: test/html/gltf/export.html
Synchronous : test/html/gltf/export-texenv.html

API:

xworld.xport('filename.glb'); // or *.gltf