如何实现对象交互

在本篇随笔中,我们学习下什么是对象选择,投影和反投影是如何工作的,怎样使用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的功能。

时间: 2024-10-12 15:31:56

如何实现对象交互的相关文章

C#简单的对象交互

在对象的世界里,一切皆为对象;对象与对象相互独立,互不干涉,但在一定外力的作用下对象开始共同努力 对象交互的实例 电视机大家都有吧,依照万物皆对象的思维模式来看,电视机可以是一个类,然后电视机有一些基本的方法如: 电视机类 TV 1.打开电视的方法open(); 2.关闭电视的方法Close(); 3.切换电视频道的方法change(); class TV { //电视的状态 bool flag; //打开电视的方法 public void open() { if (flag) { //如果电视

写给自己看的小设计7 - 对象设计过程之对象交互

对象创建完了以后,就是互相协作完成系统的功能.对象的协作方式通常有如下方式: 直接引用,互通有无 这种方式最为自然,最为直接,最为简单,也是通常情况下的首选.不管是传参数,还是直接创建后直接使用对象的方法,都是属于这种情况: public class ComponentB { public void Run(ComponentA componentA) { componentA.Say(); } } 依靠中介通信 当对象之间的交互复杂起来以后,直接的通信可能耦合度就太高了,这个时候要靠辅助对象来

对象交互

面向对象程序设计的第一步,就是在问题领域中识别出有效的对象,然后从识别出的对象中抽象出类来.面对纷繁复杂的现实问题,往往存在多种对象划分的方式,而不同的划分会带来类的设计以至于程序结构的各种不同.对象划分有一些理论,但是不是这门面向对象的入门课程能覆盖的.而且目前的理论也还不是放诸四海皆准的简单操作指南.我们举了一个数字钟的例子,希望通过这个例子表明对象划分的重要性,给你一个感性认识.在今后面对实际的问题领域时,可以试试用这里提到的概念来做对象的划分和类的设计.但是这只是一个例子,遇到具体情况一

NVisionXR_iOS教程六 —— 场景中对象交互

本章节将介绍如何与场景中的对象进行交互,接着上一章节的代码,我们往立方体对象 添加如下代码,并实现它的代理<HitEventDelegate>  代码:     // 创建一个立方体      NVBoxWidget *cube = [[NVBoxWidget alloc] initWithScenePlay:self WidgetName:@"cube"];      // 添加材质,传入的是对应的material 材质名      [cube setAppearance

中国MOOC_面向对象程序设计——Java语言_第2周 对象交互_1有秒计时的数字时钟

第2周编程题 查看帮助 返回 第2周编程题,在课程所给的时钟程序的基础上修改 依照学术诚信条款,我保证此作业是本人独立完成的. 温馨提示: 1.本次作业属于Online Judge题目,提交后由系统即时判分. 2.学生可以在作业截止时间之前不限次数提交答案,系统将取其中的最高分作为最终成绩. 1 有秒计时的数字时钟(10分) 题目内容: 这一周的编程题是需要你在课程所给的时钟程序的基础上修改而成.但是我们并不直接给你时钟程序的代码,请根据视频自己输入时钟程序的Display和Clock类的代码,

角色对象模式

意图 单个对象透过不同的角色对象来满足不同客户的不同需求.每一个角色对象针对不同的客户内容来扮演其角色.对象能够动态的管理其角色集合.角色作为独立的对象是的不同的内容能够简单的被分离开来,系统间的配置也变得容易. 译注:为了行文的流畅性及内容意思的准确性,尽量贴近原文使用英文单词标记特定内容, 如Customer表示客户,Client表示客户端,Component表示组件等.因为有各种图例说明,所以在图例说明时,使用原题中的英文单词对应图中内容.有时也中英文交叉使用.因为网页显示的问题,中文黑体

ES6对象扩展

前面的话 随着JS应用复杂度的不断增加,开发者在程序中使用对象的数量也在持续增长,因此对象使用效率的提升就变得至关重要.ES6通过多种方式来加强对象的使用,通过简单的语法扩展,提供更多操作对象及与对象交互的方法.本章将详细介绍ES6对象扩展 对象类别 在浏览器这样的执行环境中,对象没有统一的标准,在标准中又使用不同的术语描述对象,ES6规范清晰定义了每一个类别的对象,对象的类别如下 1.普通(Ordinary)对象 具有JS对象所有的默认内部行为 2.特异(Exotic)对象 具有某些与默认行为

第12讲——对象和类

[抽象和类] 引言:生活中充满复杂性,处理复杂性的方法之一是简化和抽象.如果我们要用信息与用户之间的的接口来表示计算,那么抽象将是至关重要的.也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案.在上一讲的垒球统计数据示例中,接口描述了用户如何初始化.更新和显示数据.抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽奖接口的类设计. 1.1  类型是什么 当我们看到一个给定的基本数据类型,我们会想到: 它定义的数据对象需要的内存: 它定义的数据对象能执行的操作: 它决定如

中介者模式-对象行为型

原理 用一个中介对象来封装一系列的对象交互.中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互.中介者模式又称为调停者模式. 1)迪米特法则的一个典型应用:在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代.因此,中介者模式就是迪米特法则的一个典型应用.2) 通过引入中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,中介者承担了中转