개발/three.js / / 2022. 10. 26. 15:32

three.js 도미노 시뮬레이션

반응형

도미노 객체를 만들어서 넘어뜨리는 시뮬레이션을 구현해보자.

 

도미노 객체 정의

// Domino.js 

import { Mesh, BoxGeometry, MeshBasicMaterial } from 'three';
import { Body, Box, Vec3 } from 'cannon-es';

export class Domino{
    constructor(info) {
        this.scene = info.scene;
        this.cannonWorld = info.cannonWorld;

        this.width = info.width || 0.6;
        this.height = info.height || 1;
        this.depth = info.depth || 0.2;

        this.x = info.x || 0;
        this.y = info.y || 0.5;
        this.z = info.z || 0;
        
        this.rotationY = info.rotationY || 0;

        info.gltfLoader.load(
            './models/domino.glb',
            glb => {
                this.modelMesh = glb.scene.children[0];
                this.modelMesh.castShadow = true;
                this.modelMesh.position.set(this.x, this.y, this.z);
                this.scene.add(this.modelMesh);
            }
        );
    }
}

도미노 객체를 정의하고 생성자에서 각종 매개변수 값들과 모델을 load 하도록 한다.

 

// main.js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

...

// create domino
const dominos = [];
let domino;
for(i = 0; i < 20; i++){
    domino = new Domino({
        scene,
        cannonWorld,
        gltfLoader,
        z: -i * 0.8
    });
    dominos.push(domino);
}

도미노 객체를 생성하고 배열에 넣어놓는다. 도미노의 mesh를 scene에 넣는 것은 생성자에서 진행한다.

 

물리엔진 적용

생성한 도미노에 물리엔진을 적용해보자.

 

// Domino.js

export class Domino{
    constructor(info) {
    	
        ...
        
        info.gltfLoader.load(
            './models/domino.glb',
            glb => {
                ...

                this.setCannonBody();
            }
        );
    }

    setCannonBody() {
        const shape = new Box(new Vec3(this.width/2, this.height/2, this.depth/2));
        this.cannonBody = new Body({
            mass: 1,
            position: new Vec3(this.x, this.y, this.z),
            shape
        })

        this.cannonBody.quaternion.setFromAxisAngle(
            new Vec3(0, 1, 0), // y축
            this.rotationY
        );

        this.cannonWorld.addBody(this.cannonBody);
    }
}

도미노 클래스에 body를 설정하는 함수를 정의한다. 질량과 위치, 모양, rotation등을 설정하고 world에 body를 추가하여 적용한다.

 

// main.js

...

function draw() {
    const delta = clock.getDelta();

    let cannonStepTime = 1/60;
    if (delta < 0.01) cannonStepTime = 1/120;
    cannonWorld.step(cannonStepTime, delta, 3)

    dominos.forEach(item => {
        if(item.cannonBody){
            item.modelMesh.position.copy(item.cannonBody.position);
            item.modelMesh.quaternion.copy(item.cannonBody.quaternion);
        }
    });

    renderer.render(scene, camera);
    renderer.setAnimationLoop(draw);
}

물리엔진을 적용하기 위해 rendering에서 domino에 body가 존재하면 body의 위치와 mesh를 동기화시킨다.

 

Raycast로 도미노 체크하기

이번에는 raycast를 이용해서 특정 도미노를 클릭하면 넘어뜨리도록 해보자.

// main.js

...

// create domino
const dominos = [];
let domino;
for(let i = -3; i < 17; i++){
    domino = new Domino({
        index: i,
        scene,
        cannonWorld,
        gltfLoader,
        z: -i * 0.8
    });
    dominos.push(domino);
}

function checkIntersects() {
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(scene.children);
    console.log(intersects[0].object.name);
}

// event
window.addEventListener('resize', setSize);
canvas.addEventListener('click', (e) => {
    mouse.x = e.clientX / canvas.clientWidth * 2 - 1;
    mouse.y = -(e.clientY / canvas.clientHeight * 2 - 1);

    checkIntersects();
});

클릭이벤트로 마우스가 클릭한 위치에 raycaster를 쏴서 충돌검사를 한다.

domino를 생성할 때는 index를 매개변수에 포함한다.

 

// Domino.js

export class Domino{
    constructor(info) {
        this.scene = info.scene;
        this.cannonWorld = info.cannonWorld;

        this.index = info.index; 
        
        ...

        info.gltfLoader.load(
            './models/domino.glb',
            glb => {
                this.modelMesh = glb.scene.children[0];
                this.modelMesh.name = `${this.index}번 도미노`;
                this.modelMesh.castShadow = true;
                this.modelMesh.position.set(this.x, this.y, this.z);
                this.scene.add(this.modelMesh);

                this.setCannonBody();
            }
        );
    }
    
    ...
}

도미노 객체에서는 매개변수로 받아온 index를 name으로 설정한다.

 

도미노 넘어뜨리기

이제 힘을 가해서 도미노를 넘어뜨리자

// Domino.js

export class Domino{
    ...
    
    setCannonBody() {
    
        ...

        this.modelMesh.cannonBody = this.cannonBody;

        this.cannonWorld.addBody(this.cannonBody);
        console.log('ee');
    }
}

Domino 객체에서 Body를 가져올 수 있도록 하기 위해서 modelMesh에 cannonBody를 집어넣는다.

 

// main.js

...

// Contact Material
const defaultMaterial = new CANNON.Material('default');
const defaultContactMaterial = new CANNON.ContactMaterial(
    defaultMaterial,
    defaultMaterial,
    {
        friction: 0.01,
        restitution: 0.9
    }
);
cannonWorld.defaultContactMaterial = defaultContactMaterial;

...

function checkIntersects() {
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(scene.children);

    /// another method to applyForce
    // for(const item of intersects){
    //     if(item.object.cannonBody){
    //         item.object.cannonBody.applyForce(
    //             new CANNON.Vec3(0, 0, -100),
    //             new CANNON.Vec3(0, 0, 0)
    //         );
    //         break;
    //     }
    // }

    if(intersects[0].object.cannonBody){
        intersects[0].object.cannonBody.applyForce(
            new CANNON.Vec3(0, 0, -100),
            new CANNON.Vec3(0, 0, 0)
        );
    }
}

click하여 raycast를 쏘면 맞은 도미노 객체 body에 힘을 준다. 이때 도미노가 아닌 다른 오브젝트를 건드릴 것을 대비하여 body가 있는지를 검토한다.

raycast로 감지한 intersects의 오브젝트 body에 접근하는 방법은 for-break문을 이용해서도 할 수 있다.

 

넘어질 때 마찰력이나 반발력에 따라서 잘 넘어가지 않을 수 있으므로 ContactMaterial의 값들을 조절한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유