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 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)
Set up.png
  • 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)旋轉乱顾,使用的是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軸的引力值券时,正數(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炭懊、floorBodymaterial屬性
  // 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
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({
      ...
    })
    // 發(fā)力方向,局部發(fā)力點
    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個部分稻励,創(chuàng)建three.jsmesh和創(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})
    
    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()創(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')
    
    multiple boxes.png
    • 完成至這一步時望抽,我們會發(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下落時碰撞在一起會翻轉
      }
      ...
    }
    
碰撞翻轉.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)  // 距離相距較遠的物體不參與相互的碰撞監(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)
    ...
  }
  • 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
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舰蟆,一起剝皮案震驚了整個濱河市趣惠,隨后出現(xiàn)的幾起案子狸棍,更是在濱河造成了極大的恐慌,老刑警劉巖味悄,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件草戈,死亡現(xiàn)場離奇詭異,居然都是意外死亡侍瑟,警方通過查閱死者的電腦和手機唐片,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涨颜,“玉大人费韭,你說我怎么就攤上這事⊥ス澹” “怎么了星持?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弹灭。 經(jīng)常有香客問我督暂,道長,這世上最難降的妖魔是什么穷吮? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任逻翁,我火速辦了婚禮,結果婚禮上捡鱼,老公的妹妹穿的比我還像新娘八回。我一直安慰自己,他們只是感情好驾诈,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布缠诅。 她就那樣靜靜地躺著,像睡著了一般翘鸭。 火紅的嫁衣襯著肌膚如雪滴铅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天就乓,我揣著相機與錄音,去河邊找鬼拱烁。 笑死生蚁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的戏自。 我是一名探鬼主播邦投,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼擅笔!你這毒婦竟也來了志衣?” 一聲冷哼從身側響起屯援,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎念脯,沒想到半個月后狞洋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡绿店,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年吉懊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片假勿。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡借嗽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出转培,到底是詐尸還是另有隱情恶导,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布浸须,位于F島的核電站甲锡,受9級特大地震影響,放射性物質發(fā)生泄漏羽戒。R本人自食惡果不足惜缤沦,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望易稠。 院中可真熱鬧缸废,春花似錦、人聲如沸驶社。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亡电。三九已至届巩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間份乒,已是汗流浹背恕汇。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留或辖,地道東北人瘾英。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像颂暇,于是被迫代替她去往敵國和親缺谴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容