본문으로 건너뛰기

3D 모델의 내부를 들여다보는 Clipping Plane

CAD나 엔지니어링 분야에서 모델의 단면을 보는 건 기본 기능입니다. 하지만 웹 3D 뷰어에서 이걸 구현하려면 생각보다 까다롭습니다.

Clipping이란?​

Clipping은 3D 공간에 가상의 평면을 배치하고, 그 평면을 기준으로 한쪽의 렌더링을 제거하는 기술입니다. 모델의 geometry를 실제로 변형하지 않고, WebGL shader 단계에서 fragment를 버리는 방식이라 성능 부담이 거의 없습니다.

Clipping 전              Clipping 후
┌──────────┐ ┌─────┐
│ │ │ │←평면
│ 모델 │ → │ 모델 │
│ │ │ │
└──────────┘ └─────┘
뒤쪽이 잘려 내부가 보임

Voxel3D의 Clipping 시스템​

Voxel3D의 Clipping은 세 개의 독립적인 레이어로 구성됩니다.

Clipping 시스템 아키텍처

1. ClippingPlane (데이터 레이어)​

평면의 상태를 관리하는 데이터 모델입니다.

Material-Level Clipping

Three.js에서 Clipping을 적용하는 방법은 두 가지입니다:

// 방법 1: 전역 클리핑
renderer.clippingPlanes = [plane];
// 문제: Grid, Axes, Gizmo까지 전부 잘림

// 방법 2: 머티리얼별 클리핑 (Voxel3D)
mesh.material.clippingPlanes = [plane];
// 장점: 모델만 잘리고, 보조 객체는 영향 없음

Voxel3D는 머티리얼별 클리핑을 사용합니다. 이렇게 하면 Grid, Axes, PlaneHelper 같은 보조 객체는 클리핑 영향을 받지 않습니다.

세 가지 ClipSide 모드

모드동작사용 사례
far평면 뒤쪽(법선 방향) 제거기본 단면 보기
near평면 앞쪽 제거반전 단면
both두 평면 사이만 표시슬라브(slab) 시각화

both 모드는 내부적으로 두 개의 Plane을 생성합니다. 지정된 두께(slabThickness)의 절반만큼 양쪽으로 오프셋하여, 얇은 단면 슬라이스를 만듭니다.

Constant 클램핑

평면의 위치(constant)는 모델의 바운딩 박스 범위 내로 제한됩니다. 평면이 모델 밖으로 벗어나는 것을 방지합니다.

2. ClippingPlaneGizmo (UI 레이어)​

사용자가 평면을 인터랙티브하게 조작할 수 있는 핸들입니다.

이중 시각화

  • PlaneHelper: 항상 표시되는 반투명 사각형. 평면의 위치와 법선 방향을 시각적으로 보여줍니다.
  • PivotControls: React Three Fiber의 drei 라이브러리에서 영감을 받은 Transform 핸들. 선택 시에만 표시되며, 이동과 회전을 동시에 지원합니다.

Transform 파이프라인

사용자가 Gizmo를 드래그하면 다음 과정이 실행됩니다:

  1. PivotControls에서 world matrix를 받음
  2. position(위치)과 quaternion(회전)을 추출
  3. 기본 법선(baseNormal)에 회전을 적용하여 현재 법선 계산
  4. constant = -position.dot(currentNormal)로 평면 위치 계산
  5. 범위 내로 클램핑 후 평면 갱신
  6. 머티리얼 clippingPlanes 동기화 + Edge 재생성

Z-fighting(깜빡임) 방지를 위해 PlaneHelper는 depthWrite=false와 polygonOffset을 적용합니다.

3. ClippedEdgeGenerator (렌더링 레이어)​

잘린 단면의 윤곽선을 시각화하는 부분입니다. 이전 글에서 다뤘던 BVH가 핵심 역할을 합니다.

BVH 기반 Edge 생성 과정

Merged Geometry + BVH

개별 메시를 하나씩 처리하는 대신, 씬의 모든 모델을 하나의 geometry로 병합합니다.

개별 처리:
메시 1의 삼각형 순회 → 교차 계산
메시 2의 삼각형 순회 → 교차 계산
...
메시 N의 삼각형 순회 → 교차 계산
→ 비효율적 (메시 개수에 비례)

Merged BVH:
모든 메시를 월드 좌표로 병합 → 단일 BVH 구축
→ shapecast()로 교차 삼각형만 탐색
→ 메시 개수와 무관한 O(log n) 성능

삼각형-평면 교차 계산

shapecast()가 찾은 각 삼각형에 대해:

  1. 삼각형의 세 변(edge)과 평면의 교차점을 계산합니다
  2. 보통 2개의 교차점이 나오며, 이 두 점을 잇는 선분이 윤곽선의 일부가 됩니다
  3. 꼭짓점이 정확히 평면 위에 있으면 3개의 교차점이 나올 수 있으며, 거리 기반으로 중복을 제거합니다

멀티 평면 Edge Clipping

여러 평면이 동시에 활성화되면, 각 edge를 다른 평면들로 추가 클리핑합니다:

Edge A-B에 대해 다른 평면 P를 적용:
A, B 모두 같은 쪽 → 유지 또는 제거
A, B가 다른 쪽 → 교차점에서 절단

→ 모든 평면의 교차 영역에 해당하는 edge만 남음

성능 최적화

  • rAF 스로틀링: 빠른 드래그 중에도 프레임당 최대 1회만 edge 재계산
  • 세그먼트 제한: 기본 100,000개로 메모리 보호
  • 캐시: 병합된 geometry는 모델 추가/삭제 시에만 재구축
  • WorldUnits 라인: 줌 레벨과 관계없이 일관된 선 두께 (0.01m)

전체 동작 흐름​

사용자: "X축 Clipping Plane 생성"
↓
ClippingManager.createPlane('x')
├─ 모델 바운딩 박스 계산 (중심점, 범위)
├─ ClippingPlane 생성 (constant = center.x)
├─ ClippingPlaneGizmo 생성 (PlaneHelper + PivotControls)
├─ SceneGraph에 노드 등록 (체크/삭제 가능)
└─ 모든 모델 머티리얼에 clippingPlanes 적용
└─ BVH shapecast로 단면 Edge 생성

사용자: "Gizmo 드래그"
↓
PivotControls.onDrag
├─ world matrix → position, quaternion 추출
├─ normal + constant 재계산 (범위 클램핑)
├─ 머티리얼 clippingPlanes 갱신
└─ scheduleEdgeUpdate() (rAF 스로틀)
└─ 다음 프레임에서 Edge 재생성

사용자: "메시 선택"
↓
Selection 이벤트 → setTargetMesh(mesh)
├─ 선택된 메시의 머티리얼에만 clippingPlanes 적용
└─ Edge 재생성 (선택된 메시 기준)

이전 직장과의 비교​

이전 직장에서 산업용 3D 뷰어에 Clipping을 구현했을 때는 Worker + WASM으로 교차 계산을 오프로드했습니다. 수백만 삼각형의 대형 CAD 모델을 다뤄야 했기 때문입니다.

Voxel3D에서는 메인 스레드 BVH shapecast + rAF 스로틀링으로 충분한 성능을 확보하면서, 구조를 훨씬 단순하게 유지했습니다.

적정 규모와 목적에 맞는 기술 선택이 중요합니다.