13 Three.js場景互動
由於瀏覽器是一個2d
視口,而在裡面顯示three.js
的內容是3d
場景,場景互動就是在二維平面控制三維場景的模型,問題就是如何將2d
視口的x
和y
座標轉換成three.js
場景中的3d
座標。好在three.js
已經有了解決相關問題的方案,那就是THREE.Raycaster
射線,用於滑鼠拾取(計算出滑鼠移過的三維空間中的物件)等等。我們看一張圖片:
我們一般都會設定三維場景的顯示區域,如果,指明當前顯示的2d
座標給THREE.Raycaster
的話,它將生成一條從顯示的起點到終點的一條射線。也就是說,我們再螢幕上點選了一個點,在three.js
裡面獲取的則是一條直線。
THREE.Raycaster建構函式和物件方法
例項化
new Raycaster( origin, direction, near, far );
origin
- 光線投射的原點向量。
direction
- 光線投射的方向向量,應該是被歸一化的。
near
- 投射近點,用來限定返回比near要遠的結果。near不能為負數。預設為0。
far
- 投射遠點,用來限定返回比far要近的結果。far不能比near要小。預設為無窮大。
屬性
far
當前射線的最遠距離,射線的終點。此值不應該為負值,而且比near
值要大。
near
當前射線的最近距離,射線的起點。此值不應為負值,比far
值要小。
linePrecision
射線和線相交的精度,浮點數型別的值。
方法
.set()
此方法可以重新設定射線的原點和方向
.set(origin,direction)
origin
- 射線的新的原點向量位置
direction
- 基於原點位置的射線的方向向量
.setFromCamera ()
使用當前相機和介面的2d座標設定射線的位置和方向。
.setFromCamera ( coords, camera )
coords
- 滑鼠的二維座標,在歸一化的裝置座標(NDC)中,也就是X 和 Y 分量應該介於 -1 和 1 之間。
camera
- 射線起點處的相機,即把射線起點設定在該相機位置處。
.intersectObject ()和 .intersectObjects ()
檢查射線和物體之間的所有交叉點(包含或不包含後代)。交叉點返回按距離排序,最接近的為第一個。 返回一個交叉點物件陣列。
.intersectObject ( object, recursive, optionalTarget)
object
- 用來檢測和射線相交的物體。
recursive
- 如果為true,它還檢查所有後代。否則只檢查該物件本身。預設值為false。
optionalTarget
- 可選引數,用於設定放置結果的陣列。如果沒有則將例項化一個新陣列並將獲取到的資料放入,。
.intersectObjects ( array, recursive, optionalTarget)
intersectObject
和intersectObjects
的區別就是,intersectObject
第一個引數需要傳入一個3d物件,而intersectObjects
需要傳入一個3d物件組成的陣列。
返回陣列每一個物件的內容
如果射線與場景內的模型沒有相交,將返回一個空陣列,否則,將返回從近到遠的順序生成的一個物件陣列。
[ { distance, point, face, faceIndex, indices, object }, ... ]
distance
– 射線的起點到相交點的距離
point
– 在世界座標中的交叉點
face
– 相交的面
faceIndex
– 相交的面的索引
indices
– 組成相交面的頂點索引
object
– 相交的物件
當一個網孔(Mesh)物件和一個快取幾何模型(BufferGeometry)相交時,faceIndex 將是 undefined,並且 indices 將被設定; 而當一個網孔(Mesh)物件和一個幾何模型(Geometry)相交時,indices 將是 undefined。
當計算這個物件是否和射線相交時,Raycaster 把傳遞的物件委託給 raycast 方法。 這允許 mesh 對於光線投射的反應可以不同於 lines 和 pointclouds(mesh,lines,pointclouds都是不同的計算相交的方法)。
注意,對於網格,面(faces)必須朝向射線原點,這樣才能被檢測到;通過背面的射線的交叉點將不被檢測到。 為了光線投射一個物件的正反兩面,你得設定 material 的 side 屬性為 THREE.DoubleSide。
實現一個模型的點選事件
上面講解了射線的相關內容,接下來,我們來看一下,如何使用射線實現一個普通的點選事件
- 首先,我們通過點選事件回撥的
event
獲取到點選的位置:
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
預設沒有經過矩陣轉換過的顯示區域的寬和高分別是2,即中心點也是webgl
場景的座標原點,左上角的座標是(-1.0, 1.0, 0.0)
, 右下角的座標軸是(1.0, -1.0, 0.0)
。我們可以通過點選點的位置計算出當前點選的點在場景中,沒有矩陣轉換過的平面座標。如果webgl
的渲染區域不是佔滿視窗狀態,我們還需要獲取到顯示區域距離視窗左上角的偏移量,再計算位置:
//通過dom的getBoundingClientRect方法獲得當前顯示區域距離左上角的偏移量
var left = renderer.domElement.getBoundingClientRect().left;
var top = renderer.domElement.getBoundingClientRect().top;
//根據瀏覽器的裝置型別來獲取到當前點選的位置
var clientX = dop.browserRedirect() === "pc" ? event.clientX - left : event.touches[0].clientX - left;
var clientY = dop.browserRedirect() === "pc" ? event.clientY - top : event.touches[0].clientY - top;
//計算出場景內的原始座標
mouse.x = (clientX / renderer.domElement.offsetWidth) * 2 - 1;
mouse.y = -(clientY / renderer.domElement.offsetHeight) * 2 + 1;
- 獲取到座標以後,我們需要使用射線的
setFromCamera()
方法配合場景座標和相機更新射線的位置:
raycaster.setFromCamera( mouse, camera );
- 使用
intersectObjects()
方法獲取射線和所有模型相交的陣列集合
var intersects = raycaster.intersectObjects( scene.children );
這裡在提醒一句,很多小夥伴有時候發現點選了以後射線無法獲取到相交的物體,那是因為為了節約效能,我們需要設定第二個引數為true
,讓Three.js
遍歷模型所有的子類去判斷是否相交。
- 最後,如果有與射線相交的模型,返回的
intersects
陣列長度將不為零:
if(intersects.length > 0){
alert("有相交的模型");
}
下面是一個點選案例,點中物體後,模型顏色將會變色:點選這裡 案例原始碼地址:點選這裡
實現一個簡單的框選案例
最近小夥伴都想實現一個簡單的框選案例,那我在這一篇文章裡面新增上,本框選案例是通過模型的位置進行判斷實現的框選。相對於其它實現方式,這種實現節約效能,簡單易懂,能夠應付大部分場景。 接下來,我講解一下這個框選的實現思路:
- 在滑鼠按下時,記錄滑鼠按下時的場景座標:
//獲取到顯示區域距離視窗左上角的偏移量
domClient.x = renderer.domElement.getBoundingClientRect().left;
domClient.y = renderer.domElement.getBoundingClientRect().top;
//計算出當前滑鼠距離顯示區域左上角的距離
down.x = e.clientX - domClient.x;
down.y = e.clientY - domClient.y;
- 使用之前學習到的
box
物件方法來計算出模型的包圍盒中心位置,這樣對多個複雜模型比較管用,如果簡單的幾何體的話,可以直接使用mesh
的位置來計算。通過相機將世界座標的位置轉換為平面座標,並將模型放到一個數組內以便後期使用:
for (let i = 0; i < group.children.length; i++) {
let box = new THREE.Box3();
box.expandByObject(group.children[i]);
//獲取到平面的座標
let vec3 = new THREE.Vector3();
box.getCenter(vec3);
let vec = vec3.project(camera);
modelsList.push(
{
component: group.children[i],
position: {
x: vec.x * half.width + half.width,
y: -vec.y * half.height + half.height
},
normalMaterial: group.children[i].material
}
)
}
- 繫結
document
的mousemove
事件和mouseup
事件,滑鼠移動事件是為了判斷每個模型是否處於框內,滑鼠擡起事件將繫結的事件清除。
//繫結滑鼠按下移動事件和擡起事件
document.addEventListener("mousemove", movefun, false);
document.addEventListener("mouseup", upfun, false);
- 在滑鼠移動事件中,我們計算出當前四個邊的位置,並且迴圈判斷哪些模型的位置處於框內,處於框內的模型的材質將被修改為框選材質:
for (let i = 0; i < modelsList.length; i++) {
let position = modelsList[i].position;
//判斷當前位置是否處於框內
if (position.x > min.x && position.x < max.x && position.y > min.y && position.y < max.y) {
modelsList[i].component.material = material;
}
else{
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}
- 在最後的滑鼠擡起事件內,將框選框隱藏,並將所有材質修改為預設材質:
function upfun(e) {
//清除事件
document.body.removeChild(div);
document.removeEventListener("mousemove", movefun, false);
document.removeEventListener("mouseup", upfun, false);
//將所有的模型修改為當前預設的材質
for (let i = 0; i < modelsList.length; i++) {
modelsList[i].component.material = modelsList[i].normalMaterial;
}
}