three.js - Physics

  • 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
最终效果.png
  • We will use the cannon-es,it's a 3D library
  • First,create a basic scene,we need OrbitControlsAxesHelper可根据自己的习惯可加可不加
  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)
Set up.png
  • 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中实现旋转,使用的是quaternionthree.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')
    • 不要忘记修改sphereBodyfloorBodymaterial属性
  // 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
    • 使当前物理世界中的物体都使用同一种材料
    • 创建sphereBodyfloorBody时就不用再设置material属性了,也就是说原先的material: defaultMaterial就可以删除了
  // material
  ...
  const defaultContactMaterial = new CANNON.ContactMaterial(
    ...
  )
  ...
  world.defaultContactMaterial = defaultContactMaterial  // 使用同样的material
sphere可自由下落并弹起.png
  • 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)
    
    applyLocalForce().png
    • 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()
    
    applyForce.png
  • 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.jsmesh和创建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})
    
    sphere.png
    • 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')
    
    multiple spheres.png
    • add boxs and add to gui
    /**
     * 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')
    
    multiple boxes.png
    • 完成至这一步时,我们会发现当我们创建了很多个物体后,他们在发生碰撞时并不会翻转,这显然是不符合物理规律的
    const tick = () => {
      ...
      ...
      for(const object of objectsToUpdate) {
        object.mesh.position.copy(object.body.position)
        object.mesh.quaternion.copy(object.body.quaternion)  // 使box下落时碰撞在一起会翻转
      }
      ...
    }
    
碰撞翻转.png
  • 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)
    ...
  }
  • Remove thing
  /**
   * 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')
  • Done
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容