3D맵 구현하기 - 캐릭터가 움직일 때 fbx 파일로 움직임 구현하기

2022. 2. 21. 18:07Technology[Front]

이번 게시글에서는 캐릭터의 움직임을 구현하기 위해 fbx파일을 FBXLoader를 이용해 가져와서 캐릭터의 애니메이션을 구현하는 방법에 대해 알아보겠습니다.

 

(1) test.js

import {FBXLoader} from 'https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/loaders/FBXLoader.js';

class BasicCharacterControllerProxy {
  constructor(animations) {
    this._animations = animations;
  }

  get animations() {
    return this._animations;
  }
};

class FiniteStateMachine {
  constructor() {
    this._states = {};
    this._currentState = null;
  }

  _AddState(name, type) {
    this._states[name] = type;
  }

  SetState(name) {
    const prevState = this._currentState;
    
    if (prevState) {
      if (prevState.Name == name) {
        return;
      }
      prevState.Exit();
    }

    const state = new this._states[name](this);

    this._currentState = state;
    state.Enter(prevState);
  }

  Update(timeElapsed, input) {
    if (this._currentState) {
      this._currentState.Update(timeElapsed, input);
    }
  }
};

class CharacterFSM extends FiniteStateMachine {
  constructor(proxy) {
    super();
    this._proxy = proxy;
    this._Init();
  }

  _Init() {
    this._AddState('idle', IdleState);
    this._AddState('walk', WalkState);
    this._AddState('run', RunState);
  }
};

class State {
  constructor(parent) {
    this._parent = parent;
  }

  Enter() {}
  Exit() {}
  Update() {}
};

class WalkState extends State {
  constructor(parent) {
    super(parent);
  }

  get Name() {
    return 'walk';
  }

  Enter(prevState) {
    const curAction = this._parent._proxy._animations['walk'].action;
    if (prevState) {
      const prevAction = this._parent._proxy._animations[prevState.Name].action;

      curAction.enabled = true;

      if (prevState.Name == 'run') {
        const ratio = curAction.getClip().duration / prevAction.getClip().duration;
        curAction.time = prevAction.time * ratio;
      } else {
        curAction.time = 0.0;
        curAction.setEffectiveTimeScale(1.0);
        curAction.setEffectiveWeight(1.0);
      }

      curAction.crossFadeFrom(prevAction, 0.5, true);
      curAction.play();
    } else {
      curAction.play();
    }
  }

  Exit() {
  }

  Update(timeElapsed, input) {
    if (moveForward || moveBackward || moveLeft || moveRight) {
      if (moveShift) {
        console.log("run");
        this._parent.SetState('run');
      }
      else {
        this._parent.SetState('walk');
      }
      return;
    }

    this._parent.SetState('idle');
  }
};

_LoadModels() {
    const loader = new FBXLoader();
    loader.setPath('./resources/remy/');
    loader.load('remy.fbx', (fbx) => {
      fbx.scale.setScalar(0.01);
      fbx.traverse(c => {
        c.castShadow = true;
      });

      this._target = fbx;
      this._params.scene.add(this._target);

      this._mixer = new THREE.AnimationMixer(this._target);

      this._manager = new THREE.LoadingManager();
      this._manager.onLoad = () => {
        this._stateMachine.SetState('idle');
      };

      const _OnLoad = (animName, anim) => {
        const clip = anim.animations[0];
        const action = this._mixer.clipAction(clip);
  
        this._animations[animName] = {
          clip: clip,
          action: action,
        };
      };

      const loader = new FBXLoader(this._manager);
      loader.setPath('./resources/remy/');
      loader.load('walk.fbx', (a) => { _OnLoad('walk', a); });
      loader.load('run.fbx', (a) => { _OnLoad('run', a); });
      loader.load('idle.fbx', (a) => { _OnLoad('idle', a); });
    });
  }
  
   let mixers = [];
   RAF();
   
   function RAF() {
        requestAnimationFrame((t) => {
          if (previousRAF === null) {
            previousRAF = t;
          }
    
          RAF();
    
          renderer.render(scene, camera);
          Step(t - previousRAF);
          previousRAF = t;
        });
      }

      function Step(timeElapsed) {
        const timeElapsedS = timeElapsed * 0.001;
        if (mixers) {
          mixers.map(m => m.update(timeElapsedS));
        }
    
        if (new_controls) {
          new_controls.Update(timeElapsedS);
        }
      }

위의 코드는 실제 프로젝트에서 일부 발췌한 것으로 사용시 일부 수정해서 사용하시면 됩니다. FBXLoader를 사용하여 _LoadModels()에서 remy.fbx파일을 로드하고 이를 scene에 추가합니다. 이 때 걷는 움직임, 뛰는 움직임 등은AnimationMixer를 이용하여 로드해서 움직임을 구현하고 Step()에서 mixers가 존재한다면(움직임을 로드했다면) update메서드에 timeElapsedS를 파라미터로 전달해서 시간의 흐름에 따라 애니메이션을 보여주게 됩니다.

각 상태(걷기, 뛰기, 가만히 있기)를 하나의 클래스로 정의하고 WASD와 SHIFT키를 눌렀다면 이를 Update()에서 인식해서 SetState메서드를 이용해 현재 상태를 업데이트 합니다. 상태가 업데이트 되면 Enter메서드를 호출하게 되고 각 상태의 클래스에서 Enter메서드를 정의하고 해당 메서드에서 애니메이션 움직임을 time을 이용해 구현합니다.

 

이번 게시글에서는 fbx 파일을 이용해 scene에 로드하고 이를 AnimationMixer를 이용해 움직임을 구현하는 방법에 대해 알아보았습니다.