前言
最近在写前端的一些项目时,涉及到物理引擎,比如将一个物体抛向空中,然后自由落到地面上,然后就了解到了anno.js,本文对anno.js进一个简单地说明,介绍其适用场景,在摸索的过程中没有找到比较完整的官方文档,但是最后还是一点点的给把常用类和方法进行啃了一下。
anno概述
anno是一个轻量级的JavaScript物理引擎,并且他也是开源的,其内部是通过Verlet积分来模拟物理系统的运动和碰撞,通过计算所有物体在每一帧所处的位置、速度以及加速度,然后根据轴对齐碰撞箱(AABB)的碰撞检测算法来模拟真实的物理世界效果,他比较适用下面这几种场景:
- 模拟使用悬挂的铁球撞击物体

- 模拟柔软的布料

- 模拟物体之间碰撞的弹跳效果

- 制作一个地形,进行碰撞检测

- 制作一个赛车游戏

anno github地址:github.com/kripken/amm...
如何使用anno
anno的使用主要分为三个大的步骤,首先是创建物理世界,包括创建配置、分发器、创建物理世界、配置重力参数等,第二部分则是创建刚体,包括创建刚体的形状、质量、运动状态,惯性等参数,并且将创建的刚体添加到物理世界当中,最后一步就是渲染部分,通过计算每一个物体的运动情况,和物体之间的相互影响,来渲染每一个物体在每一帧的状态。
细分步骤:
- 初始化物理世界,包括定义碰撞配置、碰撞调度器对象、解算器、阶段算法
- 设置物理世界的重力参数
- 创建刚体,包括设置和质量和位置等参数,配置刚体的运动方向
- 将刚体添加到物理世界
- 在渲染过程中更新物理世界
- 根据刚体的运动状态渲染页面效果
重要属性列表
参数名称 | 说明 |
---|---|
btDefaultCollisionConfiguration | 碰撞检测的默认配置 |
btCollisionDispatcher | 碰撞调度器对象 |
btDbvtBroadphase | 使用动态AABB树(DBVT)的碰撞检测,目的是剔除没有相互作用的对象 |
btSequentialImpulseConstraintSolver | 创建解算器,处理约束和碰撞响应 |
btDiscreteDynamicsWorld | 创建了一个动态的物理世界 |
setGravity | 设置物理世界的重力加速度向量 |
btGImpactCollisionAlgorithm | 碰撞检测算法 |
registerAlgorithm | 注册自定义碰撞检测算法 |
btVector3 | 表示三维向量 |
btQuaternion | 四元数的类,表示三维空间中的旋转 |
btDefaultMotionState | 刚体的运动状态,用于同步刚体在物理引擎和图形渲染之间的位置/旋转信息 |
btRigidBody | 表示为一个刚体,包括质量、旋转四元数、角速度、线速度等参数 |
btTransform | 表示刚体的信息修改,包括位置和旋转等 |
btVehicleTuning | 用于设置车辆的调节参数,可以设置悬挂刚度等属性 |
btDefaultVehicleRaycaster | 默认的车辆射线检测器,可以用于车辆的轮胎碰撞检测 |
btRaycastVehicle | 车辆类,可以创建一个车辆 |
setCoordinateSystem | 定义车轮的坐标系统和运动方向, 参数含义: rightIndex: 表示车轮索引,左右轮应设置不同索引 wheelDirectionCS:表示车轮运动方向的坐标系统 wheelAxleCS:表示车轮轴的坐标系统 |
applyEngineForce | 对车辆施加引擎驱动力 |
setBrake | 对车轮施加制动力 |
setSteeringValue | 设置方向盘转角,控制方向 |
getChassisWorldTransform | 获取车辆底盘在世界坐标系中的位置和旋转 |
stepSimulation | 更新物理世界 |
示例讲解
这里我对官网的一个小车的代码示例进行讲解,主要是来介绍在threeJs中如何引入anno和实际使用的流程及细节说明。
1.初始化场景
创建相机灯光等基础组件。
ini
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 );
camera.position.x = -4.84;
camera.position.y = 4.39;
camera.position.z = -35.11;
camera.lookAt( new THREE.Vector3( 0.33, -0.40, 0.85 ) );
controls = new THREE.OrbitControls( camera );
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setClearColor( 0xbfd1e5 );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
var ambientLight = new THREE.AmbientLight( 0x404040 );
scene.add( ambientLight );
.....省略
2.初始化物理世界
这一部分是用于初始化物理世界
ini
//创建碰撞检测的默认配置
collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
//碰撞调度器对象
dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
//阶段算法
broadphase = new Ammo.btDbvtBroadphase();
//创建解算器
solver = new Ammo.btSequentialImpulseConstraintSolver();
//初始化物理世界
physicsWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration );
//设置重力及加速度
physicsWorld.setGravity( new Ammo.btVector3( 0, -9.82, 0 ) );
3.创建立方体
创建立方体,下面这是创建立方体的公共方法,根据传入的参数不同,可以创建墙体、地面以及斜坡。
ini
//根据质量不同,创建不同颜色的材质
var material = mass > 0 ? materialDynamic : materialStatic;
var shape = new THREE.BoxGeometry(w, l, h, 1, 1, 1);
//创建立方体形状的物体
var geometry = new Ammo.btBoxShape(new Ammo.btVector3(w * 0.5, l * 0.5, h * 0.5));
//如果质量、摩擦力为空,则初始化
if(!mass) mass = 0;
if(!friction) friction = 1;
//添加到场景中
var mesh = new THREE.Mesh(shape, material);
mesh.position.copy(pos);
mesh.quaternion.copy(quat);
scene.add( mesh );
//初始化刚体的位置和旋转
var transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
var motionState = new Ammo.btDefaultMotionState(transform);
//计算惯性
var localInertia = new Ammo.btVector3(0, 0, 0);
geometry.calculateLocalInertia(mass, localInertia);
//初始化一个刚体
var rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, geometry, localInertia);
var body = new Ammo.btRigidBody(rbInfo);
//设置刚体的摩擦力
body.setFriction(friction);
//body.setRestitution(.9);
//body.setDamping(0.2, 0.2);
//添加刚体到物理世界
physicsWorld.addRigidBody( body );
//如果质量大于零,放入集合中,用于渲染物体后续的变化情况
if (mass > 0) {
body.setActivationState(DISABLE_DEACTIVATION);
// Sync physics and graphics
function sync(dt) {
var ms = body.getMotionState();
if (ms) {
ms.getWorldTransform(TRANSFORM_AUX);
var p = TRANSFORM_AUX.getOrigin();
var q = TRANSFORM_AUX.getRotation();
mesh.position.set(p.x(), p.y(), p.z());
mesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
}
}
syncList.push(sync);
}
4.创建一个小车
anno还提供了创建和操作车辆的属性,这里只对部分关键代码进行说明,因为这块代码还是有点长的,主要就是包括创建底盘和轮子。
ini
// 创建车辆底盘形状,并且初始化位置和旋转及计算惯性等属性
var geometry = new Ammo.btBoxShape(new Ammo.btVector3(chassisWidth * .5, chassisHeight * .5, chassisLength * .5));
var transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
var motionState = new Ammo.btDefaultMotionState(transform);
var localInertia = new Ammo.btVector3(0, 0, 0);
geometry.calculateLocalInertia(massVehicle, localInertia);
//创建底盘,添加到物理世界当中
var body = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(massVehicle, motionState, geometry, localInertia));
body.setActivationState(DISABLE_DEACTIVATION);
physicsWorld.addRigidBody(body);
var chassisMesh = createChassisMesh(chassisWidth, chassisHeight, chassisLength);
// 车辆配置信息
var engineForce = 0;
var vehicleSteering = 0;
var breakingForce = 0;
//调节参数设置
var tuning = new Ammo.btVehicleTuning();
//定义车辆的轮胎碰撞检测
var rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld);
//创建车辆
var vehicle = new Ammo.btRaycastVehicle(tuning, body, rayCaster);
//设置车轮的坐标系统
vehicle.setCoordinateSystem(0, 1, 2);
physicsWorld.addAction(vehicle);
//新增轮子
function addWheel(isFront, pos, radius, width, index) {
//初始化轮子,参数说明:pos位置,wheelDirectionCS0 车轮方向坐标系,wheelAxleCS 车轮轴坐标系
//suspensionRestLength 悬挂静止长度 radius
var wheelInfo = vehicle.addWheel(
pos,
wheelDirectionCS0,
wheelAxleCS,
suspensionRestLength,
radius,
tuning,
isFront);
//设置悬挂刚度
wheelInfo.set_m_suspensionStiffness(suspensionStiffness);
//设置减震伸长阻尼
wheelInfo.set_m_wheelsDampingRelaxation(suspensionDamping);
//设置减震压缩阻尼
wheelInfo.set_m_wheelsDampingCompression(suspensionCompression);
//设置摩擦力
wheelInfo.set_m_frictionSlip(friction);
//设置轮胎侧倾角影响因子
wheelInfo.set_m_rollInfluence(rollInfluence);
wheelMeshes[index] = createWheelMesh(radius, width);
}
//新增四个轮子
addWheel(true, new Ammo.btVector3(wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_LEFT);
addWheel(true, new Ammo.btVector3(-wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_RIGHT);
addWheel(false, new Ammo.btVector3(-wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_LEFT);
addWheel(false, new Ammo.btVector3(wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_RIGHT);
// 设置键盘控制
function sync(dt) {
var speed = vehicle.getCurrentSpeedKmHour();
speedometer.innerHTML = (speed < 0 ? '(R) ' : '') + Math.abs(speed).toFixed(1) + ' km/h';
breakingForce = 0;
engineForce = 0;
if (actions.acceleration) {
if (speed < -1)
breakingForce = maxBreakingForce;
else engineForce = maxEngineForce;
}
if (actions.braking) {
if (speed > 1)
breakingForce = maxBreakingForce;
else engineForce = -maxEngineForce / 2;
}
if (actions.left) {
if (vehicleSteering < steeringClamp)
vehicleSteering += steeringIncrement;
}
else {
if (actions.right) {
if (vehicleSteering > -steeringClamp)
vehicleSteering -= steeringIncrement;
}
else {
if (vehicleSteering < -steeringIncrement)
vehicleSteering += steeringIncrement;
else {
if (vehicleSteering > steeringIncrement)
vehicleSteering -= steeringIncrement;
else {
vehicleSteering = 0;
}
}
}
}
//对车辆施加驱动力
vehicle.applyEngineForce(engineForce, BACK_LEFT);
vehicle.applyEngineForce(engineForce, BACK_RIGHT);
//对车轮施加制动力
vehicle.setBrake(breakingForce / 2, FRONT_LEFT);
vehicle.setBrake(breakingForce / 2, FRONT_RIGHT);
vehicle.setBrake(breakingForce, BACK_LEFT);
vehicle.setBrake(breakingForce, BACK_RIGHT);
//设置方向盘转角
vehicle.setSteeringValue(vehicleSteering, FRONT_LEFT);
vehicle.setSteeringValue(vehicleSteering, FRONT_RIGHT);
//更新轮胎位置和旋转
var tm, p, q, i;
var n = vehicle.getNumWheels();
for (i = 0; i < n; i++) {
vehicle.updateWheelTransform(i, true);
tm = vehicle.getWheelTransformWS(i);
p = tm.getOrigin();
q = tm.getRotation();
wheelMeshes[i].position.set(p.x(), p.y(), p.z());
wheelMeshes[i].quaternion.set(q.x(), q.y(), q.z(), q.w());
}
//车辆底盘在世界坐标系中的位置和旋转
tm = vehicle.getChassisWorldTransform();
p = tm.getOrigin();
q = tm.getRotation();
chassisMesh.position.set(p.x(), p.y(), p.z());
chassisMesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
}
5.实时渲染
threeJs可以通过requestAnimationFrame不停的调用tick方法,进行渲染,但是渲染的时机是不确定的,调用函数执行的时间在16.7ms~33.3ms之间,一般情况渲染时间间隔是不需要我们来关注的。
ini
function tick() {
requestAnimationFrame( tick );
var dt = clock.getDelta();
//执行syncList中存储的方法,更新各个物体
for (var i = 0; i < syncList.length; i++)
syncList[i](dt);
//更新物理世界
physicsWorld.stepSimulation( dt, 10 );
controls.update( dt );
renderer.render( scene, camera );
time += dt;
stats.update();
}
总结
本章对anno.js中一些重要的方法进行简单的介绍,对使用流程也做了整体的说明,除此之外,官方还提供了一些其他的示例,大家可以按照自己的需求来进行参考,我觉得这个物理引擎的话,可能难点是在于存在很多的api需要了解其含义是使用方法,其他得难点倒是没有很多。