3D

[3D] Cesium ↔ Three.js 연계 구조 작성

teddy bear 2026. 7. 2. 09:28
반응형

Cesium ↔ Three.js 가우시안 스플랫 연계 구조

Cesium 3D 지도 위에 @mkkellogg/gaussian-splats-3d 가우시안 스플랫을 겹쳐 보여주는 co-viewer 구현 문서.

핵심 파일: [CesiumSplatOverlay.ts](./CesiumSplatOverlay.ts)


1. 전체 컨셉 (Overlay 방식)

tebben/cesium-gaussian-splatting 방식 채택. 하나의 WebGL 컨텍스트를 공유하지 않고, Cesium 캔버스 위에 별도의 Three.js 캔버스를 겹쳐 놓고 매 프레임 카메라만 동기화한다.

┌───────────────────────────────────────────┐
│  cesiumWidget.container (position:relative) │
│  ┌─────────────────────────────────────┐   │
│  │  Cesium canvas (지형/건물/지도)       │   │  z-index: 0
│  └─────────────────────────────────────┘   │
│  ┌─────────────────────────────────────┐   │
│  │  overlayCanvas (Three.js 스플랫)      │   │  z-index: 1
│  │  position:absolute, pointer-events:none│  │  (클릭은 Cesium으로 통과)
│  └─────────────────────────────────────┘   │
└───────────────────────────────────────────┘

항목내용

장점 가시화가 안정적, 구현 단순, Cesium 렌더링에 간섭 없음
단점 지형/건물과의 오클루전(가림)이 부정확 (깊이 버퍼 비공유)

2. 연계를 가능하게 한 결정적 로직 4가지

(A) 오버레이 캔버스 + 독립 Three 렌더러 — ensureInitialized()

Cesium 컨테이너에 투명 캔버스를 붙이고, 그 위에 Three.js WebGLRenderer와 GaussianSplats3D.Viewer를 selfDrivenMode: false (자체 RAF 루프 끔)로 생성한다. 렌더 구동은 전적으로 Cesium 프레임에 종속시킨다.

const overlay = document.createElement('canvas');
overlay.style.cssText =
  'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;';
this.container.appendChild(overlay);

const splatViewer = new GaussianSplats3D.Viewer({
  threeScene,
  renderer,
  camera: threeCamera,
  rootElement: this.container,
  selfDrivenMode: false,     // ← Cesium 프레임이 렌더를 구동
  useBuiltInControls: false, // ← 카메라 조작은 Cesium이 담당
  gpuAcceleratedSort: false,
  // ...
});

(B) ENU 부동 원점(Floating Origin) — setEnuOrigin() / toEnuLocal()

지구 중심(ECEF) 좌표는 값이 수백만 m라 float32 정밀도가 깨진다. 그래서 스플랫 앵커 지점(lon/lat/height)을 원점으로 하는 ENU(East-North-Up) 로컬 좌표계를 만들고, 모든 Three 좌표를 이 작은 로컬 공간에서 다룬다.

  • eastNorthUpToFixedFrame(원점) → ENU→ECEF 변환행렬
  • 그 역행렬 fixedToEnu를 저장해 두고, 카메라/스플랫 위치를 전부 ENU로 투영
private setEnuOrigin(lon: number, lat: number, height: number): void {
  const originEcef = Cartesian3.fromDegrees(lon, lat, height);
  const enuToFixed = Transforms.eastNorthUpToFixedFrame(originEcef);
  this.fixedToEnu = Matrix4.inverse(enuToFixed, new Matrix4());
}

(C) 매 프레임 카메라 동기화 — postRender → syncCamera()

연계의 심장부. Cesium의 scene.postRender 이벤트에 핸들러를 걸어, Cesium이 한 프레임을 그릴 때마다 Cesium 카메라의 위치/방향/up/FOV를 ENU로 변환해 Three 카메라에 그대로 복사한다. 결과적으로 두 캔버스의 시점이 픽셀 단위로 일치한다.

const posEnu = Matrix4.multiplyByPoint(fixedToEnu, cesiumCamera.positionWC, /* ... */);
const dirEnu = Matrix4.multiplyByPointAsVector(fixedToEnu, cesiumCamera.directionWC, /* ... */);
const upEnu  = Matrix4.multiplyByPointAsVector(fixedToEnu, cesiumCamera.upWC, /* ... */);

camera.position.set(posEnu.x, posEnu.y, posEnu.z);
camera.up.set(upEnu.x, upEnu.y, upEnu.z);
camera.lookAt(posEnu.x + dirEnu.x, posEnu.y + dirEnu.y, posEnu.z + dirEnu.z);
camera.updateMatrixWorld(true);          // ← sort용 quaternion 갱신
camera.fov = CesiumMath.toDegrees(fovy); // Cesium FOV → Three FOV
camera.updateProjectionMatrix();

추가로, Cesium이 정지 시 렌더를 멈추지 않도록 requestRenderMode=false, shouldAnimate=true로 강제하여 postRender가 계속 돌게 한다 (applySceneFlags()).

(D) 렌더 준비 가드 — isSplatGeometryReady() / ensureFirstSort()

스플랫은 로드 직후 첫 정렬(sort)이 끝나야 geometry의 instanceCount가 채워진다. 그 전에 render()를 호출하면 빈 dummyGeometry를 그리려다 Cannot read properties of undefined (reading 'count')가 난다. 그래서 준비 상태를 확인하고, 첫 sort 완료를 기다리는 가드를 둔다.

private isSplatGeometryReady(): boolean {
  const mesh = this.splatViewer?.splatMesh;
  if (!mesh?.visible) return false;
  const splatCount = mesh.getSplatCount?.() ?? 0;
  if (splatCount <= 0) return false;
  const geom = mesh.geometry as THREE.InstancedBufferGeometry | null;
  if (!geom?.attributes?.position) return false;
  const instanceCount = geom.instanceCount;
  return typeof instanceCount === 'number' && instanceCount > 0;
}

renderFrame()은 라이브러리의 self-driven 루프와 동일하게 update() → shouldRender() → render() 순서로 안전하게 호출한다.


3. 한 프레임의 데이터 흐름

Cesium 카메라 이동
      │
      ▼
scene.postRender 이벤트 발생  ──(매 프레임)
      │
      ▼
renderFrame()
      ├─ syncOverlayCanvasSize()  : 캔버스 크기를 Cesium 캔버스에 맞춤
      ├─ syncCamera()             : Cesium 카메라 → ENU → Three 카메라 복사
      ├─ canRunSplatUpdate()      : initialized && splatRenderReady 확인
      ├─ splatViewer.update()     : 스플랫 정렬/유니폼 갱신
      ├─ isSplatGeometryReady()   : instanceCount>0 가드
      └─ shouldRender() → render(): 오버레이 캔버스에 스플랫 그리기

4. 좌표/자세 배치 로직 (스플랫을 지도에 꽂기)

addSplat()에서:

  1. splatUrlBypass304() — ?cb=타임스탬프를 붙여 304 응답 회피
  2. (mkkellogg는 response.ok만 성공으로 보기 때문)
  3. toEnuLocal() — 스플랫 위치(lon/lat/height)를 ENU 로컬 좌표로 변환
  4. buildRotationQuaternion() — alignYUpToZUp(스플랫 Y-up → 지도 Z-up 보정) +
  5. heading/pitch/roll 적용
  6. addSplatScene(url, { position, rotation, scale, format })로 로드
  7. ensureFirstSort()로 첫 렌더 준비 대기

5. three.js 단일화 (연계의 전제 조건)

이 모든 게 동작하려면 앱과 스플랫 라이브러리가 같은 three 인스턴스를 써야 한다. 서로 다른 three 사본을 쓰면 한 버전의 SplatMesh geometry를 다른 버전의 WebGLRenderer로 그리며 Cannot read properties of undefined (reading 'count') 오류가 난다.

  • 루트 node_modules/three → 0.184.0 (gaussian-splats-3d가 사용)
  • (과거) projects/master/node_modules/three → 0.137.5 (앱 코드가 사용) ← 충돌 원인

해결: package.json의 three를 ^0.184.0으로 올리고 yarn install로 통일. 추가로 umirc.shared.ts에서 webpack alias로 루트 단일 사본을 강제 (MFSU 포함 일괄 적용).

// projects/master/umirc.shared.ts
config.resolve.alias.set(
  'three',
  path.resolve(__dirname, '../../node_modules/three'),
);

alias 변경 후에는 dev 서버 재시작 + MFSU 캐시(node_modules/.cache) 삭제가 필요하다.


6. 관련 파일

파일역할

CesiumSplatOverlay.ts 오버레이 엔진 (캔버스/카메라 동기화/로드/렌더/dispose)
splatPresets.ts 스플랫 목록 + lon/lat/height 배치 프리셋
useCesiumSplatOverlay.ts React 훅: 토글 로드/dispose, flyTo
SplatViewerPanel.tsx UI 패널 (메뉴 ID 143)

요약: ① 투명 오버레이 캔버스 + 독립 Three 렌더러를 띄우고, ② ENU 부동 원점으로 좌표 정밀도를 확보한 뒤, ③ scene.postRender마다 Cesium 카메라를 Three 카메라에 복사(syncCamera)하고, ④ 스플랫 준비 가드를 거쳐 오버레이에 렌더 — 이 네 가지가 Cesium 뷰와 three.js를 묶은 결정적 로직.

'3D' 카테고리의 다른 글

[3D] Cesium Terrain Server 학습  (0) 2026.07.04
[3D] Cesium Terrain Builder  (0) 2026.07.04
[3D] Gaussian Splatting 기술 학습  (0) 2026.07.03
[3D] ml-sharp 기술 학습  (0) 2026.07.03
[3D] Gaussian Splatting Rendering(Cesium + Three.js)  (0) 2026.07.02