ThreeJs學(xué)習(xí)筆記——ObjLoader加載以及渲染分析

一、前言

這篇文章主要學(xué)習(xí) ThreeJs 中的 demo loader/obj2,主要是分析一下 obj 是如何加載的,紋理以及材質(zhì)是如何加載的,3d camera 以及 camera controller 這些是如何實現(xiàn)的等。那么,先來 2 個 gif 圖震撼一下吧。

objloader2-拖動
objloader2-放大.gif

二、代碼分析

1.html 部分

<div id="glFullscreen">
    <!-- 渲染 3D 場景的 canvas -->
    <canvas id="example"></canvas>
</div>
<!-- dat gui 的 div 占位-->
<div id="dat">

</div>
<!--three.js 的其他一些信息說明-->
<div id="info">
    <a  target="_blank" rel="noopener">three.js</a> - OBJLoader2 direct loader test
    <div id="feedback"></div>
</div>

這一部分最重要的就是這個 <canvas></canvas> 標(biāo)記的添加,也說明了 WebGL 的主要實現(xiàn)就去用這個 canvas 去繪制。這和 Android 端上的原生 API 很像嘛。

2.script 導(dǎo)入

<!-- 導(dǎo)入 threejs 核心庫 -->
<script src="../build/three.js"></script>
<!-- 導(dǎo)入 camera controller,用于響應(yīng)鼠標(biāo)/手指的拖動,放大,旋轉(zhuǎn)等操作 -->
<script src="js/controls/TrackballControls.js"></script>
<!-- 材質(zhì)加載 -->
<script src="js/loaders/MTLLoader.js"></script>
<!-- 三方庫 dat gui 庫的導(dǎo)入-->
<script src="js/libs/dat.gui.min.js"></script>
<!-- 三方庫 stats 的導(dǎo)入-->
<script type="text/javascript" src="js/libs/stats.min.js"></script>
<!-- 構(gòu)建 mesh,texture 等支持 -->
<script src="js/loaders/LoaderSupport.js"></script>
<!-- 加載 obj 的主要實現(xiàn) -->
<script src="js/loaders/OBJLoader2.js"></script>

3.模型加載

objloader2時序圖.jpg

3.1 定義OBJLoader2Example

ThreeJS 學(xué)習(xí)筆記——JavaScript 中的函數(shù)與對象中了解到,JavaScript 中是通過原型(prototype)來實現(xiàn)面向?qū)ο缶幊獭_@里先定義了函數(shù) OBJLoader2Example(),然后再指定OBJLoader2Example的 prototype 的 constructor 為 OBJLoader2Example() 本身,這也就定義了一個 “類” OBJLoader2Example,我們可以使用這個類來聲明新的對象。

var OBJLoader2Example = function ( elementToBindTo ) {......};
OBJLoader2Example.prototype = {
    constructor: OBJLoader2Example,
        initGL: function () {......},
        initContent: function () {......},
        _reportProgress: function () {......},
        resizeDisplayGL: function () {......},
        recalcAspectRatio: function () {......},
        resetCamera: function () {......},
        updateCamera: function () {......},
        render: function () {......}
}

3.2 OBJLoader2Example 的構(gòu)造方法

var OBJLoader2Example = function ( elementToBindTo ) {
                // 渲染器,后面它會綁定 canvas 節(jié)點
                this.renderer = null;
                // canvas 節(jié)點
                this.canvas = elementToBindTo;
                // 視圖比例
                this.aspectRatio = 1;
                this.recalcAspectRatio();
                // 3D 場景
                this.scene = null;
                // 默認(rèn)相機(jī)參數(shù)
                this.cameraDefaults = {
                    // 相機(jī)的位置,就是相機(jī)該擺在哪里
                    posCamera: new THREE.Vector3( 0.0, 175.0, 500.0 ),
                    // 相機(jī)的目標(biāo)
                    posCameraTarget: new THREE.Vector3( 0, 0, 0 ),
                    // 近截面
                    near: 0.1,
                    // 遠(yuǎn)截面
                    far: 10000,
                    // 視景體夾角
                    fov: 45
                };
                // 3D 相機(jī)
                this.camera = null;
                // 3D 相機(jī)的目標(biāo),就是相機(jī)該盯著哪里看
                this.cameraTarget = this.cameraDefaults.posCameraTarget;
                // 3D 相機(jī)控制器,當(dāng)然也可理解就是一個手勢控制器
                this.controls = null;
            };

構(gòu)造方法主要是屬性的定義,代碼中添加了注釋簡要介紹了各個屬性的作用,總體來說就是3D場景,3D 相機(jī),相機(jī)控制器以及最重要的渲染器,渲染器綁定了 canvas,3D 場景及其所有的物件都會通過這個渲染器渲染到 canvas 中去。

3.3 initGL()

initGL: function () {
                    // 創(chuàng)建渲染器
                    this.renderer = new THREE.WebGLRenderer( {
                        // 綁定 canvas
                        canvas: this.canvas,
                        // 抗鋸齒
                        antialias: true,
                        autoClear: true
                    } );
                    this.renderer.setClearColor( 0x050505 );

                    this.scene = new THREE.Scene();
                    // 初始化透視投影相機(jī),這是一個三角的景錐體,物體在其里面呈現(xiàn)的效果是近大遠(yuǎn)小
                    this.camera = new THREE.PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
                    this.resetCamera();
                    // 初始化 controller
                    this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );

                    // 添加環(huán)境光與平行光
                    var ambientLight = new THREE.AmbientLight( 0x404040 );
                    var directionalLight1 = new THREE.DirectionalLight( 0xC0C090 );
                    var directionalLight2 = new THREE.DirectionalLight( 0xC0C090 );

                    directionalLight1.position.set( -100, -50, 100 );
                    directionalLight2.position.set( 100, 50, -100 );

                    this.scene.add( directionalLight1 );
                    this.scene.add( directionalLight2 );
                    this.scene.add( ambientLight );
                    // 添加調(diào)試網(wǎng)格
                    var helper = new THREE.GridHelper( 1200, 60, 0xFF4444, 0x404040 );
                    this.scene.add( helper );
                },

initGL() 方法中初始化了各個屬性,同時還添加了環(huán)境光與平行光源,以用于調(diào)試的網(wǎng)格幫助模型。在 3D 場景中很多物體都可看成是一個模型,如這里的光源。而 camera 在有一些渲染框架中也會被認(rèn)為是一個模型,但其只是一個用于參與 3D 渲染時的參數(shù)。camera 最主要的作用是決定了投影矩陣,在投影矩陣內(nèi)的物體可見,而不在里面則不可見。

4. initContent()

initContent: function () {
                    var modelName = 'female02';
                    this._reportProgress( { detail: { text: 'Loading: ' + modelName } } );

                    var scope = this;
                    // 聲明 ObjLoader2 對象
                    var objLoader = new THREE.OBJLoader2();
                    // 模型加載完成的 call back,加載完成后便會把模型加載到場景中
                    var callbackOnLoad = function ( event ) {
                        scope.scene.add( event.detail.loaderRootNode );
                        console.log( 'Loading complete: ' + event.detail.modelName );
                        scope._reportProgress( { detail: { text: '' } } );
                    };
                    // 材質(zhì)加載完成的回調(diào),材質(zhì)加載完成后便會進(jìn)一步加 obj
                    var onLoadMtl = function ( materials ) {
                        objLoader.setModelName( modelName );
                        objLoader.setMaterials( materials );
                        objLoader.setLogging( true, true );
                        // 開始加載 obj
                        objLoader.load( 'models/obj/female02/female02.obj', callbackOnLoad, null, null, null, false );
                    };
                    // 開始加載材質(zhì)
                    objLoader.loadMtl( 'models/obj/female02/female02.mtl', null, onLoadMtl );
                },

內(nèi)容加載這一塊是重點,其主要是通過 ObjLoader2 先是加載了材質(zhì)然后加載模型。關(guān)于 obj 和 mtl 文件, 請打開 female02.obj 和 female02.mtl,可以發(fā)現(xiàn)它就是一個文本文件,通過注釋來感受一下其文件格式如何。

female02.obj部分?jǐn)?shù)據(jù)

# Blender v2.54 (sub 0) OBJ File: ''
# www.blender.org
# obj對應(yīng)的材質(zhì)文件
mtllib female02.mtl
# o 對象名稱(Object name)
o mesh1.002_mesh1-geometry
# 頂點
v 15.257854 104.640892 8.680023
v 14.044281 104.444138 11.718708
v 15.763498 98.955704 11.529579
......
# 紋理坐標(biāo)
vt 0.389887 0.679023
vt 0.361250 0.679023
vt 0.361250 0.643346
......
# 頂點法線
vn 0.945372 0.300211 0.126926
vn 0.794275 0.212683 0.569079
vn 0.792047 0.184729 0.581805
......
# group
g mesh1.002_mesh1-geometry__03_-_Default1noCulli__03_-_Default1noCulli
# 當(dāng)前圖元所用材質(zhì)
usemtl _03_-_Default1noCulli__03_-_Default1noCulli
s off
# v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3(索引起始于1)    
f 1/1/1 2/2/2 3/3/3
f 1/1/1 4/4/4 2/2/2
f 4/4/4 1/1/1 5/5/5
......

female02.mtl部分?jǐn)?shù)據(jù)

......
# 定義一個名為 _03_-_Default1noCulli__03_-_Default1noCulli 的材質(zhì)
newmtl _03_-_Default1noCulli__03_-_Default1noCulli
# 反射指數(shù) 定義了反射高光度。該值越高則高光越密集,一般取值范圍在0~1000。
Ns 154.901961
# 材質(zhì)的環(huán)境光(ambient color)
Ka 0.000000 0.000000 0.000000
# 散射光(diffuse color)用Kd
Kd 0.640000 0.640000 0.640000
# 鏡面光(specular color)用Ks
Ks 0.165000 0.165000 0.165000
# 折射值 可在0.001到10之間進(jìn)行取值。若取值為1.0,光在通過物體的時候不發(fā)生彎曲。玻璃的折射率為1.5。
Ni 1.000000
# 漸隱指數(shù)描述 參數(shù)factor表示物體融入背景的數(shù)量,取值范圍為0.0~1.0,取值為1.0表示完全不透明,取值為0.0時表示完全透明。
d 1.000000
# 指定材質(zhì)的光照模型。illum后面可接0~10范圍內(nèi)的數(shù)字參數(shù)。各個參數(shù)代表不同的光照模型
illum 2
# 為漫反射指定顏色紋理文件
map_Kd 03_-_Default1noCulling.JPG
......

關(guān)于 obj 和 mtl 文件中各個字段的意思都在注釋中有說明了,至于每個字段參數(shù)如何使用,就需要對 OpenGL 如何渲染模型有一定的了解了。繼續(xù)來看材質(zhì)的加載和obj 的加載。

4.1 ObjectLoader2#loadMtl()

loadMtl: function ( url, content, onLoad, onProgress, onError, crossOrigin, materialOptions ) {
        ......
        this._loadMtl( resource, onLoad, onProgress, onError, crossOrigin, materialOptions );
},

調(diào)用了內(nèi)部的_loadMtl(),_loadMtl() 函數(shù)的實現(xiàn)代碼是有點多的,不過不要緊,我給做了精簡。

_loadMtl: function ( resource, onLoad, onProgress, onError, crossOrigin, materialOptions ) {
    ......
    // 7. 創(chuàng)建了 materialCreator 后,就會加載到這里。這里最后通過 onLoad 通知給調(diào)用者,調(diào)用者繼續(xù)加載模型。
    var processMaterials = function ( materialCreator ) {
        ......
        // 8.創(chuàng)建材質(zhì)
        materialCreator.preload();
       // 9.回調(diào)給調(diào)用者
        onLoad( materials, materialCreator );
    }
    ......
    // 1. 構(gòu)建一個 MTLLoader
    var mtlLoader = new THREE.MTLLoader( this.manager );
   // 4.文件加載成功后回調(diào)到 parseTextWithMtlLoader 這里
    var parseTextWithMtlLoader = function ( content ) {
        ......
        contentAsText = THREE.LoaderUtils.decodeText( content );
        ......
        // 5.對文件內(nèi)容進(jìn)行解析,解析完成后得到一個 materialCreator 對象,然后再調(diào)用 processMaterials
        processMaterials( mtlLoader.parse( contentAsText ) );
    }
    ......
    // 2.構(gòu)建一個 FileLoader
    var fileLoader = new THREE.FileLoader( this.manager );
    ......
   // 3. 加載文件,文件加載成功能后回調(diào) parseTextWithMtlLoader
    fileLoader.load( resource.url, parseTextWithMtlLoader, onProgress, onError );
}

注釋里包含了材質(zhì)加載的整個邏輯,一共 9 個步驟,但這里重點只需要關(guān)注以下 3 個步驟:

(1)文件加載——FileLoader#load()

load: function ( url, onLoad, onProgress, onError ) {
    ......
    var request = new XMLHttpRequest();
    request.open( 'GET', url, true );
    ......
}

FileLoader 是 ThreeJs 庫中的代碼,關(guān)于 load() 方法中的前后代碼這里都略去了,重點是知道了它是通過 Get 請求來獲取的。

(2)文件parse——MTLLoader#parse()

parse: function ( text, path ) {

        var lines = text.split( '\n' );
        var info = {};
        var delimiter_pattern = /\s+/;
        var materialsInfo = {};

        for ( var i = 0; i < lines.length; i ++ ) {

            var line = lines[ i ];
            line = line.trim();

            if ( line.length === 0 || line.charAt( 0 ) === '#' ) {

                // Blank line or comment ignore
                continue;

            }

            var pos = line.indexOf( ' ' );

            var key = ( pos >= 0 ) ? line.substring( 0, pos ) : line;
            key = key.toLowerCase();

            var value = ( pos >= 0 ) ? line.substring( pos + 1 ) : '';
            value = value.trim();

            if ( key === 'newmtl' ) {

                // New material

                info = { name: value };
                materialsInfo[ value ] = info;

            } else {

                if ( key === 'ka' || key === 'kd' || key === 'ks' ) {

                    var ss = value.split( delimiter_pattern, 3 );
                    info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ];

                } else {

                    info[ key ] = value;

                }

            }

        }

        var materialCreator = new THREE.MTLLoader.MaterialCreator( this.resourcePath || path, this.materialOptions );
        materialCreator.setCrossOrigin( this.crossOrigin );
        materialCreator.setManager( this.manager );
        materialCreator.setMaterials( materialsInfo );
        return materialCreator;

    }

parse() 方法的代碼看起來有點多,但其實很簡單,就是對著 mtl 文件一行一行的解析。這里的重點是創(chuàng)建了 MaterialCreator并且保存在了 materialsInfo 中。materialsInfo 是一個 map 對象,其中保存的值最重要的是包括了 map_Kd,這個在創(chuàng)建材質(zhì)時要加載的紋理。

(3)創(chuàng)建材質(zhì)——MaterialCreator#preload()

preload: function () {
        for ( var mn in this.materialsInfo ) {
            this.create( mn );
        }
    },

preload() 中就遍歷每一個 material 然后分別調(diào)用 create() 。而 create() 又是進(jìn)一步調(diào)用了 createMaterial_() 方法。

createMaterial_: function ( materialName ) {
        // Create material
        var scope = this;
        var mat = this.materialsInfo[ materialName ];
        var params = {
            name: materialName,
            side: this.side
        };
        function resolveURL( baseUrl, url ) {
            if ( typeof url !== 'string' || url === '' )
                return '';
            // Absolute URL
            if ( /^https?:\/\//i.test( url ) ) return url;
            return baseUrl + url;
        }
        function setMapForType( mapType, value ) {
            if ( params[ mapType ] ) return; // Keep the first encountered texture
            var texParams = scope.getTextureParams( value, params );
            var map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) );
            map.repeat.copy( texParams.scale );
            map.offset.copy( texParams.offset );
            map.wrapS = scope.wrap;
            map.wrapT = scope.wrap;
            params[ mapType ] = map;
        }
        for ( var prop in mat ) {
            var value = mat[ prop ];
            var n;
            if ( value === '' ) continue;
            switch ( prop.toLowerCase() ) {
                // Ns is material specular exponent
                case 'kd':
                    // Diffuse color (color under white light) using RGB values
                    params.color = new THREE.Color().fromArray( value );
                    break;
                case 'ks':
                    // Specular color (color when light is reflected from shiny surface) using RGB values
                    params.specular = new THREE.Color().fromArray( value );
                    break;
                case 'map_kd':
                    // Diffuse texture map
                    setMapForType( "map", value );
                    break;
                case 'map_ks':
                    // Specular map
                    setMapForType( "specularMap", value );
                    break;
                case 'norm':
                    setMapForType( "normalMap", value );
                    break;
                case 'map_bump':
                case 'bump':
                    // Bump texture map
                    setMapForType( "bumpMap", value );
                    break;
                case 'map_d':
                    // Alpha map
                    setMapForType( "alphaMap", value );
                    params.transparent = true;
                    break;
                case 'ns':
                    // The specular exponent (defines the focus of the specular highlight)
                    // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
                    params.shininess = parseFloat( value );
                    break;
                case 'd':
                    n = parseFloat( value );
                    if ( n < 1 ) {
                        params.opacity = n;
                        params.transparent = true;
                    }
                    break;
                case 'tr':
                    n = parseFloat( value );
                    if ( this.options && this.options.invertTrProperty ) n = 1 - n;
                    if ( n > 0 ) {
                        params.opacity = 1 - n;
                        params.transparent = true;
                    }
                    break;
                default:
                    break;
            }
        }
        this.materials[ materialName ] = new THREE.MeshPhongMaterial( params );
        return this.materials[ materialName ];
    },

這里就是告知我們該怎么用 mtl 文件中的每個字段了,這里主要關(guān)注一下紋理圖片是如何加載的,其他的字段參數(shù)再看看 mtl 的注釋就可以理解了。map-kd、map_ks、norm、map_bump、bump 以及 map_d 的處理是調(diào)用了setMapForType(),他們都是去加載紋理的,只是紋理的形式不一樣。

function setMapForType( mapType, value ) {
            ......
            var map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) );
            ......
}

這里的 loadTexture() 就是加載紋理的實現(xiàn),一般來說在材質(zhì)文件中對紋理的地址要寫成相對的,這里會根據(jù)材質(zhì)的地址的 base url 來 resolve 出一個紋理的地址。繼續(xù)來看loadTexture()。

loadTexture: function ( url, mapping, onLoad, onProgress, onError ) {
    ......
    var loader = THREE.Loader.Handlers.get( url );
    ......
    loader = new THREE.TextureLoader( manager );
   ......
    texture = loader.load( url, onLoad, onProgress, onError );
    return texture;
}

其主要是構(gòu)建一個 TextureLoader,然后調(diào)用其 load() 進(jìn)行加載。

load: function ( url, onLoad, onProgress, onError ) {
  ......
  var loader = new ImageLoader( this.manager );
  ......
  loader.load( url, function ( image ) {}
}

又進(jìn)一步通過了 ImageLoader 來加載。

load: function ( url, onLoad, onProgress, onError ) {
  ......
  var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );
......
image.src = url;
return image;
}

原來圖片的加載是通過創(chuàng)建一個 <img> 標(biāo)記來加載的。創(chuàng)建一個 <img> 標(biāo)記,在不添加到 dom 樹中的情況下,只要給 src 賦了值,就會去下載圖片了。

到這里,終于把材質(zhì)以及紋理的加載分析完了。接下來繼續(xù)分析 obj 的加載。

4.2ObjLoader2#load()

    load: function ( url, onLoad, onProgress, onError, onMeshAlter, useAsync ) {
        var resource = new THREE.LoaderSupport.ResourceDescriptor( url, 'OBJ' );
        this._loadObj( resource, onLoad, onProgress, onError, onMeshAlter, useAsync );
    },

同樣是進(jìn)一步的調(diào)用,這里調(diào)用的是 _loadObj()。

_loadObj: function ( resource, onLoad, onProgress, onError, onMeshAlter, useAsync ) {
    ......
    var fileLoaderOnLoad = function ( content ) {
       ......
       ......
       // 3.解析 obj
       loaderRootNode: scope.parse( content ),
       ......
    },
    // 1.構(gòu)建 FileLoader
    var fileLoader = new THREE.FileLoader( this.manager );
    ......
    // 2.加載文件,這里在加載 mtl 的時候已經(jīng)分析過了,并且最后會回調(diào)到 fileLoaderOnLoad
    fileLoader.load( resource.name, fileLoaderOnLoad, onProgress, onError );
}

_loadObj() 的代碼這里也精簡了一下,并在注釋中說明了邏輯。文件加載已經(jīng)在前面分析過了,這里就關(guān)注一下解析 obj。

/**
* Parses OBJ data synchronously from arraybuffer or string.
*
* @param {arraybuffer|string} content OBJ data as Uint8Array or String
*/
parse: function ( content ) {
    ......
    // 1.初始化 meshBuilder
    this.meshBuilder.init(); 
    // 2.創(chuàng)建一個 Parser
    var parser = new THREE.OBJLoader2.Parser();
    ......
    var onMeshLoaded = function ( payload ) {
        // 4.從 meshBuilder 中獲取 mesh ,并把 mesh 都加到節(jié)點中
        var meshes = scope.meshBuilder.processPayload( payload );
        var mesh;
        for ( var i in meshes ) {
            mesh = meshes[ i ];
            scope.loaderRootNode.add( mesh );
        }
    } 
   ......
   // 3.解析文本,因為這里傳輸?shù)木褪俏谋?   parser.parseText( content );
   ......
}

這里的重點是parseText()。

parseText: function ( text ) {
    ......
    for ( var char, word = '', bufferPointer = 0, slashesCount = 0, i = 0; i < length; i++ ) {
          ......
          this.processLine( buffer, bufferPointer, slashesCount );
          ......
    }
    ......
}

同樣,省略的部分這里可以先不看,來看一看具體解析 obj 文件的 processLine()。

     processLine: function ( buffer, bufferPointer, slashesCount ) {
        if ( bufferPointer < 1 ) return;

        var reconstructString = function ( content, legacyMode, start, stop ) {
            var line = '';
            if ( stop > start ) {

                var i;
                if ( legacyMode ) {

                    for ( i = start; i < stop; i++ ) line += content[ i ];

                } else {


                    for ( i = start; i < stop; i++ ) line += String.fromCharCode( content[ i ] );

                }
                line = line.trim();

            }
            return line;
        };

        var bufferLength, length, i, lineDesignation;
        lineDesignation = buffer [ 0 ];
        switch ( lineDesignation ) {
            case 'v':
                this.vertices.push( parseFloat( buffer[ 1 ] ) );
                this.vertices.push( parseFloat( buffer[ 2 ] ) );
                this.vertices.push( parseFloat( buffer[ 3 ] ) );
                if ( bufferPointer > 4 ) {

                    this.colors.push( parseFloat( buffer[ 4 ] ) );
                    this.colors.push( parseFloat( buffer[ 5 ] ) );
                    this.colors.push( parseFloat( buffer[ 6 ] ) );

                }
                break;

            case 'vt':
                this.uvs.push( parseFloat( buffer[ 1 ] ) );
                this.uvs.push( parseFloat( buffer[ 2 ] ) );
                break;

            case 'vn':
                this.normals.push( parseFloat( buffer[ 1 ] ) );
                this.normals.push( parseFloat( buffer[ 2 ] ) );
                this.normals.push( parseFloat( buffer[ 3 ] ) );
                break;

            case 'f':
                bufferLength = bufferPointer - 1;

                // "f vertex ..."
                if ( slashesCount === 0 ) {

                    this.checkFaceType( 0 );
                    for ( i = 2, length = bufferLength; i < length; i ++ ) {

                        this.buildFace( buffer[ 1 ] );
                        this.buildFace( buffer[ i ] );
                        this.buildFace( buffer[ i + 1 ] );

                    }

                    // "f vertex/uv ..."
                } else if  ( bufferLength === slashesCount * 2 ) {

                    this.checkFaceType( 1 );
                    for ( i = 3, length = bufferLength - 2; i < length; i += 2 ) {

                        this.buildFace( buffer[ 1 ], buffer[ 2 ] );
                        this.buildFace( buffer[ i ], buffer[ i + 1 ] );
                        this.buildFace( buffer[ i + 2 ], buffer[ i + 3 ] );

                    }

                    // "f vertex/uv/normal ..."
                } else if  ( bufferLength * 2 === slashesCount * 3 ) {

                    this.checkFaceType( 2 );
                    for ( i = 4, length = bufferLength - 3; i < length; i += 3 ) {

                        this.buildFace( buffer[ 1 ], buffer[ 2 ], buffer[ 3 ] );
                        this.buildFace( buffer[ i ], buffer[ i + 1 ], buffer[ i + 2 ] );
                        this.buildFace( buffer[ i + 3 ], buffer[ i + 4 ], buffer[ i + 5 ] );

                    }

                    // "f vertex//normal ..."
                } else {

                    this.checkFaceType( 3 );
                    for ( i = 3, length = bufferLength - 2; i < length; i += 2 ) {

                        this.buildFace( buffer[ 1 ], undefined, buffer[ 2 ] );
                        this.buildFace( buffer[ i ], undefined, buffer[ i + 1 ] );
                        this.buildFace( buffer[ i + 2 ], undefined, buffer[ i + 3 ] );

                    }

                }
                break;

            case 'l':
            case 'p':
                bufferLength = bufferPointer - 1;
                if ( bufferLength === slashesCount * 2 )  {

                    this.checkFaceType( 4 );
                    for ( i = 1, length = bufferLength + 1; i < length; i += 2 ) this.buildFace( buffer[ i ], buffer[ i + 1 ] );

                } else {

                    this.checkFaceType( ( lineDesignation === 'l' ) ? 5 : 6  );
                    for ( i = 1, length = bufferLength + 1; i < length; i ++ ) this.buildFace( buffer[ i ] );

                }
                break;

            case 's':
                this.pushSmoothingGroup( buffer[ 1 ] );
                break;

            case 'g':
                // 'g' leads to creation of mesh if valid data (faces declaration was done before), otherwise only groupName gets set
                this.processCompletedMesh();
                this.rawMesh.groupName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 2, this.globalCounts.currentByte );
                break;

            case 'o':
                // 'o' is meta-information and usually does not result in creation of new meshes, but can be enforced with "useOAsMesh"
                if ( this.useOAsMesh ) this.processCompletedMesh();
                this.rawMesh.objectName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 2, this.globalCounts.currentByte );
                break;

            case 'mtllib':
                this.rawMesh.mtllibName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 7, this.globalCounts.currentByte );
                break;

            case 'usemtl':
                var mtlName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 7, this.globalCounts.currentByte );
                if ( mtlName !== '' && this.rawMesh.activeMtlName !== mtlName ) {

                    this.rawMesh.activeMtlName = mtlName;
                    this.rawMesh.counts.mtlCount++;
                    this.checkSubGroup();

                }
                break;

            default:
                break;
        }
    },

這段代碼就比較長了,有 150 多行,但內(nèi)容其實很簡單,就是根據(jù) obj 的文件格式進(jìn)行解析。如果看到這里忘記了 obj 的文件格式,那建議先回顧一下。解析的過程已經(jīng)非常細(xì)節(jié)了,就不詳細(xì)展開了。這里最后的解析結(jié)果就是頂點,紋理坐標(biāo)以及法向根據(jù) face 的索引進(jìn)行展開,得到的結(jié)果是 vvv | vtvt | vnvnvn 這樣的 n 組頂點數(shù)組 以及 n 組索引數(shù)組。頂點數(shù)組,索引數(shù)組以及材質(zhì)/紋理構(gòu)成了用于渲染的3D網(wǎng)格 mesh。

到這里 obj 的加載也分析完了。obj 的加載是主體,但也是最簡單的。容易出問題的是在材質(zhì)和紋理的加載上,需要注意的問題比較多。

5.render()

var render = function () {
    requestAnimationFrame( render );
    app.render();
};

這個 render 是一個函數(shù),不是OBJLoader2Example 的方法,是在 <script></script> 里面的。其首先請求了動畫刷新回調(diào),使得其可以監(jiān)聽到瀏覽器的刷新。刷新時把回調(diào)函數(shù)設(shè)為自己,使得瀏覽器在不斷刷新的過程中調(diào)用 render() 函數(shù)。然后才是調(diào)用 OBJLoader2Example 的 render() 方法進(jìn)行 3D 場景的繪制。這里簡單的看一下 MDN 對 requestAnimationFrame 的描述。

window.requestAnimationFrame() 方法告訴瀏覽器您希望執(zhí)行動畫并請求瀏覽器在下一次重繪之前調(diào)用指定的函數(shù)來更新動畫。該方法使用一個回調(diào)函數(shù)作為參數(shù),這個回調(diào)函數(shù)會在瀏覽器重繪之前調(diào)用。
當(dāng)你需要更新屏幕畫面時就可以調(diào)用此方法。在瀏覽器下次重繪前執(zhí)行回調(diào)函數(shù)。回調(diào)的次數(shù)通常是每秒60次,但大多數(shù)瀏覽器通常匹配 W3C 所建議的刷新頻率。

看到加粗的字體了嗎,這和端的刷新頻率是一樣的,即 60 fps。然后再來簡單分析下 OBJLoader2Example 的 render() 方法。

render: function () {
    if ( ! this.renderer.autoClear ) this.renderer.clear();
    this.controls.update();
    this.renderer.render( this.scene, this.camera );
}

可以看到這里主要就是通過 WebGLRenderer 進(jìn)行實際的渲染,那這里再進(jìn)一步分析就到 OpenGL 了。關(guān)于 OpenGL 是一個比較大的課題,就不在這里分析了,也不合適。

三、后記

文章主要分析了 ThreeJs 是如何加載一個 Obj 模型并將其渲染出來的過程,分析的過程很長,但實際并不復(fù)雜,并不涉及到什么難理解的概念。分析前由于 JavaScript 的水平實在有限,所以還特定去補(bǔ)了一刀《ThreeJS 學(xué)習(xí)筆記——JavaScript 中的函數(shù)與對象》。在比較深入的理解了函數(shù)與對象之后,再加上基本的 OpenGL 基礎(chǔ),一步一步的分析這個加載的過程其實還是比較輕松的。

最后,感謝你能讀到并讀完此文章。希望我簡陋的分析以及分享對你有所幫助,同時也請幫忙點個贊,鼓勵我繼續(xù)分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容