web前端技术分享
GLTFLoader加载解析流程

一、在你使用GLTFLoader加载gltf文件时,你会发现你只需要调用GLTFloader的load方法就可以在,回调函数中拿到Scene对象。所以当你调用GLTFLoader.load时就完成了整个加载解析过程。所以,我们从GLTFLoader.load开始了解整个GLTF文件的加载解析流程。

load源码如下:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 load( url, onLoad, onProgress, onError ) {

        const scope = this;

        let resourcePath;

        if ( this.resourcePath !== '' ) {

            resourcePath = this.resourcePath;

        } else if ( this.path !== '' ) {

            resourcePath = this.path;

        } else {

            resourcePath = LoaderUtils.extractUrlBase( url );

        }

        // Tells the LoadingManager to track an extra item, which resolves after
        // the model is fully loaded. This means the count of items loaded will
        // be incorrect, but ensures manager.onLoad() does not fire early.
        this.manager.itemStart( url );

        const _onError = function ( e ) {

            if ( onError ) {

                onError( e );

            } else {

                console.error( e );

            }

            scope.manager.itemError( url );
            scope.manager.itemEnd( url );

        };

        const loader = new FileLoader( this.manager );

        loader.setPath( this.path );
        loader.setResponseType( 'arraybuffer' );
        loader.setRequestHeader( this.requestHeader );
        loader.setWithCredentials( this.withCredentials );

        loader.load( url, function ( data ) {
            
            try {

                scope.parse( data, resourcePath, function ( gltf ) {

                    onLoad( gltf );

                    scope.manager.itemEnd( url );

                }, _onError );

            } catch ( e ) {

                _onError( e );

            }

        }, onProgress, _onError );

    }

关注这段源码的第54行,前面的代码完成了文件的网络请求获取到二进制数据放在data变量中,然后调用scope.parse就是完成二进制数据转为Scene对象的方法。关注这段源码第3行scope其实就是GLTFLoader实例自身,所以这里调用的scope.parse就是GLTFLoader类中实现的parse方法,源码如下:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 parse( data, path, onLoad, onError ) {

        let json;
        const extensions = {};
        const plugins = {};
        const textDecoder = new TextDecoder();

        if ( typeof data === 'string' ) {

            json = JSON.parse( data );

        } else if ( data instanceof ArrayBuffer ) {

            const magic = textDecoder.decode( new Uint8Array( data, 0, 4 ) );

            if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {

                try {

                    extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );

                } catch ( error ) {

                    if ( onError ) onError( error );
                    return;

                }

                json = JSON.parse( extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content );

            } else {

                json = JSON.parse( textDecoder.decode( data ) );

            }
            
        } else {

            json = data;

        }

        if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {

            if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) );
            return;

        }

        const parser = new GLTFParser( json, {

            path: path || this.resourcePath || '',
            crossOrigin: this.crossOrigin,
            requestHeader: this.requestHeader,
            manager: this.manager,
            ktx2Loader: this.ktx2Loader,
            meshoptDecoder: this.meshoptDecoder

        } );

        parser.fileLoader.setRequestHeader( this.requestHeader );

        for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) {

            const plugin = this.pluginCallbacks[ i ]( parser );
            plugins[ plugin.name ] = plugin;

            // Workaround to avoid determining as unknown extension
            // in addUnknownExtensionsToUserData().
            // Remove this workaround if we move all the existing
            // extension handlers to plugin system
            extensions[ plugin.name ] = true;

        }

        if ( json.extensionsUsed ) {

            for ( let i = 0; i < json.extensionsUsed.length; ++ i ) {

                const extensionName = json.extensionsUsed[ i ];
                const extensionsRequired = json.extensionsRequired || [];

                switch ( extensionName ) {

                    case EXTENSIONS.KHR_MATERIALS_UNLIT:
                        extensions[ extensionName ] = new GLTFMaterialsUnlitExtension();
                        break;

                    case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
                        extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader );
                        break;

                    case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
                        extensions[ extensionName ] = new GLTFTextureTransformExtension();
                        break;

                    case EXTENSIONS.KHR_MESH_QUANTIZATION:
                        extensions[ extensionName ] = new GLTFMeshQuantizationExtension();
                        break;

                    default:

                        if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) {

                            console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' );

                        }

                }

            }

        }

        parser.setExtensions( extensions );
        parser.setPlugins( plugins );
        parser.parse( onLoad, onError );

    }

这段代码的执行截止到第48行,完成了二进制流到json串的转化。下面是我使用了一个gltf模型文件进行调试时在第48行后面进行打印出的一个数据结果: image.png 如果你尝试用文本编辑器打开gltf文件,你会发现它其实就是一个json文本,下面是我在vscode中打开了一个gltf文件: image.png 关于gltf更详细的文件定义可以参看https://github.com/KhronosGroup/glTF 这里只将重要的文件定义内容展示: image.png 图片中为你描述了json结构中各个属性的含义,在后面会详细解释如何理解每个字段。 接下来让我们继续将视线回到threejs源码。拿到gltf中的json数据之后,50-60行将其传给GLTFParser的构造函数,60-116为解析过程准备需要的插件和扩展工具。最后一行调用GLTFParser.parse进行解析,源码如下:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 parse( onLoad, onError ) {

        const parser = this;
        const json = this.json;
        const extensions = this.extensions;

        // Clear the loader cache
        this.cache.removeAll();
        this.nodeCache = {};

        // Mark the special nodes/meshes in json for efficient parse
        this._invokeAll( function ( ext ) {

            return ext._markDefs && ext._markDefs();

        } );

        Promise.all( this._invokeAll( function ( ext ) {

            return ext.beforeRoot && ext.beforeRoot();

        } ) ).then( function () {

            return Promise.all( [

                parser.getDependencies( 'scene' ),
                parser.getDependencies( 'animation' ),
                parser.getDependencies( 'camera' ),

            ] );

        } ).then( function ( dependencies ) {

            const result = {
                scene: dependencies[ 0 ][ json.scene || 0 ],
                scenes: dependencies[ 0 ],
                animations: dependencies[ 1 ],
                cameras: dependencies[ 2 ],
                asset: json.asset,
                parser: parser,
                userData: {}
            };

            addUnknownExtensionsToUserData( extensions, result, json );

            assignExtrasToUserData( result, json );
            
            Promise.all( parser._invokeAll( function ( ext ) {

                return ext.afterRoot && ext.afterRoot( result );

            } ) ).then( function () {

                onLoad( result );

            } );

        } ).catch( onError );

    }

注意与前面GLTFLoader类中实现的parse方法区分,这里是GLTFParser中的parse。GLTFLoader.parse准备好json数据和需要的插件和扩展后,调用GLTFParser.parse来做最终的解析。 你可以看到这段代码的第54行,就是你在调用时传入的onLoad回调函数,层层传参至此,做最终的回调。而这里的result就是你想拿到的最终数据。 因为前面在GLTFLoader.parse中拿到gltf中的json数据之后,在那段源码的50~60行将其传给GLTFParser的构造函数,所以GLTFParser.parse这段源码第4行的const json = this.json。就是gltf中的json数据,通过传给GLTFParser的构造函数然后通过this获取到这里。

二、那么从这段源码第4行的json到第54行的result中间都经历了什么,让它从一个json最终转变成了你最终想要的场景对象了呢?

其实这个解析过程,就是你去理解这个gltf文件json格式的过程,我先为你讲解,我们应当如何理解gltf的json的一些关键字段。 1.首先是scene:是一个场景索引值,合理取值范围为scenes属性的数组长度,可以没有,默认值为0。 2.scenes字段: 一个对象数组,每一个数组项代表一个scene场景,我们会根据scene的索引值取对应的数组元素。比如这个例子: image.png 那scene索引就指向了这个name为Sketchfab_Scene,nodes索引为0的这个场景。 3.nodes字段: nodes字段同样是一个数组,里面放置的就是在上面的举例中,所要解析的scene中nodes索引为0,我们要取得就是该数组得第0个元素。 image.png 如你所见,这个字段就是一个对象数组,但是数组中可能包含得对象包括children, mesh, camera,skin等等不同的对象。mesh, camera,skin就是最终要渲染得内容,当然一般一个场景必然包含mesh,但未必有camera,skin。如果解析到的是这些对象,直接转去这些json字段中去取即可。但是children对象则需要根据children属性中的索引值,继续在nodes属性中去深度递归,查找替换。如这个例子,我们取得是第一个nodes节点,那么他是一个children,那就意味着这个节点有一个子节点,该子节点是nodes属性数组索引为1得节点(注意这里得1是编程中的索引1,也即数组得第二个元素)。然后按照树得深度递归去查找直到,子节点均为直接渲染属性如mesh。 4.meshs字段: 按照第3步中的递归解析,最终解析到直接渲染元素如mesh,可以参照3中图示的那条mesh数据,它索引值为0,也即这个要渲染的mesh指向了meshs字段的第一个元素: image.png 我们都知道,mesh主要应当包含的就是顶点数据,和材质。这个元素,attributes就描述了顶点数据,material就描述了材质信息。依旧按上述索引逻辑在相应的字段去找。这里可能要说明一下的就是,attributes属性的POSITION属性就是这个mesh顶点信息的索引值,该值对应accessors字段。 5.accessors字段: 在上例中POSITION属性值为0,则指向accessors字段的0号索引数据。 image.png

bufferView json中bufferView字段的索引
componentType 数据的类型,如:5120表示byte类型;5121表示ubyte类型;5122表示short类型;5123表示ushort类型;5124表示int类型;5125表示uint类型;5126表示float类型;5130表示double类型。
count 数据的个数
type 表示每个元素的数据类型。它可以是以下值之一:
  • "SCALAR":表示每个元素是标量(一个值)。
  • "VEC2":表示每个元素是二维矢量。
  • "VEC3":表示每个元素是三维矢量。
  • "VEC4":表示每个元素是四维矢量。
  • "MAT2":表示每个元素是2x2矩阵。
  • "MAT3":表示每个元素是3x3矩阵。
  • "MAT4":表示每个元素是4x4矩阵。 | | max | 指定每个分量的最大值。它是一个数字数组,其长度取决于type属性 | | min | 指定每个分量的最小值。它是一个数字数组,其长度取决于type属性 |

6.bufferview字段: image.png buffer属性为json的buffer字段的索引 7.buffer字段: image.png uri就指向了最终顶点数据,以二进制形式存放在了scene.bin文件中。当然,也可以直接在里存放所有的二进制数据。

三、threejs源码又是如何执行这个流程的呢?

再把视线挪回GLTFParser.parse的源码,注意这几行源码: image.png 从函数名和传参你大概可以猜出,parser.getDependencies( 'scene' )就是递归解析场景数据的方法。沿着 GLTFParse.getDependencies->GLTFParse.getDependency的调用链你可以看到GLTFParse.getDependency源码实现如下:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 getDependency( type, index ) {

        const cacheKey = type + ':' + index;
        let dependency = this.cache.get( cacheKey );

        if ( ! dependency ) {

            switch ( type ) {

                case 'scene':
                    dependency = this.loadScene( index );
                    break;

                case 'node':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadNode && ext.loadNode( index );

                    } );
                    break;

                case 'mesh':
                    dependency = this._invokeOne( function ( ext ) {
                        
                        return ext.loadMesh && ext.loadMesh( index );

                    } );
                    break;

                case 'accessor':
                    dependency = this.loadAccessor( index );
                    break;

                case 'bufferView':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadBufferView && ext.loadBufferView( index );

                    } );
                    break;

                case 'buffer':
                    dependency = this.loadBuffer( index );
                    break;

                case 'material':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadMaterial && ext.loadMaterial( index );

                    } );
                    break;

                case 'texture':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadTexture && ext.loadTexture( index );

                    } );
                    break;

                case 'skin':
                    dependency = this.loadSkin( index );
                    break;

                case 'animation':
                    dependency = this._invokeOne( function ( ext ) {

                        return ext.loadAnimation && ext.loadAnimation( index );

                    } );
                    break;

                case 'camera':
                    dependency = this.loadCamera( index );
                    break;

                default:
                    dependency = this._invokeOne( function ( ext ) {

                        return ext != this && ext.getDependency && ext.getDependency( type, index );

                    } );

                    if ( ! dependency ) {

                        throw new Error( 'Unknown type: ' + type );

                    }

                    break;

            }

            this.cache.add( cacheKey, dependency );

        }

        return dependency;

    }

现在,有了前面对gltf文件的json格式讲解,switch分支内的每个case分支是什么含义你应该十分清楚了吧。最开始传参是scene,也就是解析场景,然后就产生了以下调用链: GLTFParse.getDependency('scene')->GLTFParse.loadScene()->GLTFParse.getDependency('node') ->GLTFParse.loadNode()->...(node递归)...->GLTFParse.getDependency('mesh') ->GLTFParse.loadGeometries->addPrimitiveAttributes->GLTFParser.getDependency( 'accessor') ->GLTFParser.loadAccessor()->GLTFParser.getDependency( 'bufferView')->GLTFParser.loadBufferView() ->GLTFParser.getDependency( 'buffer')->GLTFParser.loadBuffer() 这样层层调用最终读取到二进制顶点数据。并将其组装在mesh对象中,然后所有mesh对象按照gltf的层级结构组成整个场景对象。mesh对象中的geometry属性有很多的buffertrribute,代表了这些数据缓冲区,如position为顶点缓冲区,uv为纹理映射坐标缓冲区,color为片段着色器颜色缓冲区等等。而loadMatarial则将uv缓冲区放进了geometry中,以及在material中存放了纹理资源。