示例瀏覽地址:https://ithanmang.gitee.io/threejs/home/201807/20180703/02-raycasterDemo.html
雙擊鼠標左鍵選中模型並顯示信息。 首先,解釋一下三種坐標系的概念:場景坐標系(世界坐標系)、屏幕坐標系、視點坐標系。 場景坐標 通過three.js構建出來的場景,都具有一個固定不變的坐標系(無論相機的位置在哪),並且放置的任何物體都要以這個坐標系來確定自己的位置,也就是(0,0, 0)坐標。例如我們創建一個場景並添加箭頭輔助。 屏幕坐標 在顯示屏上的坐標就是屏幕坐標系。 如下圖所示,其中的clientX和clientY的最值由,window.innerWidth,window.innerHeight決定。 視點坐標 視點坐標系就是以相機的中心點為原點,但是相機的位置,也是根據世界坐標系來偏移的,webGL會將世界坐標先變換到視點坐標,然后進行裁剪,只有在視線范圍(視見體)之內的場景才會進入下一階段的計算 如下圖添加了相機輔助線. 射線檢測 若想獲取鼠標點擊的物體,name就需要把屏幕坐標系轉換為three.js中的三維坐標系。 three.js提供了一個類THREE.Raycaster可以用來解決這個問題。 看個示例圖 THREE.Raycaster THREE.Raycaster對象從屏幕上的點擊位置向場景中發射一束光線。 // 計算出鼠標經過的3d空間中的對象 Raycaster( origin, direction, near, far ) { } 1 參數 origin — 射線的起點向量。 direction — 射線的方向向量,應該歸一化。 near — 所有返回的結果應該比 near 遠。Near不能為負,默認值為0。 far — 所有返回的結果應該比 far 近。Far 不能小於 near,默認值為無窮大。 2 方法 setFromCamera 用新的原點和方向來更新射線 方法名 .setFromCamera(coords : Vector2, camera : Camera ) : null 參數 coords - 鼠標的二維坐標,在歸一化的設備坐標(NDC)中,也就是X 和 Y 分量應該介於 -1 和 1 之間。 camera - 射線起點處的相機,即把射線起點設置在該相機位置處。 **intersectObject ** 來判斷指定對象有沒有被這束光線擊中,返回被擊中對象的信息,相交的結果會以一個數組的形式返回,其中的元素依照距離排序,越近的排在越前。 方法名 .intersectObject ( object, recursive : Boolean, optionalTarget : Array ) : Array 參數 object - 檢測與射線相交的物體 recursive- 若為 true 則檢查后代對象,默認值為false optionalTarget - (可選參數)用來設置方法返回的設置結果。若不設置則返回一個實例化的數組。如果設置,必須在每次調用之前清除這個數組(例如,array.length= 0;) 返回值 Array [ { distance, point, face, faceIndex, object }, … ] distance - 射線的起點到相交點的距離 point - 在世界坐標中的交叉點 face -相交的面 faceIndex - 相交的面的索引 object - 相交的對象 uv - 交點的二維坐標 當計算這個對象是否和射線相交時,Raycaster 把傳遞的對象委托給 raycast 方法。 這允許 meshes 對於光線投射的響應可以不同於 lines 和 pointclouds. 注意,對於網格,面(faces)必須朝向射線原點,這樣才能被檢測到;通過背面的射線的交叉點將不被檢測到。 為了光線投射一個對象的正反兩面,你得設置 material 的 side 屬性為 THREE.DoubleSide **intersectObjects ** intersectObjects 與 intersectObject 類似,除了傳入的參數是一個數組之外,並無大的差別。 方法名 .intersectObjects ( objects : Array, recursive : Boolean, optionalTarget : Array ) : Array objects - 傳入的參數。 3 主要代碼 // 獲取與射線相交的對象數組 function getIntersects(event) { event.preventDefault(); console.log("event.clientX:"+event.clientX) console.log("event.clientY:"+event.clientY) // 聲明 raycaster 和 mouse 變量 var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); // 通過鼠標點擊位置,計算出 raycaster 所需點的位置,以屏幕為中心點,范圍 -1 到 1 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; //通過鼠標點擊的位置(二維坐標)和當前相機的矩陣計算出射線位置 raycaster.setFromCamera(mouse, camera); // 獲取與raycaster射線相交的數組集合,其中的元素按照距離排序,越近的越靠前 var intersects = raycaster.intersectObjects(scene.children); //返回選中的對象數組 return intersects; }
導入外部模型注意事項 // 獲取與raycaster射線相交的數組集合,其中的元素按照距離排序,越近的越靠前 var intersects = raycaster.intersectObjects(scene.children); 上面的raycaster.intersectObjects()的參數是scene.children,因為這里是測試的模型,沒有涉及到外部模型的導入,但是再開發的時候我們一般都是對外部模型進行處理。 首先,你通過加載器把模型加載到場景中的時候需要在回調函數中打印一下加載進來的是一個什么對象,有可能是一個Mesh或者Group當然大部分模型資源基本上都是Group但是不排除還有別的類型例如Scene、Object… 此時,我們不能盲目的去直接把整個scene.children中的東西都放到raycaster.intersectObjects()來直接進行檢測,因為整個scene.children中可能有一另一個scene或者是three.js不能識別的對象,所以我們需要先對加載進來的對象進行處理; 最好是先創建一個組對象new THREE.Group(),然后用這個組里面的對象來進行射線檢測; 看下上面方法的第一個參數,是一個 Array,Group.children也是一個數組,所以我們可以把需要進行射線檢測的物體放進一個組對象里面,便於處理; raycaster.intersectObjects(group.children); 元素按照距離排序,越近的越靠前 這句話的意思是,首先,點擊或者觸發方法創建THREE.Raycaster()對象,然后從點擊位置,發出一條射線,先被射線穿過的對象,會在數組中排序靠前。 例如我們從y軸對着球點擊,然后看一下返回的數組: 返回了三個Mesh對象,因為這三個物體同時被從鼠標點擊處發出的射線給穿透,因此都被返回,而球幾何體離點擊的位置最近,所以第一個元素就是球體。 4 動態創建DIV 部分代碼 // 更新div的位置 function renderDiv(object) { // 獲取窗口的一半高度和寬度 var halfWidth = window.innerWidth / 2; var halfHeight = window.innerHeight / 2; // 逆轉相機求出二維坐標 var vector = object.position.clone().project(camera); // 修改 div 的位置 $("#label").css({ left: vector.x * halfWidth + halfWidth, top: -vector.y * halfHeight + halfHeight - object.position.y }); // 顯示模型信息 $("#label").text("name:" + object.name); } 這需要將場景坐標,轉換成二維屏幕坐標。 首先,我們需要得到當前點在世界中的坐標位置,如果是某個場景組Group里面的模型的位置坐標那種,我們可以通過模型的方法localToWorld方法獲取到世界坐標。 localToWorld方法名 .localToWorld ( vector : Vector3 ) : Vector3 作用:將矢量從本地空間坐標轉換為世界坐標。\ 求出二維坐標 // 逆轉相機求出二維坐標 var vector = object.position.clone().project(camera); 修改DIV的位置 通過求出的二維坐標,來計算位置。 left: vector.x * halfWidth + halfWidth, top: -vector.y * halfHeight + halfHeight - object.position.y
示例完整代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>點擊事件</title> <style> body { margin: 0; overflow: hidden; } #label { position: absolute; padding: 10px; background: rgba(255, 255, 255, 0.6); line-height: 1; border-radius: 5px; } </style> <script src="../../libs/build/three.js"></script> <script src="../../libs/jquery-1.9.1.js"></script> <script src="../../libs/examples/js/Detector.js"></script> <script src="../../libs/examples/js/controls/TrackballControls.js"></script> <script src="../../libs/examples/js/libs/dat.gui.min.js"></script> <script src="../../libs/examples/js/libs/stats.min.js"></script> </head> <body> <div id="WebGL-output"></div> <div id="Stats-output"></div> <div id="label"></div> <script> var stats = initStats(); var scene, camera, renderer, controls, light, selectObject; // 場景 function initScene() { scene = new THREE.Scene(); } // 相機 function initCamera() { camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000); camera.position.set(0, 400, 600); camera.lookAt(new THREE.Vector3(0, 0, 0)); } // 渲染器 function initRenderer() { if (Detector.webgl) { renderer = new THREE.WebGLRenderer({antialias: true}); } else { renderer = new THREE.CanvasRenderer(); } renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x050505); document.body.appendChild(renderer.domElement); } // 初始化模型 function initContent() { var helper = new THREE.GridHelper(1200, 50, 0xCD3700, 0x4A4A4A); scene.add(helper); var cubeGeometry = new THREE.BoxGeometry(100, 100, 100); var cubeMaterial = new THREE.MeshLambertMaterial({color: 0x9370DB}); var cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.y = 50; cube.name = "cube"; scene.add(cube); var sphereGeometry = new THREE.SphereGeometry(50, 50, 50, 50); var sphereMaterial = new THREE.MeshLambertMaterial({color: 0x3CB371}); var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); sphere.position.x = 200; sphere.position.y = 50; sphere.name = "sphere"; // sphere.position.z = 200; scene.add(sphere); var cylinderGeometry = new THREE.CylinderGeometry(50, 50, 100, 100); var cylinderMaterial = new THREE.MeshLambertMaterial({color: 0xCD7054}); var cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial); cylinder.position.x = -200; cylinder.position.y = 50; cylinder.name = "cylinder"; // cylinder.position.z = -200; scene.add(cylinder); } // 鼠標雙擊觸發的方法 function onMouseDblclick(event) { // 獲取 raycaster 和所有模型相交的數組,其中的元素按照距離排序,越近的越靠前 var intersects = getIntersects(event); // 獲取選中最近的 Mesh 對象 if (intersects.length != 0 && intersects[0].object instanceof THREE.Mesh) { selectObject = intersects[0].object; changeMaterial(selectObject); } else { alert("未選中 Mesh!"); } } // 獲取與射線相交的對象數組 function getIntersects(event) { event.preventDefault(); console.log("event.clientX:"+event.clientX) console.log("event.clientY:"+event.clientY) // 聲明 raycaster 和 mouse 變量 var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); // 通過鼠標點擊位置,計算出 raycaster 所需點的位置,以屏幕為中心點,范圍 -1 到 1 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; //通過鼠標點擊的位置(二維坐標)和當前相機的矩陣計算出射線位置 raycaster.setFromCamera(mouse, camera); // 獲取與射線相交的對象數組,其中的元素按照距離排序,越近的越靠前 var intersects = raycaster.intersectObjects(scene.children); //返回選中的對象 return intersects; } // 窗口變動觸發的方法 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // 鍵盤按下觸發的方法 function onKeyDown(event) { switch (event.keyCode) { case 13: initCamera(); initControls(); break; } } // 改變對象材質屬性 function changeMaterial(object) { var material = new THREE.MeshLambertMaterial({ color: 0xffffff * Math.random(), transparent: object.material.transparent ? false : true, opacity: 0.8 }); object.material = material; } // 初始化軌跡球控件 function initControls() { controls = new THREE.TrackballControls(camera, renderer.domElement); // controls.noRotate = true; controls.noPan = true; } // 初始化燈光 function initLight() { light = new THREE.SpotLight(0xffffff); light.position.set(-300, 600, -400); light.castShadow = true; scene.add(light); scene.add(new THREE.AmbientLight(0x5C5C5C)); } // 初始化 dat.GUI function initGui() { // 保存需要修改相關數據的對象 gui = new function () { } // 屬性添加到控件 var guiControls = new dat.GUI(); } // 初始化性能插件 function initStats() { var stats = new Stats(); stats.domElement.style.position = 'absolute'; stats.domElement.style.left = '0px'; stats.domElement.style.top = '0px'; document.body.appendChild(stats.domElement); return stats; } // 更新div的位置 function renderDiv(object) { // 獲取窗口的一半高度和寬度 var halfWidth = window.innerWidth / 2; var halfHeight = window.innerHeight / 2; // 逆轉相機求出二維坐標 var vector = object.position.clone().project(camera); // 修改 div 的位置 $("#label").css({ left: vector.x * halfWidth + halfWidth, top: -vector.y * halfHeight + halfHeight - object.position.y }); // 顯示模型信息 $("#label").text("name:" + object.name); } // 更新控件 function update() { stats.update(); controls.update(); controls.handleResize(); } // 初始化 function init() { initScene(); initCamera(); initRenderer(); initContent(); initLight(); initControls(); initGui(); addEventListener('dblclick', onMouseDblclick, false); addEventListener('resize', onWindowResize, false); addEventListener('keydown', onKeyDown, false); } function animate() { if (selectObject != undefined && selectObject != null) { renderDiv(selectObject); } requestAnimationFrame(animate); renderer.render(scene, camera); update(); } init(); animate(); </script> </body> </html>