- We are going to use
three.js
to 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
- 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)
- Next create a Cannon.js world
- run
npm i cannon-es --save
and import, 当前版本 "cannon-es": "^0.20.0"
- 在物理世界中也要同步创建一个平面和一个球,大体的流程和使用
three.js
差不多
-
cannon-es
中创建一个三维向量要使用new CANNON.Vec3(x, y, z)
,跟three.js
中的Vector3
是一个意思
-
cannon-es
中实现旋转,使用的是quaternion
,three.js
中也有遇到过
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.js
scene
// 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)
- 完成以上步骤后,我们应该已经实现了球体自由下落的状态,但是当前的球体仿佛是一个很重的物体,下落后并没有弹起,看似不太真实,we can change the friction and bouncing behavior by setting a
material
- we're going to create
material
for sphere
and floor
- 可以调整摩擦力系数、恢复系数感受不同的效果
// 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
material
by just one default material
- only use the
new CANNON.Material('default')
- 不要忘记修改
sphereBody
、floorBody
的material
属性
// 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
-
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
applyLocalForce
to apply a small push on the sphere
// 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)
- mimic the wind by using
applyForce
on each frame before updating the world
const tick = () => {
...
// update force
sphereBody.applyForce(new CANNON.Vec3(-0.5, 0, 0), sphereBody.position)
// update world
...
...
}
tick()
-
Handle multiple objects
- the first, remove the
sphere
, remove the sphereShape
and the sphereBody
- 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})
- nothing is moving because we don't update the
three.js
meshes, and then loop this array in the tick
function and update the mesh.position
with body.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')
/**
* 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')
- 完成至这一步时,我们会发现当我们创建了很多个物体后,他们在发生碰撞时并不会翻转,这显然是不符合物理规律的
const tick = () => {
...
...
for(const object of objectsToUpdate) {
object.mesh.position.copy(object.body.position)
object.mesh.quaternion.copy(object.body.quaternion) // 使box下落时碰撞在一起会翻转
}
...
}
- 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)
...
}
/**
* 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')