본문 바로가기
자바스크립트와 캔버스

바빌론JS - 블랜더에서 만든거 불러오며 뒤집힘 문제 해결

지난 게시글에서는 바빌론JS 의 중력에 대해 알아보았는데요.

https://itadventure.tistory.com/709

 

바빌론JS - 중력과 컬리젼

바빌론JS의 컬리젼(Collision)에 대해 알아보았는데요.바빌론JS에서 중력과 같은 것을 가능하게 하는 것입니다.근데 이 컬리젼을 적용하려면 그동안 작업했던 것들을 포기하고 거의 새로 만들어야

itadventure.tistory.com

블렌더3D에서 제작한 모델 파일도 이렇게 순순히 잘 되면 좋으련만 그게 한번에 쉽게 안되는데요.
해외 게시글을 찾아보니 오른손/왼손 좌표계 차이 문제라고도, 바빌론JS의 버그 때문일거라고도 하더군요.
그래서 최소한의 간단한 해결책을 알아냈습니다 :)


블렌더로 준비한 그라운드 

평범한 바닥 대신 블랜더로 아래와 같이 바닥을 모델링하였습니다.

해당 파일은  GLB 파일로 저장하였는데요. 아래 파일을 다운받아 주시면 됩니다.

ground.glb
5.39MB

 


뒤집히는 문제?!

바빌론js에서 아래코드로 위 모델 파일을 불러오면, 이런 일이 발생합니다.

BABYLON.SceneLoader.ImportMeshAsync("", "./", "ground.glb")
.then((result) => {
    var mesh = result.meshes[0];
    mesh.scaling = new BABYLON.Vector3(1, 1, 1);
} );

뭔가 이상한 점을 발견하셨나요?   :


   :
   :

물체들의 방향이 뒤집혀진 것이지요!
중간에 비스듬한 산 모양이 위쪽으로 가버렸습니다.

이 상태에서는 컬리젼도 제대로 작동하지 않습니다.
헤결 방법이 없을까요?


Z축 스케일을 뒤집어 !

이 문제는 근본적으로 Z축 스케일 방향을 -1 을 곱해주어야 하는데요.
결론은 아래 코드를 사용해야 합니다.

    BABYLON.SceneLoader.ImportMeshAsync("", "./", "ground.glb")
    .then((result) => {
        var mesh = result.meshes[0];
        mesh.scaling = new BABYLON.Vector3(1, 1, -1);
    } );

만약 크기를 2배로 키우고 싶다면 코드도 아래와 같이 Z값으로 -2를 주어야 하는 것이지요.

mesh.scaling = new BABYLON.Vector3(2, 2, -2);

재미있는 것은 mesh.scaling 코드를 삭제해도 정상 작동합니다.
아마도 기본 스케일 값이 ( 1, 1, 1) 이 아니라 ( 1, 1, -1 ) 이기 떄문일 겁니다.

BABYLON.SceneLoader.ImportMeshAsync("", "./", "ground.glb")
.then((result) => {
    
} );

컬리젼 적용하기

불러온 모델 파일은 여러개의 3차원 도형으로 구성되어 있을수 있습니다.
본 바닥 모델 파일은 단 하나이긴 하지만, 모든 도형에 각각 컬리젼을 적용해야 합니다.
getChildrenMeshes()를 사용하면 자식 메시들 각각에 컬리젼을 적용할 수 있습니다.

BABYLON.SceneLoader.ImportMeshAsync("", "./", "ground.glb")
.then((result) => {
    var mesh = result.meshes[0];
    mesh.scaling = new BABYLON.Vector3(1, 1, -1);

    const childMeshes = mesh.getChildMeshes();
    for (let mesh of childMeshes) {
        mesh.checkCollisions = true;
    }
} );

main.js 소스

main.js 소스는 아래와 같습니다.

const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas,true);
// 입력 시스템
const deviceInputSystem = new BABYLON.DeviceSourceManager(engine);
// 마우스
const mouseDeviceSource = deviceInputSystem.getDeviceSource(BABYLON.DeviceType.Mouse);

let mesh_npc = null;
let currentCamera = null;
let arc_camera;
let inputMap = {};
let walk_anim = null;

var createScene = function () {
    var scene = new BABYLON.Scene(engine);

    scene.gravity = new BABYLON.Vector3(0, -0.9, 0);
    scene.collisionsEnabled = true;

    // 키보드 이벤트     
    scene.actionManager = new BABYLON.ActionManager(scene);
    scene.actionManager.registerAction(
        new BABYLON.ExecuteCodeAction(
            BABYLON.ActionManager.OnKeyDownTrigger, 
            function (evt) {
                inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
    }));
    scene.actionManager.registerAction(
        new BABYLON.ExecuteCodeAction(
            BABYLON.ActionManager.OnKeyUpTrigger, 
            function (evt) {
                inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
    }));

    arc_camera = new BABYLON.ArcRotateCamera(
        "ArcCam", 
        BABYLON.Tools.ToRadians(90), 
        BABYLON.Tools.ToRadians(65), 
        2,  // 카메라 거리
        BABYLON.Vector3.Zero(), 
        scene
    );

    ArcCam(scene, null);

    // 빛
    var light = new BABYLON.HemisphericLight(
        "light", 
        new BABYLON.Vector3(0, 1, 0), 
        scene
    );
    // 빛강도
    light.intensity = 1.5;

    BABYLON.SceneLoader.ImportMeshAsync("", "./", "ground.glb")
    .then((result) => {
        var mesh = result.meshes[0];
        mesh.scaling = new BABYLON.Vector3(1, 1, -1);

        const childMeshes = mesh.getChildMeshes();
        for (let mesh of childMeshes) {
            mesh.checkCollisions = true;
        }
    } );

    BABYLON.SceneLoader.ImportMeshAsync("", "./", "character01.glb")
    .then((result) => npc_callback(scene, result) );

    // 씬 리턴
    return scene;
};

function npc_callback(scene, result){
    var mesh = result.meshes[0];
    mesh_npc = mesh;
    mesh.scaling = new BABYLON.Vector3(0.08, 0.08, 0.08);
    mesh.position.y = 0.55;
    mesh.position.y = 2.55;
    
    ArcCam(scene, mesh_npc);
    
    // 컬리전
    mesh.checkCollisions = true;
    mesh.ellipsoid = new BABYLON.Vector3(0.15, 0.5, 0.15); // 컬리전 크기
    
    let step = 0.005;
    let p = 0;

    // 랜더 프레임별 실행 코드
    scene.onBeforeRenderObservable.add(() => {        
        walk_anim = scene.getAnimationGroupByName("walk");
        let forward_speed = 0.02;
        let backward_speed = -0.01;
        let turnleft_speed = -Math.PI/90; 
        let turnright_speed = Math.PI/90;
        let keydown = false;
        
        let vec;
        if (inputMap["w"] || inputMap["W"]) {
            vec = mesh.forward.scaleInPlace(forward_speed);
            mesh.moveWithCollisions(vec);
            keydown = true;
            walk_anim.play(true);
        }

        if (inputMap["s"] | inputMap["S"]) {
            mesh.moveWithCollisions(mesh.forward.scaleInPlace(backward_speed));
            keydown = true;
            walk_anim.play(true);
        }
        
        if (inputMap["a"] || inputMap["A"])
            mesh.rotate(BABYLON.Axis.Y, turnleft_speed);

        if (inputMap["d"] || inputMap["D"])
            mesh.rotate(BABYLON.Axis.Y, turnright_speed);
        
        if(keydown == false)
            walk_anim.stop();
        
        mesh.moveWithCollisions(new BABYLON.Vector3(0,-0.1,0.0));
       
    });
    
}

function ArcCam(scene, mesh){
    if(mesh!=null)arc_camera.lockedTarget = mesh;
    // 대상과의 거리
    arc_camera.radius = 3;
    arc_camera.wheelDeltaPercentage = 0.01; // 마우스휠 속도
    arc_camera.minZ = 0;    // 근거리 커팅 X
    camToggle(scene, arc_camera);
}

function camToggle(scene, camera){
    // 카메라 캔버스 연결
    if(currentCamera != camera)
    {
        if(currentCamera!=null) currentCamera.detachControl(canvas);
        camera.attachControl(canvas, true);
        currentCamera = camera;
        scene.activeCamera = camera;
    }   
}

const scene = createScene();

engine.runRenderLoop(function () {
    scene.render();
});

window.addEventListener("resize", function () {
    engine.resize();
});

index.html 과 character01.glb 는 지난번과 동일하고
오늘 다운받은 ground.glb 파일까지 하면 폴더 내 파일 구성은 아래와 같습니다.


결과물 구경

index.html 페이지를 띄우면 케이군이 떠오르는 연출은 동일합니다.

케이군이 앞으로 이동하다 낭떠러지에서 뚝 떨어지는 것도 동일한데요.

경사진 곳으로 케이군을 이동해서 올라가면, 와우! 마치 계단을 오르듯 경사진 곳을 올라갈 수 있습니다.
컬리전의 묘미이지요.

아직 바닥의 끝 부분으로 전진하면 낭떠러지로 한없이 떨어지는 문제가 있긴 한데요.
이건 나중에 벽을 만들어 막아 버려야 겠습니다 :)

위 결과물은 크레이의 웹사이트에서도 확인하실 수 있습니다. ( 캐릭터 이동은 아직 PC 전용 )

http://dreamplan7.cafe24.com/babylon/ex22/


마무~리

컬리전의 매력이 느껴지셨나 모르겠습니다 :)
오늘도 방문해주신 모든 분들께 감사합니다 !