Published on

threejs物理引擎介绍-cannon-es

Authors

在学习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;