Skip to main content

Looking inside 3D models with Clipping Planes

Viewing cross-sections of a model is a basic feature in CAD and engineering. But implementing it in a web-based 3D viewer is trickier than it looks.

What is Clipping?​

Clipping places a virtual plane in 3D space and removes rendering on one side of it. It doesn't modify the model's geometry. Instead, it discards fragments at the WebGL shader stage, so there's virtually no performance cost.

Before Clipping          After Clipping
┌──────────┐ ┌─────┐
│ │ │ │←plane
│ Model │ → │Model│
│ │ │ │
└──────────┘ └─────┘
Back side removed, interior visible

Voxel3D's Clipping System​

Voxel3D's Clipping system is composed of three independent layers.

Clipping System Architecture

1. ClippingPlane (Data Layer)​

The data model that manages plane state.

Material-Level Clipping

There are two ways to apply Clipping in Three.js:

// Method 1: Global clipping
renderer.clippingPlanes = [plane];
// Problem: Grid, Axes, Gizmo all get clipped too

// Method 2: Per-material clipping (Voxel3D)
mesh.material.clippingPlanes = [plane];
// Advantage: Only models are clipped, helpers remain intact

Voxel3D uses per-material clipping. This keeps helper objects like Grid, Axes, and PlaneHelper unaffected by clipping.

Three ClipSide Modes

ModeBehaviorUse Case
farRemoves geometry behind the plane (normal direction)Standard cross-section
nearRemoves geometry in front of the planeInverted cross-section
bothShows only between two planesSlab visualization

The both mode internally creates two Plane objects, offset by half the slabThickness in each direction, producing a thin cross-section slice.

Constant Clamping

The plane's position (constant) is restricted to the model's bounding box range, preventing the plane from moving outside the model.

2. ClippingPlaneGizmo (UI Layer)​

Interactive handles that let users manipulate planes in real time.

Dual Visualization

  • PlaneHelper: Always visible semi-transparent rectangle showing plane position and normal direction.
  • PivotControls: Transform handles inspired by drei's PivotControls from React Three Fiber. Shown only when selected, supporting simultaneous translation and rotation.

Transform Pipeline

When a user drags the Gizmo:

  1. Receive world matrix from PivotControls
  2. Extract position and quaternion
  3. Apply rotation to baseNormal to compute the current normal
  4. Calculate constant = -position.dot(currentNormal)
  5. Clamp within bounds and update the plane
  6. Sync material clippingPlanes + regenerate edges

Z-fighting prevention is handled with depthWrite=false and polygonOffset on the PlaneHelper.

3. ClippedEdgeGenerator (Rendering Layer)​

Visualizes the outline of the clipped cross-section. The BVH covered in the previous post plays a central role here.

BVH-based Edge Generation Flow

Merged Geometry + BVH

Instead of processing each mesh individually, all scene models are merged into a single geometry.

Individual processing:
Iterate mesh 1 triangles → intersection calc
Iterate mesh 2 triangles → intersection calc
...
Iterate mesh N triangles → intersection calc
→ Inefficient (proportional to mesh count)

Merged BVH:
Merge all meshes in world coordinates → Build single BVH
→ shapecast() traverses only intersecting triangles
→ O(log n) performance regardless of mesh count

Triangle-Plane Intersection

For each triangle found by shapecast():

  1. Calculate intersection points between the triangle's three edges and the plane
  2. Usually 2 points are found, forming a line segment that's part of the outline
  3. If a vertex lies exactly on the plane, 3 points may result, with duplicates removed by distance threshold

Multi-Plane Edge Clipping

When multiple planes are active simultaneously, each edge is additionally clipped against the other planes:

For edge A-B, apply other plane P:
A, B on same side → keep or discard entirely
A, B on different sides → cut at intersection point

→ Only edges within the intersection of all plane regions remain

Performance Optimizations

  • rAF throttling: Maximum one edge recalculation per frame, even during rapid dragging
  • Segment limit: Default 100,000 to protect memory
  • Cache: Merged geometry is reused, rebuilt only when models are added/removed
  • WorldUnits lines: Consistent line thickness regardless of zoom level (0.01m)

End-to-End Flow​

User: "Create X-axis Clipping Plane"
↓
ClippingManager.createPlane('x')
├─ Compute model bounding box (center, range)
├─ Create ClippingPlane (constant = center.x)
├─ Create ClippingPlaneGizmo (PlaneHelper + PivotControls)
├─ Register node in SceneGraph (checkable, deletable)
└─ Apply clippingPlanes to all model materials
└─ Generate cross-section edges via BVH shapecast

User: "Drag Gizmo"
↓
PivotControls.onDrag
├─ Extract position, quaternion from world matrix
├─ Recalculate normal + constant (clamped to bounds)
├─ Update material clippingPlanes
└─ scheduleEdgeUpdate() (rAF throttled)
└─ Edge regeneration on next frame

User: "Select mesh"
↓
Selection event → setTargetMesh(mesh)
├─ Apply clippingPlanes only to selected mesh's material
└─ Regenerate edges (based on selected mesh)

Comparison with Previous Work​

At my previous job, I offloaded clipping intersection calculations to Workers + WASM for an industrial 3D viewer. We had to handle large CAD models with millions of triangles.

In Voxel3D, main-thread BVH shapecast + rAF throttling delivers sufficient performance while keeping the architecture much simpler.

Choosing the right technology for the right scale matters.