A-Frame 是一款用于构建虚拟现实体验的 WebVR 框架。它附带一些捆绑组件,可以让你轻松地将行为添加到你的 VR 场景中,但你也可以下载更多组件 - 甚至创建你自己的组件。
在这篇文章中,我将分享我是如何通过第三方组件构建集成物理引擎的 VR 场景的。虽然 A-Frame 允许你向场景添加对象和行为,但如果你希望这些对象相互交互或被用户操作,你可能需要使用 物理引擎 来处理所需的计算。如果你刚接触 A-Frame,我建议你查看 入门指南 并先尝试一下。
我创建的场景是一个保龄球馆,它与 HTC Vive 头显配合使用。你的右手有一个球,你可以通过按住右手控制器的触发按钮并像移动手臂一样释放它来投球。要将球返回到你的手中并再次尝试,请按菜单按钮。你可以在这里尝试演示! (注意:你需要 Firefox Nightly 和一个 HTC Vive。请按照 WebVR.rocks 中的设置说明)
你可以在 Github 上获得源代码,你可以对其进行调整并玩得开心。
在 A-Frame 中添加物理引擎
我选择了 aframe-physics-system
,它在幕后使用 Cannon.js。Cannon 是一个纯 JavaScript 物理引擎(不是从 C/C++ 编译的 ASM 版本),因此我们可以轻松地与它交互 - 并查看它的代码。
aframe-physics-system
是一个中间件,它初始化物理引擎并为我们提供 A-Frame 组件,以便应用于实体。当我们使用它的 static-body
或 dynamic-body
组件时,aframe-physics-system
会创建一个 Cannon.Body
实例并将其“附加”到我们的 A-Frame 实体,因此在每一帧,它都会调整实体的位置、旋转等,使其与主体的相匹配。
如果你想使用其他引擎,请查看 aframe-physics-system
或 aframe-physics-components
。这些组件并不复杂,用另一个引擎模仿它们的行为应该不会很复杂。
静态和动态主体
静态主体是那些不可移动的主体。想想地面,或者那些无法拆除的墙壁等。在场景中,不可移动的实体是地面和保龄球道两侧的缓冲器。
动态主体是那些能够移动、反弹、倾倒等的主体。显然,球和保龄球瓶是我们的动态主体。请注意,由于这些物体可以移动并坠落,或者碰撞并撞倒其他物体,因此质量属性会产生很大的影响。以下是一个保龄球瓶的示例
<a-cylinder dynamic-body="mass: 1" ...>
化身和物理世界
为了显示用户的“手”(即,将跟踪的 VR 控制器显示为手),我使用了 vive-controls
组件,它已捆绑在 A-Frame 中。
<a-entity vive-controls="hand: right" throwing-hand></a-entity>
<a-entity vive-controls="hand: left"></a-entity>
这里的挑战在于,用户的化身(“头部”和“手”)并不属于物理世界 - 即,它超出了物理引擎的范围,因为头部和手必须遵循用户的移动,不受物理规则(如重力或摩擦力)的影响。
为了让用户能够“握住”球,我们需要获取右手控制器的坐标并手动设置球的坐标以使其在每一帧都与之匹配。我们还需要重置其他物理属性,例如速度。
这在自定义的 throwing-hand
组件(我将其添加到表示右手的实体中)的 tick
回调函数 中完成。
ball.body.velocity.set(0, 0, 0);
ball.body.angularVelocity.set(0, 0, 0);
ball.body.quaternion.set(0, 0, 0, 1);
ball.body.position.set(position.x, position.y, position.z);
注意:更好的选择可能是将球的旋转与控制器的旋转相匹配。
投球
投球机制的工作原理如下:用户必须按住控制器的触发器,当她松开触发器时,球就被投出。
Cannon.Body
中有一个方法可以对动态物体施加力:applyLocalImpulse
。但是,我们应该对球施加多少冲量以及施加在哪个方向呢?
我们可以通过计算投球手的速度来获得正确的方向。但是,由于化身不受物理引擎的控制,因此我们需要手动计算速度。
let velocity = currentPosition.vsub(lastPosition).scale(1/delta);
此外,由于球的质量非常大(使其对球瓶产生更大的“冲击力”),我在应用冲量时需要在该速度向量上添加一个乘数。
ball.body.applyLocalImpulse(
velocity.scale(50),
new CANNON.Vec3(0, 0, 0)
);
注意:如果我允许球旋转以匹配控制器的旋转,我需要将该旋转也应用于速度向量,因为 applyLocalImpulse
使用球的局部坐标系进行工作。
为了检测控制器的触发器何时被释放,只需要在表示右手的实体中为 triggerup
事件设置一个监听器。由于我在那里添加了我的自定义 throwing-hand
组件,因此我在 它的 init
回调函数 中设置了监听器。
this.el.addEventListener('triggerup', function (e) {
// ... throw the ball
});
一个故障
一开始,我通过按空格
键来模拟投球。代码如下所示
document.addEventListener('keyup', function (e) {
if (e.keyCode === 32) { // spacebar
e.preventDefault();
throwBall();
}
});
但是,这超出了 A-Frame 循环的范围,并且投球手的 lastPosition
和 currentPosition
的计算不同步,因此我在计算速度时得到了奇怪的结果。
这就是为什么我设置了一个标志而不是直接调用 launch,然后,在 throwing-hand
的 tick
回调函数中,如果该标志被设置为 true
,则调用 throwBall
。
另一个故障:摇晃的球瓶
使用 aframe-physics-system
的默认设置,我发现当我缩小保龄球瓶时会出现一个故障:它们摇晃并最终倒在地上!
当使用物理引擎时,如果计算不够精确,就会出现这种情况:存在一个小的错误,它会逐帧累积,并且… 你会看到物体坍塌或倾倒,尤其是当这些物体很小的时候 - 它们需要更少的错误才能让变化更加明显。
解决此问题的一个方法是提高物理模拟的精度 - 尽管会降低性能。你可以使用 aframe-physics-system
的组件配置中的迭代设置来控制此精度(默认情况下设置为 10
)。我将其提高到了 20
。
<a-scene physics="iterations: 20">
为了更好地观察此更改的效果,以下是对迭代设置为 5
和 20
的并排比较。
注意:将此视频上传到 YouTube 并将其插入此处:https://drive.google.com/a/mozilla.com/file/d/0B45CULzwzeLdNGdDTk9QOFFqQUE/view?usp=sharing
Cannon 的“休眠”功能 提供了另一种可能的解决方法,可以解决这种情况,而不会影响性能。当物体处于休眠模式时,物理引擎不会让它移动,直到它与另一个物体发生碰撞并被唤醒。
轮到你了:玩一下吧!
我已经将项目上传到 Glitch,以及一个 Github 仓库,如果你想玩一玩并进行自己的修改的话。你可以尝试一些事情
- 允许玩家使用两只手(也许可以用一个按钮将球从一只手换到另一只手?)
- 当所有的球瓶都倒下后,自动将它们重置到原来的位置。你可以检查它们的旋转来实现这一点。
- 添加声音效果!有一个碰撞事件的回调函数,你可以使用它来检测球何时与另一个元素发生碰撞… 你可以为球与球瓶碰撞或球撞击地面时添加声音效果。
如果你对 A-Frame 有任何疑问,或者想更多地参与使用 A-Frame 构建 WebVR,请查看 我们活跃的 Slack 社区。我们很乐意看到你在做什么。
关于 Belén Albeza
Belén 是一位工程师和游戏开发者,在 Mozilla 开发者关系部门工作。她关心 Web 标准、高质量代码、可访问性和游戏开发。