# 공간 분할·선택 로딩 시험

2026-10-02. 같은 Knock Hall 마스터를 실제로 나눈 다섯 SOG를 두 앱에서 사용했다. 시작 시 전체 SOG를 요청하지 않고, 이동에 따라 구역 파일을 로드하고 렌더러 자원을 해제하는 동작을 확인했다. 이 단계는 데스크톱의 기능 검증이며 엔진 성능 순위는 정하지 않는다.

2026-10-03 후속: [원거리 저해상도 표현](FAR-LOD.md)을 추가했다. 아래 최초 65.4% 절감과 브라우저 기록은 원거리 생략 단계의 결과다. 현재는 저해상도 포함 약 54.2% 절감이며 교체 순간 선명도 변화가 남는다.

## 분할 자료와 최초 로딩량

`scripts/make-spatial-chunks.py`가 900,000개 원본 레코드를 Gaussian 중심의 X 좌표로 4 m씩 나눈다. 각 레코드는 인코딩 전에 한 구역에만 들어간다. SH0으로 다시 인코딩한 결과와 해시는 [분할 메타데이터](../../shared/source/spatial-chunks.json)에 있다. 기존 런타임 `current.sog`도 SH0이다.

| 구역 | X 범위(m) | Gaussian 수 | SOG 크기(B) |
|---|---:|---:|---:|
| 0 | 0–4 | 69,967 | 931,680 |
| 1 | 4–8 | 231,735 | 3,060,137 |
| 2 | 8–12 | 267,979 | 3,541,172 |
| 3 | 12–16 | 215,380 | 2,836,047 |
| 4 | 16–20 | 114,939 | 1,510,967 |
| 합계 | 0–20 | 900,000 | 11,880,003 |

시작 위치 X=3에서는 구역 0·1만 준비한다. **301,702점, 3,991,817 B**로 분할본 전체의 33.5% 점 수·33.6% 파일 크기다. 기존 단일 SOG 11,543,748 B와 비교하면 **최초 SOG 파일 크기를 약 65.4% 줄였다**. HTML·JS·물리 엔진·JSON·충돌 메시까지 합한 페이지 전송량이나 GPU 메모리 감소율은 아니다. 전체 분할본은 개별 압축 때문에 단일 파일보다 약 2.9% 크다.

Gaussian의 중심은 중복되지 않지만 타원체의 범위는 경계를 넘을 수 있다. 재인코딩의 양자화와 압축도 있으므로 원본과 픽셀이 완전히 같다는 의미는 아니다.

## 실행 정책

- 공통 `SpatialStreamer`가 요청·준비·실패·해제 상태와 예산을 관리한다. 엔진별 어댑터가 실제 Splat 생성·표시·해제를 맡는다.
- 로드 거리 1.5 m, 해제 거리 2.5 m, 상주 예산 최대 3구역·750,000점이다. 로딩 중인 구역도 예산에 예약하며 해독을 한 번에 하나씩 수행한다.
- 구역 경계에 캡슐이 닿기 전에 필요한 구역이 준비돼야 이동할 수 있다. 지연 때 수평 이동을 막고 중력 처리는 유지한다.
- 멀어진 구역은 실제 렌더러 객체를 정리한다. 이미 진행 중인 SDK 해독은 취소하지 못하므로 늦은 결과가 오면 표시하지 않고 해제한다.
- 실패는 자동 반복 요청하지 않는다. 사용자가 `구역 로딩 재시도`로 다시 요청한다.
- 전체 공통 충돌 메시 24삼각형·1,052 B는 유지한다. 구역별 충돌 메시의 로드·해제까지 구현한 시험은 아니다.
- 설정 검증은 연속된 X 구역과 공통 Y/Z 범위를 요구한다. 해제 거리 안의 구역들이 예산을 초과하는 설정은 거부해 경계에서 영구 대기하는 구성 오류를 막는다. 임의 모양의 공간 분할이나 LRU 스케줄러는 지원하지 않는다.

## 브라우저 확인

Playwright MCP의 headed Chromium에서 앱을 하나씩 실행했다. 릴리스 `local-4d5905679d2e-cbfaf693149e`, 화면 1200×863·DPR 1.5. [전체 JSON](measurements/2026-10-02/spatial-streaming.json), [정상 경로](measurements/2026-10-02/spatial-stream-driver.js), [실패 주입 경로](measurements/2026-10-02/spatial-stream-failure-driver.js).

| 구성 | 시작 준비 | 경로 중 최대 예약 | 끝 구역 이동→시작 복귀 | 로드/해제 횟수 | 낙하 복구 / JS 예외 |
|---|---|---|---|---:|---|
| Three.js + BVH | 0·1 / 301,702점 | 2구역 / 499,714점 | 통과 | 9 / 7 | 0 / 0 |
| Three.js + Rapier | 동일 | 동일 | 통과 | 8 / 6 | 0 / 0 |
| PlayCanvas + Ammo | 동일 | 동일 | 통과 | 8 / 6 | 0 / 0 |

세 구성 모두 전체 `current.sog` 요청 없이 이동과 복귀가 됐다. 로드 횟수 차이는 시간과 위치에 따른 해제·재요청 차이이며 성능 우열의 근거가 아니다. `requestedBytes`는 논리적 파일 요청 크기의 합이다. 브라우저 캐시 재사용을 포함한 실제 네트워크 전송량과 다르다.

BVH·Ammo에서는 구역 2의 응답 지연 중 경계 정지, 응답 후 이동 재개, 요청 실패 후 자동 재시도 없음·수동 복구, 로딩 중 시작점 복귀 후 늦은 구역 결과 폐기를 확인했다. Rapier는 정상 경로와 공통 상태 관리 단위 검사를 통과했지만 이 실패 주입 브라우저 시험은 별도로 하지 않았다.

추가로 Three.js BVH에서 전체 경로를 세 번 왕복했다. 각 복귀 후 `renderer.info.memory`는 geometry 2개·texture 10개로 유지됐고 상주 구역은 0·1로 돌아왔다. [자원 수 기록](measurements/2026-10-02/spatial-resource-cycles.json). GPU 바이트·프로세스 RAM을 측정한 결과가 아니며 PlayCanvas의 동등한 GPU 자원 수는 확보하지 않았다.

최종 릴리스 `local-4d5905679d2e-f6ec268ff18a`에서 `pnpm check`를 통과했다. 15개 자산의 크기·해시, 54개 자동 검사, 타입 검사와 두 앱 빌드·배포 목록 검증을 포함한다. 이후 headed Chromium으로 세 구성의 최초 선택 로딩과 PlayCanvas 왕복, BVH 패널 입력 복귀를 추가 확인했다. [최종 확인 JSON](measurements/2026-10-02/final-capabilities-sanity.json). 최종 확인에서 실패 주입과 전체 콘텐츠 경로를 모두 다시 실행한 것은 아니다.

## 아직 남은 핵심 제한

이 문서의 초기 시험에서는 로드하지 않은 먼 구역이 보이지 않았다. 현재는 [저해상도 표현으로 유지](FAR-LOD.md)하지만 정밀 자산 교체 순간의 선명도 변화와 경계 품질 검수가 남아 있다. 엔진 고유 LOD 기능을 비교한 결과도 아니다.

이 시험은 작은 공통 충돌 바닥 위의 현재 장면만 지원한다. 분할과 시대 전환의 결합, 실제 복잡한 충돌 메시 분할, iPhone에서 분할 장면의 준비·이동·장시간 유지, 큰 야외 공간의 배경 유지가 남아 있다. 데스크톱의 성공을 모바일 안전 한도로 사용하지 않는다.

## 직접 확인

`./dev.sh --preview both` 후 장면 메뉴에서 `구역별 로딩 · 분할 시험`을 선택한다. [Three.js BVH](http://127.0.0.1:5173/?scene=stream&controller=bvh), [Rapier](http://127.0.0.1:5173/?scene=stream&controller=rapier), [PlayCanvas](http://127.0.0.1:5174/?scene=stream).

마우스를 잠그지 않아도 D로 오른쪽 구역까지, A로 시작 구역까지 이동할 수 있다. 상단 구역 상태가 바뀌는지, 경계에서 바닥을 잃지 않는지 확인한다. 현재 먼 구역은 저해상도로 유지된다. 이동 중 건물의 선명도가 바뀌는 정도도 확인한다.
