基于Threejs的國際象棋3D棋盤——練習(xí)尋路算法

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 2.0場景描述結(jié)構(gòu)

對glTF格式的支持

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上的概述。由羅伯特·彭納方程派生而來的.
具體漸變方式列表如下
}

1 2 3 4
backIn backInOut backOut bounceIn
bounceInOut bounceOut circIn circInOut
circOut cubicIn cubicInOut cubicOut
elasticIn elasticInOut elasticOut get
getBackIn getBackInOut getBackOut getElasticIn
getElasticInOut getElasticOut getPowIn getPowInOut
getPowOut linear none ·quadIn
quadInOut quadOut quartIn quartInOut
quartOut quintIn quintInOut quintOut
sineIn sineInOut sineOut

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},

  1. 首先沿著隊列中index=0的節(jié)點的邊找到所有相鄰節(jié)點,第一層搜索將V4、V5、V1加入隊列{V2,V4,V5,V1}
  2. 隨后沿著index=1的節(jié)點的邊找到起相鄰節(jié)點V2、V8,其中V2已經(jīng)在隊列中因此只加入V8,{V2,V4,V5,V1,V8}
  3. 沿著index=index+1的節(jié)點的邊重復(fù)2
  4. 當(dāng)找到目標(biāo)時遞歸過程返回(或結(jié)束循環(huán)),不再搜索。
  5. 當(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ù)雜度有兩種理論:

  1. O(|V| + |E|),其中 |V| 是節(jié)點的數(shù)目,而 |E| 是圖中邊的數(shù)目。
  2. 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事件是比較理想的兼容了移動端的操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容