在本文中,我们将对比看一下当前三个非常流行的和一个目前还在开发中的JavaScript 物理引擎库,分别是: box2dweb,Ammo.js,JigLibJS 以及 Connon.js。我们会简短的介绍下每个 JS库,之后开始按照使用、性能和特征来评分。
不过运行其中任意一个库文件都是不带任何可视化效果的,这样很无趣,因此我们会设置一个小的环境来查看这些模拟运行的结果。由于 Three.js 的流行和使用简易,我使用它和它的 CanvasRenderer 来呈现结果。除了可以展示物体是如何交互的,Three.js 还可以提取出每个库中的场景信息。这里的场景由滑到地面的坡道组成;会有小球从以上场景中的任意位置上掉落到坡道上,再滑到地面上。
设置
我们的基础场景中使用了两个滑到地面的坡道。小球会从坡道上方的任意位置掉落,向下滚动,之后滚动到地面上。这个简单的场景可以高亮出这四个库文件的相似处和不同处。
[javascript] view plain copy
var viewport = document.getElementById( 'viewport' ), // The canvas element we're going to use
renderer = new THREE.CanvasRenderer({ canvas: viewport }), // Create the renderer
scene = new THREE.Scene, // Create the scene
camera = new THREE.PerspectiveCamera( 35, 1, 1, 1000 );
renderer.setSize( viewport.clientWidth, viewport.clientHeight );
camera.position.set( -10, 30, -200 );
camera.lookAt( scene.position ); // Look at the center of the scene
scene.add( camera );
function addLights() {
var ambientLight = new THREE.AmbientLight( 0x555555 );
scene.add( ambientLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( -.5, .5, -1.5 ).normalize();
scene.add( directionalLight );
}
function buildScene() {
var ramp_geometry= new THREE.CubeGeometry( 50, 2, 10 ),
material_red = new THREE.MeshLambertMaterial({ color: 0xdd0000, overdraw: true }),
material_green = new THREE.MeshLambertMaterial({ color: 0x00bb00, overdraw: true });
var ramp_1 = new THREE.Mesh( ramp_geometry, material_red );
ramp_1.position.set( -20, 25, 0 );
ramp_1.rotation.z = -Math.PI / 28;
scene.add( ramp_1 );
var ramp_2 = new THREE.Mesh( ramp_geometry, material_red );
ramp_2.position.set( 25, 5, 0 );
ramp_2.rotation.z = Math.PI / 16;
scene.add( ramp_2 );
var floor = new THREE.Mesh(
new THREE.PlaneGeometry( 100, 50 ),
material_red
);
floor.position.y = -15;
scene.add( floor );
}
addLights();
buildScene();
renderer.render( scene, camera );
jsfiddle View: http://jsfiddle.net/chandlerprall/CXxH4/light/
box2dweb
box2dweb 是 C++ box2d 项目的一个分支,它在这次对比的4个库文件中独特之处在于它只能模拟二维场景。它所有的类都是命名空间的形式,这让代码有些难以阅读,所以建议创建你经常使用的本地变量。不要傻傻的认为 box2dweb 只能呈现 2D 的模拟就很弱了;它拥有的简单的 API 却蕴含了巨大的能量。box2dweb 拥有一个完整的约束机制(被称为连接器),它可以检测更多精确迅速移动的物体之间的连续碰撞,对于全部场景和独立对象都有许多配置选项。
使用 box2dweb 配合 b2ContactListener 检测碰撞非常简单。你可以单独使用4个碰撞状态的事件处理程序:BeginContact,EndContact,PreSolve和PostSolve。你也能够通过迭代world中每一步之后的接触列表来检测碰撞。
[javascript] view plain copy
var contactListener = new Box2D.Dynamics.b2ContactListener;
contactListener.BeginContact = function( contact_details ) {
// `contact_details` can be used to determine which two
// objects collided and in what way
};
world.SetContactListener(contactListener);
连接器可以以不同的方式来约束对象,可以让目标进行类似铰链和滑动的单方向动作,以及模拟贴附到绳子上的运动。另外还有其他许多种类的配置。要让连接器按照你想要的方式工作,我们需要一点点测试,一旦你找到诀窍之后想要操作和增加新的joints就变得非常非常快。作为示例,我们创建一个公转的连接器,通俗的说法就是一个铰链。box2dweb 中所有连接器有两个部分组成,并且在 world 中都有一个位置。不过摩擦连接器只需要一个部分。获取更多可用连接器和不同配置选项的细节可以查看Box2D 手册或者在线的 Box2D 连接器教程。
[javascript] view plain copy
var body_a = world.CreateBody( bodyDef_A ).CreateFixture( fixDef );
var body_b = world.CreateBody( bodyDef_B ).CreateFixture( fixDef );
var jointDef = new Box2D.Dynamics.Joints.b2RevoluteJointDef;
jointDef.Initialize(
body_a.GetBody(), // Body A
body_b.GetBody(), // Body B
body_b.GetBody().GetWorldCenter() // Anchor location in world
// coordinates,in this case it's the center of body B
);
var joint = world.CreateJoint( jointDef );
最后想要看到 box2dweb 演示还需要一个步骤:在一步模拟之后如何更新你的场景,例如在游戏中更新一个图形。Javascript 中的 world 对象有一个可以返回场景中第一个目标的方法,被称作 GetBodyList。利用GetBody后你可以重复调用body.getNext() 直到GetNext方法返回null,这样可以处理场景中剩下的目标。正如你所见到每一个部分,你可以更新关联的图像对象或者其他任何你需要关联的。你还可以使用对象定义的 userData 属性在你创建对象的时候附加到每一个对象中。userData 通常包含了几何目标的位置和简单旋转的参考。
概述:
性能:我从见过box2dweb 场景运行缓慢,除非设置不佳。反而是游戏逻辑和实际图像渲染通常是对 FPS 的影响最大。
特色:Box2D 永远不会用来做3D 的工作,因为大多数“缺少”的特性其实并不是缺少,它们就是不在 3D 环境下运行生效而已。如果你只在二维的环境,这个库应该拥有你需要的一切。
可用性:API非常简单,虽然有时候在命名空间的不同部分里想要找到一些对象有点困难。而且 Box2D 的手册也不太透彻,因为你需要通过flash节点的文档来查找以及在网上搜索具体的主题文档。
总结:这是一个不需要复杂设置就能够使用的强大引擎,也没有学习的弯路。如果你只需要二维的物理引擎,你只要选择 box2dweb。
浏览器支持:Chrome,Firefox,IE9,Safari,Opera
[javascript] view plain copy
// Create some local variables for convenience
var state = false, // true
when the simulation is running
b2Vec2 = Box2D.Common.Math.b2Vec2,
b2BodyDef = Box2D.Dynamics.b2BodyDef,
b2Body = Box2D.Dynamics.b2Body,
b2FixtureDef = Box2D.Dynamics.b2FixtureDef,
b2Fixture = Box2D.Dynamics.b2Fixture,
b2World = Box2D.Dynamics.b2World,
b2MassData = Box2D.Collision.Shapes.b2MassData,
b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape,
b2CircleShape = Box2D.Collision.Shapes.b2CircleShape,
b2DebugDraw = Box2D.Dynamics.b2DebugDraw,
viewport = document.getElementById( 'viewport' ), // The canvas element we're going to use
// If your computer supports it, switch to the WebGLRenderer for a much smoother experience
// most of the lag and jittering you see in the simulation is from the CanvasRenderer, not the physics
renderer = new THREE.CanvasRenderer({ canvas: viewport }), // Create the renderer
//renderer = new THREE.WebGLRenderer({ canvas: viewport }), // Create the renderer
scene = new THREE.Scene, // Create the scene
camera = new THREE.PerspectiveCamera( 35, 1, 1, 1000 ),
ball_geometry = new THREE.SphereGeometry( 3 ), // Create the ball geometry with a radius of `3`
ball_material = new THREE.MeshLambertMaterial({ color: 0x0000ff, overdraw: true }), // Balls will be blue
large_ball_geometry = new THREE.SphereGeometry( 4 ), // Create the ball geometry with a radius of `4`
large_ball_material = new THREE.MeshLambertMaterial({ color: 0x00ff00, overdraw: true }), // Large balls are be green
time_last_run, // used to calculate simulation delta
world, // This will hold the box2dweb objects
bodyDef = new b2BodyDef, // `bodyDef` will describe the type of bodies we're creating
// Create a fixture definition
// `density` represents kilograms per meter squared.
// a denser object will have greater mass
// `friction` describes the friction between two objects
// `restitution` is how much "bounce" an object will have
// "0.0" is no restitution, "1.0" means the object won't lose velocity
fixDef = new b2FixtureDef;
fixDef.density = 1.0;
fixDef.friction = 0.3;
fixDef.restitution = 0.3;
renderer.setSize( viewport.clientWidth, viewport.clientHeight );
camera.position.set( -10, 30, -200 );
camera.lookAt( scene.position ); // Look at the center of the scene
scene.add( camera );
function addLights() {
var ambientLight = new THREE.AmbientLight( 0x555555 );
scene.add( ambientLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( -.5, .5, -1.5 ).normalize();
scene.add( directionalLight );
}
function buildScene() {
// Create the physics world
world = new b2World(
new b2Vec2( 0, -20 ), // Gravity
true // Allow objects to sleep
);
bodyDef.type = b2Body.b2_staticBody; // Objects defined in this function are all static
var ramp_geometry= new THREE.CubeGeometry( 50, 2, 10 ),
material_red = new THREE.MeshLambertMaterial({ color: 0xdd0000, overdraw: true }),
material_green = new THREE.MeshLambertMaterial({ color: 0x00bb00, overdraw: true });
var ramp_1 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_1 );
// position the ramp
bodyDef.position.x = ramp_1.position.x = -20;
bodyDef.position.y = ramp_1.position.y = 25;
bodyDef.angle = ramp_1.rotation.z = -Math.PI / 28;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox( 25, 1 ); // "25" = half width of the ramp, "1" = half height
bodyDef.userData = ramp_1; // Keep a reference to `ramp_1`
world.CreateBody( bodyDef ).CreateFixture( fixDef ); // Add this physics body to the world
var ramp_2 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_2 );
// position the ramp
bodyDef.position.x = ramp_2.position.x = 25;
bodyDef.position.y = ramp_2.position.y = 5;
bodyDef.angle = ramp_2.rotation.z = Math.PI / 16;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox( 25, 1 ); // "25" = half width of the ramp, "1" = half height
bodyDef.userData = ramp_2; // Keep a reference to `ramp_2`
var body_a = world.CreateBody( bodyDef ).CreateFixture( fixDef ); // Add this physics body to the world
// Create the floor
var floor = new THREE.Mesh( new THREE.PlaneGeometry( 100, 50 ), material_red );
scene.add( floor );
bodyDef.position.x = 0;
bodyDef.position.y = floor.position.y = -15; // position the floor
bodyDef.angle = 0;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox( 50, .1 ); // "50" = half width of the floor, ".1" = half height
bodyDef.userData = floor; // Keep a reference to `floor`
world.CreateBody( bodyDef ).CreateFixture( fixDef ); // Add this physics body to the world
}
function addBall() {
var ball;
if ( !state ) return;
fixDef.shape = new b2CircleShape;
if ( Math.random() >= .25 ) {
ball = new THREE.Mesh( ball_geometry, ball_material );
fixDef.shape.SetRadius( 3 );
} else {
ball = new THREE.Mesh( large_ball_geometry, large_ball_material );
fixDef.shape.SetRadius( 4 );
}
scene.add( ball );
bodyDef.type = b2Body.b2_dynamicBody; // balls can move
bodyDef.position.y = ball.position.y = 50;
bodyDef.position.x = Math.random() * 40 - 20; // Random positon between -20 and 20
bodyDef.userData = ball; // Keep a reference to `ball`
world.CreateBody( bodyDef ).CreateFixture( fixDef ); // Add this physics body to the world
}
function updateWorld() {
requestAnimationFrame( updateWorld );
if ( !state ) return;
var delta, now = (new Date()).getTime();
if ( time_last_run ) {
delta = ( now - time_last_run ) / 1000;
} else {
delta = 1 / 60;
}
time_last_run = now;
world.Step(
delta * 2, // double the speed of the simulation
10, // velocity iterations
10 // position iterations
);
// Update the scene objects
var object = world.GetBodyList(), mesh, position;
while ( object ) {
mesh = object.GetUserData();
if ( mesh ) {
// Nice and simple, we only need to work with 2 dimensions
position = object.GetPosition();
mesh.position.x = position.x;
mesh.position.y = position.y;
// GetAngle() function returns the rotation in radians
mesh.rotation.z = object.GetAngle();
}
object = object.GetNext(); // Get the next object in the scene
}
renderer.render( scene, camera );
}
addLights();
buildScene();
document.getElementById( 'startStop' ).addEventListener('click',
function() {
if ( this.innerHTML === 'Start' ) {
this.innerHTML = 'Stop';
time_last_run = (new Date()).getTime();
state = true;
} else {
this.innerHTML = 'Start';
state = false;
}
}
)
updateWorld();
setInterval( addBall, 500 );
Ammo.js
证明 Javascript 在性能方面走了很远的路的是Ammo.js ,一个用 C++ 语言编写的物理库,它是 Bullet 项目的 Emscripten 分支。由于 Emscripten 将库转换成 Javascript 的方式,Ammo.js 有些功能很难使用– 例如使用指针在复杂层中再增加一个层。庆幸的是,Ammo.js 并没有很多地方受到这点的影响以及很少有项目需要进行很不同的运算。这样说来,Ammo.js 是一个特点丰富的库,它拥有很多内置的形状和许多用户定义的凸多边形,连续碰撞检测,约束条件,强大的传送系统以及多方式的场景调节功能。
为了更新场景的视觉画面,你需要持续的跟踪所有增加到 world 中的对象– 这点,Ammo.js 不会帮你完成。每次场景渲染完成,你可以迭代这些对象同时更新它们的位置和旋转。在Ammo.js 中创建对象可以看成是一个大动作的的拖拽同时很复杂,不过这样也意味着对每一个对象更好的调整。很多时候你创建一个功能的时候它会帮你创建对象,而不需要复制代码,这是很有帮助的。同时,不像 box2dweb,Ammo.js 不支持碰撞事件。所以为了找到和控制碰撞,你就需要一份 world 中的接触副本并且循环它们。每一个副本都会告诉你是哪些对象在哪个点碰撞了。
在你搞清楚对象约束条件是怎么定义之前想要增加它们都有点难度。大多数约束条件可以将一个对象固定到 world 中的一个点或者将两个对象约束到一起。举个例子来说明,你可以给一个单独对象增加一个铰链条件,让它绕着world 中的一个单独的点旋转,但是指定第二个对象意味着相同的约束看起来像是两个对象之间的铰链– 和一扇门固定到墙上一样。每种形式的约束条件都有它自己的设置,让它只有有限的动作或者指定一个对象在到达某些区域时如何运动。
[javascript] view plain copy
// Create a Point2Point constraint to keep two objects bound together
// position_a is the point of constraint relative to object_a's position
// position_b is the point of constraint relative to object_b's position
var constraint = new Ammo.btPoint2PointConstraint(
object_a,
object_b,
new Ammo.btVector3( position_a.x, position_a.y, position_a.z ),
new Ammo.btVector3( position_b.x, position_b.y, position_b.z )
);
world.addConstraint( constraint );
概述:
性能:Ammo.js 是一个庞大的库,另外作为一个 Emscripten 的分支意味着没有任何 Javascript优化。在功能和性能之间有着很明确的平衡,不过大多数游戏画面在每秒50帧的时候都应该没有什么问题。
特色:作为最完整的物理引擎库之一,在所有编程语言中都是可用的。Bullet 物理引擎已经被用于许多商用产品,例如侠盗猎车手4,荒野大镖客:救赎等游戏和电影2012,神探夏洛克等,同时,它还被许多3D创作工具使用,其中就包括了Blender和Cinema 4D – 现在我们可以在 Javascript 中拥有这些相同的资源了。
可用性:API 有时候会很让人疑惑,而且自生成的文档也没多大帮助。很多次我都要去Bullet 代码库里找一个功能函数的使用方法,甚至它已经被弃用了。大量时间都花在了配置上,理解他们找到了哪些影响以及如何为自己的游戏环境进行正确的设置。
总结:如果你要找一个比Ammo.js还要特色更多的物理引擎库,现在这个任务可以完成了。为了可以快速的开发复杂的场景和丰富的互动,在多次讨论以及熟悉库的陌生部分后,Ammo.js 就是你想要的。
浏览器支持:Chrome,Firefox,Safari,Opera
[javascript] view plain copy
var state = false, // true
when the simulation is running
viewport = document.getElementById( 'viewport' ), // The canvas element we're going to use
// If your computer supports it, switch to the WebGLRenderer for a much smoother experience
// most of the lag and jittering you see in the simulation is from the CanvasRenderer, not the physics
renderer = new THREE.CanvasRenderer({ canvas: viewport }), // Create the renderer
//renderer = new THREE.WebGLRenderer({ canvas: viewport }), // Create the renderer
scene = new THREE.Scene, // Create the scene
camera = new THREE.PerspectiveCamera( 35, 1, 1, 1000 ),
ball_geometry = new THREE.SphereGeometry( 3 ), // Create the ball geometry with a radius of `3`
ball_material = new THREE.MeshLambertMaterial({ color: 0x0000ff, overdraw: true }), // Balls will be blue
large_ball_geometry = new THREE.SphereGeometry( 4 ), // Create the ball geometry with a radius of `4`
large_ball_material = new THREE.MeshLambertMaterial({ color: 0x00ff00, overdraw: true }), // Large balls are be green
time_last_run, // used to calculate simulation delta
world, // This will hold the Ammo.js objects
localInertia = new Ammo.btVector3(0, 0, 0),
shape, // Will be each shape's definition
transform = new Ammo.btTransform, // will be used to position the objects
balls = [], // This will hold all of the balls we create
motionState, rbInfo, body; // Used for creating each physics body
renderer.setSize( viewport.clientWidth, viewport.clientHeight );
camera.position.set( -10, 30, -200 );
camera.lookAt( scene.position ); // Look at the center of the scene
scene.add( camera );
function addLights() {
var ambientLight = new THREE.AmbientLight( 0x555555 );
scene.add( ambientLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( -.5, .5, -1.5 ).normalize();
scene.add( directionalLight );
}
function buildScene() {
// Create the physics world
var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration;
world = new Ammo.btDiscreteDynamicsWorld(
new Ammo.btCollisionDispatcher( collisionConfiguration ), // Dispatcher for collision handling
new Ammo.btDbvtBroadphase, // Broadphase interface
new Ammo.btSequentialImpulseConstraintSolver, // Constraint solver
collisionConfiguration // Collision configuration
);
var ramp_geometry= new THREE.CubeGeometry( 50, 2, 10 ),
material_red = new THREE.MeshLambertMaterial({ color: 0xdd0000, overdraw: true }),
material_green = new THREE.MeshLambertMaterial({ color: 0x00bb00, overdraw: true });
var ramp_1 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_1 );
shape = new Ammo.btBoxShape(new Ammo.btVector3( 25, 1, 5 )); // "25" = half of the width of the ramp, "1" = half of the height, "5" = half of the depth
shape.calculateLocalInertia( 0, localInertia ); // "0" is the ramp's mass. For a static shape this must be 0
// position the ramp
ramp_1.position.x = -20;
ramp_1.position.y = 25;
ramp_1.rotation.z = -Math.PI / 28;
transform = new Ammo.btTransform;
transform.setIdentity(); // reset any existing transform
transform.setOrigin(new Ammo.btVector3( -20, 25, 0 ));
transform.setRotation(new Ammo.btQuaternion( 0, 0, -Math.PI / 28 ));
motionState = new Ammo.btDefaultMotionState( transform );
rbInfo = new Ammo.btRigidBodyConstructionInfo( 0, motionState, shape, localInertia ); // mass, motion state, shape, inertia
body = new Ammo.btRigidBody( rbInfo );
world.addRigidBody( body ); // Add this physics body to the world
var ramp_2 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_2 );
shape = new Ammo.btBoxShape(new Ammo.btVector3( 25, 1, 5 )); // "25" = half of the width of the ramp, "1" = half of the height, "5" = half of the depth
shape.calculateLocalInertia( 0, localInertia ); // "0" is the ramp's mass. For a static shape this must be 0
// position the ramp
ramp_2.position.x = 25;
ramp_2.position.y = 5;
ramp_2.rotation.z = Math.PI / 16;
transform = new Ammo.btTransform;
transform.setIdentity(); // reset any existing transform
transform.setOrigin(new Ammo.btVector3( 25, 5, 0 ));
transform.setRotation(new Ammo.btQuaternion( 0, 0, Math.PI / 16 ));
motionState = new Ammo.btDefaultMotionState( transform );
rbInfo = new Ammo.btRigidBodyConstructionInfo( 0, motionState, shape, localInertia ); // mass, motion state, shape, inertia
body = new Ammo.btRigidBody( rbInfo );
world.addRigidBody( body ); // Add this physics body to the world
// Create the floor
var floor = new THREE.Mesh( new THREE.PlaneGeometry( 100, 50 ), material_red );
scene.add( floor );
shape = new Ammo.btBoxShape(new Ammo.btVector3( 50, .01, 25 )); // "50" = half of the width of the floor, ".01" = small height number to represent the plane, "25" = half of the depth
shape.calculateLocalInertia( 0, localInertia ); // "0" is the ramp's mass. For a static shape this must be 0
// position the floor
floor.position.y = -15;
transform = new Ammo.btTransform;
transform.setIdentity(); // reset any existing transform
transform.setOrigin(new Ammo.btVector3( 0, -15, 0 ));
transform.setRotation(new Ammo.btQuaternion( 0, 0, 0 ));
motionState = new Ammo.btDefaultMotionState( transform );
rbInfo = new Ammo.btRigidBodyConstructionInfo( 0, motionState, shape, localInertia ); // mass, motion state, shape, inertia
body = new Ammo.btRigidBody( rbInfo );
world.addRigidBody( body ); // Add this physics body to the world
}
function addBall() {
var ball, mass;
if ( !state ) return;
if ( Math.random() >= .25 ) {
ball = new THREE.Mesh( ball_geometry, ball_material );
mass = 5;
shape = new Ammo.btSphereShape( 3 ); // "3" = radius
shape.calculateLocalInertia( mass, localInertia ); // "5" is the ball's mass.
} else {
ball = new THREE.Mesh( large_ball_geometry, large_ball_material );
mass = 10;
shape = new Ammo.btSphereShape( 3 ); // "4" = radius
shape.calculateLocalInertia( mass, localInertia ); // "10" is the ball's mass.
}
ball.position.y = 50;
ball.position.x = Math.random() * 40 - 20; // Random positon between -20 and 20
ball.useQuaternion = true; // Makes updating the rotations much easier as Ammo.js uses quaternions
scene.add( ball );
transform = new Ammo.btTransform;
transform.setIdentity(); // reset any existing transform
transform.setOrigin(new Ammo.btVector3( ball.position.x, ball.position.y, 0 ));
transform.setRotation(new Ammo.btQuaternion( 0, 0, 0 ));
motionState = new Ammo.btDefaultMotionState( transform );
rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, shape, localInertia ); // mass, motion state, shape, inertia
rbInfo.set_m_friction( .3 );
rbInfo.set_m_restitution( .3 );
body = new Ammo.btRigidBody( rbInfo );
body.mesh = ball; // Save a reference from the body to the 3D mesh
world.addRigidBody( body ); // Add this physics body to the world
balls.push( body );
body.setCollisionFlags( body.getCollisionFlags() | 8 );
}
function updateWorld() {
requestAnimationFrame( updateWorld );
if ( !state ) return;
var delta,
now = (new Date()).getTime(),
i,
origin, rotation;
if ( time_last_run ) {
delta = ( now - time_last_run ) / 1000;
} else {
delta = 1 / 60;
}
time_last_run = now;
world.stepSimulation(
delta * 2, // double the speed of the simulation
10, // max substeps
1 / 60 // size of each substep
);
// Update the scene objects
for ( i = 0; i < balls.length; i++ ) {
transform = balls[i].getCenterOfMassTransform();
origin = transform.getOrigin();
rotation = transform.getRotation();
balls[i].mesh.position.set( origin.x(), origin.y(), origin.z() );
balls[i].mesh.quaternion.set( rotation.x(), rotation.y(), rotation.z(), rotation.w() );
}
renderer.render( scene, camera );
}
addLights();
buildScene();
document.getElementById( 'startStop' ).addEventListener('click',
function() {
if ( this.innerHTML === 'Start' ) {
this.innerHTML = 'Stop';
time_last_run = (new Date()).getTime();
state = true;
} else {
this.innerHTML = 'Start';
state = false;
}
}
)
updateWorld();
setInterval( addBall, 500 );
JigLibJS
完善流行的物理库的是JigLibJS,C++库的一个分支。然而,不像Ammo.js,它是不是一个自动化的分支,而是手工制作成的一个JavaScript代码库,并且有大量的调整和优化;项目甚至已经收到两家公司的财政支持。这种定制为JigLibJS提供了相比Ammo.js更多的额外性能,虽然它比不上Ammo.js的功能丰富。
总的来说,API是一致的,而且容易找到东西,不过有一些容易被羁绊到的地方。例如,几乎所有的方法都用的是如你所愿的X,Y,Z上的空间和旋转的参数。然后,当用X,Z,Y定义一个新盒子形状的时候,文档清楚的声明了这是需要按照该顺序来的,不过如果你看的太快,就会被忽视掉。JigLibJS 和其他库的另外一点不同之处是在页面上包含的方式。对Ammo.js和box2dweb.js而言,只需要包含一种脚本,但是当使用JigLibJS的时候,必须单独包含每个类文件。这样不加载不需要的库的内容可以帮你让页面的存储使用率降低,不过将所有文件单独分开是大家争论的焦点。另外一点值得注意的是方法名– 有些是简单的_用下划线_分开,不过也有些是遵循 thecamelCaseStyle风格,还有些主要的方法名看起来是随机的,我猜测这些可能是和原始的C++分支相关的。
最让我惊讶的也是我接下来要介绍的最大的挑战就是如何使用JigLibJS让对象正确的旋转,事实上官方的构建中的弧度转角度的计算中有一个错误。虽然它修复起来很容易,但是这样错误的存在让人有的不安。另外需要注意角度旋转的一点是:你在设置旋转时需要将阻尼设置成看起来很合理的数值。简单的说,阻尼的作用是降低旋转的线性和速度。默认阻尼是[.5, .5, .5, 0],这个数值严格控制着旋转的度量。另外一个具体数值是[.95, .95, .95, 0],这是默认线性阻尼。使用JigLibJS更新图像和使用box2dweb很接近– 通过调用 world.get_bodies() 你会得到一个物理目标的列表,这时候再循环它们。简单的调用 get_position() 就可以获得目标位置,只不过旋转必须从矩阵中计算出,然后再转换成你图像引擎中要用的格式(如欧拉变换、四元数等)
约束条件在JigLibJS 中大多是缺失的,它只有3个基础约束条件:Point(点)(连接两个目标),WorldPoint(将一个目标固定到 World 中的一个位置)和MaxDistance(最大距离)(限制两个目标之间的距离)。当需要使用限制时,这三个基础条件应该够应付大多数情况了。
[javascript] view plain copy
// Create a Point2Point constraint to keep two objects bound together
// position_a is the point of constraint relative to object_a's position
// position_b is the point of constraint relative to object_b's position
var constraint = new jigLib.JConstraintPoint(
object_a,
object_b,
[ postition_a.x, postition_a.y, postition_a.z ],
[ postition_b.x, postition_b.y, postition_b.z ],
0, // max distance
1 // timescale
);
world.addConstraint( constraint );
概述:
性能:为Javascript定制和调整,不过还是会有帧数不稳定的时候,通常是场景顶部很多盒子挨着的时候,这对于很多物理引擎来说都是很难控制的。
特色:除了比Ammo.js要少一些不常用和不常见的特色,JigLibJS拥有你需要的大多数特性。不过我还是希望它能有更多的约束条件。
实用性:将库单独列出和看起来随机的API差异让JigLibJS和易于开发分开了。有时候开发者会觉得它很难用,不过整体来说,良好的结构化算是优点之一,一旦你学习了并且设置好环境后,还是很简单的。
总结:随着物理游戏在网上的火热发展,我们在渴求一款强大而且快速的物理引擎库。我相信JigLibJS 可以满足大多数开发者的需求,而且不需要用户电脑拥有更加强大的配置。遗憾的是我们需要让旋转工作正常并且缺少基础的物理概念也限制了该库的应用。
浏览器支持:Chrome,Firefox,IE9,Safari,Opera
[javascript] view plain copy
// Create some local variables for convenience
var state = false, // true
when the simulation is running
viewport = document.getElementById( 'viewport' ), // The canvas element we're going to use
// If your computer supports it, switch to the WebGLRenderer for a much smoother experience
// most of the lag and jittering you see in the simulation is from the CanvasRenderer, not the physics
renderer = new THREE.CanvasRenderer({ canvas: viewport }), // Create the renderer
//renderer = new THREE.WebGLRenderer({ canvas: viewport }), // Create the renderer
scene = new THREE.Scene, // Create the scene
camera = new THREE.PerspectiveCamera( 35, 1, 1, 1000 ),
ball_geometry = new THREE.SphereGeometry( 3 ), // Create the ball geometry with a radius of `3`
ball_material = new THREE.MeshLambertMaterial({ color: 0x0000ff, overdraw: true }), // Balls will be blue
large_ball_geometry = new THREE.SphereGeometry( 4 ), // Create the ball geometry with a radius of `4`
large_ball_material = new THREE.MeshLambertMaterial({ color: 0x00ff00, overdraw: true }), // Large balls are be green
time_last_run, // used to calculate simulation delta
world, // This will hold the jiglibjs objects
shape; // Will hold each physics shape as they're defined
renderer.setSize( viewport.clientWidth, viewport.clientHeight );
camera.position.set( -10, 30, -200 );
camera.lookAt( scene.position ); // Look at the center of the scene
scene.add( camera );
function addLights() {
var ambientLight = new THREE.AmbientLight( 0x555555 );
scene.add( ambientLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( -.5, .5, -1.5 ).normalize();
scene.add( directionalLight );
}
function buildScene() {
// Create the physics world
world = jigLib.PhysicsSystem.getInstance();
//world.setGravity([ 0, 10, 0 ]);
var ramp_geometry= new THREE.CubeGeometry( 50, 2, 10 ),
material_red = new THREE.MeshLambertMaterial({ color: 0xdd0000, overdraw: true }),
material_green = new THREE.MeshLambertMaterial({ color: 0x00bb00, overdraw: true });
var ramp_1 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_1 );
shape = new jigLib.JBox( null, 50, 10, 2 ); // "null" = skin, "50" = width, "10" = depth, "2" = height
shape.set_mass( 1 );
shape.set_movable( false ); // Static
shape.mesh = ramp_1;
// position the ramp
ramp_1.rotation.z = -Math.PI / 28;
shape.moveTo([ -20, 25, 0, 0 ]);
shape.set_rotationZ(shape.radiansToDegrees( ramp_1.rotation.z ));
world.addBody( shape );
var ramp_2 = new THREE.Mesh( ramp_geometry, material_red );
scene.add( ramp_2 );
shape = new jigLib.JBox( null, 50, 10, 2 ); // "null" = skin, "50" = width, "10" = depth, "2" = height
shape.set_mass( 50 );
shape.set_movable( false ); // Static
// position the ramp
shape.moveTo([ 25, 5, 0, 0 ]);
shape.set_rotationZ(shape.radiansToDegrees( -Math.PI / 16 ));
shape.mesh = ramp_2;
world.addBody( shape );
// Create the floor
var floor = new THREE.Mesh( new THREE.PlaneGeometry( 100, 50 ), material_red );
scene.add( floor );
floor.position.y = -15; // position the floor
shape = new jigLib.JBox( null, 100, 50, .01 ); // "null" = skin, "100" = width, "50" = depth, ".01" = small number to represent a plane
shape.set_mass( 1 );
shape.set_movable( false ); // Static
shape.moveTo([ 0, -15, 0, 0 ]);
world.addBody( shape );
}
function addBall() {
var ball;
if ( !state ) return;
if ( Math.random() >= .25 ) {
ball = new THREE.Mesh( ball_geometry, ball_material );
shape = new jigLib.JSphere( null, 3 ); // "null" = skin, "3" = radius
shape.set_mass( 5 );
} else {
ball = new THREE.Mesh( large_ball_geometry, large_ball_material );
shape = new jigLib.JSphere( null, 4 ); // "null" = skin, "4" = radius
shape.set_mass( 10 );
}
shape.set_rotVelocityDamping([ .95, .95, .95, 0 ]); // Set the rotational damping to a much more realistic value (`.95` matches the default linear damping)
scene.add( ball );
ball.position.set(
Math.random() * 40 - 20, // Random positon between -20 and 20
50,
0
);
shape.moveTo([ ball.position.x, ball.position.y, ball.position.z, 0 ]);
shape.mesh = ball;
world.addBody( shape );
}
function updateWorld() {
requestAnimationFrame( updateWorld );
if ( !state ) return;
var delta, now = (new Date()).getTime();
if ( time_last_run ) {
delta = ( now - time_last_run ) / 1000;
} else {
delta = 1 / 60;
}
time_last_run = now;
world.integrate( delta * 2 ); // double the speed of the simulation
// Update the scene objects
var bodies = world.get_bodies(), i, mesh, position, glmatrix, matrix;
matrix = new THREE.Matrix4;
for ( i = 0; i < bodies.length; i++ ) {
mesh = bodies[i].mesh;
if ( mesh ) {
position = bodies[i].get_position();
mesh.position.set(
position[0],
position[1],
position[2]
);
glmatrix = bodies[i].get_currentState().get_orientation().glmatrix;
matrix.set(
glmatrix[0],glmatrix[1],glmatrix[2],glmatrix[3],
glmatrix[4],glmatrix[5],glmatrix[6],glmatrix[7],
glmatrix[8],glmatrix[9],glmatrix[10],glmatrix[11],
glmatrix[12],glmatrix[13],glmatrix[14],glmatrix[15]
);
mesh.rotation.getRotationFromMatrix( matrix );
}
}
renderer.render( scene, camera );
}
addLights();
buildScene();
document.getElementById( 'startStop' ).addEventListener('click',
function() {
if ( this.innerHTML === 'Start' ) {
this.innerHTML = 'Stop';
time_last_run = (new Date()).getTime();
state = true;
} else {
this.innerHTML = 'Start';
state = false;
}
}
)
updateWorld();
setInterval( addBall, 500 );
Cannon.js
以上三个库都是已经存在的物理引擎的分支。因为它们都还是分支而且没有用 Javascript 编写,所以并没有为网页优化。Javascript有自己的特征和独特的要求,这让它的开发变的很不同,而且没有自动的代码转换工具可以有效的优化Javascript代码。由于这些原因,有人已经觉得需要从Javascript底层开始构建的物理引擎库。其中最值得注意的是由Stefan Hedman创造的 Cannon.js。他吸取了Ammo.js和Three.js易用的API特点。虽然Cannon.js现在还处在初期阶段,不过我还是觉得有必要向大家介绍展示一下它的特点。目前Cannon.js已经支持盒子,球形,平面和自定义的凸多边形,而且还非常稳定– 虽然你很深入的使用后还是会发现有一些BUG存在。如果你对未来网页上的物理引擎感兴趣,你最好现在开始就关注 Cannon.js,而且如果你对物理引擎还很熟悉,更加可以考虑为 Cannon.js做出一些贡献。
浏览器支持:Chrome,Firefox,Safari,Opera
结论
尽管现在Javascript物理已经进步许多了,不过还是处于问题多多的状态。Box2dweb 不支持3D场景,Ammo.js 在表现问题上举步维艰,而JigLibJS受到它的API设计影响并且缺乏功能性。像Cannon.js这样的挑战者虽然都是100%用Javascript编写,不过才刚刚起步,并没有被广泛运用。好消息是我们并不是要在开发物理游戏时抛弃Javascript引擎,而是要认真的权衡后决定采用什么引擎来创造出最好的游戏。尽管现在很多复杂和动态的场景对于Javascript引擎库来说还是无法触及的,而且许多生动有趣的技术都是通过自己呈现,但易于接触的Javascript引擎库正在当今世界上的浏览器中传递开来,它们为新的游戏开发创造了一个非常有趣的环境。我迫不及待的看到物理引擎被用来创建一个个独特而又精彩绝伦的用户体验。