如何实现对象交互

    在本篇随笔中,我们学习下什么是对象选择,投影和反投影是如何工作的,怎样使用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鼠标的位置投射一根光线,沿着相机的当前方向,看射线是否投射到任何对象上。如果是,那么我们就选中了某个对象。没有,那么我们就没选中如何对象。

    听起来有些疑惑。我们看看下面的图片:

    firing rays!

    图片中,左边是一个抽象的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 );
}

    所有的集合对象都使用同一个材质,它这些材质的颜色是不同的,每个立方体都设置了一种随机的灰度颜色。接下来,我们准备两个关键对象:射线对象、鼠标坐标。

var raycaster = new THREE.Raycaster();
var 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坐标。但笛卡尔坐标的却如下所示:

    image

    理解了这两个坐标系,上面的代码你就知道为什么会那样写了。

    现在我们将使用mouseVector和camera生成具体的摄像方向:

raycaster.setFromCamera(mouseVector.clone(), camera);

    这里我们克隆了mouseVector,而表示直接传递它。那是因为setFromCamera函数内部会修改mouseVector的值,你可以查看three.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的功能。

posted @ 2017-05-07 16:07  heavi  阅读(...)  评论(... 编辑 收藏