프로세스 제약사항
이 실습에서는 주로 three.js와 GreenSock 라이브러리를 사용했으며, 3D나 애니메이션 소프트웨어를 사용하지 않고 수기로 직접 코딩했습니다.
실습은 한 번에 큐브 하나씩 캐릭터를 프로그래밍하는 방식으로 진행됐습니다. 주로 코드 값을 미세하게 조정해 비율, 위치 및 전체 렌더링을 수정하려고 했죠. 그리고 최종적으로 사용자 입력 (마우스 이동, 클릭, 드래그 등)에 따라 각 요소가 움직이도록 했습니다.
이런 프로세스를 이용하면 어떤 점이 좋은지 확실하지 않습니다. 하지만 텍스트 편집기만 사용해도 모든 실습이 가능하기 때문에, 힘들게 여러 툴을 사용해 자산asset을 만들거나 캐릭터의 속성을 조정하지 않아도 되죠. 거기다 Codepen에서 제공하는 실시간 미리보기를 활용했더니 전체 프로세스가 더욱 유연해졌습니다.
이렇게 해서 프로세스는 자체적인 제약 조건이 생겨났습니다. 항목 관리가 쉽도록 말이죠. 여기서 제약 조건이란 가능한 적은 요소로 캐릭터를 구성해야 한다는 것입니다. 각 요소를 구성하는 버틱스vertices 수도 매우 낮습니다. 그리고 애니메이션의 움직임도 몇 가지로 제한할 수밖에 없죠.
참고 : 저는 이 프로세스를 유용하게 이용했지만, 3D 프로그램을 능숙하게 다룬다면 원하는 모델을 더 수월하게 만들 수 있습니다. 여러분만의 기술을 최대로 활용하세요. 참고로 저는 모든 프로세스를 하나의 툴로 모아 사용하는 방식이 편합니다.
제약을 기회로 바꾸기
Moments of Happiness는 미니멀리즘을 추구합니다. 이 덕분에 동작(편안함, 기쁨, 실망 등)을 표현하는 움직임을 정확하게 나타낼 수 있었죠.
큐브와 움직임마다 모두 의문을 가지고 작업했습니다. ‘정말로 필요할까? 사용자 경험이 더 좋아졌나? 캐릭터 디자이너가 되고 싶은 마음에 기분 내키는 대로 만든 것은 아닐까?’ 같은 의문이었죠.
결국 매우 단순한 장난감 형태를 만들었고, 장난감이 사는 환경은 조용하고 단순하게 설정했습니다.
프로그래밍 방식으로 사물을 애니메이션화하기란 매우 어렵습니다. 어떻게 애니메이션 소프트웨어나 시각적 타임라인 없이 자연스럽고 유기적인 움직임을 만들 수 있을까요? 애니메이션이 사용자의 입력에 자연스럽게 응답하려면 어떻게 해야 할까요?
1 단계 : 관찰
실습을 하기 전에 전달하고자 하는 느낌을 관찰하고 기억하며, 표현하기 위해 노력했습니다.
“시원한 사자Chill the Lion”를 만들었을 당시, 우리집 개를 쓰다듬으면서 영감을 얻을 수 있었습니다. 개가 즐거워하며 눈을 감는 것을 관찰하고 목을 긁어 주기도 했죠. 이런 경험을 프로그래밍 방식으로 올바르게 표현하려면 공감대를 기본 수학 능력에 녹여내야 합니다.
“편집증 새Paranoid Birds” (아래)는 몸이 불편해 보이는 남자를 모방한 것입니다. 눈과 머리가 얼마나 자주 움직이는지 관찰하며 새의 움직임을 자연스럽게 하려고 노력했죠.
경험에만 의지할 수 없는 때도 있습니다. 시각적인 영감은 때때로 특정 형태의 특징을 파악할 때는 시각적 영감도 필요하죠. 다행히도 Giphy를 이용하면 어떤 것이든 절묘하게 나타낼 수 있습니다. 저는 움직임을 올바르게 나타내고자 YouTube와 Vimeo도 주로 이용했습니다.
예시를 살펴보죠.
러닝 사이클 관찰하기
Moments of Happiness에서 가장 까다로운 애니메이션은 늑대에게서 달아나는 토끼입니다.
이 애니메이션을 만들려면 먼저 러닝 사이클이 어떻게 작동하는지 이해해야 합니다. Giphy에서 아주 재미있는 슬로우 모션 GIF을 찾아 살펴봤죠.
이 GIF에서 흥미로운 점은 달리기를 할 때 다리만 움직이는 것이 아니라는 것입니다. 가장 작은 부위부터 완벽한 싱크를 이루며 몸 전체가 움직입니다. 속도와 중력감에 따라 움직이는 귀, 입, 심지어는 혀도 관찰해야 하죠.
동물은 여러 종류고, 달리는 이유도 여러가지이므로 러닝 사이클도 많아집니다. 러닝 사이클을 깊이 있게 알고 싶다면 Pinterest의 “Run Cycle”collection과 멋진 “Quadruped Locomotion Tutorial” 비디오를 추천합니다.
이런 자료를 살펴보면 러닝 사이클의 역학을 명확히 이해할 수 있습니다. 여러분의 뇌는 신체 각 부분 사이의 관계를 파악할 것입니다. 달리는 동작 순서와 리듬으로 순환과 반복을 파악할 수 있고 어떻게 재현할지 형태도 이해할 수 있죠.
자, 이제 이들을 재현하기 위한 기술적인 솔루션이 필요합니다.
자동 시스템 관찰하기
핸들을 돌리기만해도 복잡하게 동작하는 자동 기계식(automatas) 장난감에 흥미를 느끼면서, 유사한 기술을 만들고 싶어졌습니다. 타임라인이나 키프레임 방식보다는 코드 기반의 솔루션을 찾고 싶기도 했죠.
그러다 깨달았습니다. 각 동작이 연결되는 것은 단순하든 복잡하든 전체 사이클이 어떻게 진행되는지에 달려있다는 것이죠.
러닝 사이클의 경우 다리, 귀, 눈, 몸체, 머리가 움직이는 주기가 동일합니다. 주기가 돌아가면서 움직임이 수평으로 변하거나 수직으로 변하기도 하죠.
원형 운동을 선형 운동으로 변환할 때는 삼각법trigonometry이 가장 유용합니다.
2단계 : 삼각법으로 무장하기
겁먹지 마세요! 지금부터 배울 삼각법을 매우 기본 단계입니다. 주된 수식은 다음과 같습니다.
x = cos(angle)*distance;
y = sin(angle)*distance;
위 코드는 주로 점의 극좌표(angle, distance)를 데카르트 좌표(x, y)로 변환할 때 사용합니다.
각도를 변경하면 점이 중심(center)을 잡고 회전하게 할 수 있습니다.
삼각법을 이용하면 수식에 다양한 값을 대입하는 것만으로도 움직임을 훨씬 더 정교하게 할 수 있습니다. 이 기술의 미학은 부드러운 움직임입니다.
다음 예시를 살펴보죠.
직접 해 보기
삼각법을 이해하려면 직접 만들어 봐야 합니다. 경험이 없는 이론은 지적 놀이일 뿐이죠.
위의 수식을 구현하려면 기본 환경을 설정해야 합니다. Canvas나 SVG를 사용해 보세요. 아니면 three.js나 PixiJS, BabylonJS 같은 그래픽 API가 있는 라이브러리를 사용해도 됩니다.
매우 기본적인 three.js 설정을 살펴보죠.
먼저 three.js 최신 버전을 다운로드하고 html
헤드에 라이브러리를 가져옵니다.
<script type="text/javascript" src="js/three.js"></script>
다음에는 전체 작업을 수행할 컨테이너를 추가합니다.
<div id="world"></div>
이 컨테이너에 CSS 스타일을 추가해서 화면을 채우도록 합니다.
#world {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: #ffffff;
}
JavaScript 부분은 약간 길지만 복잡하지는 않습니다.
// 변수들 초기화.
var scene, camera, renderer, WIDTH, HEIGHT;
var PI = Math.PI;
var angle = 0;
var radius = 10;
var cube;
var cos = Math.cos;
var sin = Math.sin;
function init(event) {
// 애니메이션을 보관할 컨테이너 가져오기.
var container = document.getElementById('world');
// 윈도우 사이즈 가져오기.
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
// 'three.js'를 이용해 장면 만들기. 카메라와 렌더러 셋팅.
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 50, WIDTH / HEIGHT, 1, 2000 );
camera.position.z = 100;
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
container.appendChild(renderer.domElement);
// 큐브 만들기.
var geom = new THREE.CubeGeometry(16,8,8, 1);
var material = new THREE.MeshStandardMaterial({
color: 0x401A07
});
cube = new THREE.Mesh(geom, material);
// 장면에 큐브 추가하기.
scene.add(cube);
// 광원을 만들고 추가하기.
var globalLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(globalLight);
// 윈도우 리사이즈 리스너 추가하기.
window.addEventListener('resize', handleWindowResize, false);
// 각 프레임의 애니메이션을 렌더링하는 루프 시작.
loop();
}
function handleWindowResize() {
// 윈도우의 크기가 변경되면 카메라의 화면 비율을 업데이트하기.
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
}
function loop(){
// 각 프레임에서 업데이트 함수를 호출하여 큐브 위치를 업데이트하기.
update();
// 각 프레임에서 장면 렌더링하기.
renderer.render(scene, camera);
// 다음 프레임에서 루프 함수를 호출하기.
requestAnimationFrame(loop);
}
// 페이지가 로드되면 데모를 초기화하기.
window.addEventListener('load', init, false);
여기서는 일단 화면, 카메라, 조명, 큐브를 만들었습니다. 그런 다음 각 프레임에서 큐브의 위치를 업데이트하는 루프 loop도 만들었죠.
이제 update() 함수를 추가해야 합니다. 몇 가지 삼각법 수식을 삽입하면 되죠.
function update(){
// 'angle(각도) 값'은 각 프레임마다 0.1 씩 증가함. 빠른 애니메이션을 위해서는 더 높은 값을 적용하기.
angle += .1;
// 'angle(각도) 값' 수정하기. 움직임을 다르게 하려면 'radius(반경) 값' 수정하기.
cube.position.x = cos(angle) * radius;
cube.position.y = sin(angle) * radius;
// 오브젝트의 'rotation(회전)' 속성에 동일한 원칙을 사용할 수 있음. 다음 줄의 주석을 제거하면 어떻게 작동하는지 알 수 있음.
//cube.rotation.z = cos(angle) * PI/4;
// 크기를 변경하기. 적절하지 않은 스케일 값을 피하기 위해 오프셋으로 1이 추가됨.
//cube.scale.y = 1 + cos(angle) * .5;
/*
이제 여러분 차례!
-원하는 조합이 있다면 코드를 적절히 수정하세요.
- cos 을 sin으로 바꾸거나 반대로 해 보세요.
- radius를 다른 싸이클 기능으로 바꿔도 됩니다.
예 :
cube.position.x = cos(angle) * (sin(angle) *radius);
...
*/
}
뭔가 복잡하다면 Codepen을 이용하면 이해가 쉬울 겁니다. 사인 및 코사인 함수를 사용해서 큐브를 여러 방향으로 움직일수 있죠. 애니메이션에 삼각법을 활용하는 방법을 더 잘 이해할 수 있습니다.
다음 예시에서는 걷기 또는 달리는 사이클을 만들어 보기로 합시다.
삼각법으로 걷거나 달리는 사이클을 만들기
지금까지 코드를 사용해 큐브를 움직이는 방법을 배웠습니다. 동일한 원리로 간단한 걷기 사이클을 차근차근 만들어 봅시다.
이전과 비슷한 설정을 사용하기로 하죠. 차이점은 다른 신체 부위를 만들려면 큐브가 더 많이 필요하다는 것입니다.
three.js를 사용하면 다른 그룹 안에 오브젝트 그룹을 내장할 수 있습니다. 예를 들어 다리, 팔, 머리를 지탱하는 몸체 그룹을 만들 수 있죠.
주인공이 어떻게 만들어졌는지 살펴보겠습니다.
Hero = function() {
이 부분은 각 프레임에서 나중에 수치를 높일 것이고, 사이클의 회전 각도로 사용됨.
this.runningCycle = 0;
// 몸에 들어갈 메쉬 만들기.
this.mesh = new THREE.Group();
this.body = new THREE.Group();
this.mesh.add(this.body);
// 다른 부분을 만들어서 몸에 추가하기.
var torsoGeom = new THREE.CubeGeometry(8,8,8, 1);//
this.torso = new THREE.Mesh(torsoGeom, blueMat);
this.torso.position.y = 8;
this.torso.castShadow = true;
this.body.add(this.torso);
var handGeom = new THREE.CubeGeometry(3,3,3, 1);
this.handR = new THREE.Mesh(handGeom, brownMat);
this.handR.position.z=7;
this.handR.position.y=8;
this.body.add(this.handR);
this.handL = this.handR.clone();
this.handL.position.z = - this.handR.position.z;
this.body.add(this.handL);
var headGeom = new THREE.CubeGeometry(16,16,16, 1);//
this.head = new THREE.Mesh(headGeom, blueMat);
this.head.position.y = 21;
this.head.castShadow = true;
this.body.add(this.head);
var legGeom = new THREE.CubeGeometry(8,3,5, 1);
this.legR = new THREE.Mesh(legGeom, brownMat);
this.legR.position.x = 0;
this.legR.position.z = 7;
this.legR.position.y = 0;
this.legR.castShadow = true;
this.body.add(this.legR);
this.legL = this.legR.clone();
this.legL.position.z = - this.legR.position.z;
this.legL.castShadow = true;
this.body.add(this.legL);
// 몸의 모든 부분에 그림자가 드리우고 받도록 확인.
this.body.traverse(function(object) {
if (object instanceof THREE.Mesh) {
object.castShadow = true;
object.receiveShadow = true;
}
});
}
이제 캐릭터를 장면scene에 추가해야 합니다.
function createHero() {
hero = new Hero();
scene.add(hero.mesh);
}
이렇게 해서 three.js로 간단한 캐릭터를 만들 수 있습니다. three.js를 사용해 캐릭터를 만드는 방법을 자세히 알고 싶으면 Codrops에 대한 심화 학습 기사를 읽어 보세요.
이 몸체를 만든 후에는 간단한 걷기 사이클이 이루어질 때까지 점차적으로 신체 각 부분을 하나씩 움직이게 할 것입니다.
전체 로직은 Hero
오브젝트 run
함수에 있습니다.
Hero.prototype.run = function(){
// 'angle(각도)' 값 키우기
this.runningCycle += .03;
var t = this.runningCycle;
// 사용할 각도가 0과 2 Pi 사이인지 확인.
tt = t % (2*PI);
// 'Amplitude(진폭)값'은 다리 움직임의 주요 반경으로 사용.
var amp = 4;
// 몸의 모든 부분의 위치와 회전을 업데이트.
this.legR.position.x = Math.cos(t) * amp;
this.legR.position.y = Math.max (0, - Math.sin(t) * amp);
this.legL.position.x = Math.cos(t + PI) * amp;
this.legL.position.y = Math.max (0, - Math.sin(t + PI) * amp);
if (t<PI){
this.legR.rotation.z = Math.cos(t * 2 + PI/2) * PI/4;
this.legL.rotation.z = 0;
} else{
this.legR.rotation.z = 0;
this.legL.rotation.z = Math.cos(t * 2 + PI/2) * PI/4;
}
this.torso.position.y = 8 - Math.cos( t * 2 ) * amp * .2;
this.torso.rotation.y = -Math.cos( t + PI ) * amp * .05;
this.head.position.y = 21 - Math.cos( t * 2 ) * amp * .3;
this.head.rotation.x = Math.cos( t ) * amp * .02;
this.head.rotation.y = Math.cos( t ) * amp * .01;
this.handR.position.x = -Math.cos( t ) * amp;
this.handR.rotation.z = -Math.cos( t ) * PI/8;
this.handL.position.x = -Math.cos( t + PI) * amp;
this.handL.rotation.z = -Math.cos( t + PI) * PI/8;
}
이 코드가 가장 재미있는 부분입니다. Codepen에서 걷기 사이클의 전체 코드도 확인할 수 있습니다.
따라하기 쉽도록 다음과 같은 데모를 만들었습니다. 이 데모는 동작을 분해해서, 움직이는 신체 부분과 각 단계의 수식을 나타내죠.
사인과 코사인, 거리 및 주파수에 익숙해지면 달리기, 수영, 비행 같은 사이클을 매우 쉽게 만들 수 있습니다. 문워크까지 만들 수 있죠.
당신 차례입니다!
토끼 예시를 살펴보기 전까지는 아직 끝난 게 아닙니다.
아래 Codepen을 사용하면 신체에서 부위별로 앵글 오프셋angle offset과 진폭amplitude을 다르게 적용할 수 있습니다. 결과를 빨리 보고 싶다면 사이클의 속도를 수정할 수도 있죠.
토끼의 달리기 사이클이 신선하죠? 재밌군요!
결론
코드 기반으로 만든 애니메이션 움직임은 부자연스럽다고 생각할 수 있습니다. 하지만 오히려 매우 유연하게 움직임을 조정할 수 있으므로 캐릭터에 따라 자연스러운 움직임을 쉽게 만들 수 있습니다.
Moments of Happinesss는 다양한 실습을 모아둔 곳입니다. 실습마다 도전 과제도 제각각이죠. 이 글에서는 러닝 사이클을 만들 때 사용하는 솔루션을 자세히 설명해 보았습니다. Codepen 페이지에서 이 모든 내용을 마음대로 활용해 보기 바랍니다. 나만의 인터랙티브 장난감을 만들어 보세요.