
최근에 숏폼에서 고죠사토루로 손인식을해서 기술을 날리는 영상을 보고 저도 뭔가 재밌을 것 같기도 하고 바이브 코딩으로 재밌는 프로젝트랑 기술들 써보고 싶어서 하게 되었습니다. 캐릭터는 똑같은걸로 하긴 좀 그래서 고죠사토루와 같은 주술회전에 나오는 캐릭터인 스쿠나로 해보려고 합니다.
MediaPipe Tasks Vision의 GestureRecognizer를 쓰면 웹캠 영상에서 손 관절 좌표 21개를 실시간으로 뽑아낼 수 있습니다. 브라우저 안에서 다 처리되기 때문에 영상이 서버로 나가지도 않고 서버 비용도 안 듭니다. 인터넷은 모델 파일 받을 때 딱 한 번만 필요합니다.
https://www.npmjs.com/package/@mediapipe/tasks-vision
const vision = await FilesetResolver.forVisionTasks(WASM_URL)
recognizer = await GestureRecognizer.createFromOptions(vision, {
baseOptions: { modelAssetPath: MODEL_URL, delegate: 'GPU' },
runningMode: 'VIDEO',
numHands: 2,
})
이렇게 설정하고 매 프레임마다 recognizeForVideo(video, nowMs)를 호출하면 손 랜드마크 좌표가 나옵니다. 여기까지는 별로 안 어려웠는데, 이 좌표를 게임 로직으로 연결하려니까 문제가 하나씩 계속 터졌습니다.
GestureRecognizer, WASM)MediaPipe가 기본으로 주는 제스처는 7종(주먹, 보, 엄지 척 등)인데 제가 필요한 V자, 다섯 손가락, 주먹은 잘 구분을 못 해줬습니다.
원인
처음엔 손가락 끝이 마디보다 위에 있으면 펴진 걸로 판정하는 로직을 짰는데 손을 조금만 기울여도 바로 깨졌습니다.
문제 해결
그래서 손목 기준 거리 비율로 바꿨습니다. 손가락을 펴면 끝(tip)이 밑마디(MCP)보다 손목에서 더 멀어지니까, 그 비율만 보면 됩니다.
export function fingerStates(lm) {
const w = lm[0] // 손목
// 손목 기준 tip/mcp 거리 비율 — 손의 방향이나 위치와 무관하게 안정적이다
const ratio = (tip, mcp) => d2(lm[tip], w) / (d2(lm[mcp], w) || 1)
const ext = (tip, mcp) => ratio(tip, mcp) > 1.35
const index = ext(8, 5)
const middle = ext(12, 9)
const ring = ext(16, 13)
const pinky = ext(20, 17)
}
이렇게 바꾸고 나니 손 위치나 방향이 바뀌어도 판정이 잘 안 깨졌습니다. 저 코드에 있는 번호는 관절 번호 입니다.
손모양만 잡아서 바로 발동시키면 실제 애니메이션 느낌이 나지 않아서 손모양을 잡은 다음 실제로 휘둘러야 술식이 나가게 만들어야 했습니다.
원인
손목 속도를 계산해서 일정 속도 넘으면 발동시키는 방식으로 짰는데, 문제가 두 개 겹쳤습니다. 휘두르는 순간엔 손가락 인식이 흐트러져서 손모양 판정이 풀려버렸고, 반대로 너무 빠르게 휘두르면 손 자체를 놓쳐서 추적이 끊겼습니다.
문제 해결
첫 번째는 손모양을 잠깐 기억해두는 방식으로 해결했습니다. 손모양이 잡히는 순간 그 술식을 0.8초 동안 저장해두고, 그 안에 휘두르는 동작이 들어오면 그때 발동시키는 겁니다.
if (tech) {
// 손모양을 본 순간 0.8초 동안 저장해둔다 — 휘두를 때 모양이 흐려져도 상관없다
heldTech.current[i] = { tech, until: nowMs + LATCH_MS }
}
두 번째는 손이 사라지기 직전 속도를 보고 판정했습니다. 빠르게 움직이다가 갑자기 놓쳤다면 세게 휘둘렀다고 보고 그냥 발동시켜버리는 겁니다.
// 손 추적이 끊긴 경우 — 빠른 휘두름으로 추적을 놓친 것으로 보고
// 저장해둔 술식을 발동시킨다
if (held && nowMs < held.until && armed.current[i] &&
lastSpeed.current[i] >= LOSS_FIRE_SPEED) {
fire(held.tech, ...)
}
한 번 발동되면 속도가 다시 낮아질 때까지는 또 발동되지 않게 막는 것도 추가했습니다. 안 그러면 한 번 휘둘렀는데 술식이 연달아 나갑니다.
영역전개는 양손을 가슴 앞에 모아 유지하면 발동되는데 손을 모으는 도중에 잠깐 다른 모양으로 잡히면서 다른 술식이 먼저 나가버리는 문제가 있었습니다.
원인
동작들을 각자 따로 판정하고 있어서 손을 모으는 그 짧은 순간에 다른 조건도 같이 만족되고 있었습니다.
문제 해결
그래서 매 프레임마다 우선순위를 정해서 하나만 판정하도록 바꿨습니다. 양손이 가깝고 둘 다 느리고 가슴 높이에 있으면 영역 전개, 아니면 나머지 동작들을 순서대로 확인하는 식입니다.
const shrineMode =
handInfo.length === 2 && dist < HANDS_TOGETHER_DIST && bothSlow && atChest
atChest(가슴 높이) 조건 하나 넣은 게 제일 오류가 안났었습니다. 이게 없으면 얼굴 앞에서 손 모아도 영역이 전개돼버렸던 문제가 있었는데 가슴 앞으로 위치를 한정하고 나서 오류가 줄었습니다.

배포링크
https://sukuna-ai.vercel.app/
깃헙링크
https://github.com/g-hyxn/sukuna-AI
뭔가 새로운 기술을 접하게 되어서 신기했고 또 재미있었습니다.
읽어주셔서 감사합니다.
ㅋㅋㅋ