Skip to content

캐릭터 애니메이션 2배속 정상화 - #97

Merged
pcwadarong merged 2 commits into
mainfrom
fix/character-mixer-frame-guard
Aug 31, 2026
Merged

pcwadarong merged 2 commits into
mainfrom
fix/character-mixer-frame-guard

Conversation

@pcwadarong

@pcwadarong pcwadarong commented Aug 31, 2026 •

Copy link
Copy Markdown
Owner

목적

캐릭터 애니메이션이 실시간의 2배 속도로 재생되고 있었다. 홈에 캔버스가 두 개(hero + contact) 마운트되는데, 두 캔버스가 서로의 믹서까지 갱신하고 있었기 때문이다.

전 후
믹서 진행 속도 (두 캔버스 활성) 2.00× 실시간 1.00× 실시간
rAF당 mixer.update 호출 믹서마다 2회 믹서마다 1회
갱신에 쓰이는 delta 마지막에 실행된 캔버스의 것 자기 캔버스의 것
contact가 화면 밖일 때 contactMixer 계속 진행 정지
두 렌더러 프레임 번호가 우연히 일치할 때 한쪽 갱신 통째로 드롭 영향 없음

적용 범위: useCharacterInstance 하나. 애니메이션 클립·전환 로직·캔버스 설정은 건드리지 않았다.

설계

1. 왜 gl.info.render.frame을 캔버스 간 가드로 쓸 수 없는가

기존 코드는 "한 프레임에 믹서를 한 번만 갱신"을 이렇게 표현했다.

const currentFrame = state.gl.info.render.frame;
if (characterCache.lastUpdatedFrame === currentFrame) return;
characterCache.lastUpdatedFrame = currentFrame;
characterCache.mainMixer.update(clampedDelta);
characterCache.contactMixer.update(clampedDelta);

info.render.frame은 WebGLRenderer 인스턴스마다 따로 세는 카운터다. 전역 프레임 번호가 아니다. 홈에는 캔버스가 둘이고 각자 자기 렌더러를 가지므로, 두 카운터 사이에는 아무 관계가 없다. contact 캔버스는 dynamic({ ssr: false })로 늦게 마운트되니 시작값부터 어긋난다.

characterInstanceCache는 모듈 스코프 싱글턴이라 두 캔버스가 lastUpdatedFrame 한 칸을 공유한다. 그래서 매 rAF마다:

실행 순서 비교 결과
hero 루프 lastUpdatedFrame(contact의 번호) ≠ hero 번호 통과 → 두 믹서 갱신
contact 루프 lastUpdatedFrame(hero의 번호) ≠ contact 번호 통과 → 두 믹서 또 갱신

가드가 아무것도 막지 못하고 delta만 이중 누적된다. 반대로 두 카운터가 우연히 같은 값이 되는 프레임에는 두 번째 루프가 early return 해서 그 캔버스의 갱신이 통째로 사라진다 — 이쪽이 더 조용한 버그다.

2. 왜 가드를 고치지 않고 없앴는가

가드를 살리려면 키를 렌더러 식별자 + 프레임 번호 조합으로 바꿔야 한다. 하지만 애초에 공유 가드가 필요한 이유가 없다.

훅은 이미 instance: 'main' | 'contact'를 받고, 캔버스마다 한 번씩 호출되며, 각 useFrame은 자기 캔버스의 rAF에서만 실행된다. 자기 mixer만 진행시키면 "rAF당 한 번, 자기 캔버스의 delta로"가 구조적으로 성립한다 — 지켜야 할 상태가 없으니 깨질 상태도 없다.

// 캔버스마다 한 번씩 호출되고 그 캔버스의 rAF에서만 실행되므로, 자기 인스턴스의 mixer만 진행시킨다.
useFrame((_, delta) => {
  mixer.update(Math.min(delta, MIXER_MAX_DELTA_SECONDS));
});

같은 인스턴스를 한 페이지에서 두 번 이상 마운트하는 곳이 없음을 확인했다 (HomeHeroCharacterSeatSet은 hero에서 main 1회, contact에서 contact 1회). 가드가 없어지면서 CharacterInstanceCache.lastUpdatedFrame 필드도 함께 제거했다.

3. clampedDelta(≤ 1/30초)는 그대로 둔다

RenderWhenVisible이 오프스크린·백그라운드에서 frameloop을 'never'로 멈췄다가 재개하므로, 재개 프레임의 delta가 정지해 있던 시간만큼 튄다. 상한이 없으면 복귀할 때 애니메이션이 순간이동한다. 이번 변경과 무관한 안전장치라 유지하고, 이름 있는 상수(MIXER_MAX_DELTA_SECONDS)로 올려 의도를 남겼다.

측정할 때 이 상한이 함정이 된다. 프레임이 30fps 아래로 떨어지면 delta가 상한에 걸려 버그가 "2배속"이 아니라 "프레임당 최대 1/30초 진행"으로 뭉개진다. 실제로 headless 소프트웨어 렌더링(≈4fps)에서는 전/후가 둘 다 0.12× 근처로 나와 구분되지 않았다. GPU를 켜고 60fps를 확보해야 2.00× / 1.00×가 갈린다.

4. 부수 효과 — contact가 화면 밖이면 contactMixer도 멈춘다

이전에는 contact 캔버스의 frameloop이 정지해도 hero 루프가 contactMixer를 대신 갱신했다. 이제는 멈춘다. 보이지 않는 스켈레톤을 매 프레임 갱신하던 낭비가 사라지는 것이고, 재개 시 점프는 위의 delta 상한이 막는다.

Changes

커밋 내용
b2a4944 🐛 fix: 렌더러별 프레임 카운터 충돌로 믹서가 이중 갱신되던 문제 수정
3c0ee3d 🧪 test: 캐릭터 믹서 프레임 갱신 계약 테스트 추가

건드린 파일은 src/entities/character/model/use-character-instance.ts 하나와 새 테스트 파일 하나뿐이다.

의도적으로 안 바꾼 것

항목 이유
clampedDelta 상한 #93이 넣은 frameloop 재개 안전장치. 이번 버그와 무관 (설계 3)
use-render-when-visible.ts, frameloop, <Preload all /> 오프스크린 정지는 이미 여기서 처리한다. 훅에 별도 가시성 로직을 추가하면 판단 주체가 둘이 된다
character-state-machine, use-character-animation 전환 로직 재생 속도 문제이지 전환 문제가 아니다
characterInstanceCache의 싱글턴 구조 라우트 이동 후 재마운트 동작은 이번 범위 밖 (후속)
에셋·경로 상수, 프리로더 마운트 위치 각각 3D 브랜치 01·02의 소유

리뷰 포인트

  1. info.render.frame이 렌더러별 카운터라는 전제 (설계 1). 이 전제가 틀리면 진단 전체가 무너진다. WebGLRenderer 인스턴스마다 info가 따로 생성되고, 캔버스가 둘이면 렌더러도 둘이다.
  2. 가드를 없애도 되는 근거 (설계 2) — 같은 instance를 한 페이지에서 두 번 이상 마운트하는 곳이 정말 없는지. 생긴다면 그때는 mixer 단위로 렌더러 + 프레임 키를 써야 한다.
  3. contactMixer가 오프스크린에서 멈추는 것이 의도된 변화라는 점 (설계 4). 재진입 시 애니메이션이 튀지 않는지 확인 필요.
  4. 테스트 버킷과 mock 경계 — 수정 후 판단 로직이 사라져 순수 헬퍼로 뽑을 게 없어 훅 배선 테스트(dom:model)로 뒀다. mock은 drei가 아니라 useSceneGltf 경계에 걸었다. KTX2 도입 - VRAM 666MB → 119MB #95 이후 이 모듈이 import 시점에 draco 경로를 세팅하고 호출 시점에 실제 WebGLRenderer에서 KTX2Loader를 만들기 때문에, drei를 흉내 내면 그 내부까지 따라 흉내 내야 한다.

Test plan

  • pnpm run check-types

  • pnpm run lint

  • pnpm run test:vitest — 전 버킷 통과 (신규 4건 포함)

  • pnpm run test:browser — 26 passed

  • 위 셋 모두 origin/main(KTX2 도입 - VRAM 666MB → 119MB #95 KTX2 머지) 리베이스 이후 재실행

  • 신규 테스트가 수정 전에는 실패하는지 확인 — 4건 중 3건 실패 (2.5가 1로, 1.0이 1.5로, contactMixer가 0이 아닌 1.0)

  • 실측 재현 — 프로덕션 빌드 + GPU 활성 Chromium, 1600×1100, /ko에서 스크롤 위치 0~1.0을 11지점 순회하며 mixer.time 증가율 / 실경과시간 측정

    스크롤 전 (main / contact) 후 (main / contact)
    0.0 (contact 최초 진입 전) 0.999 / 0.999 0.999 / 0
    0.1 ~ 0.6 1.87 ~ 2.01 / 동일 0.999 ~ 1.002 / 동일
    0.7 ~ 1.0 (두 캔버스 노출) 2.00 / 2.00 1.00 / 1.00

    프레임 단위로도 확인했다. 같은 rAF에서 각 인스턴스의 useFrame 호출 수를 세고 믹서 진행량을 나눈 값:

    전 후
    프레임당 믹서 진행 (110fps 구간) 0.01851초 0.00902초
    실제 프레임 간격(1/fps) 0.00902 ~ 0.00926초 0.00902초
    배율 2.00× 1.00×
  • 셀프 QA — 프로덕션 빌드 + GPU 활성 Chromium, 콘솔/페이지 에러 0건

    시나리오 결과
    홈 상태 순환 idle → typing → idle → notification 14회 전환 관측, 순서 정상
    각 LoopOnce 클립이 자기 길이만큼 재생 typing 2.459/2.5초, notification 2.491/2.5초 (샘플 간격 100ms)
    contact 노출 시 진행률 0.999×
    contact 최초 진입 전(한 번도 안 보였을 때) 0 — 캔버스와 함께 정지
    contact 재진입 첫 스텝 100ms 대기에 0.1027초 진행 — 점프 없음
    탭 백그라운드 main 0× → 복귀 후 0.999×, 복귀 첫 스텝 0.1006초 (점프 없음)
    좁은 뷰포트(900×900) 캔버스 1개, contact 0×, main 0.965×
    눈 깜빡임 / 하트 head morph 3슬롯 전부 변화, heart visible false↔true 관측
    스크롤 시퀀스 20회 왕복 main 0.864× (구간별 오프스크린 정지 포함), 에러 없음
  • 사람 눈으로 보는 최종 확인 — 위 계측은 "몇 배로 도는가"를 답하지만 "자연스러운가"는 답하지 않는다

후속

  • RenderWhenVisible이 contact 캔버스를 다시 멈추지 못한다 — 셀프 QA에서 발견. contact 캔버스가 한 번도 보인 적 없을 때는 정지하지만(useFrame 0회/초), 한 번 노출된 뒤 다시 화면 밖으로 나가면 오프스크린인데도 계속 돈다(≈84회/초). origin/main에 같은 계측을 걸어 확인한 결과 이 브랜치 이전부터 있던 동작이다 — 이 PR은 frameloop·가시성 로직을 건드리지 않는다. 다만 이번 변경으로 영향이 커졌다. 전에는 hero 루프가 contactMixer를 대신 갱신했으므로 contact 캔버스의 정지 여부가 믹서에 영향을 주지 않았지만, 이제는 그대로 드러난다. 원인이 use-render-when-visible.ts(3D 파이프라인 최적화 - gltf, 렌더 루프·화면 텍스처 정비 #93 소유)에 있어 여기서 고치면 이 PR의 진단 범위를 벗어나므로 분리한다.
  • characterInstanceCache 싱글턴의 재마운트 동작 — sourceScene 동일성으로만 캐시를 판정하므로 라우트 이동 후 useGLTF 캐시가 비워졌다 다시 채워지는 경로에서 어떻게 동작하는지 별도 확인이 필요하다. 이번 브랜치에서 같이 고치면 프레임 갱신 수정과 캐시 수명 수정이 한 diff에 섞여 되돌리기 어려워지므로 분리한다.
  • VRAM 380MB (F-1·F-2) — 브랜치 01 perf/3d-vram-ktx2 담당. 이 브랜치와 파일이 겹치지 않는다.
  • 비홈 라우트 프리로드 9.6MB (F-3) — 브랜치 02 perf/3d-preload-scope 담당.

Summary by CodeRabbit

  • 버그 수정
    • 여러 캔버스에서 캐릭터 애니메이션이 서로 영향을 주지 않고 독립적으로 업데이트됩니다.
    • 캔버스가 추가되거나 제거될 때 애니메이션 프레임이 누락되지 않습니다.
    • 렌더링이 일시 중단된 후 재개되어도 애니메이션이 과도하게 건너뛰지 않도록 보정됩니다.

@vercel

vercel Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chaen Ready Ready Preview Aug 31, 2026 2:38pm

@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64a7bb31-42bf-437a-8cf1-6cbcef0145c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6799a and 2c79dac.

📒 Files selected for processing (2)
  • src/entities/character/model/use-character-instance.test.ts
  • src/entities/character/model/use-character-instance.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

useCharacterInstance는 공유 프레임 가드 없이 각 인스턴스의 믹서를 독립적으로 갱신한다. 믹서 delta는 1/30초로 제한된다. 테스트는 독립 캔버스, 언마운트, 동일 프레임, 큰 delta 동작을 검증한다.

Changes

캐릭터 믹서 프레임 갱신

Layer / File(s) Summary
인스턴스별 믹서 갱신
src/entities/character/model/use-character-instance.ts
lastUpdatedFrame을 제거했다. 각 useCharacterInstance 호출은 자신의 믹서만 갱신한다. delta는 MIXER_MAX_DELTA_SECONDS 이하로 제한한다.
믹서 갱신 동작 검증
src/entities/character/model/use-character-instance.test.ts
독립 렌더러 프레임, 동일 프레임 번호, contact 캔버스 언마운트, 큰 delta에 대한 믹서 진행량을 검증한다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 2c79d

The change limits each character mixer to updates from its own canvas, restoring normal animation speed and stopping offscreen contact animation updates; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed 제목은 캐릭터 믹서의 이중 갱신을 수정하고 애니메이션 2배속 문제를 정상화한 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 설명은 변경 목적, 문제 원인, 해결 과정, 영향 범위, 테스트 결과, 후속 작업을 구체적으로 포함합니다. 템플릿의 제목과 일부 다르지만 작업 내용·주요 고민·참고 문서에 필요한 정보는 충분히 제공됩니다.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/character-mixer-frame-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c79dacc8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +88 to 90
useFrame((_, delta) => {
mixer.update(Math.min(delta, MIXER_MAX_DELTA_SECONDS));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required PR rationale document

This commit changes mixer ownership and intentionally stops the contact animation while its canvas is inactive, but the reviewed tree has no docs/pr/ directory or markdown record for the change. The repository requires this durable rationale to document design tradeoffs, deliberately unchanged scope, tests, and follow-up blockers; retaining that information only in the commit message does not satisfy the required file-based record. Add the corresponding document under docs/pr/.

AGENTS.md reference: AGENTS.md:L133-L140

Useful? React with 👍 / 👎.

`gl.info.render.frame`은 WebGLRenderer 인스턴스마다 따로 세는 카운터라
캔버스가 둘일 때 서로 비교할 수 없다. 홈에는 hero와 contact 캔버스가 동시에
살아 있고 `useCharacterInstance`가 캔버스마다 호출되므로, 두 useFrame 루프가
각각 "내 프레임 번호는 아까 그 번호와 다르다"고 판단해 매 rAF마다 두 믹서를
모두 갱신했다. 결과적으로 두 믹서가 실시간의 2배로 진행했고, 반대로 두
카운터가 우연히 같은 값이 되는 프레임에는 한쪽 캔버스의 갱신이 통째로
드롭됐다.

가드를 렌더러 식별자+프레임 번호 조합으로 고치는 방법도 있지만, 애초에
공유 가드가 필요한 이유가 없다. 훅은 이미 `instance`를 받고 있고 각 호출은
자기 캔버스의 rAF에서만 실행되므로, 자기 mixer만 진행시키면 "rAF당 한 번,
자기 캔버스의 delta로"가 자연히 성립한다. 그래서 가드와 `lastUpdatedFrame`
캐시 필드를 제거하는 쪽을 골랐다.

부수 효과로 contact 캔버스가 화면 밖이라 frameloop가 멈춘 동안 contactMixer도
함께 멈춘다. 이전에는 hero 루프가 대신 갱신해 주고 있었는데, 보이지도 않는
스켈레톤을 매 프레임 갱신하던 낭비였다.

`clampedDelta` 상한은 그대로 둔다. `RenderWhenVisible`이 frameloop을
정지·재개하므로 재개 시점의 delta 점프를 막는 안전장치가 여전히 필요하다.
두 개의 독립적인 렌더러가 각자 프레임을 진행하는 상황을 재현해, 각 믹서가
자기 캔버스의 delta로 rAF당 정확히 한 번만 진행하는지 고정한다. 이 버그는
캔버스 두 개가 동시에 살아 있어야만 드러나므로 단일 캔버스 픽스처로는
잡히지 않는다.

프레임 갱신 판단을 순수 헬퍼로 뽑을 수 있으면 Node 버킷에 두는 게 맞지만,
수정 후에는 판단 로직 자체가 사라지고 "훅 호출마다 자기 mixer만 갱신한다"는
배선만 남는다. 그래서 헬퍼를 억지로 만들지 않고 useFrame 배선을 그대로
검증하는 jsdom 훅 테스트(dom:model 버킷)로 뒀다.

프레임 카운터가 어긋난 경우와 우연히 일치하는 경우를 나눠 검증한다. 전자는
이중 누적, 후자는 갱신 드롭이라는 서로 다른 증상으로 나타나기 때문이다.
contact 캔버스가 마운트되지 않은 좁은 뷰포트 시나리오와, frameloop 재개 시
delta 상한(1/30초)도 함께 고정한다.
@pcwadarong
pcwadarong force-pushed the fix/character-mixer-frame-guard branch from 2c79dac to 3c0ee3d Compare August 31, 2026 14:32
@pcwadarong
pcwadarong merged commit abe876a into main Aug 31, 2026
6 checks passed
@pcwadarong
pcwadarong deleted the fix/character-mixer-frame-guard branch August 31, 2026 16:54
@pcwadarong pcwadarong changed the title 🐛 fix: 캐릭터 믹서 이중 갱신 수정 — 애니메이션 2배속 정상화 캐릭터 애니메이션 2배속 정상화 Sep 1, 2026
@pcwadarong pcwadarong self-assigned this Sep 1, 2026
@pcwadarong pcwadarong added bug Something isn't working 3D labels Sep 1, 2026
pcwadarong added a commit that referenced this pull request Sep 6, 2026
F-1~F-4를 다루던 세 브랜치가 모두 머지됐는데(#95·#97·#98) 문서는 그대로여서, 지금
읽으면 이미 끝난 일을 남은 과제로 오해한다. 지시서 3개도 "앞으로 할 일"처럼 읽힌다.

README를 "남은 과제 관리" 용도로 다시 잡았다. 끝난 것은 §1에 한 줄과 실측 확인만
남기고 근거는 각 PR로 넘긴다. 문서에 같은 내용을 두 번 적으면 한쪽이 반드시 낡는다.

이번에 다시 확인해 갱신한 것:

- GLB 이미지 10장이 전부 `image/ktx2`, 죽은 roughness는 `skin_roughness`만 생존 (F-1·F-2 완료)
- 프리페처가 `app/[locale]/(home)/layout.tsx`에 있음 (F-3 완료)
- 믹서가 인스턴스별 `mixer.update(min(delta, 1/30))` (F-4 완료)
- F-5의 `skin_normal` 항목은 이미 1024²라 해결됨 — 목록에서 뺐다
- F-5 잔여: `hair_color`/`hair_color-hair_alpha` 중복(둘 다 2048² 임베드), `thumbnail.png` 1.77MB PNG

새로 찾은 것 두 개를 추가했다:

- **N-1** 웜 캐시 재방문에도 로딩 오버레이가 2.8초. 다운로드 완료 후 꼬리가 ~1.6초로 일정하고,
  그 마지막 관문인 `<Preload all />`의 90%가 이 씬이 쓰지도 않는 큐브카메라 6면 렌더다
  (`gl.compile` 15~41ms vs `CubeCamera.update` 275~299ms)
- **N-2** SkinnedMesh 바운딩 스피어가 bind pose 기준이라 카메라가 가까워지면 눈썹이 컬링된다

VRAM 수치는 이 문서에서 재측정하지 않았으므로 #95를 단일 출처로 명시했다. 공통 규칙에
"성능·타이밍은 headless로 재지 마라"와 아티클 초안 경로도 추가했다.

지시서 3개에는 완료 배너를 달았다. 삭제하지 않은 건 그때의 판단 근거가 남아 있어서다.
pcwadarong added a commit that referenced this pull request Sep 7, 2026
`docs/3d-performance/`는 F-1~F-4를 나눠 맡을 세 브랜치를 위해 쓴 작업 문서였다.
그 세 브랜치가 모두 머지되면서(#95·#97·#98) 문서의 본래 역할이 끝났다.

남겨두면 비용만 생긴다. 지시서 3개는 그때의 코드 기준이라 지금 따라 하면 틀리고,
README는 상태가 바뀔 때마다 같이 갱신해야 한다.

살아 있어야 할 내용은 PR #103 본문으로 옮겼다.

- **아직 안 한 것**(hair 텍스처 중복, thumbnail.png) → 후속
- **파봤지만 코드를 안 바꾼 것**(큐브카메라 기각, 로딩 오버레이 690ms 구성, dev로
  재면 결론이 뒤집힌다는 함정) → Changes

곁다리 정리 두 개:

- `.gitignore`의 `!docs/3d-performance/` negation이 죽은 설정이 되어 뺐다
- `pr-workflow` 스킬의 예시가 방금 지운 경로를 가리키고 있어 일반화했다

Codex 리뷰 P2(`01-vram-ktx2.md` 번호 매기기 깨짐)는 파일 삭제로 해소된다.
pcwadarong added a commit that referenced this pull request Sep 7, 2026
`docs/3d-performance/`는 F-1~F-4를 나눠 맡을 세 브랜치를 위해 쓴 작업 문서였다.
그 세 브랜치가 모두 머지되면서(#95·#97·#98) 역할이 끝났다.

남겨두면 비용만 생긴다. 지시서 3개는 그때의 코드 기준이라 지금 따라 하면 틀리고,
README는 상태가 바뀔 때마다 같이 갱신해야 한다.

살아 있어야 할 내용은 PR #103 본문으로 옮겼다.

- **아직 안 한 것**(hair 텍스처 중복, thumbnail.png) → 후속
- **파봤지만 코드를 안 바꾼 것**(큐브카메라 기각, 로딩 오버레이 690ms 구성, dev로
  재면 결론이 뒤집힌다는 함정) → Changes

곁다리 정리 두 개:

- `.gitignore`의 `!docs/3d-performance/` negation이 죽은 설정이 되어 뺐다
- `pr-workflow` 스킬의 예시가 방금 지운 경로를 가리키고 있어 일반화했다

Codex 리뷰 P2(`01-vram-ktx2.md` 번호 매기기 깨짐)는 파일 삭제로 해소된다.

This branch was successfully deployed

1 active deployment
Preview — 3c0ee3dc Deployed Aug 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3D bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant