ThreeJs學習筆記——ObjLoader載入以及渲染分析
一、前言
這篇文章主要學習 ThreeJs 中的 demoofollow,noindex">loader/obj2 ,主要是分析一下 obj 是如何載入的,紋理以及材質是如何載入的,3d camera 以及 camera controller 這些是如何實現的等。那麼,先來 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 href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - OBJLoader2 direct loader test <div id="feedback"></div> </div>
這一部分最重要的就是這個 <canvas></canvas> 標記的新增,也說明了 WebGL 的主要實現就去用這個 canvas 去繪製。這和 Android 端上的原生 API 很像嘛。
2.script 匯入
<!-- 匯入 threejs 核心庫 --> <script src="../build/three.js"></script> <!-- 匯入 camera controller,用於響應滑鼠/手指的拖動,放大,旋轉等操作 --> <script src="js/controls/TrackballControls.js"></script> <!-- 材質載入 --> <script src="js/loaders/MTLLoader.js"></script> <!-- 三方庫 dat gui 庫的匯入--> <script src="js/libs/dat.gui.min.js"></script> <!-- 三方庫 stats 的匯入--> <script type="text/javascript" src="js/libs/stats.min.js"></script> <!-- 構建 mesh,texture 等支援 --> <script src="js/loaders/LoaderSupport.js"></script> <!-- 載入 obj 的主要實現 --> <script src="js/loaders/OBJLoader2.js"></script>
3.模型載入

objloader2時序圖.jpg
3.1 定義OBJLoader2Example
在ThreeJS 學習筆記——JavaScript/">JavaScript 中的函式與物件 中瞭解到,JavaScript 中是通過原型(prototype)來實現面向物件程式設計。這裡先定義了函式 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 的構造方法
var OBJLoader2Example = function ( elementToBindTo ) { // 渲染器,後面它會繫結 canvas 節點 this.renderer = null; // canvas 節點 this.canvas = elementToBindTo; // 檢視比例 this.aspectRatio = 1; this.recalcAspectRatio(); // 3D 場景 this.scene = null; // 預設相機引數 this.cameraDefaults = { // 相機的位置,就是相機該擺在哪裡 posCamera: new THREE.Vector3( 0.0, 175.0, 500.0 ), // 相機的目標 posCameraTarget: new THREE.Vector3( 0, 0, 0 ), // 近截面 near: 0.1, // 遠截面 far: 10000, // 視景體夾角 fov: 45 }; // 3D 相機 this.camera = null; // 3D 相機的目標,就是相機該盯著哪裡看 this.cameraTarget = this.cameraDefaults.posCameraTarget; // 3D 相機控制器,當然也可理解就是一個手勢控制器 this.controls = null; };
構造方法主要是屬性的定義,程式碼中添加了註釋簡要介紹了各個屬性的作用,總體來說就是3D場景,3D 相機,相機控制器以及最重要的渲染器,渲染器綁定了 canvas,3D 場景及其所有的物件都會通過這個渲染器渲染到 canvas 中去。
3.3 initGL()
initGL: function () { // 建立渲染器 this.renderer = new THREE.WebGLRenderer( { // 繫結 canvas canvas: this.canvas, // 抗鋸齒 antialias: true, autoClear: true } ); this.renderer.setClearColor( 0x050505 ); this.scene = new THREE.Scene(); // 初始化透視投影相機,這是一個三角的景錐體,物體在其裡面呈現的效果是近大遠小 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 ); // 新增環境光與平行光 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 ); // 新增除錯網格 var helper = new THREE.GridHelper( 1200, 60, 0xFF4444, 0x404040 ); this.scene.add( helper ); },
initGL() 方法中初始化了各個屬性,同時還添加了環境光與平行光源,以用於除錯的網格幫助模型。在 3D 場景中很多物體都可看成是一個模型,如這裡的光源。而 camera 在有一些渲染框架中也會被認為是一個模型,但其只是一個用於參與 3D 渲染時的引數。camera 最主要的作用是決定了投影矩陣,在投影矩陣內的物體可見,而不在裡面則不可見。
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: '' } } ); }; // 材質載入完成的回撥,材質載入完成後便會進一步加 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 ); }; // 開始載入材質 objLoader.loadMtl( 'models/obj/female02/female02.mtl', null, onLoadMtl ); },
內容載入這一塊是重點,其主要是通過 ObjLoader2 先是載入了材質然後載入模型。關於 obj 和 mtl 檔案, 請開啟 female02.obj 和 female02.mtl,可以發現它就是一個文字檔案,通過註釋來感受一下其檔案格式如何。
female02.obj部分資料
# Blender v2.54 (sub 0) OBJ File: '' # www.blender.org # obj對應的材質檔案 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 ...... # 紋理座標 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 # 當前圖元所用材質 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部分資料
...... # 定義一個名為 _03_-_Default1noCulli__03_-_Default1noCulli 的材質 newmtl _03_-_Default1noCulli__03_-_Default1noCulli # 反射指數 定義了反射高光度。該值越高則高光越密集,一般取值範圍在0~1000。 Ns 154.901961 # 材質的環境光(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之間進行取值。若取值為1.0,光在通過物體的時候不發生彎曲。玻璃的折射率為1.5。 Ni 1.000000 # 漸隱指數描述 引數factor表示物體融入背景的數量,取值範圍為0.0~1.0,取值為1.0表示完全不透明,取值為0.0時表示完全透明。 d 1.000000 # 指定材質的光照模型。illum後面可接0~10範圍內的數字引數。各個引數代表不同的光照模型 illum 2 # 為漫反射指定顏色紋理檔案 map_Kd 03_-_Default1noCulling.JPG ......
關於 obj 和 mtl 檔案中各個欄位的意思都在註釋中有說明了,至於每個欄位引數如何使用,就需要對 OpenGL 如何渲染模型有一定的瞭解了。繼續來看材質的載入和obj 的載入。
4.1 ObjectLoader2#loadMtl()
loadMtl: function ( url, content, onLoad, onProgress, onError, crossOrigin, materialOptions ) { ...... this._loadMtl( resource, onLoad, onProgress, onError, crossOrigin, materialOptions ); },
呼叫了內部的_loadMtl(),_loadMtl() 函式的實現程式碼是有點多的,不過不要緊,我給做了精簡。
_loadMtl: function ( resource, onLoad, onProgress, onError, crossOrigin, materialOptions ) { ...... // 7. 建立了 materialCreator 後,就會載入到這裡。這裡最後通過 onLoad 通知給呼叫者,呼叫者繼續載入模型。 var processMaterials = function ( materialCreator ) { ...... // 8.建立材質 materialCreator.preload(); // 9.回撥給呼叫者 onLoad( materials, materialCreator ); } ...... // 1. 構建一個 MTLLoader var mtlLoader = new THREE.MTLLoader( this.manager ); // 4.檔案載入成功後回撥到 parseTextWithMtlLoader 這裡 var parseTextWithMtlLoader = function ( content ) { ...... contentAsText = THREE.LoaderUtils.decodeText( content ); ...... // 5.對檔案內容進行解析,解析完成後得到一個 materialCreator 物件,然後再呼叫 processMaterials processMaterials( mtlLoader.parse( contentAsText ) ); } ...... // 2.構建一個 FileLoader var fileLoader = new THREE.FileLoader( this.manager ); ...... // 3. 載入檔案,檔案載入成功能後回撥 parseTextWithMtlLoader fileLoader.load( resource.url, parseTextWithMtlLoader, onProgress, onError ); }
註釋裡包含了材質載入的整個邏輯,一共 9 個步驟,但這裡重點只需要關注以下 3 個步驟:
(1)檔案載入——FileLoader#load()
load: function ( url, onLoad, onProgress, onError ) { ...... var request = new XMLHttpRequest(); request.open( 'GET', url, true ); ...... }
FileLoader 是 ThreeJs 庫中的程式碼,關於 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 檔案一行一行的解析。這裡的重點是建立了 MaterialCreator並且儲存在了 materialsInfo 中。materialsInfo 是一個 map 物件,其中儲存的值最重要的是包括了 map_Kd,這個在建立材質時要載入的紋理。
(3)建立材質——MaterialCreator#preload()
preload: function () { for ( var mn in this.materialsInfo ) { this.create( mn ); } },
preload() 中就遍歷每一個 material 然後分別呼叫 create() 。而 create() 又是進一步呼叫了 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 檔案中的每個欄位了,這裡主要關注一下紋理圖片是如何載入的,其他的欄位引數再看看 mtl 的註釋就可以理解了。map-kd、map_ks、norm、map_bump、bump 以及 map_d 的處理是呼叫了setMapForType(),他們都是去載入紋理的,只是紋理的形式不一樣。
function setMapForType( mapType, value ) { ...... var map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) ); ...... }
這裡的 loadTexture() 就是載入紋理的實現,一般來說在材質檔案中對紋理的地址要寫成相對的,這裡會根據材質的地址的 base url 來 resolve 出一個紋理的地址。繼續來看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; }
其主要是構建一個 TextureLoader,然後呼叫其 load() 進行載入。
load: function ( url, onLoad, onProgress, onError ) { ...... var loader = new ImageLoader( this.manager ); ...... loader.load( url, function ( image ) {} }
又進一步通過了 ImageLoader 來載入。
load: function ( url, onLoad, onProgress, onError ) { ...... var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' ); ...... image.src = url; return image; }
原來圖片的載入是通過建立一個 <img> 標記來載入的。建立一個 <img> 標記,在不新增到 dom 樹中的情況下,只要給 src 賦了值,就會去下載圖片了。
到這裡,終於把材質以及紋理的載入分析完了。接下來繼續分析 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 ); },
同樣是進一步的呼叫,這裡呼叫的是 _loadObj()。
_loadObj: function ( resource, onLoad, onProgress, onError, onMeshAlter, useAsync ) { ...... var fileLoaderOnLoad = function ( content ) { ...... ...... // 3.解析 obj loaderRootNode: scope.parse( content ), ...... }, // 1.構建 FileLoader var fileLoader = new THREE.FileLoader( this.manager ); ...... // 2.載入檔案,這裡在載入 mtl 的時候已經分析過了,並且最後會回撥到 fileLoaderOnLoad fileLoader.load( resource.name, fileLoaderOnLoad, onProgress, onError ); }
_loadObj() 的程式碼這裡也精簡了一下,並在註釋中說明了邏輯。檔案載入已經在前面分析過了,這裡就關注一下解析 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.建立一個 Parser var parser = new THREE.OBJLoader2.Parser(); ...... var onMeshLoaded = function ( payload ) { // 4.從 meshBuilder 中獲取 mesh ,並把 mesh 都加到節點中 var meshes = scope.meshBuilder.processPayload( payload ); var mesh; for ( var i in meshes ) { mesh = meshes[ i ]; scope.loaderRootNode.add( mesh ); } } ...... // 3.解析文字,因為這裡傳輸的就是文字 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 多行,但內容其實很簡單,就是根據 obj 的檔案格式進行解析。如果看到這裡忘記了 obj 的檔案格式,那建議先回顧一下。解析的過程已經非常細節了,就不詳細展開了。這裡最後的解析結果就是頂點,紋理座標以及法向根據 face 的索引進行展開,得到的結果是vvv | vtvt | vnvnvn 這樣的 n 組頂點陣列 以及 n 組索引陣列。頂點陣列,索引陣列以及材質/紋理構成了用於渲染的3D網格 mesh。
到這裡 obj 的載入也分析完了。obj 的載入是主體,但也是最簡單的。容易出問題的是在材質和紋理的載入上,需要注意的問題比較多。
5.render()
var render = function () { requestAnimationFrame( render ); app.render(); };
這個 render 是一個函式,不是OBJLoader2Example 的方法,是在 <script></script> 裡面的。其首先請求了動畫重新整理回撥,使得其可以監聽到瀏覽器的重新整理。重新整理時把回撥函式設為自己,使得瀏覽器在不斷重新整理的過程中呼叫 render() 函式。然後才是呼叫 OBJLoader2Example 的 render() 方法進行 3D 場景的繪製。這裡簡單的看一下 MDN 對 requestAnimationFrame 的描述。
window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫並請求瀏覽器在下一次重繪之前呼叫指定的函式來更新動畫。該方法使用一個回撥函式作為引數,這個回撥函式會在瀏覽器重繪之前呼叫。
當你需要更新螢幕畫面時就可以呼叫此方法。在瀏覽器下次重繪前執行回撥函式。回撥的次數通常是每秒60次 ,但大多數瀏覽器通常匹配 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 進行實際的渲染,那這裡再進一步分析就到 OpenGL 了。關於 OpenGL 是一個比較大的課題,就不在這裡分析了,也不合適。
三、後記
文章主要分析了 ThreeJs 是如何載入一個 Obj 模型並將其渲染出來的過程,分析的過程很長,但實際並不複雜,並不涉及到什麼難理解的概念。分析前由於 JavaScript 的水平實在有限,所以還特定去補了一刀《ThreeJS 學習筆記——JavaScript 中的函式與物件》 。在比較深入的理解了函式與物件之後,再加上基本的 OpenGL 基礎,一步一步的分析這個載入的過程其實還是比較輕鬆的。
最後,感謝你能讀到並讀完此文章。希望我簡陋的分析以及分享對你有所幫助,同時也請幫忙點個贊,鼓勵我繼續分析。