- Published on
threejs物理引擎介绍-cannon-es
- Authors
- Name
- LIZHI
在学习threejs,虽然内容很多,但是我们最想学习的是根据物理规律,实现物体碰撞,那就需要物理引擎了
目前更新挺勤的 具体用法大概介绍一下
// 在threejs中创建一个球
const sphereG = new THREE.SphereGeometry(1, 32, 32)
const sphereM = new THREE.MeshStandardMaterial({color: 0x888888})
const mesh = new THREE.Mesh(sphereG, sphereM)
scene.add(mesh)
//cannon.js中创建一个球
// 首先需要定义个一个cannon.js世界,这里的世界相当于three.js中的场景scene。
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
/*
shape与body
然后,在这个世界中添加一个球,three.js中的球为mesh,mesh有geometry和material组成。在cannon.js中的物体我们是看不到的,所以在cannon.js中添加的物体只是记录three.js场景中物体的尺寸(形状,shape),即保证“灵魂”和“肉体”是一样大小。要进行正式世界的模拟,还需要在shape的基础上增加一些物理参数的设置,如质量、摩擦力、弹性...加上这些之后就是cannon.js中的body。所以,在three.js中scene.add(mesh),在cannon.js中为world.addBody(body)
*/
const shape = new CANNON.Sphere(radius)
const body = new CANNON.Body({
mass: 1,
shape,
position,
material: defaultMaterial
})
world.addBody(body)
material
对于cannon.js中的一个物体(body),需要进行materil设置,不过这个material和three.js中的material的概念有所区别。
three.js material,指的是物体“表面”的材质,如灯笼表面的罩子,人的皮肤... cannon.js material,值得是物体的“材料”属性,弹性如何、摩擦力如何...
cannon.js中material的添加过程也比three.js稍稍复杂:
需要给物体(body)添加material,即定义该物体的一些物理参数 需要将相互作用的两material(ContactMaterial)告诉world
//给body的material
const defaultMaterial = new CANNON.Material('default')
//给world的contactMaterial
const defalutContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.4,
}
)
world.addContactMaterial(defalutContactMaterial)
将cannon.js计算结果给three.js
mesh.position.x = body.position.x
mesh.position.y = body.position.y
mesh.position.z = body.position.z
//或者直接用three.js中的copy方法,更加简单
mesh.position.copy(body.position)
最后还有一部需要做,就是在动画函数中,定义step,这样每帧都会进行渲染。
let oldElapsedTime = 0
function animate() {
const time = clock.getElapsedTime()
let deltaTime = time - oldElapsedTime //每帧的时间间隔
oldElapsedTime = time
//step
world.step(1 / 60, deltaTime, 3)
//位置更新
mesh.position.copy(body.position)
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
添加平面
仅一个球还看不出来物理引擎的强大,显示的世界物体间都是有相互作用的,所以这里再添加一个地面(plane),让球与地面发生碰撞。过程和创建球是一样的,在three.js和cannon.js两个世界都添加该平面。
//Plane in Three.js
const planeG = new THREE.PlaneGeometry(100, 100)
const planeM = new THREE.MeshStandardMaterial({ color: 0x333333 })
const plane = new THREE.Mesh(planeG, planeM)
plane.rotation.x = -0.5 * Math.PI
scene.add(plane)
//Plane in Cannon.js
const planeShape = new CANNON.Plane()
const planeBody = new CANNON.Body({
mass: 0, //质量为0,表示该物体固定
shape: planeShape,
material: defaultMaterial,
})
planeBody.quaternion.setFromAxisAngle(
new CANNON.Vec3(-1, 0, 0),
Math.PI * 0.5
)
world.addBody(planeBody)
附上完整的react-demo
其中的dat.gui,为debugUI,可以让我们更改参数,实时展示在项目中
import { FC, useState, useEffect, useRef } from 'react';
import styles from './index.module.less';
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'dat.gui'
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import CANNON from 'cannon'
// Sizes
const FnComp = () => {
const ref = useRef(null)
const init = () => {
const debugObject = {}
// Debug
const gui = new dat.GUI()
// Canvas
const canvas = ref.current;
// Scene
const scene = new THREE.Scene()
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const cubeTextureLoader = new THREE.CubeTextureLoader()
const environmentMapTexture = cubeTextureLoader.load([
'/images/textures/environmentMaps/0/px.ppg',
'/images/textures/environmentMaps/0/nx.png',
'/images/textures/environmentMaps/0/py.png',
'/images/textures/environmentMaps/0/ny.png',
'/images/textures/environmentMaps/0/pz.png',
'/images/textures/environmentMaps/0/nz.png',
])
/**
* Physics 物理
*/
// World 世界
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
// NaiveBroadphase: Tests every Bodies against every other Bodies
// SAPBroadphase (Sweep and prune broadphase): Tests Bod ies on arbitrary axes during multiples steps.
// The default broadphase is NaiveBroadphase, and I recommend you to switch to SAPBroadphase. Using this broadphase can eventually generate bugs where a collision doesn't occur, but it's rare, and it involves doing things like moving Bodies very fast.
// NaiveBroadphase:针对每个其他 Bodies 测试每个 Bodies
// SAPBroadphase(扫描和修剪 Broadphase):在多个步骤中测试任意轴上的实体。
// 默认的broadphase是NaiveBroadphase,我建议你切换到SAPBroadphase。 使用这种宽相最终可能会产生不发生碰撞的错误,但这种情况很少见,而且它涉及诸如快速移动物体之类的事情。
world.broadphase = new CANNON.SAPBroadphase(world) // by default every objects on the screen are testing each others
// Even if we use an improved broadphase algorithm, all the Body are tested, even those not moving anymore. We can use a feature called sleep.
// When the Body speed gets incredibly slow (at a point where you can't see it moving), the Body can fall asleep and won't be tested unless a sufficient force is applied to it by code or if another Body hits it.
world.allowSleep = true
// Materials
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.7,
}
)
world.addContactMaterial(defaultContactMaterial)
world.defaultContactMaterial = defaultContactMaterial
// Sphere
const sphereShape = new CANNON.Sphere(0.5) // same radius as as the SphereGeomtry
const sphereBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape,
// material: defaultMaterial, // can comment if as above you provide world.defaultContactMateriel
})
sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0))
world.addBody(sphereBody)
// Floor
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({
mass: 0, // it won't move
shape: floorShape,
// material: defaultMaterial, // can comment if as above you provide world.defaultContactMateriel
})
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5)
world.addBody(floorBody)
/**
* Test sphere 球体
*/
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 32),
new THREE.MeshStandardMaterial({
color: 0x00ffff,
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
})
)
sphere.castShadow = true
sphere.position.y = 0.5
scene.add(sphere)
/**
* Floor 地板
*/
let materialFloor = new THREE.MeshStandardMaterial({
color: 0xf07200,
metalness: 0.1,
roughness: 0.4,
envMap: environmentMapTexture,
})
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10),
materialFloor
)
floor.receiveShadow = true
floor.rotation.x = -Math.PI * 0.5
scene.add(floor)
/**
* Lights
*/
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)
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
}
window.addEventListener('resize', () => {
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update renderer
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
)
camera.position.set(-3, 3, 3)
scene.add(camera)
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
})
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
/**
* Sounds
*/
const hitSound = new Audio('/sounds/hit.mp3')
const playHitSound = (collision) => {
const impactStrength = collision?.contact?.getImpactVelocityAlongNormal()
if (impactStrength > 1.5) {
hitSound.volume = Math.random()
hitSound.currentTime = 0 // // when we call hitSound.play() while the sound is playing, nothing happens because it is already playing
hitSound.play()
}
}
/**
* Utils
*/
const objectsToUpdate = []
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20)
const sphereMaterial = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
color: 0x00ffff
})
const createSphere = (radius, position) => {
// Three.js mesh
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial)
mesh.castShadow = true
mesh.scale.set(radius, radius, radius)
mesh.position.copy(position)
scene.add(mesh)
// Cannon.js body
const shape = new CANNON.Sphere(radius)
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: shape,
material: defaultMaterial,
})
body.position.copy(position)
// Event
body.addEventListener('collide', playHitSound)
world.addBody(body)
// Save in objects to update
objectsToUpdate.push({
mesh: mesh,
body: body,
})
}
// Create box
const boxGeometry = new THREE.BoxGeometry(1, 1, 1)
const boxMaterial = new THREE.MeshStandardMaterial({
color: 0x00ffff,
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
})
const createBox = (width, height, depth, position) => {
// Three.js mesh
const mesh = new THREE.Mesh(boxGeometry, boxMaterial)
mesh.scale.set(width, height, depth)
mesh.castShadow = true
mesh.position.copy(position)
scene.add(mesh)
// Cannon.js body
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: shape,
material: defaultMaterial,
})
body.position.copy(position)
// Event
body.addEventListener('collide', playHitSound)
world.addBody(body)
// Save in objects
objectsToUpdate.push({ mesh, body })
}
createBox(1, 1.5, 2, { x: 0, y: 3, z: 0 })
/**
* GUI
*/
// debugObject.createSphere = () => {
// createSphere(0.5, { x: 0, y: 3, z: 0 })
// }
debugObject.createSphere = () => {
createSphere(Math.random() * 0.5, {
x: (Math.random() - 0.5) * 3,
y: 3,
z: (Math.random() - 0.5) * 3,
color:'green'
})
}
gui.add(debugObject, 'createSphere')
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')
// Reset
debugObject.reset = () => {
for (const object of objectsToUpdate) {
// Remove body
object.body.removeEventListener('collide', playHitSound)
world.removeBody(object.body)
// Remove mesh
scene.remove(object.mesh)
}
}
gui.add(debugObject, 'reset')
const parameters = {
color: 0x90f000,
}
gui.addColor(parameters, 'color').onChange((value) => {
materialFloor.color.set(parameters.color)
})
/**
* Animate
*/
const clock = new THREE.Clock()
let oldElapsedTime = 0
const tick = () => {
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - oldElapsedTime
oldElapsedTime = elapsedTime
// Update physics world
sphereBody.applyForce(new CANNON.Vec3(-0.5, 0, 0), sphereBody.position)
// like a wind for applied on the ball at the end of the animation
world.step(1 / 60, deltaTime, 3)
for (const object of objectsToUpdate) {
object.mesh.position.copy(object.body.position)
object.mesh.quaternion.copy(object.body.quaternion)
}
sphere.position.copy(sphereBody.position)
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
}
useEffect(()=>{
init();
},[])
return (
<div className={styles['cotainer']} >
<canvas ref={ref}></canvas>
</div>
);
};
export default FnComp;