在ThreeJS簡介中已經(jīng)介紹了如何使用ThreeJS框架顯示一個簡單的對象,并為其賦予了一個簡單的默認(rèn)材質(zhì)。這一章將以無規(guī)則的移動國際象棋為DEMO簡單介紹一下ThreeJS框架的模型讀取、天空盒子、使用動畫引擎Tween.js來創(chuàng)建動畫,以及簡要介紹一下廣度搜索算法。
1. 模型的讀取
三維模型有數(shù)百種文件格式,每種格式都有不同的用途、各種功能和不同的復(fù)雜性。盡管three.js提供了許多加載器支持的模型格式非常多,但是選擇正確的格式和工作流程將節(jié)省時間。有一些格式很難使用,對于實時顯示效率低下,或者目前還沒有完全支持,選擇不好模型格式可能為后期的開發(fā)帶來很多麻煩。因此官方推薦大多數(shù)用戶使用GLTF格式模型,并提供了各種的轉(zhuǎn)換工具,可以說官方對GLTF格式模型支持的是最好的。
1.1. glTF格式簡介
glTFTM(GL TransmissionFormat圖形語言交換格式)是一種免版稅(royalty-free)的規(guī)范,由Khronos Group管理(Khronos Group還管理著OpenGL系列、OpenCL等重要的行業(yè)標(biāo)準(zhǔn))。glTF的設(shè)計是面向?qū)崟r渲染應(yīng)用的,盡量提供可以直接傳輸給圖形API的數(shù)據(jù)形式,不再需要二次轉(zhuǎn)換,用于通過應(yīng)用程序高效傳輸和加載3D場景和模型。GLTF最小化了3D資源的大小,以及解包和使用這些資產(chǎn)所需的運行時處理。glTF對OpenGL ES、WebGL非常友好,作為一個標(biāo)準(zhǔn),自2015年10月發(fā)布(glTF 1.0)以來,已經(jīng)得到了業(yè)界廣泛的認(rèn)可,glTF目前最新版本為2.0已于2017年6月正式發(fā)布。GLTF具體的數(shù)據(jù)存儲格式可以去官方網(wǎng)站上看:https://www.khronos.org/gltf/,大概就是相對于XML的JSON存儲方式。


glTF有很多的轉(zhuǎn)換、導(dǎo)入、導(dǎo)出工具供用戶選擇,詳細(xì)的可以在官方網(wǎng)站查詢,下面簡要介紹一個主要的工具:
- glTF-Blender-IO by the Khronos Group,用于Blender上的導(dǎo)入導(dǎo)出工具,該工具在Blender2.80開始已經(jīng)被內(nèi)置了。
- COLLADA2GLTF by the Khronos Group,用于COLLADA上的轉(zhuǎn)化工具。
- FBX2GLTF by Facebook,用于將FBX格式模型轉(zhuǎn)換為glTF的工具。
- OBJ2GLTF by Analytical Graphics Inc,用于將obj格式模型轉(zhuǎn)換為glTF的工具。
- 3DS Max Exporter ,用于3DMAX 2015或更高版本的導(dǎo)出工具,使用BabylonJS plugin。
- Maya Exporter,用于Maya2018或更高版本的導(dǎo)出工具,使用BabylonJS plugin。
1.2. glTF格式的讀取
在ThreeJS中只有一少部分載入器是內(nèi)置在three.js中的,比如ObjectLoader,其他都需要手動添加載入器。
// global script
<script src="GLTFLoader.js"></script>
// commonjs
var THREE = window.THREE = require('three');
require('three/examples/js/loaders/GLTFLoader');
一旦導(dǎo)入了加載程序,就可以向場景中添加模型了。不同加載程序的語法有所不同,使用加載器前,請檢查該加載程序的示例和文檔。對于GLTF,基本用法是:
var loader = new THREE.GLTFLoader();
loader.load( 'path/to/model.glb', function ( gltf ) {
scene.add( gltf.scene );
}, undefined, function ( error ) {
console.error( error );
} );
瀏覽器兼容性:GLTFLoader依賴于IE6 中不支持的ES6 Promises。要在IE11中使用加載程序,必須 包含一個 提供Promise替換的polyfill。

官方樣例——地址
2. 天空盒子
有時候布置一個3D場景,沒有美麗的背景會使場景顯得非常單調(diào),因此在沒有特殊需求的場景并且想要節(jié)省性能的條件下,使用天空盒子是最好的選擇。在官方GLTF模型讀取的樣例中,就是使用了這樣的天空盒子,并且將天空盒子作為了模型的Env Map,這樣再模型反光是可以考到反光出來的天空盒子,使人感覺更加真實。
2.1. 天空盒子簡介
在實時渲染中,如果要繪制非常遠(yuǎn)的物體,例如遠(yuǎn)處的山、天空等,隨著觀察者的距離的移動,這個物體的大小是幾乎沒有什么變化的,想象一下遠(yuǎn)處有一座山,即使人走進(jìn)十米、百米、甚至千米,這座山的大小也是幾乎不怎么改變的,這個時候可以考慮采用天空盒技術(shù)。

所謂的天空盒其實就是將一個立方體展開,然后在六個面上貼上相應(yīng)的貼圖,如上圖所示。
在實際的渲染中,將這個立方體始終罩在攝像機(jī)的周圍,讓攝像機(jī)始終處于這個立方體的中心位置,然后根據(jù)視線與立方體的交點的坐標(biāo),來確定究竟要在哪一個面上進(jìn)行紋理采樣。具體的映射方法為:設(shè)視線與立方體的交點為(x,y,z),在x、y、z中取絕對值最大的那個分量,根據(jù)它的符號來判定在哪個面上采樣。

然后讓其他兩個分量都除以最大分量的絕對值,這樣就讓另外兩個分量都映射到了[0,1]內(nèi),然后就可以直接在對應(yīng)的紋理上做紋理映射就行了,這個方法就是所謂的Cube Map,是天空盒方法的核心。
天空盒的原理非常簡單,下面來探討關(guān)于天空盒的幾個細(xì)節(jié)問題。
z = w
在投影變換之后,會做一步透視除法,即讓四元向量的所有分量都除以它的W分量,從而使視錐體內(nèi)的區(qū)域的x、y映射到[?1,1],z映射到[0,1],從而根據(jù)透視除法之后的x、y、z的范圍直接剔除掉那些不可見的頂點,如果令z=w,就表示透視除法后的z=1,也就是讓天空盒始終處于遠(yuǎn)平面的位置,從而讓它被之后所有要繪制的物體遮擋住。由于z=1,要修改深度測試的比較函數(shù),如果比較函數(shù)是小于的話,由于深度緩沖的最大值本來就是1,無法通過深度測試的比較函數(shù),最終天空盒是沒辦法被繪制出來的。
消隱問題
由于攝像機(jī)位于物體的內(nèi)部,這個時候消隱的設(shè)置尤為重要,模型本身是沒有發(fā)生變化的,它的法線始終朝向外部,那么視線與法線的夾角就變成了銳角,如果采用背面消隱,整個模型就會都被剔除掉,所以在繪制天空盒的時候,要么取消消隱,要么將消隱設(shè)置為正面消隱。
天空盒模型大小的限制
原則上模型的大小對于最終的結(jié)果沒有影響,因為在模型增大的時候,雖然它的面是變大了,但它離視錐也變遠(yuǎn)了,因此最終視野內(nèi)的大小是保持不變的,但是一定要保證模型始終處在視錐體的內(nèi)部,即讓模型離攝像機(jī)最遠(yuǎn)的點到攝像機(jī)的距離不超過攝像機(jī)到遠(yuǎn)平面的距離,對于一個立方體模型來說,離中心最遠(yuǎn)的點顯然是它的頂點,

a為立方體的邊長,由

得

因此天空盒的模型邊長有一個上界,在這個最大值以內(nèi)都是可以的。
2.2. 設(shè)置天空盒子
在ThreeJS中設(shè)置天空盒子非常簡單,首先需要載入一個CubeTexture,需要6張圖片。
var loader = new THREE.CubeTextureLoader();
loader.setPath( 'textures/cube/pisa/' );
var textureCube = loader.load( [ 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png' ] );
var material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
scene.background = textureCube;
上面的代碼讀取了一個天空盒子,然后設(shè)置了場景的背景為這個盒子,并將一個材質(zhì)的envMap設(shè)置為這個盒子,這樣當(dāng)這個材質(zhì)賦予某一對象時,就可以有官方樣例中的效果了。
3. 動畫引擎Tween.js
TweenJS Javascript庫提供了一個簡單但強(qiáng)大的漸變界面。它支持漸變的數(shù)字對象屬性&CSS樣式屬性,并允許您鏈接補間動畫和行動結(jié)合起來,創(chuàng)造出復(fù)雜的序列。
3.1. 簡單的補間動畫
這個漸變將漸變目標(biāo)alpha屬性用一秒從0漸變到1,然后調(diào)用handleComplete函數(shù)。
target.alpha = 0;
createjs.Tween.get(target).to({alpha:1}, 1000).call(handleComplete);
function handleComplete() {
//漸變完成執(zhí)行
}
3.2. 參數(shù)和范圍
Tween總是提供一個call()伴隨著參數(shù)和/或一個范圍。如果沒有傳遞范圍,那么稱為匿名函數(shù)(正常JavaScript行為)。 在面向?qū)ο蟮娘L(fēng)格發(fā)展,范圍是有用的維護(hù)范圍。
createjs.Tween.get(target).to({alpha:0}).call(handleComplete, [argument1, argument2], this);
3.3. 可鏈?zhǔn)骄幊痰难a間動畫
這個漸變將會先等待0.5秒,漸變目標(biāo)的alpha屬性從0到1,并且visible屬性從true變?yōu)閒alse,這個過程用時1秒,最后調(diào)用handleComplete函數(shù)。
target.alpha = 1;
createjs.Tween.get(target).wait(500).to({alpha:0, visible:false}, 1000).call(handleComplete);
function handleComplete() {
//漸變完成執(zhí)行
}
3.4. Ease類
這個Ease類提供了一個緩動動畫函數(shù)集合,在使用TweenJs的使用中。它不使用標(biāo)準(zhǔn)的4參數(shù)緩動。相反,它使用了一個參數(shù),表示當(dāng)前線性比例(0,1)的漸變。
大多數(shù)方法緩解可以直接通過緩動函數(shù):
Tween.get(target).to({x:100}, 500, Ease.linear);
然而,方法從“get”開始將返回一個基于參數(shù)值的緩動函數(shù):However, methods beginning with "get" will return an easing function based on parameter values:
Tween.get(target).to({y:200}, 500, Ease.getPowIn(2.2));
請參見更多不同的緩動類型在TweenJS.com上的概述。由羅伯特·彭納方程派生而來的.
具體漸變方式列表如下
}
3.5. 瀏覽器支持
TweenJS會在現(xiàn)代瀏覽器工作。TweenJS在IE8或者更早的版本上,使用一個舊版本的PreloadJS(0.4.1和更早的版本)。
4. 廣度優(yōu)先搜索算法
廣度優(yōu)先搜索算法(Breadth-First Search),簡稱BFS,是一種圖搜索算法。主要思想史從起點卡斯hi,沿著圖的邊一層一層的遍歷圖,如果發(fā)現(xiàn)目標(biāo),則演算終止,基本的BFS是一種窮搜算法。
4.1. 實現(xiàn)
我們已下圖為例,V2作為起點V6為終點。

隊列初始為起點{V2},
- 首先沿著隊列中index=0的節(jié)點的邊找到所有相鄰節(jié)點,第一層搜索將V4、V5、V1加入隊列{V2,V4,V5,V1}
- 隨后沿著index=1的節(jié)點的邊找到起相鄰節(jié)點V2、V8,其中V2已經(jīng)在隊列中因此只加入V8,{V2,V4,V5,V1,V8}
- 沿著index=index+1的節(jié)點的邊重復(fù)2
- 當(dāng)找到目標(biāo)時遞歸過程返回(或結(jié)束循環(huán)),不再搜索。
- 當(dāng)index=隊列長度時已搜索完全部可能路徑,未找到目標(biāo),說明無可達(dá)到路徑。
模擬過程:
index=0,V2=>add(V4,V5,V1)=>{V2,V4,V5,V1}=>way{(V2),(V2,V4),(V2,V5),(V2,V1)}
index=1,V4=>add(V8)not(V2)=>{V2,V4,V5,V1,V8}=>way{(V2),(V2,V4),(V2,V5),(V2,V1),(V2,V4,V8)}
index=2,V5=>not(V2,V8)=>{V2,V4,V5,V1,V8}=>way{(V2),(V2,V4),(V2,V5),(V2,V1),(V2,V4,V8)}
index=3,V1=>NOT(V2)add(V3)=>{V2,V4,V5,V1,V8,V3}=> way{(V2),(V2,V4),(V2,V5),(V2,V1),(V2,V4,V8),(V2,V1,V3)}
index=4,V8=>not(V4,V5)=> {V2,V4,V5,V1,V8,V3}=> way{(V2),(V2,V4),(V2,V5),(V2,V1),(V2,V4,V8),(V2,V1,V3)}
index=5,V3=>isTarget(V6)=>return way(index)+V6=>the way is (V2,V1,V3,V6)
4.2. 空間復(fù)雜度
空間復(fù)雜度有兩種理論:
- O(|V| + |E|),其中 |V| 是節(jié)點的數(shù)目,而 |E| 是圖中邊的數(shù)目。
- O(B^M),其中 B 是最大分支系數(shù),而 M 是樹的最長路徑長度。
空間復(fù)雜度主要和如何記錄路徑有非常打的關(guān)系,廣度優(yōu)先搜索的實現(xiàn)可以使用遞歸函數(shù)也可以使用普通的循環(huán)。用遞歸函數(shù)的方法可以不單獨開辟數(shù)組記錄路徑,通過處理遞歸回溯就可以得到路徑,此時路徑實際上是存儲在程序堆棧里,若地圖較大很容易造成內(nèi)存溢出。
而使用普通的循環(huán)實現(xiàn)BFS,那么就需要將所有路徑記錄在開辟的數(shù)組中,雖然這樣會占用更多內(nèi)存空間,但路徑是存儲在內(nèi)存的數(shù)據(jù)段中,可以存儲的空間更大。而且存儲的way數(shù)組是可以優(yōu)化的,比如上面實現(xiàn)的樣例,index=0屬于第0層,index=1到index=3屬于第1層,index=4到index=5屬于第2層,第1層路徑的信息包含了第0層,第2層路徑的信息包含了第1層。那么實際上我再達(dá)到第n層的時候,第n-2層的路徑就可以丟棄了。這樣只保留2層的路徑信息,會節(jié)省很多空間。
4.3. 時間復(fù)雜度
最差情形下,BFS必須尋找所有到可能節(jié)點的所有路徑,因此其時間復(fù)雜度為 O(|V| + |E|),其中 |V| 是節(jié)點的數(shù)目,而 |E| 是圖中邊的數(shù)目。
4.4. 完全性
廣度優(yōu)先搜索算法具有完全性。這意指無論圖形的種類如何,只要目標(biāo)存在,則BFS一定會找到。然而,若目標(biāo)不存在,且圖為無限大,則BFS將不收斂(不會結(jié)束)。PS:無限大的圖存在嗎?
4.5. 最佳解
若所有邊的長度相等,廣度優(yōu)先搜索算法是最佳解——亦即它找到的第一個解,距離根節(jié)點的邊數(shù)目一定最少;但對一般的圖來說,BFS并不一定回傳最佳解。這是因為當(dāng)圖形為加權(quán)圖(亦即各邊長度不同)時,BFS仍然回傳從根節(jié)點開始,經(jīng)過邊數(shù)目最少的解;而這個解距離根節(jié)點的距離不一定最短。這個問題可以使用考慮各邊權(quán)值,BFS的改良算法成本一致搜尋法(en:uniform-cost search)來解決。然而,若非加權(quán)圖形,則所有邊的長度相等,BFS就能找到最近的最佳解。
4.6. 應(yīng)用場景——平面網(wǎng)格中的路徑搜索
BFS 可用來解決平面網(wǎng)格地圖中的路徑搜索,例如電腦游戲(例如即時策略游戲)中找尋路徑的問題。在這個應(yīng)用中,使用平面網(wǎng)格來代替圖形,而一個格子即是圖中的一個節(jié)點。所有節(jié)點都與它的鄰居(上、下、左、右、左上、右上、左下、右下)相接。在平明網(wǎng)格中搜索路徑,尤其是場景較大的,使用BFS將比DFS更快得到結(jié)果。
5. 其他內(nèi)容
5.1. BoxHelper
可以在樣例中看到,當(dāng)鼠標(biāo)選中某一對象時,會顯示該對象的立方體外輪廓,這就用到了BoxHelper。
BoxHelper以圖形方式顯示對象周圍的世界軸對齊邊界框。實際的邊界框用Box3處理,這只是一個用于調(diào)試的可視幫助器。當(dāng)它的創(chuàng)建對象被轉(zhuǎn)換時,它可以使用BoxHelper.update方法自動調(diào)整大小。請注意,對象必須具有Geometry或BufferGeometry才能工作,因此它不適用于Sprites。
樣例:
var sphere = new THREE.SphereGeometry();
var object = new THREE.Mesh( sphere, new THREE.MeshBasicMaterial( 0xff0000 ) );
var box = new THREE.BoxHelper( object, 0xffff00 );
scene.add( box );
也可以創(chuàng)建BoxHelper后,通過setFromObject(object:Object3D):BoxHelper,方法來計算object的邊界。
var box = new THREE.BoxHelper();
box.setFromObject(object);
5.2. raycaster
要在3D場景中使用鼠標(biāo)拾取/選擇對象,那么就需要用到Three的Raycaster, 該類利用光線投射的原理來檢測與對象相交。
舉例:
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseMove( event )
{
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function render() {
// update the picking ray with the camera and mouse position
raycaster.setFromCamera( mouse, camera );
// calculate objects intersecting the picking ray
var intersects = raycaster.intersectObjects( scene.children );
for ( var i = 0; i < intersects.length; i++ ) {
intersects[ i ].object.material.color.set( 0xff0000 );
}
renderer.render( scene, camera );
}
window.addEventListener( 'mousemove', onMouseMove, false );
window.requestAnimationFrame(render);
其中最重要的,就是要準(zhǔn)確的計算出鼠標(biāo)相對canvas范圍內(nèi)的坐標(biāo),在這個例子中,假設(shè)了canvas的范圍是全屏,因此可以使用onMouseMove中的方法將直接得到的鼠標(biāo)坐標(biāo)轉(zhuǎn)換到canvas范圍內(nèi)的(-1,1)范圍內(nèi)
1) 獲取畫布坐標(biāo)
需要準(zhǔn)確的得到鼠標(biāo)和canvas的相對關(guān)系,首先需要準(zhǔn)確的得到canvas的相對坐標(biāo):
var canvas = document.getElementById("canvas");
console.log(canvas.offsetTop, canvas.offsetLeft, canvas.offsetWidth, canvas.offsetHeight);
使用上面的方法可以準(zhǔn)確的得到canvas對象在當(dāng)前頁面的相對坐標(biāo),但是我們在做網(wǎng)站的時候,經(jīng)常使用嵌套頁面,例如利用ajax技術(shù)使一個網(wǎng)頁嵌套在另一個網(wǎng)頁當(dāng)中,這是這樣只是獲得了canvas在嵌套的網(wǎng)頁中的相對坐標(biāo),那么要獲得在全局網(wǎng)頁中的坐標(biāo),就需要用到下面的函數(shù):
function getOffset (e){
var t=e.offsetTop;
var l=e.offsetLeft;
while(e=e.offsetParent){
t+=e.offsetTop;
l+=e.offsetLeft;
}
return({l, t});
}
2) 歸一化轉(zhuǎn)換
得到canvas在全局網(wǎng)頁中的相對坐標(biāo)后,就可以將鼠標(biāo)的坐標(biāo)歸一化了。
function getMousePosition( left,top,width,height, x, y ) {
return [ ( x - left ) / width, ( y - top ) / height ];
}
其中x和y是鼠標(biāo)的全局坐標(biāo),就是event.clientX和event.clientY,而left,top就是通過getOffset函數(shù)返回的相對坐標(biāo)。
3) 其他
getMousePosition返回的是數(shù)組,而mouse的坐標(biāo)是Vector2,可以使用Vector2的fromArray來填充mouse的坐標(biāo)。
ThreeJS框架支持處理觸摸時間,因此在處理mouse事件的同事處理下touch事件是比較理想的兼容了移動端的操作。