物理引擎这块应该是整个项目过程中我觉得最坑的一点,文档的欠缺,还有各种不明所以的奇怪现象,有时候不小心改一个配置参数都能发生一系列奇怪的问题。。。
技术选型的时候考察了一系列物理引擎
https://github.com/bebraw/jswiki/wiki/Physics-libraries
由于需要3D,所以选择范围其实不是特别大
- JigLibJS - port of JigLib to JS
- ammo.js - port of Bullet to JS
- bulletjs - port of parts of the Java library JBullet(http://jbullet.advel.cz/)
- cannon.js
- microphysics.js - simple 3D physics engine
- Physijs - plugin for Three.js
- Oimo.js Lightweight 3d physics engine
- Goblin Physics - full physics engine specifically for JavaScript
首先我们明确下为什么要使用物理引擎。其实很简单,因为我们需要一些物体本身不具有的物理量,比如子弹从一个角度射击某个物体,我们想知道物体往哪个方向运动,还有速度大小,什么?你说你学过物理直接能算出来,拜托这不是初中高中物理试卷上万能的小方块,也就是说你不能忽视物理的体积形状等要素,比如同一个方向平行撞击物体,但是撞击点的位置不同,得到的结果也是千差万别的。
物理引擎是做怎样的事情呢?首先物理引擎是一个虚拟的世界,其中的物体都是看不见摸不着的,说到底就是一个对象,完了属性不断在变化。我们都知道一个静止的物体要运动一定会受到力的作用,而物理引擎都是模拟这些力的作用,重力、摩擦力包括其它外部的力,这些会使物体的平移坐标和旋转坐标发生改变,也就产生了两个物体碰撞的效果。再往深里讲,物理引擎所做的是对这些力的效果进行迭代求解,算出一些属性值,当然我们可以设置迭代次数以及其它求解参数,权衡精度和效率。
我们可以给发射的子弹一个初始速度和质量,这使子弹具有了动量,所以子弹碰撞到其它刚体上就会产生力的作用(ft=mv),物理世界中的position(平移坐标)和quaternion(四元数,描述旋转坐标)会发生变化,这时候可以通过帧循环同步到实际3D场景的物体坐标上。
由于很多物理引擎都没有文档,有API文档已经是很棒棒的了,但是其实大多数概念都差不多,这里翻译了下cannon的Wiki:
原文:https://github.com/schteppe/cannon.js/wiki
英语不好翻译很渣,原文的体验当然最好。
主页
cannon.js是一个可以用于游戏的刚体仿真库,程序员可以使用它使他们的游戏对象以真实的方式移动和交互。cannon.js是用JavaScript写的,可以与任何浏览器一起使用使渲染或游戏引擎。
首先,我们可以从Hello Cannon.js入手,或者看一些使用cannon.js所做的很酷的项目
名词解释/基础概念
shape:几何形状,如球体或盒子。
Rigid body(刚体):是假定无比坚硬的一件物体。假定刚体中的任何两个点总是处于彼此恒定的距离。我们可以通过说“身体”来提及刚体。刚体具有形状和多个物理性质如质量和惯性。
Constraint(约束):约束是一种物理连接,可以消除刚体的自由度。在3D中,刚体具有6个自由度(三个平移坐标和三个旋转坐标)。如果我们把门放在门铰链上,将门体限制在墙上,此时门只能围绕门铰链旋转,所以约束已经消除了5个自由度。
Contact constraint(接触约束):设计用于防止刚体渗透并模拟摩擦和恢复的特殊约束。由Cannon.js自动创建。
World(世界):一个物理世界是一组互动的物质和约束。Cannon.js支持创建多个世界,但这通常是不必要的或不可取的。
Solver(求解器):物理世界用于求解约束的解决方案。
开始项目
cannon.js/README.markdown包含一个Hello World项目,脚本创建静态接地平面和动态球体,此代码不包含任何图形,。你只能看到随着时间的推移文本在控制台输出,这是一个很好的例子,说明如何唤醒和运行Cannon.js。
创造一个世界
每个Cannon.js应用程序的初始都是创建CANNON.World对象。CANNON.World是管理对象和模拟的物理中心,通过它我们可以很容易创建一个Cannon.js世界。首先,我们创建CANNON.World对象。然后我们将负载z方向的重力设置为9.82 m /s²:1
2var world = new CANNON.World();
world.gravity.set(0,0,-9.82);
我们必须向世界提供一个broadphase算法,使它可以找到碰撞的物体。我们使用默认的NaiveBroadphase:
world.broadphase = new CANNON.NaiveBroadphase();
现在我们有了物理世界,让我们开始添加一些东西。
创造一个body
body使用以下步骤进行创建
1.定义一个形状
2.使用所需的形状和其他物理属性定义刚体
3.将body添加到世界。
1 | var mass = 5, radius = 1; |
在第1步中,我们创建一个半径为1的球形。这只是一个球体的数学描述,它必须放在一个刚体之中才能移动和碰撞。在第2步中,我们创建了这个RigidBody。如果质量设置为零,则体变为静态,但在这种情况下,我们将质量设置为5 kg,这使球体动态。静态物体不与其他静态物体碰撞,动态物体与所有其他物体相撞。对于第3步,我们将body添加到世界。通过添加到世界,它将根据它受到的力而移动,并与其他对象相冲突。
创建一个静态地平面
第一步是创造平面形状,然后是刚体。我们给刚体零质量,这将保证地面刚体变得静止。默认情况下,平面的方向在z方向。换一种说法; 其正常值为(0,0,1)。如果我们想要在另一个方向上操作,就必须改变body的方向(见Body.quaternion)。最后,我们把地面的body添加到场景。1
2
3var groundShape = new CANNON.Plane();
var groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
world.add(groundBody);
这就是初始化。我们现在准备开始模拟了。
模拟世界
我们已经初始化了静态地平面和动态球体。我们只需要再考虑几个问题。
Cannon.js使用称为积分器的计算算法。积分器在离散的时间点模拟物理方程。当游戏在循环的时候,它也一直在不断运行,在平面上移动。所以我们需要为Cannon.js选一个时间步。一般游戏中的物理引擎至少使用60HZ或者1/60s。你可以使用更大的时间步长,但是必须更加小心地设置你的物理世界中的参数。1
var timeStep = 1.0 / 60.0; // seconds
除了积分器(integrator)之外,Cannon.js还使用约束求解器(constraint solver)。约束求解器解决了仿真中的所有约束。单一约束可以完美解决。但是,当我们解决一个约束时,我们稍微破坏其他约束。为了得到一个很好的解决方案,我们需要遍历所有约束多次。在这个阶段,求解器计算机体正确移动所需的脉冲。Cannon.js的建议迭代次数约为5.您可以根据您的喜好调整此数字,但请记住,这在速度和精度之间有一个折衷。使用较少的迭代可以提高性能,但精度受损。同样,使用更多的迭代会降低性能,但是提高了模拟的质量。对于这个简单的例子,我们不需要太多的迭代。这是我们选择的迭代计数。world.solver.iterations = 2; 请注意,时间步长和迭代次数完全不相关。迭代不是一个子步骤。一个求解器迭代是在一个时间步长内的所有约束的单次传递。您可以在单个时间步长内对约束进行多次求解。
我们现在准备开始模拟循环了。在您的游戏中,模拟循环可以与您的游戏循环合并。在每次通过你的游戏循环中,你调用world.step(timeStep)。只需一个调用就足够了,这取决于你的帧速率和你的物理时间步长。
Hello World程序的设计很简单,因此它没有图形输出。代码打印出动态体的位置。
这是模拟循环,模拟60秒的时间,总共1秒的模拟时间。1
2
3
4for (var i = 0; i < 60; ++i){
world.step(timeStep);
console.log(sphereBody.position.x, sphereBody.position.y, sphereBody.position.z);
}
您的输出应如下所示:1
2
3
4
5
6
70 0 4
0 0 3.99
0 0 3.98
...
0 0 1.25
0 0 1.13
0 0 1.01
CANNON.Demo框架
一旦你征服了HelloWorld的例子,你应该开始看Cannon.js的演示框架。CANNON.Demo是一个演示环境。这里有一些功能:
相机与平移和缩放
可扩展的场景数量
用于选择场景的GUI(数字键),参数调整和调试绘图选项
暂停[p]和单步仿真[s]
演示框架有许多Cannon.js用法和框架本身的例子。当您学习Cannon.js时,我鼓励您探索CANNON.Demo。注意:测试台使用Three.js编写。CANNON.Demo不是Cannon.js库的一部分(build / cannon.js)。Cannon.js库对于渲染是不可知的。如Hello world示例所示,您不需要渲染器来使用Cannon.js。
参数调整
要获得刚体的约束,您需要做一些参数调整。以下是构建最佳模拟时可能需要注意的事项列表。
时间尺寸(Timestep size)
1 | world.step(1/60); // Steps forward in time from t=0 to t=0.01666... |
时间步长对于模拟质量非常重要。使用较小的时间步长将会增加Cannon.js中发生的大部分事情的准确性,并增加计算成本。一些实时模拟器建议1/60 = 0.01666 …秒的时间步长足以满足大多数用例。在许多情况下可能是正确的,但不是全部。对于大多数HTML5渲染引擎,我们以60Hz渲染。因此,大约60Hz的时间步长是好的。如果您想要更小的时间步长以获得更高的精度,则可以方便地进行两个半步而不是单步:1
2world.step(1/120); // Steps forward in time from t=0 to t=0.008333...
world.step(1/120); // Steps forward in time from t=0.008333 to t=0.01666...
求解器迭代(Solver iterations)
1 | world.solver.iterations = 10; |
在接触的情况下,Solver程序相当于找出物体在接触过程中所受的力(???这句翻译的不确定)。求解器是迭代的,这意味着它是逐渐求解方程式的,每次迭代逐步接近最终的代码。使用过少的求解器迭代将导致结果不准确,更多的迭代将增加精度。
求解器参数(Solver parameters)
CANNON.Equation对象上设置求解器参数。您提供约束刚度和正则化,如下所示:1
2equation.stiffness = 1e8;
equation.regularizationTime = 10;
最常用的方程式可能是ContactEquation。当您的场景中显示联系人时,Cannon.js会自动生成这些。因此,您可以通过CANNON.ContactMaterial属性控制其刚度和正则化:1
2world.defaultContactMaterial.contactEquationStiffness = 1e8;
world.defaultContactMaterial.contactEquationRegularizationTime = 3;
刚度大致对应于弹簧的刚度,其给出力F=-k*x,其中x是弹簧的位移。正则化时间对应于您需要采取的稳定约束的时间步长(较大的值=>较软的接触)。
重力(Gravity)
这可能不是很明显,但重力确实会影响约束。如果你有一个球放置在飞机上,Contact constraint将其从飞机上推下,重力会将它推到另一个方向,较小的重力会更容易使Solver得到结果。
总之,较小的重力可以使Contact更好。1
world.gravity.set(0,0,-10);
空气阻力(Air Resistance)
如果您希望对象在不受阻碍的情况下放慢速度,可以使用linearDamping。1
2var sphereBody = new CANNON.Body({mass: 5});
sphereBody.linearDamping = 0.3;
基本上以上也概括了物理引擎使用的一般步骤和一些常用的参数设置,虽然是cannon,但其它物理引擎也差不多,只不过一些方法名不同。
物理引擎本身是有做一些减幅处理的,但是实际运用中我们不能完全使用物理的一些参数,比如两个物体撞击,我们只想让一个物体接受撞击时向前后左右移动,不想让它跳起来或者“摔倒”甚至弹飞,实际用物理引擎的话其实会出现这种情况的,所以我们还是需要进行很多修正处理。获取自己想要的参数,同时设置一些参数达到我们想要的值。
最后讲讲各个物理引擎的测评,几个物理引擎我都试了一下,最后为了选择我们项目要用的物理引擎,选了几个性能还不错功能也强大的交给QA大大测试性能,这是测试结果
从使用角度来说,cannon文档是最齐全的,API文档和wiki都有,开始也想用它,但是发烫现象实在太严重了。ammo偏重量级,好像是C++编译来的,性能其实不差,偏重,稍微有点难上手。Goblin印象不深了(毕竟都过去小半年了),最终选择了oimo,性能比cannon好很多,比ammo略差,虽然什么文档都没有,但是有几个不咸不淡的官方实例可以玩(开发日常是…oh我又发现了一个方法可以用!我又发现了一个参数可以调整!!),然后源码比较清晰,虽然实践过程中发现坑也不少。