- We are going to use
three.jsto achieve physics effects, likes bounce、friction、bouncing- we create a physics world
- we create a three.js 3D world
- when we add an object to the three.js world, we also add one to the physics world
- on each frame, we let physics world update itself and we update the three.js world accordingly

最终效果.png
- We will use the cannon-es,it's a 3D library
- First,create a basic scene,we need
OrbitControls,AxesHelper可根据自己的习惯可加可不加
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as dat from 'dat.gui'
// scene
const scene = new THREE.Scene()
// light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.2)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.camera.far = 15
directionalLight.shadow.camera.left = - 7
directionalLight.shadow.camera.top = 7
directionalLight.shadow.camera.right = 7
directionalLight.shadow.camera.bottom = - 7
directionalLight.position.set(5, 5, 5)
scene.add(directionalLight)
// camera
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
100
)
camera.position.set(-3, 3, 3) // 这里注意下camera位置
// renderer
const renderer = new THREE.WebGLRenderer()
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
// axesHelper 根据自己的情况可加可不加
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
// control
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// render
const clock = new THREE.Clock()
const tick = () => {
controls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
tick()
- Add texture, a sphere and a floor,球稍稍高于平面
/**
* texture
*/
const cubeTextureLoader = new THREE.CubeTextureLoader() // 环境贴图
const environmentMapTexture = cubeTextureLoader.load([
'../public/imgs/physics/0/px.png',
'../public/imgs/physics/0/nx.png',
'../public/imgs/physics/0/py.png',
'../public/imgs/physics/0/ny.png',
'../public/imgs/physics/0/pz.png',
'../public/imgs/physics/0/nz.png',
])
/**
* sphere
*/
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32)
const sphereMaterial = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
envMapIntensity: 0.5
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
sphere.position.y = 0.5
scene.add(sphere)
/**
* floor
*/
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10),
new THREE.MeshStandardMaterial({
color: '#777777',
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
envMapIntensity: 0.5
})
)
floor.receiveShadow = true
floor.rotation.x = - Math.PI * 0.5
scene.add(floor)

Set up.png
- Next create a Cannon.js world
- run
npm i cannon-es --saveand import, 当前版本"cannon-es": "^0.20.0" - 在物理世界中也要同步创建一个平面和一个球,大体的流程和使用
three.js差不多 -
cannon-es中创建一个三维向量要使用new CANNON.Vec3(x, y, z),跟three.js中的Vector3是一个意思 -
cannon-es中实现旋转,使用的是quaternion,three.js中也有遇到过
- run
import * as CANNON from 'cannon-es' // 导入cannon-es
/**
* physics
*/
// world
const world = new CANNON.World({
// add gravity, it's vec3, the same as vector3
// 分别对应x、y、z轴的引力值,正数向上、负数向下
gravity: new CANNON.Vec3(0, -9.82, 0),
})
// sphere
const sphereShape = new CANNON.Sphere(0.5) // shape
const sphereBody = new CANNON.Body({ // body
mass: 1, // 质量
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape,
})
world.addBody(sphereBody)
// floor
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({
mass: 0, // 质量为0表示该物体是静态的、不会移动的
})
floorBody.addShape(floorShape)
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5) // 旋转,参数为方向、旋转角度
world.addBody(floorBody)
- 在创建完成以上2个主要的部分后,也就是
three.js和物理世界部分,我们要同步这2个部分的动作,才能实现球体的自由下落- use
step()to update the physics world on each frame
// render const clock = new THREE.Clock() let oldElapsedTime = 0 const tick = () => { let elapsedTime = clock.getElapsedTime() const deltaTime = elapsedTime - oldElapsedTime oldElapsedTime = elapsedTime // update physics world world.step(1 / 60, deltaTime, 3 ) // 固定时间,距离上一步的时长,多少次迭代可以弥补延迟 ... ... }- our sphere is falling but we're not update the
three.jsscene
// render const clock = new THREE.Clock() let oldElapsedTime = 0 const tick = () => { ... // update physics world world.step(1 / 60, deltaTime, 3 ) // 固定时间,距离上一步的时长,多少次迭代可以弥补延迟 sphere.position.copy(sphereBody.position) ... }- 此时,sphere已经可以自由下落了,但因为没有任何限制sphere会一直下降,原因是我们在physics world中并没有创建与
three.js相对应的floor
/** * physics */ ... ... // floor const floorShape = new CANNON.Plane() const floorBody = new CANNON.Body({ mass: 0, // 质量为0表示该物体是静态的、不会移动的 // material: defaultMaterial, }) floorBody.addShape(floorShape) // 设置轴线的角度来实现旋转 // 旋转轴,旋转角度 floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5) world.addBody(floorBody) - use
- 完成以上步骤后,我们应该已经实现了球体自由下落的状态,但是当前的球体仿佛是一个很重的物体,下落后并没有弹起,看似不太真实,we can change the friction and bouncing behavior by setting a
material- we're going to create
materialforsphereandfloor - 可以调整摩擦力系数、恢复系数感受不同的效果
- we're going to create
// world
...
// material
const concreteMaterial = new CANNON.Material('concrete') // 混凝土
const plasticMaterial = new CANNON.Material('plastic') // 塑料
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1, // 摩擦力系数
restitution: 0.7, // 恢复系数,弹起高度
}
)
world.addContactMaterial(concretePlasticContactMaterial)
// sphere
const sphereBody = new CANNON.Body({
...
material: plasticMaterial,
})
// floor
const floorBody = new CANNON.Body({
...
material: concreteMaterial,
})
- we're going to simplify everything and replace the two
materialby just one defaultmaterial- only use the
new CANNON.Material('default') - 不要忘记修改
sphereBody、floorBody的material属性
- only use the
// material
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.7
}
)
world.addContactMaterial(defaultContactMaterial)
// sphere
const sphereBody = new CANNON.Body({
...
material: defaultMaterial,
})
// floor
const floorBody = new CANNON.Body({
...
material: defaultMaterial,
})
- after use
new CANNON.Material('default'),there is a more simple way , 就是将其直接设置在world上- 使当前物理世界中的物体都使用同一种材料
- 创建
sphereBody、floorBody时就不用再设置material属性了,也就是说原先的material: defaultMaterial就可以删除了
// material
...
const defaultContactMaterial = new CANNON.ContactMaterial(
...
)
...
world.defaultContactMaterial = defaultContactMaterial // 使用同样的material

sphere可自由下落并弹起.png
-
Apply forces
- applyForce - apply a force from a specified point in space(not necessarily on the body's surface), just like a wind, a small push on a domino or a strong force on an angry bird
- applyImpulse - like applyForce but instead of adding to the force, will add to the velocity 施力使得增加速度
- applyLocalForce - same as applyForce but the coordinates are local to the Body ((0, 0, 0) would be the center of the Body 物体的重心) 局部坐标
- applyLocalImpulse - same as applyImpulse but the coordinates are local to the Body
- use
applyLocalForceto apply a small push on thesphere
// sphere const sphereShape = new CANNON.Sphere(0.5) const sphereBody = new CANNON.Body({ ... }) // 发力方向,局部发力点 sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0)) world.addBody(sphereBody)applyLocalForce().png- mimic the wind by using
applyForceon each frame before updating the world
const tick = () => { ... // update force sphereBody.applyForce(new CANNON.Vec3(-0.5, 0, 0), sphereBody.position) // update world ... ... } tick()applyForce.png -
Handle multiple objects
- the first, remove the
sphere, remove thesphereShapeand thesphereBody - autoMate with the functions, we're going to create a function that can create spheres, 这个function中主要有2个部分,创建
three.js的mesh和创建physics中的sphereBody
/** * utils */ // create sphere const sphereGeometry = new THREE.SphereGeometry(1, 20, 20) const sphereMaterial = new THREE.MeshStandardMaterial({ metalness: 0.3, roughness: 0.4, envMap: environmentMapTexture, envMapIntensity: 0.5 }) const createSphere = (radius, position) => { // mesh const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial) mesh.castShadow = true mesh.scale.set(radius, radius, radius) mesh.position.copy(position) scene.add(mesh) // body const shape = new CANNON.Sphere(radius) const body = new CANNON.Body({ mass: 1, position: new CANNON.Vec3(0, 3, 0), shape, material: defaultMaterial }) body.position.copy(position) world.addBody(body) } createSphere(0.5, {x: 0, y: 3, z: 0})sphere.png- nothing is moving because we don't update the
three.jsmeshes, and then loop this array in thetickfunction and update themesh.positionwithbody.position
/** * utils */ const objectsToUpdate = [] ... ... const createSphere = (radius, position) => { ... ... // save it to update objectsToUpdate.push({mesh, body}) } createSphere(0.5, {x: 0, y: 3, z: 0})const tick = () => { ... ... world.step(1 / 60, deltaTime, 3 ) for(const object of objectsToUpdate) { object.mesh.position.copy(object.body.position) } ... ... }- add to gui, we will have a button and when i click this button it will create a sphere
/** * gui */ const gui = new dat.GUI() const debugObject = {} debugObject.createSphere = () => { createSphere( Math.random() * 0.5, { x: (Math.random() - 0.5) * 3, y: 3, z: (Math.random() - 0.5) * 3 }) } gui.add(debugObject, 'createSphere')multiple spheres.png- add boxs and add to gui
/** * utils */ ... ... // create box const boxGeometry = new THREE.BoxGeometry(1, 1, 1) const boxMaterial = new THREE.MeshStandardMaterial({ metalness: 0.3, roughness: 0.4, envMap: environmentMapTexture, envMapIntensity: 0.5 }) const createBox = (width, height, depth, position) => { // mesh const mesh = new THREE.Mesh(boxGeometry, boxMaterial) mesh.castShadow = true mesh.scale.set(width, height, depth) mesh.position.copy(position) scene.add(mesh) // body // new CANNON.Box()创建立方体的时候,从立方体中心点出发,宽高计算就是new THREE.BoxGeometry()的一半 const shape = new CANNON.Box(new CANNON.Vec3(width * 0.5, height * 0.5, depth * 0.5)) const body = new CANNON.Body({ mass: 1, position: new CANNON.Vec3(0, 3, 0), shape, material: defaultMaterial }) body.position.copy(position) world.addBody(body) objectsToUpdate.push({mesh, body}) }/** * gui */ ... ... debugObject.createBox = () => { createBox( Math.random(), Math.random(), Math.random(), { x: (Math.random() - 0.5) * 3, y: 3, z: (Math.random() - 0.5) * 3 }) } gui.add(debugObject, 'createBox')multiple boxes.png- 完成至这一步时,我们会发现当我们创建了很多个物体后,他们在发生碰撞时并不会翻转,这显然是不符合物理规律的
const tick = () => { ... ... for(const object of objectsToUpdate) { object.mesh.position.copy(object.body.position) object.mesh.quaternion.copy(object.body.quaternion) // 使box下落时碰撞在一起会翻转 } ... } - the first, remove the

碰撞翻转.png
- when testing the collisions between objects, a naive approach to test every body against every other body 每个物体都在关注自己与其他物体的碰撞,即使是距离他很远的物体,这在性能上是很不有好的,we call this step the broadPhase
- now we can use SAPBroadPhase, sweep and prune, test bodies on arbitrary axes during multiple steps, and if the body speed is slowly, it will be not test unless a sufficient force applied
/**
* physics
*/
// world
const world = new CANNON.World({
...
})
world.broadphase = new CANNON.SAPBroadphase(world) // 距离相距较远的物体不参与相互的碰撞监测
world.allowSleep = true // 不会动的物体不参与碰撞监测
...
...
-
Events and add sounds, we're going to play hit sound when the objects collide
/**
* sounds
*/
const hitSound = new Audio('../public/sounds/hit.mp3') // 创建音频
const playHitSound = (collision) => {
const impactStrength = collision.contact.getImpactVelocityAlongNormal() // 撞击强度
if(impactStrength > 1.5) {
hitSound.volume = Math.random()
hitSound.currentTime = 0
hitSound.play()
}
}
/**
* utils
*/
...
...
const createSphere = (radius, position) => {
...
// body
...
body.addEventListener('collide', playHitSound)
...
}
const createBox = (width, height, depth, position) => {
...
// body
...
body.addEventListener('collide', playHitSound)
...
}
-
Remove thing
/**
* gui
*/
debugObject.reset = () => {
for(const object of objectsToUpdate) {
// remove body
object.body.removeEventListener('collide', playHitSound)
world.removeBody(object.body)
// remove mesh
scene.remove(object.mesh)
}
// empty objectsToUpdate
objectsToUpdate.splice(0, objectsToUpdate.length)
}
gui.add(debugObject, 'reset')
-
Done




