分析Webgl & Three.js中的物体拾取

1、引子:

在传统的web开发中,由于存在DOM树以及事件捕获冒泡等机制,我们可以很方便的在某个DOM节点上注册事件,并且执行父元素事件代理等一系列操作。但是在webgl的三维世界里,用户使用鼠标或者touch事件,事件接收方是canvas容器,如何将这种点击行为映射到三维世界,就需要借助三维世界的能力了,并且建立从canvas平面容器到三维世界的桥梁,进行所谓的物体拾取

2、基础知识:

DOM与NDC坐标转换,相机射线,观察者模式。熟悉这部分知识的同学,可以直接跳过到下面的代码实现。

  • a、DOM坐标和NDC坐标转换:在这里,我们知道DOM坐标的(0,0)点在容器左上角,NDC坐标的(0,0)点在容器中心,需要进行坐标转换。具体定义可以参考下面两个链接

三维坐标变换 屏幕坐标定义

  • b、相机射线: 根据Three.js的官方定义,射线就是用来做物体拾取的机制。相比较于传统的颜色拾取,射线拾取可以识别多个物体,得到先后顺序,在使用上更加方便并且符合人的直觉。? 下面看一个官方的code example?

javascript


  1. const raycaster = new THREE.Raycaster(); 
  2. const mouse = new THREE.Vector2(); 
  3. function onMouseMove( event ) { 
  4.     // 这里实现了DOM坐标系到NDC坐标系的转换 
  5.     mouse.x = ( event.clientX / window.innerWidth ) * 2 – 1; 
  6.     mouse.y = – ( event.clientY / window.innerHeight ) * 2 + 1; 
  7. function render() { 
  8.     // 从相机位置,发出一条射线 
  9.     raycaster.setFromCamera( mouse, camera ); 
  10.     // 检测相机发出射线相交的obj列表 
  11.     const intersects = raycaster.intersectObjects( scene.children ); 
  12.     for ( let i = 0; i < intersects.length; i ++ ) { 
  13.         // 将相交物体的材质颜色设置为红色 
  14.         intersects[ i ].object.material.color.set( 0xff0000 ); 
  15.     } 
  16.     renderer.render( scene, camera ); 
  17. window.addEventListener( 'mousemove', onMouseMove, false ); 
  18. window.requestAnimationFrame(render); 

c、观察者模式:与dom事件机制类似,观察者模式非常是适合做这种注册-触发机制的。

3、具体实现:

考虑到每次遍历intersects数组非常的不方便,特别是当场景中有实际上百个Object3D的时候。所以这里我们定义一个全局的对象存储注册事件,然后修改Object3D的原型链,增加on和on和on和off方法,来实现类似于DOM元素的事件注册和销毁。


  1. const globalEvent = {click: {}} 
  2. Object.assign(Object3D.prototype, { 
  3.     $on(eventType, cb) { 
  4.        if(globalEvent.hasOwnProperty(eventType)) { 
  5.             globalEvent[eventType][this.id] = { 
  6.                 object3d: this, 
  7.                 callback: cb 
  8.             }; 
  9.        } else { // error warn} 
  10.     } 
  11.     $off(eventType) { 
  12.         if (!eventType) throw new Error(''
  13.         if(globalEvent.hasOwnProperty(eventType)) { 
  14.             delete globalEvent[eventType][this.id] 
  15.         } else { 
  16.             throw new Error(''
  17.         } 
  18.     } 
  19. }) 
  20. init(camera) 
  21. function init(camera, container) { 
  22.     let intersectPoint, obj, mouseX, mouseY, clicked; 
  23.     const targetObj = globalEvent.click 
  24.     const rayCaster = new Raycaster(); 
  25.     function down(e) { 
  26.         obj = null
  27.         e.preventDefault(); 
  28.         mouseX = event.clientX; 
  29.         mouseY = event.clientY; 
  30.         if (!globalEvent.click) return
  31.         rayCaster.setFromCamera( 
  32.             new Vector2( 
  33.                 (mouseX / window.innerWidth) * 2 – 1, 
  34.                 -(mouseY / window.innerHeight) * 2 + 1 
  35.             ), 
  36.             camera 
  37.         );  
  38.         let intersects = rayCaster.intersectsObjects(getVisibleList(targetObj)); 
  39.         if (intersects.length > 0) { 
  40.             if (clicked) { 
  41.                 obj = null
  42.                 return
  43.             } 
  44.             clicked = true
  45.             obj = intersects[0].object; 
  46.             intersectPoint = intersects[0].point;                         
  47.         } else { 
  48.             clicked = false
  49.         } 
  50.     } 
  51.     function move(e) { 
  52.         event.preventDefault(); 
  53.         // 这里针对移动端做一些优化  
  54.     } 
  55.     function up(e) { 
  56.         event.preventDefault(); 
  57.         if (clicked && !!obj && obj.callback) { 
  58.             obj.callback(obj.object3d, intersectPoint); 
  59.         } 
  60.         clicked = false 
  61.     }    const eventOption = {      passive: false    };    container.addEventListener('mousedown', down, {passive: false});    container.addEventListener('mousemove'move, {passive: false});    container.addEventListener('mouseup', up, {passive: false});    container.addEventListener('touchstart', down, {passive: false});    container.addEventListener('touchmove'move, {passive: false});    container.addEventListener('touchend', up, {passive: false});} 
  62.  
  63. function getVisibleList(targetObj) { 
  64.     const list = [] 
  65.     for (const key in targetObj) { 
  66.         const target = targetObj[key].object3d; 
  67.         if (target.visible) list.push(target); 
  68.     } 
  69.     return list 
  70.  
  71. /* 
  72.     使用方式:直接在mesh上注册事件 
  73.     target: 命中物体 
  74.     point: 命中点的三维坐标 
  75. */ 
  76. mesh.$on('click', (target, point)) { 
  77.  
【声明】:芜湖站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。

相关文章