
본편("리그가 없는 그림을 웹캠 포즈로 움직이기")에서는 왜 그런 구조를 선택했는지를 다뤘다. 이 글은 그 구현편이다. 본편에서 걷어냈던 수치와 공식이 여기 있다.
관절은 평균으로 합성한다
포즈 모델이 직접 주지 않는 관절은 주변 랜드마크의 평균으로 만들었다. hips는 좌우 골반, chest는 좌우 어깨, neck은 좌우 어깨와 코를 평균해 만든다. head는 코와 양 눈, 양 귀 다섯 점의 평균이고, spine은 hips와 chest의 중점이다. 손은 손목·검지·새끼·엄지, 발은 발목·뒤꿈치·발끝의 평균이다. 신뢰도는 [0, 1]로 클램핑하고, 모델이 신뢰도를 주지 않는 합성 관절에는 기본값 0.6을 부여했다.
메시와 가중치
메시는 캐릭터 영역을 덮는 16×20 그리드다. 정점은 17×21로 357개, 사각형 320개를 삼각형 640개로 나눈다. 각 정점은 가장 가까운 관절 4개를 고르고 거리의 역제곱으로 가중치를 받는다. weight_i = 1/(dist_i² + ε), ε = 1e-6, 합이 1이 되도록 정규화한다. ε는 정점이 관절과 겹쳐 거리가 0에 수렴할 때 분모가 폭발하는 걸 막는 안전장치다.
자료구조는 다섯 개, 전부 18KB
정점당 (jointIndex, weight) 쌍을 정확히 4개로 고정한 덕에 자료구조가 평탄해진다. restVertices(Float32Array)와 uvs(Float32Array), jointIndices(Uint8Array), weights(Float32Array), indices(Uint16Array) 다섯 개가 전부고, 지오메트리 전체가 약 18KB다. 이 크기가 본편의 결론, "셰이더로 넘길 필요도 없을 만큼 작다"의 근거다.
회전은 참조 벡터로 구한다
각 관절의 회전은 현재 자세와 rest 자세의 참조 벡터 각도 차로 계산한다. 회전각은 atan2(현재 참조벡터) − atan2(rest 참조벡터), 스케일은 |현재| / |rest|다. 참조 관절은 후보 리스트에서 신뢰도가 기준(0.3) 이상인 것을 고르고, 쓸 만한 게 없으면 부모 관절로 폴백한다. 예를 들어 hips의 참조 후보는 spine과 좌우 골반이다.
LBS 본체는 가중 합산이다
LBS 자체는 복잡한 알고리즘이 아니다. 매 프레임, 정점마다 영향 관절 4개의 변형(회전·스케일·이동)을 가중 합산하는 계산이 전부다. 2D 회전이라 한 관절의 기여는 x' = pos.x + cos·dx − sin·dy, y' = pos.y + sin·dx + cos·dy로 끝난다. 357개 정점에 영향 관절 4개씩이면 프레임당 1,428번의 가중 합산이고, JS 루프로 돌려도 실시간에 여유가 있다.
정리
구현의 모든 선택이 한 방향을 가리킨다. 관절 수를 줄이고, 영향 관절을 4개로 고정하고, 자료구조를 평탄하게 유지한 덕에 파이프라인 전체가 JS 한 곳에 남을 수 있었다. 이 정도 규모에서는 GPU로 보내기 위한 복잡성을 더하는 것보다 CPU에서 한 번에 처리하는 편이 나았다. 규모가 작을 때 단순함은 성능보다 값지다.
다음에 읽을 글

예외가 나지 않는 버그가 더 무섭다
마이그레이션에서 가장 기억에 남는 버그는 화면을 깨뜨린 버그가 아니다. 에러 레벨로 쏟아지는 무해한 로그가 진짜 오류를 가렸고, 복제 로직이 새 필드를 몰라 보정값이 조용히 사라졌다. 시끄러운 버그는 스스로를 고발하지만, 조용한 버그는 발견 자체를 늦춘다.

SVG를 GPU가 그릴 수 있게 굽는 파이프라인
사용자 그림은 SVG인데 GPU는 삼각형만 안다. SVG 경로를 평탄화하고 earcut으로 삼각분해해 메시로 굽고, 형상(rig.svg)과 골격(rig.json)을 분리하고, 베이크 결과는 캐시했다. 깨지기 쉬운 산출물은 CI 검증이 빌드 단계에서 지킨다.

잘 돌아가던 렌더러를 갈아엎은 이유
손그림 캐릭터를 실시간으로 움직이는 렌더러는 잘 돌아갔다. 문제는 캔버스 2D 벡터 엔진이라 GPU를 적극 활용하기 어렵다는 점이었다. 코어를 테스트로 고정하고 새 WebGL 엔진을 옆에서 키운 뒤 구 라이브러리를 걷어냈다. 잘 돌아가는 코드를 버린 이유는 고장이 아니라 구조의 한계 때문이었다.