three.js의 애니메이션 시스템

개요

three.js의 애니메이션 시스템을 통해 모델의 다양한 속성을 애니메이트 할 수 있습니다 : 스킨드 앤 리지드 모델(Skinned & Rigged Model)의 뼈대, 모프 타겟(Morph Targets), 재질의 속성(색, 불투명도, boolean 값), 보이고 않보이기, 변환(위치, 크기, 회전). 애니메이트 된 속성은 점진적으로 서서히 변경될 수 있습니다. 다른 객체 뿐만 아니라 동일한 객체의 서로 다른 연속된 애니메이션의 가중치(Weight)와 타임 스케일(Time Scale)은 독립적으로 변경될 수 있습니다. 동시에 다양한 애니메이션을 다른 물체에 동기화할 수 있습니다.

하나의 동일한 차원의 시스템에서 애니메이션에 대한 이 모든 것들을 이루기 위해서 three.js의 애니메이션 시스템은 2015년에 완전히 변경되었으며, 유니티와 언리얼 엔진4와 유사한 구조를 갖게 되었습니다. 이 문서는 애니메이션 시스템의 주요 요소에 대한 개략적인 개요와 어떻게 함께 작동 되는지를 설명합니다.

애니메이션 클립(Animation Clips)

블렌더 등과 같은 3차원 모델링 툴로부터 glTF 형식으로 저장하고 이를 three.js의 GLTFLoader 코드를 통해 모델을 불러왔다면(이 모델이 Bones이나 Morph Targets 또는 둘다 없어도 상관없음) 이 모델 갹체에는 AnimationClips 타입의 객체를 가지는 배열인 “animations”라는 이름의 필드를 가지고 있습니다.

각각의 AnimationClip 객체는 객체의 특정 행위에 대한 데이터를 가지고 있습니다. 예를들어 만약 매시가 케릭터라면 첫번째는 걷기, 두번째는 점프, 세번째는 옆으로 걷기에 대한 데이터가 될 수 있습니다.

키프레임 트랙(Keyframe Tracks)

이런 AnimationClip 내부에는 개별적인 KeyframeTrack 객체에 저장된 애니메이트 속성 데이터가 저장되어 있습니다. 케릭터 객체가 스켈레턴(Skeleton)을 가지고 있다고 한다면, 하나의 키프레임 트랙은 시간에 대한 팔의 위치 변화에 대한 데이터를 저장하고 있습니다. 다른 키프레임 트랙은 회전 변화나 크기 변화 등이 저장되어 있구요. 하나의 AnimationClip은 여러개의 키프레임 트랙으로 구성될 수 있다는 것입니다.

애니메이션 믹서(Animation Mixer)

저장된 데이터는 오직 애니메이션을 위한 기반만으로 구성됩니다. 실제 재생은 AnimationMixer를 통해 제어됩니다. 이 AnimationMixer가 단순히 애니메이션을 재생하는 객체일 뿐이라고 생각할 수 있지만 이 객체는 여러개의 애니메이션을 연속적으로 자연스럽게 처리될 수 있도록 블렌딩하고 합칠 수 있는 실제 믹서(Mixer)와 같은 기능을 수행합니다.

애니메이션 액션(Animation Action)

AnimationMixer 자체는 몇개 안되는 (일반적인) 속성과 매서드를 가지고 있는데, AnimationMixer가 AnimationAction에 의해 제어되기 때문입니다. AnimationAction을 구성함으로써 어떤 AnimationClip이 재생되어지고, 잠시 멈추거나 완전히 멈추거나 할때를 결정할 수 있고 애니메이션을 얼마나 반복될 것인지, 다른 애니메이션 액선으로의 전환(Fade)이나 타임 스케일링(Time Scaling) 등을 수행할 수 있습니다.

애니메이션 객체 그룹(Animation Object Groups)

공유된 애니메이션 상태를 받기 위해 객체들의 그룹이 필요하다면 AnimationObjectGroup를 사용할 수 있습니다.

지원되는 형식과 로더(Loader)

모든 모델 형식이 애니메이션을 포함할 수 있는 것은 아닙니다. three.js 로더 들중 몇개만이 AnimationClip을 지원합니다 : THREE.ObjectLoader, THREE.BVHLoader, THREE.ColladaLoader, THREE.FBXLoader, THREE.GLTFLoader, THREE.GLTFLoader, THREE.MMDLoader

3dx max와 Maya는 단일 파일에 여러개의 애니메이션을 내보낼 수 없습니다.

예제

let mesh;

// Create an AnimationMixer, and get the list of AnimationClip instances
const mixer = new THREE.AnimationMixer( mesh );
const clips = mesh.animations;

// Update the mixer on each frame
function update () {
	mixer.update( deltaSeconds );
}

// Play a specific animation
const clip = THREE.AnimationClip.findByName( clips, 'dance' );
const action = mixer.clipAction( clip );
action.play();

// Play all animations
clips.forEach( function ( clip ) {
	mixer.clipAction( clip ).play();
} );

CSS의 @property 사용예

자주 사용하지는 않지만 CSS에도 변수를 정의해 재활용할 수 있는 문법을 제공하는데 :root를 정의하고 이 안에 원하는 값을 넣어 정의하곤 했습니다. CSS에 대한 더 높은 수준의 경험 많은 개발자는 이런 CSS에서의 변수 정의를 반드시 사용해야 하는 상황을 만나게 됩니다. 이런 CSS에서의 변수를 정의하는 좀더 표준화된 문법이 있는데 그것은 @property입니다. 표준화의 의미는 엄격함이라는 조건을 달아 실수를 줄여 견고한 코드를 작성하도록 하는 장치와 같습니다.

여튼 저는 –a라는 이름의 변수(프로퍼티)를 다음처럼 정의했습니다.

@property --a {
  syntax: "<angle>"; /* https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax */
  inherits: false;  
  initial-value: 0turn;
}

프로퍼티가 가지는 타입(syntax)와 초기값을 지정하고 있습니다. 그럼 이 프로퍼티를 사용하는 코드를 보면 다음과 같습니다.

.box::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-conic-gradient(from var(--a), #f00, #ff0, #fff, #0ff, #f0f, #f00);
  border-radius: 25px;
  animation: rotating 4s linear infinite;
}

@keyframes rotating {
  0% {
    --a: 0turn;
  }
  100% {
    --a: -4turn;
  }
}

명확하고 직관적입니다.

실제 위의 코드가 적용한 예제 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      background-color: #222;
    }

    .box {
      position: relative;
      width: 400px;
      height: 300px;
    }

    .box span {
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 2.5rem;
      font-weight: bold;
    }

    .box::before {
      content: '';
      position: absolute;
      inset: 0;
      background: repeating-conic-gradient(from var(--a), #f00, #ff0, #fff, #0ff, #f0f, #f00);
      border-radius: 25px;
      animation: rotating 4s linear infinite;
    }

    .box::after {
      content: '';
      position: absolute;
      inset: 0;
      background: repeating-conic-gradient(from var(--a), #f00, #ff0, #fff, #0ff, #f0f, #f00);
      border-radius: 25px;
      animation: rotating 4s linear infinite;
      filter: blur(40px);
      opacity: 0.75;
    }

    .box span {
      position: absolute;
      inset: 4px;
      background: #222;
      border-radius: 22px;
      z-index: 1;
    }

    @property --a {
      syntax: '<angle>'; /* https://developer.mozilla.org/en-US/docs/Web/CSS/@property/syntax */
      inherits: false;  
      initial-value: 0turn;
    }

    @keyframes rotating {
      0% {
        --a: 0turn;
      }
      100% {
        --a: -4turn;
      }
    }
  </style>
</head>
<body>
  <div class="box">
    <span>GIS DEVELOPER</span>
  </div>
</body>
</html>

결과는 다음과 같습니다.

DOM에 대한 표시 여부 감시(IntersectionObserver)

먼저 코드는 다음과 같습니다.

import './style-intersectionObserver.css'

const divApp = document.querySelector("#app")

if(divApp) {
  for(let i=0; i<20; i++) {
    const div = document.createElement("div")
    div.innerHTML = (i+1).toString()
    divApp.append(div)
  }
  
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if(entry.isIntersecting) {
        entry.target.classList.add("visible")
      } else {
        entry.target.classList.remove("visible")
      }
    })
  }, {
    threshold: 0
  })

  const divList = divApp.querySelectorAll("div")
  divList?.forEach((div) => observer.observe(div))
}

스타일은 다음과 같구요.

body {
  margin: 0;
}

#app {
  overflow-y: auto;
  position: fixed;
  background-color: rgb(45, 46, 47);
  background-image: 
    linear-gradient(to right, rgb(35, 36, 37) 1px, transparent 1px), 
    linear-gradient(to bottom, rgb(35, 36, 37) 1px, transparent 1px);
  background-size: 32px 32px;

  width: 100%;
  height: 100vh;
}

#app div {
  color: white;
  font-size: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 8px auto;
  height: 160px;
  width: 160px;
  border-radius: 10%;
  background-color: black;
  border: 2.5px solid white;
  transition: all 0.5s ease-in-out;
  transform: rotate(-90deg) scale(0.1);
  opacity: 0;
}

#app div.visible {
  transform: rotate(0deg) scale(1);
  opacity: 1;
}

실행 결과는 다음과 같습니다.

three.js를 이용한 VR(Vitrual Reality) 컨텐츠 제작

이 포스트는 three.js를 이용해서 웹 기반의 VR 어플리케이션을 만드는 기본 요소에 대한 개략적인 내용을 언급합니다.

작업 흐름

먼저, 프로젝트에 VRButton.js를 import 해야 합니다.

import { VRButton } from 'three/addons/webxr/VRButton.js';

VRButton.createButton()은 2가지 중요한 것을 수행합니다: VR 호환성을 가지는 버튼을 생성하며 사용자가 그 버튼을 활성화하면 VR 세션을 개시합니다. 이를 위해 단지 다음 코드를 작성하면 됩니다.

document.body.appendChild( VRButton.createButton( renderer ) );

다음으로는, WebGLRenderer 객체의 XR 렌더링을 활성화시킵니다.

renderer.xr.enabled = true;

마지막으로, 애니메이션 루프(Animation Loop)에 대한 코드로 자주 사용되는 requestAnimationFrame 함수 대신에 setAnimationLoop를 대신 사용하도록 합니다. 코드는 다음과 같습니다.

renderer.setAnimationLoop( function () {
    renderer.render( scene, camera );
} );

다음 단계

3D 장면에 대한 구성은 일반적인 three.js API로 개발하면 되고 VR와 관련된 UI적인 기능은 three.js에서 제공하는 VR에 특화된 추가적인 API를 사용해 개발하면 됩니다.

이 글의 원문입니다.

프론트엔드 개발 방법의 정석

IT 기술 중 웹을 위한 프론트엔드 개발을 위한 기술은 그 변화가 매우 심하고 변덕스럽습니다. 한때 jQuery는 프론트엔드 개발에서 매우 핵심적인 자리를 차지했다가 지금은 과거에 비해 사용되지 않으며 현재는 Vue나 React와 같은 기술이 그 자리를 차지하고 있습니다.

정상적으로 작동하는 웹 페이지를 만들기 위해 꼭 필요한 기술은 HTML, CSS, JS(이하 Javascript)입니다. HTML은 웹 페이지의 구조를 정의하고 CSS는 웹 페이지를 시각적으로 어떻게 보일지를 정의하며 JS는 정적인 웹 페이지를 동적으로 만드는 생명력을 부여해 줍니다. 이 3가지만 알면 웹 페이지 개발이 가능함에도 정작 실무에서는 다른 대안을 고민합니다. jQuery, Vue, React 등과 같은 것들을 말이죠.

문제는 이런 것들은 웹 표준이 아니므로 자주 변하고 변덕스럽습니다. 특정한 회사나 개발자에 의해 만들어지므로 해당 기술은 다양한 이유로 인해 시장에서 사라지기도 합니다. 사실 매우 유명한 오픈소스 중 많은 것들이 1인 개발자나 소규모의 개발 조직에 의해 만들어지고 유지됩니다. 물론 작다고 해서 문제라는 것은 아닙니다. 오히려 소프트웨어 개발에 있어서 작은 조직은 큰 조직 보다 훨씬 효율적이고 효과적으로 작동하고 더 적합할 때가 많습니다. 작고 크고는 필요에 의해 정해지는 것으로 서로 장단점이 있습니다.

jQuery나 Vue, React에 대한 다른 관점에서의 문제는 이런 기술들은 개발자에게 HTML, CSS, JavaScript에 대한 선행 학습을 요구하면서 동시에 추가적인 지식에 대한 학습을 강요합니다. 이런 추가적으로 알아야할 기술은 HTML, CSS, JavaScript에 대한 내용보다 훨씬 더 방대할 때가 많습니다. 또 자주 변하고 매우 독창적이기까지 합니다. 또 아니다 싶으면 그냥 폐기되고 다른 대안이 나옵니다. 프론트엔드 개발 기술의 흐름은 과거부터 지금까지 늘 이런 흐름의 반복입니다. 프론트엔드 개발자들은 분명 이러한 문제가 있음을 알면서도 결국에는 대세가 되는 프로트엔드 기술을 사용하데, 그 이유는 많은 개발자, 많은 개발사에서 사용되고 있다는 이유가 가장 절대적입니다.

저 역시도 제 스스로를 다독 거리며 React를 학습하고 프로젝트에 사용 했습니다. 현재 시점에서 React는 프론트엔드 분야에서 점유율 1위라 사실은 어떤 문제를 더 이상 문제가 아닌 것으로 바꿔 버립니다. React는 나쁘지 않았고 완전히 새로운 방법으로 웹 페이지를 매우 효과적으로 개발할 수 있었습니다. 하지만 프로젝트를 마무리하고 회상해 보면 웹의 기본 기술은 HTML, CSS, JS만으로도 충분히 개발이 가능하다는 것을 깨닫게 되었습니다.

이 글은 프론트엔드 개발을 위해 가장 기본적이며 근본적인, 하지만 매우 효과적인 방법이라고 생각되는 것들을 여러분에게 공유하고 더 개선해 나가기 위한 목적을 갖습니다. 대규모 프로젝트도 진행하기 위해 제 경험에 기반한 다음과 같은 기술을 사용합니다. 참고로는 저는 공간정보와 관련된 시스템 개발에 대한 백엔드와 프론트엔드 개발자입니다. 공간정보시스템에 대한 프론트엔드 개발은 대부분 웹 기반의 SPA(Single Page Application)으로 그 규모가 상대적으로 매우 큽니다. 회사에서 지오서비스웹이라는 서비스를 제공하고 있는데 이 서비스 개발 역시 아래의 기술 등을 사용했습니다.

  • HTML, CSS
  • JS 또는 TS(이하 TypeScript)
  • TS 지원과 번들링(Bundling)을 위한 vite
  • OOP(객체지향 프로그래밍)

4번째인 OOP 항목은 프론트엔드 개발에 한정되지 않은 일반적인 프로그래밍에서 언급되는 것으로 엄밀히 말하면 JS나 TS에 포함되는 것입니다. 프로젝트의 규모가 커서 작성해야할 소스 코드가 방대하다면 JS 대신 TS를 사용하는 것을 권장합니다. TS는 JS의 문제점으로 언급되는 불명확한 타입 선언을 해결하고 더 나은 class 작성 방법을 제공합니다. OOP를 사용하게 되면 훨씬 효과적인 개발이 가능합니다. OOP는 웹 어플리케이션(이하 앱)을 컴포넌트 단위로 개발하도록 유도하고 객체간의 관계를 효과적으로 맺도록 하며 기능 확장성과 효율적인 유지보수를 가능하게 됩니다. OOP가 생소한 분들이라면 OOP는 잠시 잊으시고 OOP 대신 JS나 TS의 class 키워드로 생각하시면 됩니다. 이 글에서는 OOP에 대한 깊이 있는 활용보다는 class에 대한 기초적인 사용을 통해 실습을 진행합니다.

vite는 TypeScript 언어에 대한 지원과 여러개의 소스 코드 파일을 하나로 묶어 주는 번들링 기능 제공, 웹 개발에 필요한 웹서버 제공, 배포 등과 같은 기능을 제공합니다. vite는 React 등과 같은 프로젝트를 생성하기 위해서도 사용되기도 합니다.

앞서 설명한 내용을 효과적으로 설명하기 위해서 실제 웹앱을 만들면서 설명하겠습니다. 만들 웹앱은 Tic-Tac-Toe이며 이를 선택한 이유는 리엑트 공식 사이트에서 리엑트를 설명하기 위한 웹앱으로 바로 이 Tic-Tac-Toe이기 때문입니다. 해당 사이트를 살펴보면 다음과 같습니다.

위의 화면은 react를 이용해 만든 웹앱인데, react에 대한 제법 많은 내용을 기반으로 작성되고 있습니다. 기본적인 Tic-Tac-Toe 게임의 규칙에 대한 플레이가 가능하고 게임 과정에 대한 이력을 관리하여 원하는 단계로 게임을 되돌릴 수 있는 기능을 가지고 있습니다. 이 웹앱을 앞서 언급했던 HTML/CSS와 TS를 이용해 동일하게 만들어 보겠습니다. TS가 아닌 JS로 만들수도 있지만 실무에 적합한 TS를 사용하겠습니다. TS는 타입에 대한 지정과 null 객체에 대한 엄격한 검사 등 JS보다 더 나은 코드 확장성과 유지보수적 특성을 제공합니다.

우리가 만들 이 웹앱의 클래스다이어그램(StarUML로 작성함)은 다음과 같습니다. 클래스다이어그램은 UML의 한 종류로 클래스 간의 관계를 매우 효과적으로 기술한 설계서라고 생각하면 됩니다.

App이라는 클래스가 있고 이 App은 Game이라는 클래스에 대한 객체를 생성합니다. 그리고 Game 클래스는 Board와 History 클래스의 객체를 맴버 필드로 가지고 있습니다. 그리고 Board와 History 클래스의 객체는 Game 클래스의 객체를 참조하고 있다는 것을 알 수 있습니다. Game은 Tic-Tac-Toe 게임의 진행을 관리합니다. 그리고 Board는 3×3의 게임판을 관리하며 History는 게임 플레이 과정에 대한 이력을 관리해 원하는 이전 과정으로 복구할 수 있습니다.

이제 프로젝트를 생성하겠습니다. 코드를 입력할 개발툴은 VS.Code를 사용합니다. 그리고 vite를 설치하기 위해 node.js가 필요합니다. VS.Code와 node.js에 대한 2개의 프로그램이 설치되었다면 실습을 진행하는데 충분합니다.

프로젝트 폴더를 하나 만들고 이 폴더를 VS.Code에서 오픈합니다. 그리고 터미널을 열고 다음처럼 vite를 통해 프로젝트를 생성합니다.

안내된 내용대로 다음 명령을 입력해 완료합니다.

npm install
npm run dev

웹브라우저에서 htttp://localhost/5173으로 접속해 보면 다음과 같은 결과를 볼 수 있습니다.

생성된 폴더와 파일을 보면, 먼저 node_modules 폴더에는 개발환경 또는 개발에 필요한 패키지들이 저장됩니다. public 폴더에는 이미지 등과 같은 배포될 리소스 파일이 존재하고 src에는 실제 우리가 코딩할 코드 파일이 위치하게 됩니다. src에 이미 몇가지 파일이 작성되어 있습니다. 그리고 이 index.html은 웹앱이 실행될때 가장 먼저 표시될 홈페이지입니다.

불필요한 내용을 정리하면, 먼저 index.html의 title 내용을 Tic-Tac-Toe로 변경하고 src/vite.svg 파일을 삭제합니다. 그리고 src 폴더에 있는 style.css 파일은 내용을 모두 지우고 파일만 남겨둡니다. counter.ts와 typescript.svg 파일을 삭제합니다. main.ts는 다음처럼 코드를 입력합니다.

import './style.css'

이제 새로운 코드를 작성할 준비가 모두 되었습니다. 다음에는 앞서 살펴본 클래스다이어그램에서 언급된 클래스들을 추가해 웹앱을 완성해 보겠습니다.

이후의 내용은 제 유튜브 채널을 통해 제공됩니다.