· 15 min read
OFF에 행성을 얹다 — Topology-Agnostic 아키텍처 Bolting a Planet onto OFF — Topology-Agnostic Architecture
무한 평면 분산 서버 프레임워크였던 OFF에 cube-sphere 행성을 얹은 이야기. 평면도 구체도 같은 코어 위에서 도는 구조로 어떻게 바꿨나. How we extended OFF — originally a planar distributed open-world framework — to host cube-sphere planets on the same core, with planar and spherical worlds running on identical infrastructure.

이전 글 — OFF 소개 의 후속편. OFF 위에 cube-sphere 행성 을 얹은 이야기 — 평면도 구체도 같은 코어에서 도는 구조로 만들기.
출발점 — 평면 위 분산 셀 인프라
OFF 는 무한한 XZ 평면을 격자로 잘라 셀(cell) 단위로 서버에 배분하는 프레임워크다. 셀 1 개당 GameServer 1 대, 인접 셀 서버끼리 경계 영역 동기화, 엔티티가 셀 경계를 넘으면 다음 서버로 통째 이전 (handover) — 이게 뼈대.
서버를 추가할수록 월드가 가로 / 세로로 무한 확장. 자세한 건 이전 글 에.
의문 — “구체도 돌릴 수 있지 않을까”
이 인프라가 한참 동작하던 어느 시점, 의문이 떴다.
“셀들이 펼쳐지는 바닥 모양 만 평면에서 구체로 바꾸면 행성이 되지 않을까?”
분산 인프라 / 핸드오버 / ECS / 렌더러 다 그대로 두고 바닥 만 갈아끼우는 발상. 단순한데, 손대보니 단순하지 않았다 — 평면 가정이 코드 거의 모든 곳에 박혀있었다.
추상화로 풀어야 한다는 건 처음부터 명확했다. 진짜 문제는 “어떤 구조로, 어디까지” 추상화할지. 면을 잘못 그으면 새 위상 추가할 때마다 또 다 헤집어야 하고, 너무 위에 그으면 추상화 비용만 들고 분기는 그대로 남는다. 면을 어디에 긋는가 가 이번 작업의 본질이었다.
공간을 4 차원으로 분해
면을 긋기 전에 “월드의 공간이라는 게 뭘로 구성돼 있나” 부터 분해. 4 개 차원이 나왔다.
각 층을 차근히.
L1 — 월드 형태 (토폴로지)
게임이 펼쳐지는 바닥의 모양 자체. 평면 / 닫힌 구체 / 도넛 (토러스). 토폴로지가 결정되면 같은 점 두 개 사이 거리는 어떻게 계산되는지, 직진하면 어디로 가는지 같은 가장 근본적인 공간 규칙이 결정된다.
이 글의 주인공은 평면(Planar) 과 구체(Cube-sphere) 두 개. 도넛(Toroidal) 은 인터페이스가 잘 그어졌는지 검증하기 위한 스캐폴딩으로만 박아뒀다 (실제 활성화 안 됨).
L2 — 표면 분할 (셀 자르기)
L1 의 바닥 위에 어떻게 격자 무늬를 그어 셀로 자를 것인가. 평면이라면 단순 사각 grid (X, Y 정수 인덱스) 가 자연스럽다. 구체라면 — 여기서 수학적인 제약 이 하나 끼어든다.
구면을 다각형 셀로 분할하면 반드시 720° 의 각 결손이 발생한다. (Gauss-Bonnet)
평면에서는 한 점 주위에 모인 각이 360° 로 깔끔하게 닫히지만, 구면에서는 다각형들의 모서리 각을 다 합쳐도 720° 가 모자란다. 이 720° 를 어디로 분배하느냐가 구체 분할의 본질이다.
선택지는 — Geodesic / icosahedral (12 vertex × 60° 분배), HEALPix (천체 데이터 표준), Cube-sphere (8 corner × 90° 분배) 등. OFF 는 cube-sphere 를 골랐다:
- 평면 코드가 이미 사각 셀 + 4/8-way 인접 기반이라 마이그레이션 비용 최저
- Quadtree LOD 가 산업 표준 (Google Maps · S2 · Cesium 다 cube-sphere 패턴)
- 외부 의존 없이 자체 구현 가능
이 층의 출력물은 CellId (셀 식별자) + 인접 규칙. 분산 인프라가 가장 직접적으로 보는 게 이 층 — handover, AOI, snapshot 키, wire 라우팅 전부 여기 의존.
L3 — 셀 데이터 (셀 안에는 뭐가 있나)
한 셀의 내용물. 보셀 게임이라면 TerrainWidth × TerrainDepth × LayerCount 의 블록 격자 — 작게는 한 셀에 수십만, 크게는 수천만 개 보셀까지. 이 격자가 디스크 / wire 로 흐를 때는 ChunkSize 단위로 청크화한다 (청크는 저장 / 스트리밍 단위 지 셀의 구조 단위가 아님).
여기에 더해 멀리서 본 미리보기용 heightmap 피라미드 — 같은 지형을 4× / 8× / 16× / 32× 다운샘플한 텍스처 스택. 우주에서 행성을 봤을 때 voxel mesh 도착 전까지 행성 모양만 보여주는 데 쓴다.
셀의 내용물 이지 모양 이 아니라는 게 핵심. L1 이 cube-sphere 가 되어도 L3 는 한 줄도 안 바뀐다.
L4 — 수직 (고도 처리)
같은 (X, Y) 셀 위에 여러 층의 콘텐츠를 쌓을 수 있게 하는 차원. “지하 던전 / 지표 / 하늘섬” 같은 다층 구조. LayerCount 한 파라미터로 제어. 평면이든 구체든 “고도라는 축이 따로 있다” 는 사실은 그대로다 — 다만 구체에서는 셀 좌표계의 Y 축이 receiver tangent plane 기준 고도라서 절대 world Y 와 다르다 (뒤에서 자세히).
면을 어디에 그었나
분해가 끝나니 답이 명확해졌다. L1/L2 만 위상별로 다르고, L3/L4 는 위상 무관. 추상화 면은 L2 와 L3 사이. L2 까지의 모든 결정 (월드 형태 + 셀 자르기 + 인접 규칙) 을 한 인터페이스로 묶고, 위는 그 인터페이스만 보고 살게 하면 된다.
이 인터페이스가 IWorldTopology 다.
IWorldTopology — 한 인터페이스 뒤로
L2 까지의 결정을 한 인터페이스로 묶는 건 결국 공간에 대한 모든 질문 을 이 인터페이스로만 묻겠다는 뜻이다. 게임 로직 / ECS / 분산 인프라 / 클라이언트 렌더러가 던지는 공간 질문은 사실 몇 가지 안 된다.
public interface IWorldTopology
{
// "이 좌표가 속한 셀이 뭐냐?"
CellId LocateCell(WorldPoint point);
// "이 셀의 중심 좌표가 뭐냐?"
WorldPoint CellCenter(CellId cell);
// "이 셀의 이웃은 누구누구냐?"
IEnumerable<CellId> Neighbors(CellId cell);
// "이 셀에서 저 셀로 넘어갈 때 좌표축이 어떻게 회전하느냐?"
EdgeTransform GetEdgeTransform(CellId cell, int edge);
// "셀 내부 좌표 (lx, ly, lz) ↔ 절대 좌표"
WorldPoint CellLocalToWorldPoint(CellId cell, double lx, double ly, double lz);
(double lx, double ly, double lz) WorldPointToCellLocal(CellId cell, WorldPoint point);
// "이 점에서 반경 R 안 셀들" (AOI 용)
IEnumerable<CellId> CellsInRadius(WorldPoint center, double radius);
// quadtree 부모/자식 (cube-sphere 만 의미 있음)
IEnumerable<CellId> Children(CellId cell);
bool TryGetParent(CellId cell, out CellId parent);
}이게 L1/L2 의 모든 위상-별 지식이 갇히는 면이다. 새 위상 추가 = 이 인터페이스 구현체 하나 + Factory 한 줄.
함께 도입한 위상-중립 데이터 타입 셋 — CellId (셀 식별자, 128-bit, wire/디스크/URL 공통) · WorldPoint (전역 cartesian, 송신자가 어떤 위상인지 모르고 받음) · EdgeTransform (셀 사이 좌표 변환, 평면 = Identity / 구체 face 경계 = 90° quaternion).
두 짝의 메서드 — 그림으로
Neighbors() + GetEdgeTransform() — 이웃이 누구냐 + 넘어갈 때 좌표축이 어떻게 도느냐.
평면은 8-way 인접에 EdgeTransform 이 항상 Identity. Cube-sphere 는 4-way + cube face 경계만 90° 회전. 같은 face 안 인접은 평면처럼 Identity 지만, face 를 가로지르면 90° 회전이 발동된다. 24-entry lookup table (6 face × 4 edge) 이 어느 face 의 어느 edge 로 갔고 어떻게 회전됐는지 박아둔다.
CellLocalToWorldPoint / WorldPointToCellLocal — 셀 안 좌표 ↔ 전역 좌표.
평면은 셀의 local frame 이 world frame 과 정렬돼서 변환이 그냥 translation. Cube-sphere 는 셀 local frame 이 그 셀이 sphere 표면에 닿는 지점의 tangent frame 이라, ly 가 셀마다 radial outward 방향이 다 다르다. 어느 셀의 ly 도 world Y 와 같지 않다.
큰 행성에서만 일어나는 일들
인터페이스 모양이 깔끔해도, 평면 가정이 박혀있던 코드를 옮기는 과정에서 구체로 가니까 비로소 보이는 일들 이 한둘이 아니었다. 종류만 풀어둔다 — 각각은 후속글로 따로 쓸 만한 분량이다.
좌표 정밀도. 지구 크기 행성에서는 표면 좌표 절댓값이 수백만 미터 단위라 32-bit float 가 미터 단위로 떨림. 모든 좌표를 카메라 상대로 변환해서 셰이더로 보내는 origin shift 가 거의 모든 렌더 경로에 침투해야 했다.
Stream-time 렌더링. 우주 멀리서도 행성 모양이 보여야 하고, 가까이 가면 voxel mesh 가 채워져야 하고, 그 전환이 매끄러워야 한다. Per-cell tier state machine (None / Preview / Live) 으로 풀었다.
진입/이탈 임계값을 다르게 두어 thrashing 막고, “1 cell = 1 mesh at any instant” invariant 로 preview / live depth-fight 차단.
Cross-face handover. cube 의 6 face 가 만나는 모서리에서 셀 좌표축이 90° 돌아간다는 사실은 카메라뿐 아니라 엔티티 핸드오버에도 박혀있다. cube corner (한 점을 3 셀이 공유) 에서 발생하는 ping-pong 문제, 그리고 cell-local Y 와 world Y 를 혼동해서 entity 가 셀 경계마다 50~100m 솟구치는 텔레포트 — 둘 다 평면 가정이 코드 깊숙이 박혀있던 흔적이고, 둘 다 invariant 테스트로 회귀 방지를 박아뒀다.
지금 동작
평면 OFF 가 그대로 돌면서 (회귀 0) 같은 코어 위에 cube-sphere 행성이 얹혔다. 우주 멀리서 행성 전체를 preview 로 보다가, 가까이 들어가면 voxel mesh 가 채워지고, 표면 위를 비행하면서 face 경계를 매끄럽게 통과하는 데까지 모두 같은 인프라로 동작한다.
우주 → 표면 줌인 + 행성 회전 시연
상용 게임 엔진/미들웨어 중 분산 서버 simulation + LOD streaming + 평면 / 구체 듀얼 위상 을 한 코드베이스로 굴리는 건 거의 못 봤다.
- Unity / Unreal — 월드는 평면 또는 specific level
- SpatialOS — 분산은 잘 하지만 위상은 평면 가정
- Outerra / Cesium — 행성 렌더링은 잘 하지만 분산 simulation 은 안 함
OFF 의 시도는 셋이 동시에 한 코어 위에 얹힌 형태다.
후속글들
이번 글은 전체 그림 한 번이 목적이라 디테일은 의도적으로 잘랐다. 후속으로 풀 만한 갈래들:
- 좌표 정밀도와 origin shift — 큰 행성에서 f32 살아남기, cam-relative 셰이더 변환
- LOD pyramid + screen-space-error — atlas / scheduler / dirty-tracker / memory(LRU) 4 컴포넌트
- Stream-time rendering —
drawMissing(stale-self), child-dirty re-enqueue, invalidation coalescing - Handover 의 cross-face quaternion — 24-entry edge lookup table, valence-3 corner
- Terrain scripting 의 footgun — 큰 행성에서 noise.octave3D 가 cell-uniform 결과 내는 케이스
링크는 글이 올라가는 대로 여기에 붙인다.
참고
- 본문 SoT:
docs/architecture/TOPOLOGY_AGNOSTIC_ARCHITECTURE.md(OFF 리포 내부) - 행성 사이징:
docs/design/SPHERICAL_WORLD_SCENARIOS.md - 외부: Google S2, Outerra cube-sphere blog, Cesium 3D Tiles spec, Snyder “An Equal-Area Map Projection For Polyhedral Globes” (1992)


