目前在 Github 上搜索到的 3D 物理引擎库有 Cannon.js、Oimo.js、Ammo.js、Energy.js、Physijs 等等,大部分都已许久没有更新迭代了(长达好几年),项目的 Star 数量和 Issues 数量也不多,我们该如何选择?
Cannon.js、Oimo.js 和 Energy.js 作为 Babylon.js 的内置物理引擎,我们试着从这三个下手。
Energy.js:使用 C++ 编写转 JavaScript 的 3D 物理引擎,源码不可读,目前 Github 比较冷清。
Oimo.js:一款轻量级的 3D 物理引擎,文件大小 153 KB。
Cannon.js:完全使用 JavaScript 编写的优秀 3D 物理引擎,包含简单的碰撞检测、各种形状的摩擦力、弹力、约束等功能。
从综合性来看,我更偏向于 Cannon.js ,所以下面主要讲讲 Cannon.js。
Cannon.js 的特性
以下是 Cannon.js 的特性,基本可以满足大部分的 3D 物理开发场景。
使用 Cannon.js
我们以官方的一个平面加球刚体的例子来快速上手 Cannon.js。
1、初始化物理世界
使用 Cannon.js 前需要创建 CANNON.World 对象,CANNON.World 对象是负责管理对象和模拟物理的中心。
创建完 CANNON.World 对象后,接着设置物理世界的重力,这里设置了负 Y 轴为 10 m/s²。
let world = new CANNON.World()
world.gravity.set(0, -10, 0)
Cannon.js 提供了 Broadphase、NaiveBroadphase 两种碰撞检测阶段,默认是 NaiveBroadphase。
world.broadphase = new CANNON.NaiveBroadphase()
2、创建动态球体
创建 Body 分三个步骤:
创建形状
为形状添加刚体
将刚体添加到世界
let sphereShape = new CANNON.Sphere(1) // Step 1
let sphereBody = new CANNON.Body({ // Step 2
mass: 5,
position: new CANNON.Vec3(0, 10, 0),
shape: sphereShape
})
world.add(sphereBody) // Step 3
第一步创建了半径为 1 的球形,第二步创建球的刚体,如果刚体的 mass 属性设置为 0,刚体则会处于静止状态,静止的物体不会和其他静止的物体发生碰撞,我们这里为球的刚体设置了 5kg,球会处于动态的状态,会受的重力的影响而移动,会与其他物体发生碰撞。
3、创建静态平面和动态球体
// 平面 Body
let groundShape = new CANNON.Plane()
let groundBody = new CANNON.Body({
mass: 0,
shape: groundShape
})
// setFromAxisAngle 旋转 X 轴 -90 度
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -1.5707963267948966)
world.add(groundBody)
创建平面形状,接着是刚体,这里设置了平面刚体的 mass 为 0,保证刚体处于静止状态。默认情况下平面的方向是朝向 Z 方向的(竖立着),可以通过 Body.quaternion.setFromAxisAngle 对平面进行旋转。
4、创建平面和球的网格
前面创建的刚体在场景中并没有实际的视觉效果,这一步创建平面、球的网格。
// 平面网格
let groundGeometry = new THREE.PlaneGeometry(20, 20, 32)
let groundMaterial = new THREE.MeshStandardMaterial({
color: 0x7f7f7f,
side: THREE.DoubleSide
})
let ground = new THREE.Mesh(groundGeometry, groundMaterial)
scene.add(ground)
// 球网格
let sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
let sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00 })
let sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
5、模拟世界
接着我们为物理世界开启持续更新,并且将创建的球刚体与球网格关联起来。
function update() {
requestAnimationFrame(update)
world.step(1 / 60)
if (sphere) {
sphere.position.copy(sphereBody.position)
sphere.quaternion.copy(sphereBody.quaternion)
}
}
通过这几步,一个简单的物理场景就完成了,另外更多官方例子可以点击这里,可以查看到 Cannon.js 各个约束、摩擦力、模拟汽车等特性的例子。
其他:
1、自定义物理材质需关联
还是上面的例子,现在场景中刚体的物理特性都为默认的,我希望球的恢复系数高一点,即掉落时弹跳的更高。首先需要通过 CANNON.Material 实例物理材质,刚体使用该物理材质,最后通过 CANNON.ContactMaterial 来定义两个刚体相遇后会发生什么。
// 平面
let ground_cm = new CANNON.Material() // Step 1 : 实例 CANNON.Material
let groundBody = new CANNON.Body({
…
material: groundMaterial // Step 2 : 使用该物理材质
…
})
// 球
let sphere_cm = new CANNON.Material()
let sphereBody = new CANNON.Body({
…
material: sphere_cm
…
})
let sphere_ground = new CANNON.ContactMaterial(ground_cm, sphere_cm, { // Step 3 : 定义两个刚体相遇后会发生什么
friction: 1,
restitution: 0.4
})
world.addContactMaterial(sphere_ground) // Step 4 : 添加到世界中
如果刚体需要缩放,则需要为刚体添加此属性,来更新刚体大小。
boxBody.updateMassProperties()
let tween = new TimelineMax()
tween.to(sphereBody.shapes[0], 2, {
radius: 0.2 // 缩放至 0.2
})
点击交互
在 3D 的世界中不能像我们在 DOM 中为一个节点绑定点击事件那么容易,在 Three.js 中提供了 THREE.Raycaster 方法处理点击交互,使用鼠标或者手指点击屏幕时,会将二维坐标进行转换,发射一条射线判断与哪个物体发生了碰撞,由此得知点击了哪个物体。点击这里官方例子
let raycaster = new THREE.Raycaster()
let mouse = new THREE.Vector2()
function onTouchEnd(ev) {
// 点击获取屏幕坐标
var event = ev.changedTouches[0]
mouse.x = (event.clientX / window.innerWidth) 2 - 1
mouse.y = -(event.clientY / window.innerHeight) 2 + 1
raycaster.setFromCamera(mouse, camera)
let intersects = raycaster.intersectObjects(scene, true)
for (let i = 0; i < intersects.length; i++) {
console.log(intersects[i]) // 与射线发生碰撞的物体
}
}
性能方面
模型精细程度
在 Web 端由于性能的限制,在开发过程中要尽量避免做一些损耗性能较大的事情。
首先是模型的精细程度,在保证效果的前提下,尽量降低模型面的数量,也就是说采用低模模型,一些模型的凹凸褶皱感也可以通过凹凸贴图的方式去实现,越是复杂的模型在实时渲染的过程中就越占用手机性能。
光源与阴影
另外一方面光源、阴影也是占性能,尤其是阴影。光源一般会使用平行光或者聚光灯,这种光源照射在物体上更为真实,使用半球光会稍微提升帧数,但效果略差些,阴影效果前面提到过要遍历每一个子 Mesh 接收产生阴影 castShadow 和接收阴影 receiveShadow,这相当耗费性能,开启后对阴影的精细程度以及阴影类型进行参数优化,在 Android 系统性能不太好,iOS 系统基本能保证流畅运行,所以建议根据设备系统优化。
var n = navigator.userAgent
if (/iPad|iPhone|iPod/.test(n) && !window.MSStream) { } // 针对 iOS 系统使用阴影
抗锯齿与像素比
抗锯齿是让模型的边缘效果更加圆滑不粗糙,也会占用一些性能,默认是关闭的,视情况开启。
renderer.antialias = true // 开启抗锯齿
另外像素比 setPixelRatio,移动端由于 Retina 屏的缘故,一般会设置为 2,所以使用window.devicePixelRatio 获取实际设备像素比动态设置的话,部分大屏手机的像素比有 3 的情况,所有会因为像素比过高造成性能问题。
renderer.setPixelRatio(2) // 推荐
renderer.setPixelRatio(window.devicePixelRatio) // 不推荐
工具推荐
最后推荐一些在开发过程中常用的工具:
OrbitControls 轨道控制器
OrbitControls 是用于调试 Camera 的方法,实例化后可以通过鼠标拖拽来旋转 Camera 镜头的角度,鼠标滚轮可以控制 Camera 镜头的远近距离,旋转和远近都会基于场景的中心点,在调试预览则会轻松许多。
new THREE.OrbitControls(camera, renderer.domElement)
开启 Camera Helper 调试模式后,可以直观的看到 Camera 的 Fov、 Nera、Far的参数效果。
let camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
let helper = new THREE.CameraHelper(camera)
scene.add(helper)
Light Helper 光源调试模式
聚光灯开启 Light Helper 调试模式后,可以直观的看到 distance、angle 的参数效果。
let light = new THREE.DirectionalLight(0xffffff)
let helper = new THREE.DirectionalLightHelper(0xffffff)
scene.add(helper)
AxesHelper 坐标轴调试模式
AxesHelper 是在场景的中心点,添加一个坐标轴(红色:X 轴、绿色:Y 轴、蓝色:Z 轴),方便辨别方向。
let axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
Cannon.js 3D 物理引擎调试模式
Cannon.js 3D 物理引擎提供的调试模式需引入 Debug renderer for Three.js,可以将创建的物理盒子、球、平面等显示线框,便于在使用过程中实时查看效果。
let cannonDebugRenderer = new THREE.CannonDebugRenderer(scene, world)
function render() {
requestAnimationFrame(render)
cannonDebugRenderer.update() // Update the debug renderer
}