threeJS射线拾取机制及案例
前言
在浏览器中浏览三维图形的时候,有时想要与三维图形之间做一些点击事件和交互操作,其中比较常用的一个解决方案就是使用Raycaster对象来实现(射线拾取)。
基础知识
- 世界坐标系:webGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。面对屏幕时,右边是x正轴,上面是y正轴,由屏幕内指向屏幕外的是z正轴。
- 屏幕坐标系:webGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终计算出它在显示设备上对应的位置,这个位置就称为设备坐标,也就是二维坐标。
- 视点坐标系: 是以视点(照相机)为原点,以视线的方向为Z轴正方向的坐标系。
原理
利用webGL,既可以将三维坐标换算为二维坐标,也可以将二维坐标换算成三维坐标。webGL会将世界坐标先换算到视点坐标,然后进行裁剪,只有在视线范围之内的场景才会进入下一阶段的计算。
效果图1
- 图中圆柱体是演示拾取的三维图形;
- 图中绿、红、蓝三条轴线分别代表三维坐标中的Y正轴、X正轴和Z正轴;
- 图中蓝色五角形,其实是由视点(照相机)坐标出发,以鼠标点击位置为正方向,绘制的一条箭头图形,代表绘制的射线,五角形中心即鼠标点击位置。
当鼠标点击圆柱体边缘以外时,圆柱体材质颜色没有发生变化,说明此时鼠标点击位置并没有拾取到圆柱体。
效果图2
当鼠标点击圆柱体边缘以外时,圆柱体材质颜色发生了变化,说明此时鼠标点击位置已经拾取到了圆柱体,并在控制台打印出了圆柱体的mesh对象,并可以对它进行操作。
演示代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>钉钉分享</title> <!--threejs版本是71版本--> <script src="three.js"></script> </head> <body style="margin: 0;overflow: hidden;"> <!-- 输出容器 --> <div id="webgl-output"></div> <!-- 操作代码 --> <script> // 声名Threejs对象(场景、相机、渲染器) var scene, camera, renderer; // 初始化 function init() { // 添加浏览器窗口大小监听事件(画布自适应方法onResize) window.addEventListener('resize', onResize, false); // 鼠标点击拾取 document.addEventListener('click', initRay, false); // 创建场景 scene = new THREE.Scene(); // 创建相机(设置相机位置,设置相机朝向) camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(10, 50, 50); camera.lookAt(scene.position); // 创建渲染器 renderer = new THREE.WebGLRenderer(); renderer.setClearColor(0xEEEEEE); renderer.setSize(window.innerWidth, window.innerHeight); // 创建辅助轴线 var axis = new THREE.AxisHelper(100); scene.add(axis); // 添加图形---圆柱体 let _radius = 5; var cylinderGeometry = new THREE.CylinderGeometry(_radius, _radius, 20, 40, 40); var cylinderMaterial = new THREE.MeshBasicMaterial({ color: 0x765432 }); var cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial); scene.add(cylinder); // 将图形输出到页面 document.getElementById('webgl-output').appendChild(renderer.domElement); // 逐帧绘制方法 function renderScene() { requestAnimationFrame(renderScene); renderer.render(scene, camera); } // 开启渲染 renderScene(); } // 画布自适应 function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // 射线拾取 function initRay(event) { // 获取画布 let mainCanvas = document.querySelector("#webgl-output canvas"); // 将屏幕坐标转为标准设备坐标(支持画布非全屏的情况) let x = ((event.clientX - mainCanvas.getBoundingClientRect().left) / mainCanvas.offsetWidth) * 2 - 1; // 设备横坐标 let y = -((event.clientY - mainCanvas.getBoundingClientRect().top) / mainCanvas.offsetHeight) * 2 + 1; // 设备纵坐标 let standardVector = new THREE.Vector3(x, y, 1); // 设备坐标 // 标准设备坐标转为世界坐标 let worldVector = standardVector.unproject(camera); // 射线投射方向单位向量(worldVector坐标减相机位置坐标) let ray = worldVector.sub(camera.position).normalize(); // 绘制射线 drawRay(scene, camera.position, ray); // 创建射线投射器对象 let rayCaster = new THREE.Raycaster(camera.position, ray); // 返回射线选中的对象数组(第二个参数默认值是false,意为是否遍历图形内部的所有子图形) let intersects = rayCaster.intersectObjects(scene.children, true); if (intersects.length > 0) { // 射线拾取的首个对象 let currObj = intersects[0]; console.log(currObj); // 改变被拾取对象的材质颜色(随机) let currcolor = `rgb(${Math.floor(Math.random() * 256).toString()},${Math.floor(Math.random() * 256).toString()},${Math.floor(Math.random() * 256).toString()})` currObj.object.material.color.set(currcolor); } } // 绘制射线(箭头射线) function drawRay(scene, start, dir) { let prevRay = scene.getObjectByName("customRay"); if (prevRay) { scene.remove(prevRay); } let arrow = new THREE.ArrowHelper(dir, start, 1000, 0x0000ff); arrow.name = "customRay"; scene.add(arrow); } // 初始化 window.onload = init(); </script> </body> </html>