1. 程式人生 > >13 Three.js場景互動

13 Three.js場景互動

由於瀏覽器是一個2d視口,而在裡面顯示three.js的內容是3d場景,場景互動就是在二維平面控制三維場景的模型,問題就是如何將2d視口的xy座標轉換成three.js場景中的3d座標。好在three.js已經有了解決相關問題的方案,那就是THREE.Raycaster射線,用於滑鼠拾取(計算出滑鼠移過的三維空間中的物件)等等。我們看一張圖片: 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)

intersectObjectintersectObjects的區別就是,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
        }
    )
}
  • 繫結documentmousemove事件和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;
    }
}

最後,附上一個可以檢視的案例地址:點選這裡 案例原始碼地址:點選這裡