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();
} );

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를 사용해 개발하면 됩니다.

이 글의 원문입니다.

GLSL 코드와 Blender의 쉐이더 노드

쉐이더 프로그래밍 언어 중에 하나인 GLSL의 프레그먼트 쉐이더 코드를 블렌더의 쉐이더 노드로 구성하는 내용에 대한 정리입니다. 먼저 GLSL에 대한 코드는 다음과 같습니다.

varying vec2 vUv;

void main() {
  float strength = step(0.01, abs(distance(vUv, vec2(0.5)) - 0.3));
  gl_FragColor = vec4(vec3(strength), 1.0);
}

결과는 다음과 같습니다. three.js를 사용했습니다.

위의 쉐이더 코드를 블렌더의 쉐이더 노드로 구성하면 다음과 같습니다.

쉐이더의 코드는 블렌더의 쉐이더 노드로 구성할 수 있고, 그 반대로도 가능합니다. 쉐이더 코드는 매우 함축적이고 작은 코드 변화에도 그 결과는 예상하기 어려운 경우가 있습니다. 하지만 블렌더의 쉐이더 노드를 통해 먼저 그 결과를 만들고 이를 다시 쉐이더 코드로 변환한다면 좀 더 나은 개발 접근이 될 수 있습니다.

dFdx와 dFdy를 이용한 법선 벡터 계산

버텍스 쉐이더에서 정점을 흔들었을 경우 법선 벡터 역시 다시 계산을 해줘야 하는데, 이때 정점의 x와 y에 대한 편미분 값을 얻을 수 있는 dFdx와 dFdy를 이용하면 법선 벡터를 얻을 수 있습니다.

이런 경우 버텍스 쉐이더에 전달된 normal을 vary를 통해 프레그먼트 쉐이더로 전달할 필요가 없고 프레그먼트 쉐이더에서 법선 벡터를 계산해 주면 되는데, 프레그먼트 쉐이더에서 이에 대한 코드는 다음과 같습니다.

vec3 normal = normalize(
    cross(
        dFdx(vPosition.xyz),
        dFdy(vPosition.xyz)
    )
);

위의 vPosition은 버텍스 쉐이이더에서 재계산된 정점인데, 버텍스 쉐이더의 코드에서 보면 다음과 같습니다.

varying vec3 vPosition;

void main() {	
    vec3 posClone = position;
    posClone = /* 정점 흔들기(변경) */
    
    ...

    vPosition = modelMatrix * vec4(posClone, 1.0)).xyz;

    ...
}

적용 결과로 비교하면 먼저 dFdx와 dFdy를 통한 법선 벡터를 사용하지 않고 지오메트리를 통해 제공되는 법선 벡터를 그대로 사용한 경우는 아래와 같습니다.

버텍스 쉐이더에서 정점을 흔들어서 원래 제공된 법선 벡터가 맞지 않아 음영 효과가 제대로 표현되지 않는데, 이를 개선하기 위해 dFdx와 dFdy를 통한 법선 벡터를 사용한 결과는 다음과 같습니다.