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
- const raycaster = new THREE.Raycaster();
- const mouse = new THREE.Vector2();
- function onMouseMove( event ) {
- // 这里实现了DOM坐标系到NDC坐标系的转换
- mouse.x = ( event.clientX / window.innerWidth ) * 2 – 1;
- mouse.y = – ( event.clientY / window.innerHeight ) * 2 + 1;
- }
- function render() {
- // 从相机位置,发出一条射线
- raycaster.setFromCamera( mouse, camera );
- // 检测相机发出射线相交的obj列表
- const intersects = raycaster.intersectObjects( scene.children );
- for ( let i = 0; i < intersects.length; i ++ ) {
- // 将相交物体的材质颜色设置为红色
- intersects[ i ].object.material.color.set( 0xff0000 );
- }
- renderer.render( scene, camera );
- }
- window.addEventListener( 'mousemove', onMouseMove, false );
- window.requestAnimationFrame(render);
c、观察者模式:与dom事件机制类似,观察者模式非常是适合做这种注册-触发机制的。
3、具体实现:
考虑到每次遍历intersects数组非常的不方便,特别是当场景中有实际上百个Object3D的时候。所以这里我们定义一个全局的对象存储注册事件,然后修改Object3D的原型链,增加on和on和on和off方法,来实现类似于DOM元素的事件注册和销毁。
- const globalEvent = {click: {}}
- Object.assign(Object3D.prototype, {
- $on(eventType, cb) {
- if(globalEvent.hasOwnProperty(eventType)) {
- globalEvent[eventType][this.id] = {
- object3d: this,
- callback: cb
- };
- } else { // error warn}
- }
- $off(eventType) {
- if (!eventType) throw new Error('')
- if(globalEvent.hasOwnProperty(eventType)) {
- delete globalEvent[eventType][this.id]
- } else {
- throw new Error('')
- }
- }
- })
- init(camera)
- function init(camera, container) {
- let intersectPoint, obj, mouseX, mouseY, clicked;
- const targetObj = globalEvent.click
- const rayCaster = new Raycaster();
- function down(e) {
- obj = null;
- e.preventDefault();
- mouseX = event.clientX;
- mouseY = event.clientY;
- if (!globalEvent.click) return;
- rayCaster.setFromCamera(
- new Vector2(
- (mouseX / window.innerWidth) * 2 – 1,
- -(mouseY / window.innerHeight) * 2 + 1
- ),
- camera
- );
- let intersects = rayCaster.intersectsObjects(getVisibleList(targetObj));
- if (intersects.length > 0) {
- if (clicked) {
- obj = null;
- return;
- }
- clicked = true;
- obj = intersects[0].object;
- intersectPoint = intersects[0].point;
- } else {
- clicked = false;
- }
- }
- function move(e) {
- event.preventDefault();
- // 这里针对移动端做一些优化
- }
- function up(e) {
- event.preventDefault();
- if (clicked && !!obj && obj.callback) {
- obj.callback(obj.object3d, intersectPoint);
- }
- clicked = false
- } 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});}
- function getVisibleList(targetObj) {
- const list = []
- for (const key in targetObj) {
- const target = targetObj[key].object3d;
- if (target.visible) list.push(target);
- }
- return list
- }
- /*
- 使用方式:直接在mesh上注册事件
- target: 命中物体
- point: 命中点的三维坐标
- */
- mesh.$on('click', (target, point)) {
- }