
Oracle 무료 티어에서 3D 캐릭터를 구동했더니 처참해서 iPhone을 GPU로 활용한 이야기
요약
저사양 클라우드 VM 환경에서 LLM 에이전트에게 3D 신체를 부여하기 위해, 렌더링 부하를 스마트폰 GPU로 분산하는 실험적 구성을 소개합니다. 서버는 에이전트 연산을 담당하고, iPhone의 WebGL을 통해 3D 캐릭터를 직접 렌더링하여 성능 문제를 해결했습니다.
핵심 포인트
- 클라우드 VM의 CPU 렌더링 한계를 스마트폰 GPU 활용으로 극복
- 서버는 에이전트와 OS를, 스마트폰은 WebGL로 3D 캐릭터 렌더링 담당
- SSE(Server-Sent Events)를 이용한 효율적인 이벤트 데이터 전송
- VNC 환경 위에서 투명 WebGL Canvas를 겹치는 하이브리드 방식 구현
이 기사는 저사양 클라우드 VM 상의 LLM 에이전트에게, 스마트폰 GPU로 렌더링하는 3D 신체를 부여한 이야기입니다.
「PC나 클라우드 VM의 사양이 부족하지만, 3D 모델을 표시하여 LLM에게 신체를 부여하고 싶다!」라는 분들을 위한 내용입니다. VNC를 사용하여 화면 조작과 3D 렌더링을 스마트폰 측에 맡겨도 괜찮다면, 구성의 한 예로서 참고가 될 것이라고 생각합니다.
단, 이것은 아이디어가 떠오른 후 채 하루가 되지 않아 만든 급조된 구현입니다. 엄밀한 벤치마크나 그대로 재현할 수 있는 절차서가 아니라, 「이런 방식의 분할로도 일단 동작했다」라는 실험 기록으로서 읽어주시기 바랍니다.
서론
최근 Oracle Cloud로 Hermes Agent를 옮기고, XFCE 데스크톱을 VNC를 통해 스마트폰에서 조작할 수 있도록 설정했습니다.
그러던 중 X(구 Twitter)에서
GitHub URL: Persona
이 게시물을 발견하고, 「나도 LLM에게 몸을 주고 싶다…」라고 생각한 것이 시작이었습니다.
그래서 3D 모델인 Ao를 배치해 보았는데, 처음에 생각한 것은 「4코어 24GB라면 3D 정도는 가능하겠지」였습니다.
사용한 Ao와 이용 조건
전혀 불가능했습니다.
GPU가 없는 Oracle 측에서 3D를 렌더링하고, 그 결과를 다시 VNC용 이미지로 압축하여 iPhone으로 보내기 때문에, Ao는 화질이 깨지고 움직임도 끊겼습니다. 개발 중에 확인한 범위에서는 데스크톱 전체가 대략 10~12fps였으며, 조작까지 무거워졌습니다.

화질이 깨져버린 Ao
그래서 구성을 변경하여, Hermes 본체와 Linux 데스크톱은 Oracle, Ao만 iPhone의 WebGL로 렌더링하도록 했습니다. 서버는 뇌와 작업장을, 스마트폰은 화면과 신체를 담당하는 형태입니다.
요약
- Oracle 측은 Hermes Agent, WebUI, XFCE, TigerVNC를 상시 가동
- iPhone의 Safari에서는 noVNC 화면 위에 투명한 WebGL Canvas를 겹침
- Ao의 VRM과 모션은 iPhone 측에서 직접 렌더링
- Hermes의 발언이나 Tool 이벤트는 영상이 아닌 SSE(Server-Sent Events)의 작은 이벤트로 전송
- VNC 배경은 10~12fps 정도여도 Ao만은 목표 30fps로 구동 가능
- 3D를 Oracle의 CPU로부터 분리했기 때문에, 적어도 「Ao를 구동하느라 데스크톱까지 무거워지는」 상태는 해소됨
환경
사용 중인 VM은 Oracle Cloud의 Ampere A1 Flex입니다. 계정은 PAYG로 이전했지만, 이 인스턴스 자체는 Always Free 범위 내에 두고 있습니다.
| 항목 | 내용 |
|---|---|
| CPU | Arm / aarch64, 4 OCPU (OS상에서는 4 CPU) |
| ... |
Hermes Agent와 WebUI는 Oracle 측에서 구동하며, VNC 데스크톱 상에는 Hermes용 데스크톱 앱을 상주시키고 있습니다.
Safari로 연 웹을 스마트폰에서 조작하기 쉽게 만드는 팁
- 오른쪽 하단 ・・・ 버튼을 누른다
- 공유를 클릭한다
- 조금 아래로 내려가서 홈 화면에 추가를 선택한다
- 그 후 웹 앱으로 열기를 ON으로 하고 추가한다
이것만으로도 툴바가 나타나지 않아 편하게 조작할 수 있습니다.
처음에는 Oracle 측에서 전부 렌더링했다
초기 구성은 상당히 단순했습니다.
- Oracle의 데스크톱 상에서 Ao를 3D 렌더링한다
- TigerVNC가 데스크톱 전체를 프레임 버퍼(Frame Buffer)로 가져온다
- 이미지를 압축하여 iPhone으로 보낸다
- iPhone의 VNC Viewer로 본다
GPU가 없기 때문에 3D 렌더링은 CPU의 몫이 됩니다. 게다가 렌더링된 Ao를 매 프레임 VNC용으로 압축하므로, Oracle은 Hermes, 데스크톱, 3D 렌더링, VNC 압축을 4 OCPU로 동시에 처리해야 했습니다.
결과적으로 3D가 느려질 뿐만 아니라 VNC 조작까지 영향을 받았습니다. 게다가 압축된 영상 형태로 보내기 때문에 흰 옷의 윤곽이나 머리카락이 상당히 뭉개집니다. 「귀여운 신체를 부여한다」는 것은커녕, Oracle 전체를 괴롭히는 장치가 되어 있었습니다.
3D만 iPhone으로 분리
여기서 「iPhone에는 GPU가 달려 있고, 어차피 VNC를 사용해 스마트폰에서 조작하고 있으니 스마트폰에서 렌더링할 수는 없을까?」라는 생각이 들었습니다.
Codex에게 물어보니,
『VNC로 보낼 필요가 있는 것은 Hermes의 데스크톱뿐입니다. Ao는 모델 데이터와 모션만 단말기에 있다면, 매 프레임 이미지를 보낼 필요 없이 Safari의 WebGL로 그릴 수 있습니다.』
라고 합니다.
그래서 iPhone 측의 Viewer를 다음과 같이 두 층으로 겹쳤습니다.
- 하단: noVNC가 그리는 Oracle의 데스크톱
- 상단: Three.js가 그리는 투명 배경의 Ao
Ao 측의 Canvas는 pointer-events: none으로 설정되어 있어, 3D 모델이 화면을 가리고 있어도 아래에 있는 VNC를 그대로 클릭할 수 있습니다. 겉모습만 전면에 두고, 조작은 배면으로 통과시키는 형태입니다.
iPhone 측의 렌더링
3D 측에서는 Three.js, @pixiv/three-vrm, @pixiv/three-vrm-animation을 사용하고 있습니다. WebGLRenderer는 투명 배경, 안티앨리어싱(Anti-aliasing) 활성화, 그림자 없음으로 설정했으며, 단말기의 픽셀 비율(pixel ratio)도 최대 1.6으로 억제했습니다. 화질을 너무 높여서 iPhone이 핫팩(손난로)이 되는 것을 방지하기 위해서입니다.
렌더링 루프는 다음과 같습니다.
const targetFps = document.hidden ? 2 : currentVrm ? 30 : 4;
const elapsed = performance.now() - startedAt;
frameTimer = window.setTimeout(
...
Ao 로딩 후에는 목표 30fps, Safari가 백그라운드로 넘어가면 2fps, 모델 로딩 전에는 4fps입니다. 실측 30fps 고정이라는 의미는 아니지만, 적어도 VNC의 프레임 업데이트와는 완전히 분리되었습니다.
모델은 Bounding Box로부터 높이를 계산하여 약 1.8의 표시 크기에 맞추고, 발밑이 y=0에 오도록 배치했습니다. 이렇게 하면 화면 크기가 변해도 Ao만 지면에서 떠오르거나 크게 벗어나는 것을 방지할 수 있습니다.
Hermes의 발언과 Tool을 통한 Ao 구동
단순히 대기 모션만 재생한다면 화려한 스탠딩 일러스트와 별반 다를 게 없습니다. 그래서 Hermes에 작은 플러그인을 추가하여, LLM과 Tool의 라이프사이클(Lifecycle)을 Ao의 움직임으로 변환했습니다.
| Hermes 측 이벤트 | Ao의 움직임 |
|---|---|
| LLM 호출 전 | 생각하기, 고개 갸우뚱하기 |
| ... |
플러그인에서 Viewer Bridge로 보내는 것은 대화 본문이나 Tool 인수가 아니라, 다음과 같은 동작 메타데이터뿐입니다.
{
"action": "tool_done",
"duration": 1400,
...
Hermes의 플러그인이 127.0.0.1:6080으로 POST를 보내면, Node 기반의 Bridge가 접속 중인 Safari로 SSE(Server-Sent Events)를 통해 전달합니다. 영상을 보내는 것이 아니기 때문에 이벤트 1건의 크기는 매우 작습니다.
Viewer 측에는 우선순위 0~2와 1건 분량의 대기열을 두었습니다. 예를 들어 일반적인 대기 동작 중에 "손을 흔들어줘"라는 명령이 오면 명시된 동작을 우선시하고, 높은 우선순위의 동작 중에 낮은 우선순위의 이벤트가 오면 다음 순서로 넘깁니다. 여러 동작이 동시에 일어나 Ao가 버그를 일으키는 것을 방지하기 위해서입니다.
「손을 흔들어줘」가 경례와 "이리 오렴"이 된 사연
처음에는 어깨, 팔꿈치, 손목의 회전을 사인(sin)파로 조합하여 코드만으로 손을 흔들게 하려고 했습니다. 그랬더니 팔이 대각선 앞을 향해 뻗으면서 거의 히틀러처럼 되더군요. 축을 수정했더니 이번에는 손목이 앞뒤로 움직이며 "이리 오렴" 하는 동작이 되었습니다.
결국 손 흔들기 전체를 수식으로 만드는 것을 포기하고, BOOTH에서 배포 중인 (용도가 애매한) 모션 세트인 004_hello_1을 가져왔습니다. VRMA와 Unity anim이 포함된 무료 팩으로, 라이선스는 CC0이며 수정, 재배포, 상업적 이용이 가능하고 크레딧 표기도 필요 없습니다.
다만 원본 모션은 "만남 & 인사"이기 때문에 그대로 사용하면 앞부분이 너무 깁니다. 그래서 VRMA를 VRM용 AnimationClip으로 변환하여, 225프레임 이후의 부분만 잘라내어 1회 재생하도록 했습니다.
const fullClip = createVRMAnimationClip(animation, vrm);
const greetingClip = THREE.AnimationUtils.subclip(
fullClip,
...
현재는 wave 또는 hello 이벤트가 들어오면 이 VRMA를 우선적으로 재생합니다. 파일 로딩에 실패했을 때만 손바닥을 정면으로 향하고 Y축으로 좌우로 흔드는 절차적(Procedural) 모션으로 폴백(Fallback)합니다.
즉, 경례에서 "이리 오렴"으로 끝난 것이 아니라, 배포된 모션에서 인사 부분만 잘라내는 방식으로 문제를 해결했습니다. 처음부터 외부 모션을 찾았다면 더 빨랐을지도 모르겠습니다.
발이 지면에 파묻히는 문제
평상시에 약간 흔들리도록 설정했을 때는 모델의 root 전체를 회전시켰습니다. 그랬더니 몸의 중심이 흔들리는 것이 아니라 Ao 전체가 비스듬히 기울어지면서 발이 지면에 박히게 됩니다.
이 문제는 다음과 같은 방침으로 변경하여 해결했습니다.
- VRM의 root 위치와 회전은 매 프레임 읽어들인 직후의 값으로 되돌린다
- hips도 기준 회전으로 고정한다
- 호흡이나 무게 중심 이동은 spine, chest, upperChest로 분배한다
- 고개 돌리기는 neck과 head로 분배한다
- 어깨와 팔은 아주 작게 반대 방향으로 움직인다
currentVrm.scene.position.copy(baseModelPosition);
currentVrm.scene.rotation.copy(baseModelRotation);
hips.rotation.x = baseRotations.hips.x;
...
발과 허리는 지면에 고정하고, 상체의 여러 뼈(bone)로 작은 움직임을 나누는 방식입니다. 이렇게 함으로써 발밑은 멈춰 있는 상태를 유지하면서, 호흡이나 무게 중심 이동만 보이게 되었습니다.
아무것도 하지 않을 때도 가끔 움직임
Hermes가 아무것도 하지 않을 때 완전히 정지해 있으면 조금 무서우므로, 기동 직후에는 4.59초 후, 그 이후에는 1128초 간격으로 다음 중 하나를 랜덤하게 재생합니다.
- 주변을 둘러봄
- 고개를 갸우뚱함
- 작게 고개를 끄덕임
- 소매를 만지작거림
직전과 동일한 동작은 선택하지 않으며, Hermes의 발언이나 Tool 동작이 시작되면 대기 모션을 중단합니다. 동작 종료 직후에도 9~18초 정도의 간격을 둡니다. 항상 부산스럽게 움직이면 귀엽다기보다 안절부절못하는 메이드가 되기 때문에, 이 정도가 현재로서는 적절한 타협점이라는 느낌입니다.
전후 비교
| 항목 | 처음 | 현재 |
|---|---|---|
| Hermes / 데스크톱 | Oracle | Oracle |
| ... |
조작 화면의 fps와 캐릭터의 fps를 분리하는 것만으로 체감 성능이 상당히 달라졌습니다. 데스크톱은 다소 끊기더라도 앞에 있는 캐릭터가 부드럽기 때문에 "3D 모델 + LLM" 같은 느낌이 난다는 인상을 줍니다.

스마트폰에서 움직이고 있는 Ao
tool을 사용하여 실제로 손을 흔드는 Ao
접속과 보안
Viewer나 VNC를 인터넷에 직접 공개하는 것은 위험하므로, 접속은 Tailscale 내부로만 제한했습니다.
- TigerVNC는 127.0.0.1:5901
- Viewer Bridge는 127.0.0.1:6080
- 외부에서는 Tailscale Serve의 HTTPS를 통해서만 접속
- noVNC는 wss로 Bridge에 접속
- Viewer Bridge 측에서는 VNC 비밀번호를 저장하지 않음
- 저장하도록 선택한 경우에도 iPhone 측의 localStorage에만 저장
- SSE로 대화 본문, Tool 인자, API 키를 흘려보내지 않음
Bridge는 15초마다 keepalive를 보내며, SSE가 끊어지면 Safari 측의 EventSource가 재접속합니다. 참고로 VNC와 아바타 이벤트의 경로도 별도로 분리되어 있어, 어느 한쪽이 멈추더라도 원인을 파악하기 쉽게 만들었다고 합니다.
아직 부족한 점
- Ao와 데스크톱 사이에 깊이(depth) 관계가 없어, 창 뒤로 숨을 수 없음
- Safari를 장시간 열어두면 iPhone이 평범하게 뜨거워짐
- iOS가 백그라운드 처리나 WebGL을 중단시킬 가능성이 있음
- 모델이나 모션을 바꾸면 뼈의 방향이 맞지 않아 조정이 필요함
- 모션 종료 시 대기 자세로 보간(interpolation)하지 않고 바로 되돌리기 때문에, 자세가 한꺼번에 바뀌어 끊겨 보임
등등...
앞으로는 음성에 맞춘 입 모양(lip sync), 시선, 감정 표현을 늘려가며 여러 가지 개선을 해나갈 생각입니다.
요약
GPU가 없는 Oracle VM에 Hermes의 신체까지 올리려고 했더니, 3D 렌더링과 VNC 압축이 동일한 CPU를 점유하려고 다투느라 상당히 버거웠습니다.
그래서 Hermes 본체, Tool, Linux 데스크톱은 Oracle에 남겨두고, Ao의 VRM 렌더링만 iPhone으로 옮겼습니다. VNC 배경과 Ao를 별도의 Canvas로 분리함으로써, 배경이 10~12fps 정도라도 Ao는 목표치인 30fps로 구동할 수 있습니다. Hermes로부터는 SSE를 통해 동작 메타데이터만 전송하며, 발언·Tool·실패·명시적인 요청에 따라 신체가 반응합니다.
손을 흔들게 했더니 경례를 하고, 그다음에는 "이리 오세요" 동작이 나왔지만, 최종적으로는 CC0 라이선스의 VRMA에서 인사 부분만 잘라내어 해결했습니다. 발이 파묻히는 문제도 root와 hips를 고정하고 상체의 뼈만 움직이는 방식으로 변경하여 수정했습니다.
AI에게 이 구성의 이름을 물어보니 "분리 렌더링형 AI 아바타 (Separated Rendering-type AI Avatar)"라거나 "클라우드 뇌 + 단말기 신체 아키텍처 (Cloud Brain + Terminal Body Architecture)"라고 하더군요. 하지만 이름이 너무 길어서, 저는 역시 iPhone을 GPU로 만들었다고 말하고 있습니다.
재미있는 활용법이나 더 쉬운 방법을 알고 계신 분이 있다면 꼭 알려주세요!
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기