在本篇隨筆中,我們學習下什么是對象選擇,投影和反投影是如何工作的,怎樣使用Three.js構建可使用鼠標和對象交互的應用。例如當鼠標移到對象,對象變成紅色,鼠標移走,對象又恢復原來的顏色。
本篇隨筆的源代碼來自於:https://github.com/sole/three.js-tutorials/tree/master/object_picking
這里還有更多的例子可供參考:
和立方體交互,當你在立方體盒子上點擊,鼠標和立方體交互的點會出現一個黑點;
畫布交互,你可以增加方格到畫布上,也可以移出它;
當你在操作這些例子,會防線它們有一個共同特性。我們使用的2D坐標系(屏幕)檢測3D空間中的對象。這就是對象的選擇。
1.如何工作
在寫代碼之前,了解計算機3D圖形是如何工作是非常有幫助的,即使是非常粗糙的方式。我們如何從抽象的3D場景映射到我們屏幕中的2D圖像?
當你使用相機渲染場景時,一大堆數據機制開始對3D場景進行運算和處理,以便生成可從相機看到的場景片段的2D表示。有許多步驟涉及,但我們這里感興趣的是投影,就是它將3D的對象變成了屏幕中的2D實體。那如果反向操作又是怎樣的?
為什么需要知道反向操作?你可能會提這樣的問題。那么,如果你想知道你的鼠標指針下面是哪個對象,你需要把這些2D坐標重新轉換到3D坐標中,然后才能確定是選擇的那個3D對象。這叫做“反投影”。所有得到以下兩個定義:
Porjection:從3D到2D的投影。
UnProjection:反投影,從2D反向投影到3D。
這里還許紹一個步驟:一旦我們反向投影到3D坐標中,我們怎么確認是否選中了某個對象?答案是:投射光線。我們從3D鼠標的位置投射一根光線,沿着相機的當前方向,看射線是否投射到任何對象上。如果是,那么我們就選中了某個對象。沒有,那么我們就沒選中如何對象。
聽起來有些疑惑。我們看看下面的圖片:
圖片中,左邊是一個抽象的3D場景,包含兩個立方體和一個金字塔。中間代表了我們的屏幕。在屏幕上可看到我們的目標位置。右邊是我們視線角度的攝像頭,另外還有一條藍色射線,使用它來選擇對象。
以上的介紹,讓我們簡單了解了選擇對象的理論原理,接下來我們看看在three.js中是如何體現這樣的過程。
對象選擇代碼實現
在thres.js中實現這種的功能是非常簡單的。我們先創建場景、渲染器、攝像頭等:
var container = document.getElementById( 'container' ), containerWidth, containerHeight, renderer, scene, camera; containerWidth = container.clientWidth; containerHeight = container.clientHeight; renderer = new THREE.CanvasRenderer(); renderer.setSize( containerWidth, containerHeight ); container.appendChild( renderer.domElement ); renderer.setClearColorHex( 0xeeeedd, 1.0 ); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 45, containerWidth / containerHeight, 1, 10000 ); camera.position.set( 0, 0, range * 2 ); camera.lookAt( new THREE.Vector3( 0, 0, 0 ) );
上面的代碼都非常簡單,沒什么可介紹的。接着,我們添加一些對象。我們創建灰色立方體並且把他們隨機設置他們的3D坐標。我把這些所有的對象都存放在一個類型為Object3D對象中。
geom = new THREE.CubeGeometry( 5, 5, 5 ); cubes = new THREE.Object3D(); scene.add( cubes ); for(var i = 0; i < 100; i++ ) { var grayness = Math.random() * 0.5 + 0.25, mat = new THREE.MeshBasicMaterial(), cube = new THREE.Mesh( geom, mat ); mat.color.setRGB( grayness, grayness, grayness ); cube.position.set( range * (0.5 - Math.random()), range * (0.5 - Math.random()), range * (0.5 - Math.random()) ); cube.rotation.set( Math.random(), Math.random(), Math.random() ).multiplyScalar( 2 * Math.PI ); cube.grayness = grayness; // *** NOTE THIS cubes.add( cube ); }
所有的集合對象都使用同一個材質,它這些材質的顏色是不同的,每個立方體都設置了一種隨機的灰度顏色。接下來,我們准備兩個關鍵對象:射線對象、鼠標坐標。
var raycaster = new THREE.Raycaster(); var mouseVector = new THREE.Vector3();
當鼠標移動時,我們想選擇對象。所以需要監聽mousemove事件:
window.addEventListener( 'mousemove', onMouseMove, false );
然后,所有感興趣的功能都會在這個事件里邊實現。當查看源代碼使,你需要特別小心下邊兩行代碼,這兩天代碼稍有差錯,可能我們后面的選擇功能將無法實現:
mouseVector.x = 2 * (e.clientX / containerWidth) - 1;
mouseVector.y = 1 - 2 * ( e.clientY / containerHeight );
這兩行代碼將鼠標坐標轉換為 x、y范圍在(-1, 1)的笛卡爾坐標。你可能主要到計算的y坐標為什么是負的?那是因為經典的DOM坐標系原點(0,0)是從左上角開始。往右是x軸,往下是y坐標。但笛卡爾坐標的卻如下所示:
理解了這兩個坐標系,上面的代碼你就知道為什么會那樣寫了。
現在我們將使用mouseVector和camera生成具體的攝像方向:
raycaster.setFromCamera(mouseVector.clone(), camera);
這里我們克隆了mouseVector,而表示直接傳遞它。那是因為setFromCamera函數內部會修改mouseVector的值,你可以查看three.js源代碼看看,是否真的有修改。創建raycaster對象之后,我們調用它的intersectObjects函數:
var intersects = raycaster.intersectObjects( cubes.children );
傳遞的參數為cubes.chidren,也就是說我們要選擇的對象來自於cubes的children中。intersects將返回查詢一個選中對象的集合。並且某個對象包含了以下屬性:
distance:攝像頭和對象有距離。
point:在對象上表面上和射線交互的點的位置。
face:對象和射線交互的面。
object:和射線交互的對象。
既然已經獲取到這些對象了,那么我們也可以操作這些對象。首選我們把所有對象的顏色復原為之前設置的灰色:
cubes.children.forEach(function( cube ) { cube.material.color.setRGB( cube.grayness, cube.grayness, cube.grayness ); });
接着我們再設置交互的對象。把這些對象的顏色設置成紅色。
for( var i = 0; i < intersects.length; i++ ) { var intersection = intersects[ i ], obj = intersection.object; obj.material.color.setRGB( 1.0 - i / intersects.length, 0, 0 ); }
以上就是OnMouseMove函數的所有代碼了,通過這些代碼我們初步了解了選擇對象操作,其實我們要寫的代碼很少,three.js已經幫我們實現了具體的步驟。
涉及到的鼠標操作功能很多,選擇對象是最基礎的,萬變不離其宗。像對象的拖動功能,選擇也是基礎功能。接下來我們就再看看three.js是如何實現拖拽功能的。
three.js實現拖拽功能
實現拖拽功能,主要使用了three.js的兩個擴展控件:TrackballControls和DragControls。
首先,我們先創建隨機位置的200個立方體:
var objects = []; var geometry = new THREE.BoxGeometry(40, 40, 40); for(var i = 0; i < 200; i++){ var object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); object.position.set(Math.random() * 1000 - 500, Math.random() * 600 - 300, Math.random() * 800 - 400); object.rotation.set(Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI); object.scale.set(Math.random() * 2 + 1, Math.random() * 2 + 1, Math.random() * 2 + 1); object.castShadow = true; object.receiveShadow = true; scene.add(object); objects.push(object); }
為了然各個立方體顯示隨機,每個object都使用Math.random()函數隨機設置了position、rotation、scale。並且對象可產生投影可接收投影。
接下來我們創建剛才提到的兩個控件:
var controls = new THREE.TrackballControls(camera); controls.rotateSpeed = 1.0; controls.zoomSpeed = 1.2; controls.panSpeed = 0.8; controls.noZoom = false; controls.noPan = false; controls.staticMoving = true; controls.dynamicDampingFactor = 0.3; var dragControls = new THREE.DragControls(objects, camera, webGLRenderer.domElement);
TrackballControls可用來通過旋轉移動攝像頭位置,實習整個場景的旋轉和移動。DragControls包含兩個事件:dragstart、dragend。
dragControls.addEventListener("dragstart", function(event){ currentColor = event.object.material.color; event.object.material.color = new THREE.Color(0xffff00); event.object.material.transparent = true; event.object.material.opacity = 0.6; controls.enabled = false; }); dragControls.addEventListener("dragend", function(event){ event.object.material.opacity = 1.0; event.object.material.color = currentColor; controls.enabled = true; });
dragStart事件表示開始執行拖拽了,而dragend表示拖拽結束。可通過event.object獲取當前拖拽的對象,然后就可以設置對象的屬性了。這里需要特別注意的是,在拖拽開始時,我們需要禁止TrackballControls功能,才能夠拖動物體。所以需要設置controls.enabled = false。當拖動結束,設置controls.enabled = true恢復TrackballControls的功能。