Three.js Chapter 3 section 20 Physics

황상진·2024년 4월 15일
0

Three.js

목록 보기
8/15
post-thumbnail

Physics

  • 수학을 활용해서 직접 Physics를 구현할 수 있다.
  • 그러나 tension, friction, bouncing, constraints, pivots등과 같은 실제 physics를 구현하고 싶으면 library를 추천한다.

to do

  • phsyics world 만들기
  • Three.js 3D world 만들기
  • Three.js world에 object를 추가하면 phsics world에도 object 추가
  • 매 프레임 마다, physics world를 update하고 이를 Three.js에 따라서 update한다.

Library

2D library와 3D library가 있다.
Three.js는 무조건 3D library만 써야하는 건 아니다.
2D로도 충분한 경우 2D library를 사용하면 더 효율적이다.

3D libarary

2D library

Physi.js
https://chandlerprall.github.io/Physijs/

Cannon.js

  • Ammo.js가 가장 많이 사용된다.
  • 그러나 Cannon.js가 사용하기 쉽고 이해하기 편리하다.

사용법

  • install Cannon.js
npm install --save cannon
  • import Cannon.js
import CANNON from 'cannon'
  • Physics world 생성
/**
 * Physics
 */
const world = new CANNON.World()
  • 중력 생성
world.gravity.set(0, - 9.82, 0)
  • object 생성
const sphereShape = new CANNON.Sphere(0.5)
  • Body 생성
  • mass를 0으로 설정하면 움직이지 않는 object가 된다.
const sphereBody = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: sphereShape
})
  • Physics world에 Body 추가
world.addBody(sphereBody)
  • Cannon.js world를 Three.js에 update하기
  • step 함수 사용하기 (3개의 parameter - timestamp, 최근 업데이트한 시간, delay time)
  • Three.js object에 update된 Physics object position update하기
const clock = new THREE.Clock()
let oldElapsedTime = 0

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

    world.step(1 / 60, deltaTime, 3)

    sphere.position.copy(sphereBody.position)

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}
  • 바닥 만들어주기

    plane은 처음 만들면 세로로 세워져있으므로 바닥으로 사용할려면 x축기준으로 반을 돌리면 된다.

const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({
    mass:0,
    shape: floorShape
})
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(- 1, 0, 0), Math.PI * 0.5)
world.addBody(floorBody)

Material

  • friction, bouncing 정도를 Material을 통해서 변경할 수 있습니다.
  • Three.js material이 아닌 Cannon Material 입니다.
  • Contact Material도 만들어주어야합니다.
  • Contact Material은 다른 두 Material이 만났을 때, friction, restituion 을 설정해줍니다.
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)
  • Body에 추가한 Material을 적용해줍니다.
const sphereBody = new CANNON.Body({
    // ...
    material: plasticMaterial
})

// ...

const floorBody = new CANNON.Body()
floorBody.material = concreteMaterial

  • 하나의 Material만 사용해도 된다면 default material을 사용하면 좋다
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(
    defaultMaterial,
    defaultMaterial,
    {
        friction: 0.1,
        restitution: 0.7
    }
)
world.addContactMaterial(defaultContactMaterial)

// ...

const sphereBody = new CANNON.Body({
    // ...
    material: defaultMaterial
})

// ...

floorBody.material = defaultMaterial
  • 아래 코드로 더 쉽게 적용이 가능하다.
world.defaultContactMaterial = defaultContactMaterial

Apply Forces

  • applyForce - 공간의 특정 지점에 force를 적용한다. (바람, 도미노, 앵그리버드)
  • applyImpulse - velocity를 가한다.
  • applyLocalForce - local object 좌표에 적용 가능하한 force
  • applyLocalImpulse - local object 좌표에 적용 가능하한 velocity
sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0))
  • 아래와 같이 매 틱마다 불러와서 적용시킬 수 있다.
const tick = () =>
{
    // ...

    // Update physics
    sphereBody.applyForce(new CANNON.Vec3(- 0.5, 0, 0), sphereBody.position)

    world.step(1 / 60, deltaTime, 3)

    // ...
}

Handle multiple objects

  • geometry와 material을 미리 만들어준다.
  • create 함수에서 radius, position을 받아서 만들어준다.
  • Three mesh를 만들고 Cannon에서 shape과 body를 만들어준다.
  • 만들어준 mesh와 body를 배열에 보관하여 관리한다.
  • tick 함수에서 이 mesh와 body를 update해준다.
const objectsToUpdate = []

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) => {
    // 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)
    world.addBody(body)

    objectsToUpdate.push({mesh,body})
}

const tick = () =>
{
    // ...

    world.step(1 / 60, deltaTime, 3)

    for(const object of objectsToUpdate)
    {
        object.mesh.position.copy(object.body.position)
    }
}

Box with Quaternion

  • sphere의 경우 그냥 떨어져서 구르면 이상한점을 못느끼지만
  • box의 경우 떨어지면 rotation을 해주어야한다.
  • 이를 위해서 position 뿐만 아니라 quternion도 Cannon.js body와 동기화 시켜준다.
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) => {
    // Three.js mesh
    const mesh = new THREE.Mesh(boxGeometry, boxMaterial)
    mesh.castShadow = true
    mesh.scale.set(width,height,depth)
    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)
    world.addBody(body)

    objectsToUpdate.push({mesh,body})
}

const tick = () =>
{
    // ...

    for(const object of objectsToUpdate)
    {
        object.mesh.position.copy(object.body.position)
        object.mesh.quaternion.copy(object.body.quaternion)
    }

    // ...
}

Broadphase

  • 충돌하는 object 쌍들의 리스트의 근사값을 빠르게 계산하는 알고리즘

  • 여러 종류의 broadphase 알고리즘이 존재한다.

  • Naive Broadphase - 모든 body들을 다 테스트

  • Grid Broadphase - world를 grid화해서 본인의 grid나 바로 인접한 grid에 있는 object들과만 테스트한다.

  • SAP Boradphase - 무작위의 축으로 object들을 여러 단계로 테스트한다.(Sweet and Prune) - 효율적이다. 사용 추천

  • object가 극도로 낮은 속도가 되면 sleep을 시킬 수 있다. - 효율적이다. 사용 추천

world.broadphase = new CANNON.SAPBroadphase(world)
world.allowSleep = true

Event

  • Body가 colide, sleep, wakeup의 Event가 발생하는지 체크 및 사용할 수 있다.

  • collide 되었을 때 sound를 내고 싶으면 아래와 같이 사용한다.

  • body 생성 후에 해당 body에 collide event를 연결해준다.

  • sound의 경우 재시작 되도록 하면 자연스럽다.

  • 너무 많은 sound가 계속 들리면 문제이기 때문에 impact되는 정도에 따라서 sound를 play시키면 자연스럽다.

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 createBox = (width, height, depth, position) =>
{
    // ...

    body.addEventListener('collide', playHitSound)

    // ...
}

Remove

  • 생성된 object들과 body들을 reset하는 기능
  • object, body를 담고 있는 배열을 활용하고
  • 지우기 전에 body에 있는 eventListener를 없애준다.
  • 이후 body를 없애고
  • scene에서 object mehs를 없앤다.
  • object, body가 담긴 배열도 비운다.
debugObject.reset = () =>
{
    for(const object of objectsToUpdate)
    {
        // Remove body
        object.body.removeEventListener('collide', playHitSound)
        world.removeBody(object.body)

        // Remove mesh
        scene.remove(object.mesh)
    }
  	objectsToUpdate.splice(0, objectsToUpdate.length)
}

Cannon.js 다른 기능

  • HingeConstraint - 문의 hinge 처럼 작동
  • DistanceConstraint - body들의 거리를 유지시켜줌
  • LockConstraint - body들을 하나처럼 합쳐줌
  • PointToPointConstraint - 바디들의 특정 지점을 연결해줌

https://schteppe.github.io/cannon.js/docs/
https://schteppe.github.io/cannon.js/

  • js파일을 나누어서 여러 thread가 나누어서 처리하도록 하여 성능을 높여라

cannon은 업데이트 안되고 있으니, cannon-es를 사용해라

npm install cannon-es 

import * as CANNON from 'cannon-es'


Physi.js를 사용해도 좋다.
<https://chandlerprall.github.io/Physijs/>


결과물
<https://20-physics-c37grrr7k-hwangsangjins-projects.vercel.app/>
profile
Web FrontEnd Developer

0개의 댓글

관련 채용 정보