three.js--物理碰撞

1,633 阅读3分钟

使用物理动画库

npm install --save cannon (旧的库)

npm install --save cannon-es@0.15.1 使用es库(新的库)

schteppe.github.io/cannon.js/

对单个物体创建物理碰撞及施加力

import CANNON from 'cannon'

/**
 * 原理就是在物理世界部分创建相同的物体将计算的值给Threejs的物体
 */ 

// 物理世界部分
  const world = new CANNON.World()
  /* 设置重力x,y,z(y = -9.82 是地球重力) */
  world.gravity.set(0, -9.82, 0) 

// --创建物理世界材料,并设置两种材料碰撞的参数
  /*参数名随便*/
  const defaultMaterial = new CANNON.Material('default') 
  const defaultContactMaterial = new CANNON.ContactMaterial(
    defaultMaterial, /* 要碰撞的材料1 */
    defaultMaterial, /* 要碰撞的材料2 */
    {
      friction: 0.1,
      restitution: 0.7
    }
  )
  /* 将材料添加到物理世界 */
  world.addContactMaterial(defaultContactMaterial) 
  /* 设置世界碰撞的默认材料 【设置世界基本碰撞材料就可以不单个使用】 */
  world.defaultContactMaterial = defaultContactMaterial 

// --创建物理世界物体
  const sphereShape = new CANNON.Sphere(0.5)
  /* 创建球体body,定位,物体 */
  const sphereBody = new CANNON.Body({ 
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: sphereShape,
    material: defaultMaterial /* 球体的碰撞材料 | C */
  })
  /* 给物体的一点加力 | L */
  sphereBody.applyLocalForce(
    new CANNON.Vec3(150, 0, 0), /* 要应用的力矢量 */
    new CANNON.Vec3(0, 0, 0) /* 施加力的身体局部点 */
  )
  /* 在世界中添加身体是球体的body */
  world.addBody(sphereBody) 

// --创建物理地面并旋转
  const floorShape = new CANNON.Plane()
  const floorBody = new CANNON.Body()
  floorBody.mass = 0
  /* 地板的碰撞材料 | C */
  floorBody.material = defaultMaterial 
  floorBody.addShape(floorShape)
  /* 沿着x轴旋转半圈 */
  floorBody.quaternion.setFromAxisAngle( 
    new CANNON.Vec3(-1, 0, 0),
    Math.PI * 0.5
  )


// 在更新动画中推进物理世界
  const clock = new THREE.Clock()
  const oldElapsedTime = 0

  const tick = () => {
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - oldElapsedTime
    oldElapsedTime = elapsedTime

    /* 模拟风吹的力[对世界点施加力] */
    sphereBody.applyForce(
        new CANNON.Vec3(-0.5, 0, 0),
        sphereBody.position
    )

    /* 此时物理世界的物体值已经在运动 * /
    world.setp(1/60, deltaTime , 3) 
    /* 将物理世界物体的值复制给threejs物体 */
    sphere.position.copy(sphereBody.position) 
  }
  tick()

对多个物体添加物理碰撞及施加力

// 环境贴图
const environmentMapTexture = ...??
      
// 创建一个声音,物体碰撞的时候发出声音
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
        hitSound.play()
    }
}
      
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0) 
// 优化性能
world.broadphase = new CANNON.SAPBroadphase(world)
world.allowSleep = true /* 让没有碰撞的物体休眠,不检测,提升很大性能 */

// 用来保存所有球体的数组
const objectsToUpdate = []
const sphereGeometry = new THREE.SphereBufferGeometry(1, 20, 20)
const sphereMaterial = new THREE.MeshStandardMaterial({
    metalness: 0.3,
    roughness: 0.4,
    envMap: environmentMapTexture
})
// utils 创建球体
const createSphere = (raduis, position) => {
    // Three.js mesh
    const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial)
    mesh.scale.set(raduis, raduis, raduis)
    mesh.castShadow = true
    mesh.position.copy(position)
    scene.add(mesh)

    // CANNON.js body
    const shape = new CANNON.Sphere(raduis)
    const body = new CANNON.Body({
        mass: 1,
        position: new CANNON.Vec3(0, 3, 0),
        shape,
        material: defaultMaterial
    })
    body.position.copy(position)
        world.addBody(body)

        // save in objects to update(保存更新对象到数组中)
        objectsToUpdate.push({
        mesh,
        body
    })
}

createSphere(0.5, {x: 0, y: 3, y: 0})

// 创建盒子,物理盒子threejs中创建盒子不同
// 物理盒子值需要提供长宽高的一般(因为他是从盒子中心点开始计算的)
const boxGeometry = new THREE.BoxBufferGeometry(1, 1, 1)
const boxMaterial = new THREE.MeshStandardMaterial({
    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 box = new CANNON.Box(new CANNON.Vec3(width/2, height/2, depth/2))
    const body = new CANNON.Body({
        mass: 1,
        position: new CANNON.Vec3(0, 3, 0),
        box,
        material: defaultMaterial
    })
    body.position.copy(position)
  	// 添加碰撞事件,碰撞的时候发出声音
  	body.addEventListener('collide', playHitSound)
  	world.addBody(box)
        // save in objects to update(保存更新对象到数组中)
  	objectsToUpdate.push({
    	mesh,
        body
    })
}

// 在gui控制面板中添加按钮创建随机小球
const gui = new dat.GUI()
const debugObject = {}

debugObject.createShpere = () => {
    createSphere(
        Math.random() * 0.5, 
        {
          x: (Math.random() - 0.5) * 3, 
          y: 3, 
          y: (Math.random() - 0.5) * 3
        }
    )
}
debugObject.createBox = () => {
    createBox(
        Math.random(),
        Math.random(), 
        Math.random(), 
        {
          x: (Math.random() - 0.5) * 3, 
          y: 3, 
          y: (Math.random() - 0.5) * 3
        }
    )
}
// 删除操作
debugObject.reset = () => {
    for(let object of objectsToUpdate) {
        object.body.removeEventListener('collide', playHitSound)
        world.removeBody(object.body)
        scene.remove(object.mesh)
    }
}

gui.add(debugObject, 'createShpere')
gui.add(debugObject, 'createBox')
gui.add(debugObject, 'reset')

// 在更新动画中为所有要更新的对象创建物理碰撞
const tick = () => {
    for(let object of objectsToUpdate) {
        // 拷贝物理定位
        object.mesh.position.copy(object.body.position)
        // 拷贝物理局部旋转(立方体物理碰撞有旋转)
        object.mesh.quaternion.copy(object.body.quaternion)
    }
}