在本篇随笔中,我们学习下什么是对象选择,投影和反投影是如何工作的,怎样使用Three.js构建可使用鼠标和对象交互的应用。例如当鼠标移到对象,对象变成红色,鼠标移走,对象又恢复原来的颜色。
本篇随笔的源代码来自于:https://github.com/sole/three.js-tutorials/tree/master/object_picking
这里还有更多的例子可供参考:
和立方体交互,当你在立方体盒子上点击,鼠标和立方体交互的点会出现一个黑点;
画布交互,你可以增加方格到画布上,也可以移出它;
当你在操作这些例子,会防线它们有一个共同特性。我们使用的2D坐标系(屏幕)检测3D空间中的对象。这就是对象的选择。
1.如何工作
在写代码之前,了解计算机3D图形是如何工作是非常有帮助的,即使是非常粗糙的方式。我们如何从抽象的3D场景映射到我们屏幕中的2D图像?
当你使用相机渲染场景时,一大堆数据机制开始对3D场景进行运算和处理,以便生成可从相机看到的场景片段的2D表示。有许多步骤涉及,但我们这里感兴趣的是投影,就是它将3D的对象变成了屏幕中的2D实体。那如果反向操作又是怎样的?
为什么需要知道反向操作?你可能会提这样的问题。那么,如果你想知道你的鼠标指针下面是哪个对象,你需要把这些2D坐标重新转换到3D坐标中,然后才能确定是选择的那个3D对象。这叫做“反投影”。所有得到以下两个定义:
Porjection:从3D到2D的投影。
UnProjection:反投影,从2D反向投影到3D。
这里还许绍一个步骤:一旦我们反向投影到3D坐标中,我们怎么确认是否选中了某个对象?答案是:投射光线。我们从3D鼠标的位置投射一根光线,沿着相机的当前方向,看射线是否投射到任何对象上。如果是,那么我们就选中了某个对象。没有,那么我们就没选中如何对象。
听起来有些疑惑。我们看看下面的图片:
图片中,左边是一个抽象的3D场景,包含两个立方体和一个金字塔。中间代表了我们的屏幕。在屏幕上可看到我们的目标位置。右边是我们视线角度的摄像头,另外还有一条蓝色射线,使用它来选择对象。
以上的介绍,让我们简单了解了选择对象的理论原理,接下来我们看看在three.js中是如何体现这样的过程。
对象选择代码实现
在thres.js中实现这种的功能是非常简单的。我们先创建场景、渲染器、摄像头等:
var container = document.getElementById( ‘container‘ ), containerWidth, containerHeight, renderer, scene, camera; containerWidth = container.clientWidth; containerHeight = container.clientHeight; renderer = new THREE.CanvasRenderer(); renderer.setSize( containerWidth, containerHeight ); container.appendChild( renderer.domElement ); renderer.setClearColorHex( 0xeeeedd, 1.0 ); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 45, containerWidth / containerHeight, 1, 10000 ); camera.position.set( 0, 0, range * 2 ); camera.lookAt( new THREE.Vector3( 0, 0, 0 ) );
上面的代码都非常简单,没什么可介绍的。接着,我们添加一些对象。我们创建灰色立方体并且把他们随机设置他们的3D坐标。我把这些所有的对象都存放在一个类型为Object3D对象中。
geom = new THREE.CubeGeometry( 5, 5, 5 ); cubes = new THREE.Object3D(); scene.add( cubes ); for(var i = 0; i < 100; i++ ) { var grayness = Math.random() * 0.5 + 0.25, mat = new THREE.MeshBasicMaterial(), cube = new THREE.Mesh( geom, mat ); mat.color.setRGB( grayness, grayness, grayness ); cube.position.set( range * (0.5 - Math.random()), range * (0.5 - Math.random()), range * (0.5 - Math.random()) ); cube.rotation.set( Math.random(), Math.random(), Math.random() ).multiplyScalar( 2 * Math.PI ); cube.grayness = grayness; // *** NOTE THIS cubes.add( cube ); }
所有的集合对象都使用同一个材质,它这些材质的颜色是不同的,每个立方体都设置了一种随机的灰度颜色。接下来,我们准备两个关键对象:投影对象、鼠标坐标。
projector = new THREE.Projector(); mouseVector = new THREE.Vector3();
当鼠标移动时,我们想选择对象。所以需要监听mousemove事件:
window.addEventListener( ‘mousemove‘, onMouseMove, false );
然后,所有感兴趣的功能都会在这个事件里边实现。当查看源代码使,你需要特别小心下边两行代码,这两天代码稍有差错,可能我们后面的选择功能将无法实现:
mouseVector.x = 2 * (e.clientX / containerWidth) - 1; mouseVector.y = 1 - 2 * ( e.clientY / containerHeight );
这两行代码将鼠标坐标转换为 x、y范围在(-1, 1)的笛卡尔坐标。你可能主要到计算的y坐标为什么是负的?那是因为经典的DOM坐标系原点(0,0)是从左上角开始。往右是x轴,往下是y坐标。但笛卡尔坐标的却如下所示:
理解了这两个坐标系,上面的代码你就知道为什么会那样写了。
现在我们将使用mouseVector和camera生成一个射线对象:
var raycaster = projector.pickingRay( mouseVector.clone(), camera );
这里我们克隆了mouseVector,而表示直接传递它。那是因为pickingRay函数内部会修改mouseVector的值,你可以查看Projector.js源代码看看,是否真的有修改。创建raycaster对象之后,我们调用它的intersectObjects函数:
var intersects = raycaster.intersectObjects( cubes.children );
传递的参数为cubes.chidren,也就是说我们要选择的对象来自于cubes的children中。intersects将返回查询一个选中对象的集合。并且某个对象包含了以下属性:
distance:摄像头和对象有距离。
point:在对象上表面上和射线交互的点的位置。
face:对象和射线交互的面。
object:和射线交互的对象。
既然已经获取到这些对象了,那么我们也可以操作这些对象。首选我们把所有对象的颜色复原为之前设置的灰色:
cubes.children.forEach(function( cube ) { cube.material.color.setRGB( cube.grayness, cube.grayness, cube.grayness ); });
接着我们再设置交互的对象。把这些对象的颜色设置成红色。
for( var i = 0; i < intersects.length; i++ ) { var intersection = intersects[ i ], obj = intersection.object; obj.material.color.setRGB( 1.0 - i / intersects.length, 0, 0 ); }
以上就是OnMouseMove函数的所有代码了,通过这些代码我们初步了解了选择对象操作,其实我们要写的代码很少,three.js已经帮我们实现了具体的步骤。
涉及到的鼠标操作功能很多,选择对象是最基础的,万变不离其宗。像对象的拖动功能,选择也是基础功能。接下来我们就再看看three.js是如何实现拖拽功能的。
three.js实现拖拽功能
实现拖拽功能,主要使用了three.js的两个扩展控件:TrackballControls和DragControls。
首先,我们先创建随机位置的200个立方体:
var objects = []; var geometry = new THREE.BoxGeometry(40, 40, 40); for(var i = 0; i < 200; i++){ var object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); object.position.set(Math.random() * 1000 - 500, Math.random() * 600 - 300, Math.random() * 800 - 400); object.rotation.set(Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI); object.scale.set(Math.random() * 2 + 1, Math.random() * 2 + 1, Math.random() * 2 + 1); object.castShadow = true; object.receiveShadow = true; scene.add(object); objects.push(object); }
为了然各个立方体显示随机,每个object都使用Math.random()函数随机设置了position、rotation、scale。并且对象可产生投影可接收投影。
接下来我们创建刚才提到的两个控件:
var controls = new THREE.TrackballControls(camera); controls.rotateSpeed = 1.0; controls.zoomSpeed = 1.2; controls.panSpeed = 0.8; controls.noZoom = false; controls.noPan = false; controls.staticMoving = true; controls.dynamicDampingFactor = 0.3; var dragControls = new THREE.DragControls(objects, camera, webGLRenderer.domElement);
TrackballControls可用来通过旋转移动摄像头位置,实习整个场景的旋转和移动。DragControls包含两个事件:dragstart、dragend。
dragControls.addEventListener("dragstart", function(event){ currentColor = event.object.material.color; event.object.material.color = new THREE.Color(0xffff00); event.object.material.transparent = true; event.object.material.opacity = 0.6; controls.enabled = false; }); dragControls.addEventListener("dragend", function(event){ event.object.material.opacity = 1.0; event.object.material.color = currentColor; controls.enabled = true; });
dragStart事件表示开始执行拖拽了,而dragend表示拖拽结束。可通过event.object获取当前拖拽的对象,然后就可以设置对象的属性了。这里需要特别注意的是,在拖拽开始时,我们需要禁止TrackballControls功能,才能够拖动物体。所以需要设置controls.enabled = false。当拖动结束,设置controls.enabled = true恢复TrackballControls的功能。