一、前言
這篇文章主要學(xué)習(xí) ThreeJs 中的 demo loader/obj2,主要是分析一下 obj 是如何加載的,紋理以及材質(zhì)是如何加載的,3d camera 以及 camera controller 這些是如何實現(xiàn)的等。那么,先來 2 個 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.模型加載
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ù)分析。