마우스를 움직이면(모바일은 패널 위를 드래그) 아래 서소영이 그 방향을 본다. 데모 토크나 마이크를 켜면 입이 움직인다 — 트마리 구루구루의 구조를 그대로 서소영으로 옮긴 것이다. 자세한 제작 과정은 본문 아래쪽에 적었다.
3줄 요약
rotejin/tomari-guruguru는 트마리라는 캐릭터용 브라우저 아바타다. 마우스를 따라 25방향으로 고개를 돌리는 ぐるぐる(구루구루) 버전과, 마이크·음성 파일에 맞춰 입을 움직이는 トーク(토크) 버전 두 가지로 되어 있다.- 비결은 3D도 라이브2D도 아니다. 머리 방향 25종 × 눈·입 상태 6종 = 150장의 정지 이미지를 1장씩 갈아끼우는 것이 전부다. 마우스 위치를 격자 칸으로 환산하고, 음량을 임계값으로 끊어 입 모양을 고른다.
- 진짜 까다로운 부분은 그림 자체가 아니라 캐릭터 시트를 깔끔하게 잘라내는 슬라이싱이다. 단순 격자 분할은 옆 칸 머리 혼입·머리카락 잘림을 일으켜, 연결성분(component) 검출로 캐릭터 단위를 떼어낸다.
가벼운 토이 프로젝트지만, “정지 이미지 교체만으로 살아 있는 아바타를 만든다"는 발상과 그걸 실제로 굴러가게 하는 디테일이 알차서 정리해 둔다. 그리고 같은 구조를 치비 서소영으로 직접 옮겨, 이 글 안에서 움직이게 만들어 봤다.
무엇을 하는가
마우스에 따라 캐릭터가 25방향으로 이쪽을 돌아보고, 음성에 맞춰 입을 뻥긋거리며, 일정 간격으로 눈을 깜빡인다. 두 가지 모드가 있다.
- トマリぐるぐる — 마우스를 따라 캐릭터가 시선을 옮기는 단순 버전 (
guruguru.html). - トマリトーク — 마이크 입력 또는 음성 파일에 맞춰 입을 움직이는 토크 버전 (
talk.html). 공개 페이지의 첫 화면이 이쪽이다.
화면 우하단 Tweaks 패널에서 마이크 감도·입 임계값·자동 깜빡임·추종 범위·캐릭터 크기·배경색을 조절한다. OBS 같은 방송 도구에서 쓰도록 배경을 크로마키 색으로 맞출 수 있게 해 둔 점이 용도를 분명히 드러낸다.
25방향 × 6상태 = 150장
핵심 자료 구조는 단순하다. public/slices2/ 안에 다음과 같이 이미지가 들어 있고, 캐릭터의 향함과 표정에 따라 한 장씩 교체된다.
25방향 — 5열 × 5행의 향함 차분.
| c0 | c1 | c2 | c3 | c4 | |
|---|---|---|---|---|---|
| 행(상하) | 좌향 | 좌 비스듬 | 정면 | 우 비스듬 | 우향 |
| r0 | 강하게 위 | ||||
| r2 | 수평 | 정면 | |||
| r4 | 강하게 아래 |
6상태 — 눈(뜸/감음) × 입(다묾/중간/열림)의 조합.
| 폴더 | 눈 | 입 |
|---|---|---|
A | 뜸 | 다묾 |
B | 뜸 | 중간 |
C | 뜸 | 열림 |
D | 감음 | 다묾 |
E | 감음 | 중간 |
F | 감음 | 열림 |
이미지 경로는 slices2/A/r2c2.webp처럼 {상태}/r{행}c{열}.webp 꼴이다. 25 × 6 = 150장. 참조 위치는 src/character-config.js 한 곳에서 basePath·ext로 일원 관리한다.
앱 쪽 로직도 단순하다. 마우스 좌표를 캐릭터 중심 기준으로 정규화(~−1~1)하고, 부드럽게 보간한 뒤 5×5 격자 칸으로 반올림해 해당 칸의 이미지를 띄운다. 토크 모드는 WebAudio AnalyserNode로 음량을 재 입을 다묾·중간·열림으로 끊고, 자동 깜빡임은 불규칙한 간격에 이따금 두 번 깜빡임·느린 깜빡임을 섞어 자연스러움을 낸다.

진짜 어려운 건 ‘자르기’
캐릭터 차분을 만드는 권장 흐름은 이렇다. 참조 이미지와 5×5 각도 템플릿을 ChatGPT Images 2.0에 첨부해 목·입을 닫은 정면 기준의 5×5 각도 시트를 만들고, 같은 이미지에서 눈만/입만 바꾼 차분 시트 6장(A~F)을 뽑은 뒤, 4배 업스케일·배경 투명화하고 슬라이싱한다.
문제는 슬라이싱이다. 동봉된 docs/新キャラ差し替え手順.md는 단순 900px 격자 절단의 함정을 길게 경고한다.
- 우상/좌상을 보는 칸에 옆 칸 캐릭터가 혼입된다.
- 머리 위·포니테일이 잘린다.
- 아래 칸 머리가 위 칸으로 비집고 들어온다.
그래서 tools/slice_character_sheets.py의 component mode를 쓴다. 시트 전체에서 캐릭터 단위의 연결 성분을 검출해 25방향에 배정하는 방식이다. 생성 로그에서 large=25와 각 행의 comps(area)가 전부 1:xxxxx로 나오면 정상 — 한 칸에 한 캐릭터 성분만 잡혔다는 뜻이다. 투명 PNG로 보여도 경계에 남는 반투명 회색 잔재는 --remove-gray-residue로 떼어낸다.
검증 기준도 구체적이다. 캔버스 1200×1200, 캐릭터 중심 X ≈ 600px, 발끝 Y = 900px, A~F 상태 간 외곽 어긋남 최대 1px, 캔버스 끝 접촉 없음. 토이 프로젝트치고 자르는 일에 들인 공이 상당하다.
라이선스는 둘로 나뉜다
이 리포는 프로그램과 캐릭터 에셋의 라이선스를 분리한다.
- 프로그램(
*.html,*.jsx,tools/*.py등) — MIT License. - 캐릭터 이미지·슬라이스 프레임·음성 — MIT 대상 외. 동작 확인·소개·감상 목적의 스크린샷·짧은 화면 녹화 SNS 게시는 허용하되, 상업 이용, 자신의 캐릭터/아바타로의 사용, 재배포·개변, AI 학습·LoRA·데이터셋·음성 합성 목적 이용은 금지한다.
자기 캐릭터로 바꾸려면 slices2/ 안 이미지를 권리 있는 이미지로 교체하면 되고, 그 교체분의 권리는 교체한 사람에게 따른다.
직접 서소영으로 옮겨 봤다
구조가 단순하고 명료해서, 같은 메커니즘을 서소영으로 옮겨 봤다. 정본 화풍(치비 라인아트·순백 배경·하늘색 한복 포인트)으로 5×5 각도 시트와 입·눈 차분을 생성한 뒤, 위에서 본 자르기 문제를 그대로 만났다 — 생성된 시트가 균등 격자가 아니어서, 연결성분으로 머리 25개를 직접 검출해 떼어내고 균등 격자로 재배치했다. 말하는 프레임은 정지 프레임과 교차 정합(registration)해 머리가 튀지 않도록 패닝 옵셋을 보정했다.
맨 위 데모에서 직접 만져 볼 수 있다 — 마우스(모바일은 패널 위 드래그)로 시선을 옮기고, 데모 토크·마이크로 입을 움직인다.

원본이 150장의 프레임을 webp로 교체하는 것을, 여기서는 4장(눈뜸·입다묾 / 입중간 / 입열림 / 눈감음)의 스프라이트 시트를 CSS background-position으로 잘라 좌표만 갈아끼우는 방식으로 압축했다. 흰 배경은 mix-blend-mode: multiply로 비쳤다.
가장 흥미로운 지점
이 프로젝트의 매력은 “최소한의 수단으로 살아 있음을 흉내 낸다"는 데 있다. 3D 리깅도, 라이브2D 메시도, 실시간 추론도 없다. 미리 그려 둔 정지 이미지 150장과, 마우스 좌표→격자 칸·음량→입 모양이라는 두 줄짜리 매핑이 전부다. 그런데도 화면 속 캐릭터는 이쪽을 보고, 말을 따라 입을 움직이고, 가끔 눈을 깜빡인다.
그리고 직접 옮겨 보고서야 분명해진 것 — 정작 손이 많이 가는 곳은 그림을 그리는 단계가 아니라 그림을 정확히 잘라 정렬하는 단계였다. 생성 모델은 25칸을 균등하게 그려 주지 않고, 차분 시트마다 머리를 몇 px씩 흘린다. “살아 있는 느낌"의 절반은 화풍이 아니라 프레임 사이의 정합에서 나온다. 트마리의 문서가 자르기 주의사항에 가장 많은 분량을 쓴 이유를, 같은 함정을 밟고서 납득했다.
출처
rotejin, トマリぐるぐる / トマリトーク. MIT License(프로그램) / 별도 에셋 라이선스(캐릭터). 공개 페이지: https://rotejin.github.io/tomari-guruguru/ 원문: https://github.com/rotejin/tomari-guruguru
본문의 서소영 삽화·인터랙티브 데모는 원본 구조를 참고해 직접 제작한 것으로, 트마리의 캐릭터 에셋은 일절 사용하지 않았다.
