剖析three.js案例中物理引擎Ammo.js的使用方法,其中包含运动、碰撞、检测、自由落体、碰撞器、冻结、自定义更新等功能,有大量代码通过链接方式转到gitee,可以根据代码对照使用
主要剖析的是threejs中提供的ammo案例,相对于原本案例,本篇文章精简了一些代码,提取了一些方法,新增了一些可配置参数,并结合我上一篇文章three.js——镜头跟踪 做了一个小Demo。
先看一下效果
从图片中可以看出来,小球在落地时,触碰到人物模型的头部,改变了运动方向,后面人物模型行走时,可以踢球,改变球的运动速度。
场景
基础场景
基础场景参照three.js——镜头跟踪
物理引擎
引入ammo.js
<script src="../node_modules/three/examples/jsm/libs/ammo.wasm.js"></script>
因为是demo所以直接在html入口文件通过链接方式引入的ammo.js
,项目中有需要也可以通过modlue模式在ts文件import
方式引用;
在使用ammo的文件中从window中提取该变量
let Ammo = (window as any).Ammo
渲染
Ammo().then(function (AmmoLib) {
Ammo = AmmoLib;
(window as any).Ammo = Ammo
init();
animate();
});
threejs
的渲染要在物理引擎注册之后调用,然后在runder中更新物理引擎,渲染先放下,先从注册物理引擎开始
注册物理引擎
export let physicsWorld: any
export let dispatcher: any
// 初始化物理引擎
export function initPhysics() {
let Ammo = (window as any).Ammo
// 注册碰撞器
let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
// 注册碰撞器内容 之后的碰撞检测会使用到
dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
let broadphase = new Ammo.btDbvtBroadphase();
let solver = new Ammo.btSequentialImpulseConstraintSolver();
// 注册物理世界 后续受物理作用的模型都需要放在这里,供ammp更新
physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
// 设置初始化重力,重力是向下作用力,所以在这里取反,其实初始值设置成负数是同样的
physicsWorld.setGravity(new Ammo.btVector3(0, - gravityConstant, 0));
}
除了必须的参数,setGravity
这个方法是经常用到的,设置重力,上述代码设置的是物理引擎的重力(引力),可以模拟地球引力、月球引力等,参数<0为向下作用力,参数>0为向上作用力即有重力的物体在这个物理世界,是做向上运动的,例子等讲到注册物理体的时候一起看一下
更新物理引擎
// 更新物理引擎
/**
*
* @param deltaTime 更新时间
* @param rigidBodies 刚体
* @param cb 回调 pos:在重力影响下导致的位置变换 dir:在物理引擎影响下的角度变化 objThree 当前被影响的模型
*/
export function updatePhysics(deltaTime: number, rigidBodies: Object3D[], cb?: (pos: Vector3, dir: Vector3, objThree: Object3D) => void) {
let Ammo = (window as any).Ammo
// 定义一个变量用来存储对象的transform
const transformAux1 = new Ammo.btTransform();
// Step world
physicsWorld.stepSimulation(deltaTime, 10);
// Update rigid bodies
for (let i = 0, il = rigidBodies.length; i < il; i++) {
const objThree = rigidBodies[i];
const objPhys = objThree.userData.physicsBody;
const PhysUpdate = objThree.userData.PhysUpdate;
const ms = objPhys.getMotionState();
ms.getWorldTransform(transformAux1);
const p = transformAux1.getOrigin();
const q = transformAux1.getRotation();
objThree.position.set(p.x(), p.y(), p.z());
objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());
objThree.userData.collided = false;
...
}
}
上述代码中const transformAux1 = new Ammo.btTransform();
定义一个变量用来存储对象的transform
属性,这属性中包含getOrigin
获取对象位置,getRotation
获取角度,得到的是Ammo.btVector3
对象,rigidBodies
刚体是受控模型集合,在创建模型时,如果需要在物理世界更新,就可以将模型添加到这个数组,当然,每一个刚体都需要添加到initPhysics
方法注册的physicsWorld
方法,具体添加方法见后续代码
累了,笔者摸个鱼
十分钟后...
刚体
创建刚体
创建10个小球,作为刚体,给一个初始高度,让小球做自由落体
for (let i = 0; i < 10; i++) {
const { object: object1, body: body1 } = createRigidBody(sphere.clone(), true, true, new Vector3(0, 1, 0), null, new Vector3(0, 13, 0))
object1.name = 'sphere'
object1.userData.PhysUpdate = true
setPointerBttVec(object1, body1)
//添加到物理世界
physicsWorld.addRigidBody(body1);
作为刚体收集到刚体数组
rigidBodies.push(object1)
scene.add(object1)
}
效果如下:
参数及方法
createRigidBody
封装了一个注册刚体的方法,7个入参,一个返回值,下面会一一介绍
createRigidBody的方法参数说明
参数 | 说明 |
---|---|
object | 物体本身,刚体实体也会存储在这模型的信息userData 上面 |
isShape | 是否为实体 false为静态,即不存在碰撞器,其他刚体也会穿透模型 |
mass | 是否受引力和碰撞影响如果为false,物体运动状态不设置 反之则设置为4即代表受引力碰撞等因素影响 |
pos | 起始位置,物体和刚体初始的位置 |
quat | 四元数 表示位置方向角度等 物体和刚体的初始角度 |
vel | 线性速度 物体初始运动方向和运动速度 |
angVel | 线性角度 物体初始旋转角度 |
pointFrom | 自定义碰撞器形状 默认以object生成碰撞体,如果这个值不为空,将以传来的模型生成碰撞体 |
returns | {模型、刚体实体} 返回模型本体和模型刚体 |
const body = new Ammo.btRigidBody(rbInfo);
注册刚体部分,返回一个刚体对象其中也包含很多api可以设置运动、旋转、重力、摩擦力、以及目前笔者也没看明白的属性
属性 | 入参 | 解释 | 返回 |
---|---|---|---|
setLinearVelocity | btVector3 | 设置线性速度 | - |
getLinearVelocity | btVector3 | 获取线性速度 | btVector3 |
setGravity | btVector3 | 设置重力 | - |
getGravity | - | 获取重力 | number |
setFriction | btVector3 | 设置摩擦力 | - |
getFriction | - | 获取摩擦力 | - |
... |
有很多参数没进行验证,感兴趣的同学可以试一试
顶点信息收集器 createConvexHullPhysicsShape
这里的for循环方法可以根据具体传来的数据格式改变,position.array
结构是[1,1,1,2,2,2]
每三个为一组xyz,所以在遍历的时候 xyz取的是i,i+1,i+2;
更新刚体
通过上述提取的方法,将地板、角色都添加到物理世界, gitee代码
// 创建物理物体 包含地板和小球
createObjects();
// 创建主角,任人物模型
initPlayer()
下面以主角为例,介绍一下上述的方法作用和刚体的更新方法
// 是否通过物理世界更新模型
object.userData.PhysUpdate = true
// 是否锁定旋转
object.userData.rotateLock = true
// 位置锁定
object.userData.positionLock = ['', '', '']
PhysUpdate
字段是判断是否在更新物理引擎的时候一起更新模型,如果为false,则需要在updatePhysics
方法的回调参数手动更新,
cb?: (pos: Vector3, dir: Vector3, objThree: Object3D) => void
,这个方法会传入位置信息,角度信息,和模型自身,后期可以根据objThree.name
分类更新;
甚至也可以提前将方法绑定到模型自身 object.userData.handlePlayer = handlePlayer
类似这样。
rotateLock
判断是否可以在更新物理引擎的时候旋转物体,如果PhysUpdate
为 false,则忽略旋转锁定和位置锁定,
rotateLock和positionLock都接受两种数据格式true
和['x','y','z'],在updatePhysics
方法进行了限制
const p = transformAux1.getOrigin();
const q = transformAux1.getRotation();
const pl = (objThree.userData.positionLock || [])
let px = p.x()
let py = p.y()
let pz = p.z()
// 锁定位置
for (let i = 0; i < pl.length; i++) {
const dir = pl[i];
if (dir === 'x') px = 0
if (dir === 'y') py = 0
if (dir === 'z') pz = 0
}
const rl = (objThree.userData.rotateLock || [])
let rx = q.x()
let ry = q.y()
let rz = q.z()
let rw = q.w()
// 锁定角度
for (let i = 0; i < rl.length; i++) {
const dir = rl[i];
if (dir === 'x') rx = 0
if (dir === 'y') ry = 0
if (dir === 'z') rz = 0
if (dir === 'w') rw = 0
}
物理运动
three.js——镜头跟踪 中提到的人物运动,是通过 指针锁定控制器驱动人物运动,在物理引擎下,如果只是改变人物模型位置做运动,那么并不会触发碰撞的运动,所以在这里需要改一下,
if (controls?.isLocked) {
if (moveForward) {
const dir = new Vector3()
// 获取控制器方向参数
controls.getDirection(dir)
const v3 = dir.clone().negate().multiplyScalar(PlayerParams.speed)
// 修改运动参数
linearVelocity.set(v3.x, v3.z)
}
Player.scene.userData.physicsBody.setLinearVelocity(new Ammo.btVector3(linearVelocity.x, jumpTop, linearVelocity.y))
}
linearVelocity
参数是前文定义的一个2维向量,用来存储控制器数据的,通过getDirection
方法获取控制器旋转角度,方向取反乘以主角速度,计算出主角当前方向(旋转角度),再通过setLinearVelocity
设置线性速度,将主角往这个方向做运动,这里比较绕,参考下图实例
碰撞检测
// 碰撞检测
const rayRigidBodyCheck = () => {
// 物理世界的物体数量,在initPhysics初始化物理引擎注册的
for (let i = 0, il = dispatcher.getNumManifolds(); i < il; i++) {
const contactManifold = dispatcher.getManifoldByIndexInternal(i);
const rb0 = Ammo.castObject(contactManifold.getBody0(), Ammo.btRigidBody);
const rb1 = Ammo.castObject(contactManifold.getBody1(), Ammo.btRigidBody);
const threeObject0 = Ammo.castObject(rb0.getUserPointer(), Ammo.btVector3).threeObject;
const threeObject1 = Ammo.castObject(rb1.getUserPointer(), Ammo.btVector3).threeObject;
const n = threeObject0?.name
const n1 = threeObject1?.name
if (n1 === 'Player' && n === 'sphere') {
console.log('踢球');
}
}
}
上述代码可以在render方法中调用,主要原理就是遍历physicsWorld
中每一个碰撞体的点位,
dispatcher.getNumManifolds
获取碰撞数量,
dispatcher.getManifoldByIndexInternal(i);
通过碰撞索引获取到当前碰撞的是哪几个物体,
后续就是获取碰撞体后的信息,可以通过名称判断事件,甚至可以在加载某个碰撞物体的时候做一个埋点,检测到碰撞则执行一下;
人物的跳跃,按空格跳跃,给人物模型一个向上的力,如果一直按空格,人物则一直向上跳,甚至飞出天际, 所以这时候就需要碰撞检测,人物模型跳跃后,空格失效,可以用一个布尔值做开关,当检测人物模型碰撞到地板,空格生效,可以再次跳跃
包括子弹也是一样,发射子弹后,子弹向前运动,当子弹碰撞到敌人,敌人死亡,子弹消失,等等等等,碰撞检测可以做很多功能
也可以做机关,将一个物体只作为碰撞器,并不加入到场景中,当人物碰撞到机关,可以开门,奖励等操作
具体玩法可以参照unity游戏引擎,上述代码大部分逻辑和方法的抽离都是借鉴unity游戏引擎做的
这是unity的刚体和碰撞器的界面,感兴趣或者有时间的同学可以使用threejs做一个解密杀怪游戏
笔者对很多概念也不甚了解,如果有错误的地方,还请大佬们指正,让我们在前端的路上越走越远~
历史文章
# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)