Away3D引擎学习笔记(三)模型拾取(翻译)
原文详见http://away3d.com/tutorials/Introduction_to_Mouse_Picking。本文若有翻译不对的地方,敬请指出。
本教程详细介绍了Away3D 4.x中鼠标交互问题。
内容:
n 介绍
n Hello Picking
n Entity属性
n View属性
n UV绘制
n 总结
介绍
每个3D引擎都需要解决一个非常基础的问题:鼠标下面是什么?在3D图形学中,这通常会涉及到拾取。虽然这个问题看似简单、直接,但它实际上涉及到较难的数学和非常高效的算法。与2D拾取相比,这个问题确实会更复杂。在3D场景中检测光标下面是哪个物体可能是个编程造价昂贵的工作。但别着急!考虑到Flash Player的能力和瓶颈,Away3D为处理这个问题提供了一系列精心设计的特性,为每个特殊的实现给予使用者充分的灵活性去寻求拾取的精度和性能间的平衡点。
本文我们将会探究引擎的拾取特性。我们将会知道拾取用法的简便性,并将理解在一些高级苛刻的项目中如何掌控这种细微差别,从而达到高效的实现。
Hello Picking
单刀直入,进入话题。请看下面的app示例和示例下的代码。
示例1(请点击图片进行加载app)。3D场景中基本的鼠标交互。悬停在物体上方,物体材料会从灰色变成红色。源码。
示例中关键代码如下所示:
anObj.mouseEnabled=true; anObj.addEventListener(MouseEvent3D.MOUSE_OVER,onObjectMouseOver); privatefunction onObjectMouseOver(event:MouseEvent3D):void{ //do stuff } |
当然,如同在2D中一样,你可以使用所有其他的鼠标事件类型,不过你得知道并熟悉这个接口。这里没什么新奇之处。但是就像前面讲述的一样,在3D中拾取会变得很复杂。所以让我们看看在3D引擎中到底发生了什么。
Entity属性
在Away3D引擎中,有一系列的属性用于管理拾取。
MouseEnabled
那么,引擎是如何拾取的呢?总的来说,引擎会以照相机为源点发出一条射线,穿过鼠标在电脑屏幕上的位置,直线进入3D场景中。这条射线可能会碰撞到场景中的几个物体。一种确定碰撞到哪些物体的方式是使用原射线碰撞数学。这会涉及到计算射线与球体的碰撞,射线与轴对齐包围盒(AABB)的碰撞,射线与三角形的碰撞等等内容。这个过程中代价最为昂贵的就是要弄清楚射线是否碰撞到了网格(Mesh)内的一个三角形,而这个网格可能聚合了大量数目的三角形。目前,不是只有射线跟踪拾取一种方式。实际上,Away3D提供了两种不同的拾取方式,默认使用射线跟踪拾取法。我们将会在本文的后续内容中探讨这个原因。射线跟踪法如下图1所示。
图1.跟踪碰撞法:从照相机发出射线,穿过屏幕,进入一个网格的包围盒和网格的三角形内部.
设置物体的属性mouseEnabled为true,即可使其参与这些碰撞计算。此属性默认设置为flase,以避免进行可能的复杂的运算。
现在,你给物体上设置了mouseEnabled为true但没有进行事件监听,而这产生的结果就是射线跟踪拾取被阻断了。即物体与射线相撞了,但没有触发鼠标事件,如示例2所示。与示例1相比,示例2中仅是注释掉了立方体的事件监听。
示例2.MouseEnabled和侦听器。两个物体都设置了mouseEnabled属性,但只有一个添加了鼠标事件侦听器。未设置侦听器的物体仅是阻断了交互。源码。
若被阻断了交互的物体属性mouseEnabled为false,拾取射线则会穿过它,并且完全忽视它的存在。再次提醒,所有场景中物体的默认行为都是这样,除非你设置了mouseEnabled属性为true。在Flash 2D显示API中多数是以mouseEnabled这种方式设置是否拾取,但因为在二维中拾取代价低廉,所以mouseEnabled默认值为true。
Picking Colliders
在前面示例中(若将照相机旋转到非常特殊的一些角度),你可能注意到了球体周围的拾取不是很准确。在示例1或示例2中,若将鼠标悬停在非常接近球体的地方,即使实际上鼠标没在物体上,仍会触发一个MOUSE_OVER事件。这并不是我们所预期的。
图2. 3D物体的鼠标交互,默认设置为BOUNDS_ONLY.
Away3D引擎默认拾取精度为BOUNDS_ONLY级别。结合图1中的射线跟踪碰撞法,我们会发现这种默认设置会使射线停止在网格包围盒的表面,从而阻止了射线与网格内三角形(可能是成百上千,甚至百万级别的三角形)的碰撞计算。若要提高拾取的准确度,仅需要让射线继续前行。使用下面的属性可以做到这点。
anObject.pickingCollider = PickingColliderType.BOUNDS_ONLY; // default //anObject.pickingCollider = PickingColliderType.AS3_FIRST_ENCOUNTERED; //anObject.pickingCollider = PickingColliderType.AS3_BEST_HIT; // etc… |
BOUNDS_ONLY是引擎提供的代价最为低廉的拾取碰撞方式,在多少场景中这个精度已经够用了。若想要更高的精度,我们仅需选取一种不同的碰撞方式:
£ PickingColliderType.BOUNDS_ONLY(默认值)
计算射线与包围盒的碰撞。
£ PickingColliderType.AS3_FIRST_ENCOUNTERED
计算射线与网格所有三角形的碰撞。与BOUNDS_ONLY相比,这种方式耗费相当大,并且网格内三角形数目越大耗费越高昂。在这种碰撞算法中,一旦检测到与射线碰撞的一个三角形面就会停止与其余三角形面的碰撞检测,虽然事实上这个检测到的碰撞面可能并不是网格中离照相机最近的表面。
£ PickingColliderType.AS3_BEST_HIT
这种计算方式跟上一个一样,但是却更具体,它会计算出沿射线离照相机最近的网格三角形面是哪一个。射线与网格可能会有不止一个碰撞面,这种情况下,若我们想确切地知道在网格上的是哪个碰撞点,该计算方式便很有其存在的价值了。举个例子:网格代表的是一个咖啡杯,AS3_FIRST_ENCOUNTERED可能会错误地认为是与杯子的内侧发生了碰撞;AS3_BEST_HIT在检测到与射线发生碰撞的第一个三角形面后不会停下来,而是会找出所有的碰撞面,并计算出最优的那一个。
£ PickingColliderType.PB_FIRST_ENCOUNTERED
这种方式跟AS3_FIRST_ENCOUNTERED一样,但射线碰撞算法采用的不是纯ActionScript,而是pixel bender。经过证明,这种方式对高模拾取速度快,对低模拾取速度慢。若可能的话,pixel bender利用的是多线程。这意味着在桌面上这种方式往往会很快,但是在较为简单的CPU环境下,如移动设备上,可能就达不到这个效果了。此外,iOS的AIR版本目前不支持pixel bender,设置此种方式将不起作用。
£ PickingColliderType.PB_BEST_HIT
跟AS3_BEST_HIT一样,但射线碰撞算法采用的是pixel bender。
£ PickingColliderType. AUTO_FIRST_ENCOUNTERED
跟AS3_FIRST_ENCOUNTERED一样。但是这种方式会根据网格的多边形数量自动决定采用ActionScript还是pixel bender。
£ PickingColliderType. AUTO_BEST_HIT
同AUTO_FIRST_ENCOUNTERED。但是算法过程同AS3_BEST_HIT。
如你所见,这里提供了许多种选择!要决定使用哪种拾取碰撞方式是个微妙的话题。理解每种可用的拾取碰撞类型对于寻求性能和准确度间的最佳平衡点很重要。例如,app中渲染的是一个高聚的网格,你想在网格上获取非常精准的拾取,那么你将选取PB_BEST_HIT;如若在游戏中,要在一个低聚的网格上进行单击,并且这个网格只占屏幕的一小部分,那显然不会选择PB_BEST_HIT。这种低劣的选择尤其是在给许多低聚的物体设置这种类型的鼠标交互后,会浪费资源。决定使用哪个碰撞类型是非常重要的。但是一旦理解了每种碰撞类型的算法过程,这将是个简单的任务。所以,对于图2中的问题,我们只需给物体设置一个不同的碰撞类型。
1 sphere.pickingCollider = PickingColliderType.AS3_FIRST_ENCOUNTERED; |
或者,我们仅需将球体的包围盒由AxisAlignedBoundingBox(Away3D将所有的包围盒默认设置为AxisAlignedBoundingBox)改变为BoundingSphere。这样无需使射线进入包围盒内并且进行射线与三角形的碰撞计算,我们便可获得拾取的精准度。当然,这是一个非常特殊,几乎是理论层次的例子。但是,我们需要铭记这种拾取技术的核心是包围盒,并且不同形状的包围盒可能会很有用。记住这点很重要。如何修改包围盒,如下代码所示:
1 sphere.bounds = new BoundingSphere(); |
这两种方法都可以解决上述拾取精准度的问题,结果如示例3中所示:
示例3.通过修改拾取碰撞类型或是包围盒类型,从而在球上进行更精准的拾取。源码。
View 属性
除了Entity具有拾取属性外,Away3D的拾取还包括全局属性。现在开始学习这些全局属性。
Shader vs. Raycast Mouse Pickers
如前面所述,Away3D为拾取计算提供了一种完全不同的方法。目前为止,我们已经研究了单个对象的拾取属性。我们也可以在全局范围内改变拾取的工作方式。
1 view.mousePicker = PickingType.SHADER; |
有以下几个枚举值供选择:
£ PickingType.RAYCAST_FIRST_ENCOUNTERED(默认值)
使用射线跟踪作为拾取方式。一旦成功找到第一个被渲染的物体(first successful renderable)就会停止继续搜索。
£ PickingType.RAYCAST_BEST_HIT
使用射线跟踪作为拾取方式。在所有的碰撞渲染体(all the colliding renderables)中,计算最佳的碰撞渲染体。
£ PickingType.SHADER
使用着色技术作为拾取方式。总是计算最佳的那个碰撞体。
前两个选项间的差别是很细微的。我们首先来看一下前两个选项与第三个选项间的区别,这代表着完全不同的两种拾取技术。如前面所述,前两个枚举值所表示的拾取技术都是基于场景中射线与实体的碰撞计算。第三个选项(SHADER)根本不是计算射线与几何体的碰撞(ray-geometry collisions),而是首先用特殊的颜色将鼠标周围场景的一部分渲染成一个临时缓冲,然后分析鼠标正下方的颜色以找出是碰撞到了哪个物体。这种技术是很难形象化的。事实上是最高端的3D引擎在使用这种拾取技术。经证明,这种技术在精度和性能方面都是非常有效的。遗憾的是,目前这种技术在Flash的Stage3D上性能没那么好。这是因为,它需要通过Context3D的方法drawToBitmapData()将GPU上的图像传送到CPU,而drawToBitmapData()的速度不快。你可以从这篇文章中了解到这些限制。这种技术介绍了一个严重的瓶颈,这个瓶颈也就是Away3D还提供了可选的射线碰撞技术,并默认使用这种拾取的唯一原因。经证明,在Flash中RAYCAST速度比SHADER快。请记住,在每个视图(view)中,你只能使用一种拾取技术,不能同时使用。即就目前而言,不能让一些物体使用着色(shader),而让另外一些物体使用射线(raycast)。
你可能会产生这样的疑问:既然已经证明shader较慢了,为什么Away3D还提供基于shader的拾取技术?因为在一些场合下还是很有必要的。例如,在处理GPU动画和旋转时,几何体可能被CPU后台改变了,而在GPU上射线方法不可能知道顶点的位置,也就不能够正确进行射线几何体的碰撞计算了。因为shader方法不是基于射线几何体的碰撞,并且处理的是屏幕上实实在在看到的东西,所以在处理运动体这种需要高度精准的拾取时,就选用这种方法。示例4展示了在运动体上使用基于射线与着色拾取技术的效果。这个演示中,我们将使用一个球体来追踪鼠标射线与网格的碰撞点,还使用一条线段来追踪网格上基于这个碰撞点的法向量。
示例4.在运动体上的拾取。对非变换的几何体使用射线拾取法,对可见几何体的正确碰撞使用着色法。源码。
请注意射线法看不到GPU上顶点的变换,而是只能作用于静态的网格。而着色法中不存在这个问题。此外,基于shader的拾取法会忽略”.pickingCollider”这个实体(Entity)的属性,因为这个属性仅适用于基于射线的拾取法。而shader拾取法关心的是实体的”.shaderPickingDetails”属性。
1 anObject.shaderPickingDetails = true; |
这个属性的作用仅是无论是否存在鼠标事件,实体中是否包含位置、法向量等数据,都会告知着色拾取器进行计算。同使用射线法一样,计算这些信息也是需要开销的,所以应该在需要的时候才选取这种方式。此属性若设置为false,将会触发鼠标事件,但是事件的一些属性将是空值或无效值。该属性默认设置为false,示例4中,我们需将其启用以使用着色拾取器取到鼠标事件在场景中的发生位置。
希望Adobe公司有一天能解决drawToBitmapData()的瓶颈,使我们的生活变得更容易!
The Raycast Mouse Pickers
在前面的部分,我们讨论了射线拾取器与着色拾取器间的不同。是时候讨论这两个可用的射线拾取器——RAYCAST_FIRST_ENCOUNTERED和RAYCAST_BEST_HIT——间的不同了。这俩其实都是相同的拾取器,但略有不同的设置。这里“最佳碰撞”和“第一次”的区别同Entity属性”.pickingCollider”的枚举值“最佳碰撞”和“第一次”。只是在这种情况下,测试停止检查的标准是”renderable”,而不是三角形。
£ PickingType.RAYCAST_FIRST_ENCOUNTERED(默认值)
使用射线拾取技术,成功遇到第一个可呈现碰撞体就停止测试。
£ PickingType.RAYCAST_BEST_HIT
使用射线拾取技术,从所有可呈现碰撞体中找出最佳的。
但是“可呈现”(renderable)是指什么呢?射线拾取系统依赖的是实体的包围盒,一个实体却可以包含多个子网格。这些子网格共用一个单一的包围盒。一个子网格就是一个可呈现体,一个可呈现体指:可以绘制到屏幕上的对象。子网格的存在是因为Stage3D限制了单缓冲中可放置元素的数目。所以,一个网格中多边形的数目超出了这个限制,就会在一个新的子网格中创建一系列新缓冲区。这些缓冲区代表顶点、法向量、uv等。选取了RAYCAST_FIRST_ENCOUNTERED后,拾取系统将不会关心哪一个子可呈现碰撞体离照相机最近,而一旦找到一个活跃碰撞体就会停止碰撞检查。而选取RAYCAST_BEST_HIT后,拾取系统将会在所有的可呈现体中拾取到离照相机最近的那一个。RAYCAST_BEST_HIT专为有大网格分成多个子网格的应用场合而设计。
这是一个非常细微的差别,确实不容易掌握,但是最好注意这点。RAYCAST_BEST_HIT的使用应该不是很普遍,但是也有其应用场景。例如,如果沿着鼠标射线的轨迹对齐一组立方体,使用RAYCAST_FIRST_ENCOUNTERED对其进行拾取就可以做的很好;但是如果对于同样的几何体,导入的是一个网格,网格包含了一组对齐的立方体(是的,奇怪的情况),那么使用RAYCAST_BEST_HIT是不会有问题的。下面的示例5说明了这点。我们添加了另一个朝向击中点的圆锥示踪来告诉我们它在哪里。
示例5.含多个子网格的一个网格上的两个可用的射线拾取器。RAYCAST_BEST_HIT有效,而RAYCAST_FIRST_ENCOUNTERED没效。源码。
如你所见,如果鼠标射线击中多余一个子网格,RAYCAST_FIRST_ENCOUNTERED将辨别不出那个最佳的碰撞体。然而,RAYCAST_BEST_HIT总是能找到那个对的。请注意在网格上是我们是如何使用AS3_FIRST_ENCOUNTERED碰撞器的,否则用于检查碰撞的对象不是子网格的三角形而是网格的包围盒……更糟糕!同样,请注意我们将子对象基于z轴进行了无序排列。如果不做这点,树中的子网格顺序将会同它们与射线碰撞的顺序发生巧合,所以会仅因巧合而产生正确的结果。
目前,我们已经讲述了3个可用的鼠标拾取器和独立对象的7个可用拾取碰撞法。是挺多的!不过不要担心,到这里实际上我们已经覆盖了Away3D引擎拾取的几乎所有内容。所有这些为我们在项目工程中有效地使用鼠标交互提供了灵活性。若不懂每个特性,你会感到疑惑,但是一旦理解了,为每种实现做出决定将很容易。
UV绘制
拾取系统中最后一点值得一提的就是:MouseEvent3D。简单,但很重要。示例4和5中使用到的MouseEvent3D的属性scenePosition,允许我们追踪网格上射线与鼠标交点的位置。MouseEvent3D还提供了其他一系列有用的属性:
l scenePosition
场景空间中事件碰撞位置
l localPosition
物体本地空间中事件碰撞位置
l sceneNormal
场景空间中碰撞点的法向量
l localNormal
物体本地空间中碰撞点的法向量
l uv
在碰撞点内插的uv坐标。射线包围盒碰撞方式和着色碰撞方式下,uv是不可用的。
l screenX and screenY
鼠标事件在屏幕空间中的位置
l material
碰撞呈现体的材料
l etc…
如下示例将使用位置、法向量和uv属性绘制一条代表事件法向量的线段,并在物体材质上进行绘制。
示例6.在物体上绘制。源码。
在这个示例中,我们使用了PB_BEST_HIT作为网格的拾取器。考虑到这个网格的多边形数目这个选取是合理的。
如你所见,MouseEvent3D提供了关于事件的有用信息,提供了3D场景中复杂鼠标交互所需的几乎所有信息。太有趣了!
总结
我们已经看到了,3D拾取不是一个无足轻重的话题。要开发出高质量的应用程序,我们需要正确地理解并掌握它。很幸运Away3D为如此重要的工作提供了这么一个灵活的特性。
我们希望本教程对您有所帮助,希望您对Away3D的拾取原理有了很好的理解。