- 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
可根據(jù)自己的習慣可加可不加
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 根據(jù)自己的情況可加可不加
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() // 環(huán)境貼圖
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"
- 在物理世界中也要同步創(chuàng)建一個平面和一個球,大體的流程和使用
three.js
差不多
-
cannon-es
中創(chuàng)建一個三維向量要使用new CANNON.Vec3(x, y, z)
妻率,跟three.js
中的Vector3
是一個意思
-
cannon-es
中實現(xiàn)旋轉乱顾,使用的是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軸的引力值券时,正數(shù)向上、負數(shù)向下
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表示該物體是靜態(tài)的伏伯、不會移動的
})
floorBody.addShape(floorShape)
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5) // 旋轉橘洞,參數(shù)為方向、旋轉角度
world.addBody(floorBody)
- 在創(chuàng)建完成以上2個主要的部分后说搅,也就是
three.js
和物理世界部分炸枣,我們要同步這2個部分的動作,才能實現(xiàn)球體的自由下落
- 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已經(jīng)可以自由下落了,但因為沒有任何限制sphere會一直下降背伴,原因是我們在physics world中并沒有創(chuàng)建與
three.js
相對應的floor
/**
* physics
*/
...
...
// floor
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({
mass: 0, // 質量為0表示該物體是靜態(tài)的沸毁、不會移動的
// material: defaultMaterial,
})
floorBody.addShape(floorShape)
// 設置軸線的角度來實現(xiàn)旋轉
// 旋轉軸,旋轉角度
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5)
world.addBody(floorBody)
- 完成以上步驟后傻寂,我們應該已經(jīng)實現(xiàn)了球體自由下落的狀態(tài)息尺,但是當前的球體仿佛是一個很重的物體,下落后并沒有彈起疾掰,看似不太真實搂誉,we can change the friction and bouncing behavior by setting a
material
- we're going to create
material
for sphere
and floor
- 可以調(diào)整摩擦力系數(shù)、恢復系數(shù)感受不同的效果
// world
...
// material
const concreteMaterial = new CANNON.Material('concrete') // 混凝土
const plasticMaterial = new CANNON.Material('plastic') // 塑料
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1, // 摩擦力系數(shù)
restitution: 0.7, // 恢復系數(shù)静檬,彈起高度
}
)
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
上
- 使當前物理世界中的物體都使用同一種材料
- 創(chuàng)建
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({
...
})
// 發(fā)力方向,局部發(fā)力點
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個部分稻励,創(chuàng)建
three.js
的mesh
和創(chuàng)建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()創(chuàng)建立方體的時候父阻,從立方體中心點出發(fā),寬高計算就是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')
- 完成至這一步時望抽,我們會發(fā)現(xiàn)當我們創(chuàng)建了很多個物體后加矛,他們在發(fā)生碰撞時并不會翻轉,這顯然是不符合物理規(guī)律的
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) // 距離相距較遠的物體不參與相互的碰撞監(jiān)測
world.allowSleep = true // 不會動的物體不參與碰撞監(jiān)測
...
...
-
Events and add sounds, we're going to play hit sound when the objects collide
/**
* sounds
*/
const hitSound = new Audio('../public/sounds/hit.mp3') // 創(chuàng)建音頻
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')