당신의 게임이나 3D 웹 장면에서 캐릭터가 걷는다. 평지에서 처음 맞출 때는 괜찮았는데, 다리 뼈를 조금 늘리거나 걷기 클립(walk cycle)을 바꾸거나 언덕길에 올려놓자 캐릭터가 땅 위에 몇 센티 떠 있거나, 발이 땅을 스케이트 타듯 미끄러지거나(foot sliding), 비탈에서 한쪽 발이 지형에 묻힌다. three.js 든 다른 엔진이든 증상은 같다. 대개 "높이 = 다리 길이 × cos(각도)" 같은 공식이나 "보폭 = 다리 길이 × 2 × sin(흔듦 각)" 같은 닫힌 식으로 맞춰 두었을 때 생긴다.
접지와 보폭은 왜 공식으로 맞추면 안 되나
발이 땅에 닿는지는 공식이 아니라 그린 결과에서 잰다. 접지 높이, 보폭, 비탈에서의 자세는 뼈 길이·관절 굽힘·발목 회전·스키닝 가중치·지형 곡률이 모두 얽힌 결과라서, 그중 하나만 바뀌어도 닫힌 식은 조용히 틀어진다. 반면 매 프레임 실제로 그려질 메시와 지형을 재서 맞추면, 무엇이 바뀌든 답이 따라온다.
공식이 틀어지는 자리는 대개 셋이다.
- 관절이 늘어나거나 굽는다. 정강이를 굽히고 발목을 돌리면 "골반에서 발바닥까지"의 거리가 프레임마다 다르다. 공식은 곧은 다리를 가정한다.
- 뼈와 피부가 다르다. 발 뼈는 대개 발목 자리에 있다. 발바닥(피부)은 그보다 아래다. 뼈로 재면 캐릭터가 땅에 묻혀 있어도 시험이 통과한다.
- 땅이 평면이 아니다. 한 점의 높이에 세운 몸은 비탈에서 앞발이나 뒷발이 묻히고, 앞뒤 두 점을 이은 직선에 맞춘 몸은 볼록한 언덕 꼭대기에서 가운데가 묻힌다.

접지는 어떻게 재서 맞추나
매 프레임 이렇게 한다.
- 애니메이션을 그 프레임 자세로 갱신한다.
- 발 근처 스키닝된 정점을 세계 좌표로 계산해 그중 가장 낮은 y 를 찾는다. three.js 라면
SkinnedMesh.applyBoneTransform으로 정점을 뼈 변환까지 먹인 자리로 옮길 수 있다. 정점 전부가 아니라 발바닥 정점 몇십 개만 골라 두면 싸다. - 그 최저점과 그 자리 지형 높이의 차만큼 뿌리 뼈(골반)를 내리거나 올린다.
발을 움직이는 뼈(넓적다리·정강이)는 건드리지 않는다. 내리고 올리는 것은 뿌리 하나다. 그래야 걷기 모양은 클립이나 IK(역운동학)가 정하고, 땅과의 거리는 측정이 정하는 식으로 일이 갈린다.
비탈에서는 몇 점을 재야 하나
몸 아래를 여러 점으로 잰다. 진행 방향 앞뒤로 몸 길이만큼, 다섯 점 정도면 대부분의 지형에서 충분하다.
- 앞뒤 끝 두 점으로 기울기를 정해 몸을 진행 방향으로 눕힌다.
- 다섯 점 모두에 대해 "이 자리의 땅이 몸 바닥보다 높은가"를 보고, 어느 자리도 뚫지 않는 높이를 고른다.
- 땅과 몸을 같은 자로 잰다. 땅을 수평 거리로 샘플했으면 몸의 바닥도 수평 거리로 견준다. 한쪽은 호 길이, 한쪽은 수평 거리로 재면 곧은 비탈에서도 몇 밀리씩 뜬다.
- 몸 길이(앞뒤로 닿는 범위)는 경계 상자를 국소 좌표에서 잰다. 세계 축으로 재면 옆으로 걷는 캐릭터의 길이 대신 폭이 잡힌다.
눕히는 회전에도 함정이 하나 있다. three.js 의 오일러 회전 기본 순서 XYZ 에서 X 회전으로 "앞으로 숙이기"를 하면, 캐릭터가 동쪽을 향할 때 앞으로가 아니라 옆으로 구른다. 방위(Y)를 먼저 돌리고 그 국소 축으로 숙이도록 rotation.order = 'YXZ' 로 둔다.
보폭은 어떻게 재나
보폭이 틀리면 발이 미끄러진다. 몸은 일정 속도로 나아가는데 딛고 선 발이 그와 다른 속도로 뒤로 가기 때문이다. 그런데 제자리 미리보기에서는 미끄러짐이 절대 안 보인다. 앞으로 가지 않으니 비교할 대상이 없다.
- 클립을 제자리에서 한 주기 돌린다.
- 디딤 구간(발이 땅에 붙은 동안)에 그 발의 피부 최저점이 뒤로 간 거리를 잰다. 보폭은 그 거리 × 2 다.
- 이동 속도 = 보폭 ÷ 클립 한 주기 시간. 이 값을 이동에 넣는다.
- 공식값이 있다면 잰 값과 견준다. 1~3% 안에서 맞으면 둘 다 믿을 만하다. 어긋나면 잰 값이 맞다.
직접 만드는 걷기라면 하나 더 있다. 디딤 구간의 다리 각도를 사인파로 흔들면 딛고 선 발이 프레임마다 다른 속도로 뒤로 가서, 몸이 등속으로 나아가는 한 반드시 미끄러진다. 각도를 먼저 정하지 말고 발이 놓일 자리를 등속으로 먼저 정한 뒤 넓적다리·정강이 각을 역산한다(IK). 무릎은 발이 공중에 있는 흔듦 구간에서만 굽힌다.
캐릭터나 클립을 갈아 끼울 때 다시 잴 것
바깥 서비스에서 리깅과 걷기 클립을 받아 기존 캐릭터를 갈 때, 아래를 시험으로 고정해 둔다. 실제 모델 파일(glb 등)을 엔진으로 돌려 재는 시험이어야 한다.
- 보폭 — 위 방법으로 다시 잰다. 이전 캐릭터의 값을 쓰지 않는다
- 접지 — 발 뼈가 아니라 스키닝된 피부 정점의 최저점으로 잰다. 받은 클립의 발바닥이 이미 땅에 묻혀 있거나 출렁일 수 있다
- 뼈 — 코드가 이름으로 찾는 뼈만 남기고 나머지를 지우지 않는다. 지운 뼈에 실린 애니메이션 채널(어깨 회전, 척추 휘어짐)이 같이 사라진다. 뼈는 전부 두고 이름만 맞춘다
- 축 — 접지 보정이 "뿌리 뼈의 국소 Y 가 위"를 가정한다면, 바깥 리그에서는 그 가정이 깨질 수 있다. 세계 축으로 올리고 내린다
에이전트에게 맡길 때 지침에 넣을 문장:
캐릭터의 접지 높이·보폭·비탈 자세를 공식으로 계산하지 마라.
매 프레임 스키닝된 발바닥 정점의 최저점과 그 자리 지형 높이를 재서 뿌리 뼈만 옮긴다.
비탈에서는 몸 아래 진행 방향 다섯 점의 지형 높이를 재서 어느 점도 뚫지 않게 놓는다.
보폭은 클립을 돌려 딛고 선 발이 뒤로 간 거리 × 2 로 잰다. 공식값과 다르면 잰 값을 쓴다.
리그나 클립을 바꾸면 이 셋을 실제 모델 파일로 다시 재는 시험을 돌린다.
우리가 겪은 일
수묵화풍 3D 웹 장면(three.js)에서 사람과 짐말이 실제 둘레길 지형 위를 걷는 행렬을 만들며 세 번 같은 자리로 돌아왔다.
- 관절을 늘리자 공식 접지가 무너졌다. 다리 길이가 프레임마다 달라져 캐릭터가 공중에 떴다. 매 프레임 발바닥 최저점을 재서 골반을 내리게 바꾼 뒤로는 관절을 어떻게 바꿔도 뜨지 않았다. 디딤 구간 다리 각을 사인파로 두었을 때는 한 걸음에 약 7cm 미끄러졌다(디딘 발의 세계 좌표 이동량으로 쟀다).
- 비탈에서 말의 앞발이 통째로 묻혔다. 길의 최대 경사 약 20° 에서 말 앞뒤 끝의 낙차가 사람 키에 맞먹었다. 앞뒤 두 점으로 눕혀도 휜 언덕에서는 실제 길 위 실측으로 말이 최대 79cm(걷는 사람은 6cm) 잠겼고, 다섯 점으로 바꾸자 말 6cm·사람 0cm 가 됐다(길 전체를 따라 몸 바닥과 지형 높이의 차를 재는 시험).
- 바깥에서 받은 걷기 클립은 그대로 못 썼다. 받은 클립의 발바닥이 땅 아래로 묻힌 채 약 11.5cm 출렁였고, 계약에 쓰는 뼈 13개만 남기자 애니메이션 채널이 85개에서 42개로 줄어 어깨가 안 돌았다. 보폭·접지·채널 수를 실제 모델 파일을 돌려 재는 시험으로 고정했다.