版权声明:此文首发于我的个人博客Keyon Y,转载请注明出处。
对场景内的模型添加事件监听,实现鼠标交互,需要用到
拾取物体的原理
webGL中获取鼠标交互物体的原理:通过三维空间中相机视点与鼠标在屏幕上的位置的连线,形成一条直线,捕获与此直线相交的空间中的物体,即为交互对象物体。
在three中,
获取鼠标所在位置的物体
先放代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //... constructor: function () { INTERSECTED: null // 暂存射线相交的物体(交互的模型对象) mouse = new THREE.Vector2(); } //... /** * 作者: Keyon * 功能: 渲染时的回调 * 描述: * @param mesh {Object} Object3D、Mesh、Group均可,可不传默认使用初始化实例时传入的mesh对象 * @returns {Null} 默认返回null */ render: function (mesh = null) { if (!mesh) mesh = this.mesh; // 通过摄像机和鼠标位置更新射线 this.raycaster.setFromCamera( this.mouse, app.camera ); // 计算物体和射线的焦点 let intersects = this.raycaster.intersectObject( mesh, true ); if ( intersects.length > 0 ) { if ( this.INTERSECTED !== intersects[ 0 ].object ) { if ( this.INTERSECTED ) this.INTERSECTED.material.emissive.setHex( this.INTERSECTED.currentHex ); // 记录当前对象 this.INTERSECTED = intersects[ 0 ].object; // 记录当前对象本身颜色 this.INTERSECTED.currentHex = this.INTERSECTED.material.emissive.getHex(); // 设置颜色为灰色 this.INTERSECTED.material.emissive.setHex( 0x333333 ); } } else { // 恢复上一个对象颜色并置空变量 if ( this.INTERSECTED ) this.INTERSECTED.material.emissive.setHex( this.INTERSECTED.currentHex ); this.INTERSECTED = null; } }, |
下面来解释一下
Raycaster 类的.intersectObject() 方法:检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。- 对物体进行材质变换,调整
emissive (材质的自发光)的Hex颜色,实现hover类型的交互效果。
鼠标坐标的转换
先上最终的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | onMouseMove: function (event) { event.preventDefault(); // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1) // renderer为three的渲染器 let px = renderer.domElement.getBoundingClientRect().left; let py = renderer.domElement.getBoundingClientRect().top; this.mouse.x = ( (event.clientX - px) / (renderer.domElement.offsetWidth) ) * 2 - 1; this.mouse.y = - ( (event.clientY - py) / (renderer.domElement.offsetHeight) ) * 2 + 1; }, /* * 作者: Keyon * 功能: 添加鼠标移动监听,拾取物体 * 描述: * @param callback {Function} 拾取物体的回调,参数是拾取的mesh * @returns {Null} 默认返回null */ listenOnMouseMove: function (callback = null) { if (typeof callback === 'function') callback(); let fn = (e) => { this.onMouseMove(e); }; window.addEventListener( 'mousemove', fn, false ); } |
添加鼠标移动监听,就是一个很常见的监听事件,不做解释。
重点是将鼠标坐标转化成设备坐标,从而影响三维空间中发射射线方向的问题。
关于鼠标位置转换,官方案例中给出的代码,是如下这样的:
1 2 | this.mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; this.mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; |
这样的计算方式会有个问题,如果canvas元素并不是全屏的,即canvas元素的
坐标转换原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //得到 mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; 推导过程: 设A点为点击点(x1,y1),x1=e.clintX, y1=e.clientY 设A点在世界坐标中的坐标值为B(x2,y2); 由于A点的坐标值的原点是以屏幕左上角为(0,0); 我们可以计算可得以屏幕中心为原点的B'值 x2' = x1 - innerWidth/2 y2' = innerHeight/2 - y1 又由于在世界坐标的范围是[-1,1],要得到正确的B值我们必须要将坐标标准化 x2 = (x1 -innerWidth/2)/(innerwidth/2) = (x1/innerWidth)*2-1 同理得 y2 = -(y1/innerHeight)*2 +1 |
具体推导过程可参考:
threejs对象拾取
提炼出来就是
1 2 | mouse.x = (<鼠标相对于可视区域的横坐标> / <可视区域的宽>) * 2 - 1; mouse.y = -(<鼠标相对于可视区域的纵坐标> / <可视区域的高>) * 2 + 1; |
由于canvas并非全屏的元素,所以我们需要重新计算这几个值值。
- 首先,
renderer.domElement 的大小就是three场景可视区域的范围。所以通过renderer.domElement推算即可。 - 鼠标相对于可视区域的横坐标推算为
e.clientX - renderer.domElement.getBoundingClientRect().left - 可视区域的宽推算为
renderer.domElement.offsetWidth - 鼠标相对于可视区域的纵坐标推算为
e.clientY - renderer.domElement.getBoundingClientRect().top - 可视区域的高推算为
renderer.domElement.offsetHeight
那么,就得到了最终的代码。完美实现拾取物体的鼠标交互。
都看到这里了,还不给个赞,加个收藏吗~
点赞收藏的马上升职加薪[手动滑稽]
参考资料
ThreeJS中的点击与交互——Raycaster的用法
threejs对象拾取