在本篇隨筆中,咱們學習下什麼是對象選擇,投影和反投影是如何工做的,怎樣使用Three.js構建可以使用鼠標和對象交互的應用。例如當鼠標移到對象,對象變成紅色,鼠標移走,對象又恢復原來的顏色。html
本篇隨筆的源代碼來自於:https://github.com/sole/three.js-tutorials/tree/master/object_pickinggit
這裏還有更多的例子可供參考:github
和立方體交互,當你在立方體盒子上點擊,鼠標和立方體交互的點會出現一個黑點;web
畫布交互,你能夠增長方格到畫布上,也能夠移出它;canvas
當你在操做這些例子,會防線它們有一個共同特性。咱們使用的2D座標系(屏幕)檢測3D空間中的對象。這就是對象的選擇。app
在寫代碼以前,瞭解計算機3D圖形是如何工做是很是有幫助的,即便是很是粗糙的方式。咱們如何從抽象的3D場景映射到咱們屏幕中的2D圖像?dom
當你使用相機渲染場景時,一大堆數據機制開始對3D場景進行運算和處理,以便生成可從相機看到的場景片斷的2D表示。有許多步驟涉及,但咱們這裏感興趣的是投影,就是它將3D的對象變成了屏幕中的2D實體。那若是反向操做又是怎樣的?函數
爲何須要知道反向操做?你可能會提這樣的問題。那麼,若是你想知道你的鼠標指針下面是哪一個對象,你須要把這些2D座標從新轉換到3D座標中,而後才能肯定是選擇的那個3D對象。這叫作「反投影」。全部獲得如下兩個定義:學習
Porjection:從3D到2D的投影。spa
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的兩個擴展控件: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的功能。