Overview
When there are many annotations in a 3D viewer, backface culling (Raycast) processing becomes a performance bottleneck. This document explains the improvement techniques adopted.
Problem
Backface culling for annotations requires executing a Raycast (ray-mesh intersection test) for each annotation. This processing becomes heavy for the following reasons:
- Intersection testing with all mesh vertices is required
- Computation increases proportionally with the number of annotations
- Executing every frame makes it difficult to maintain 60 FPS
Solution: Execute Raycast Only During Idle
We adopted an approach that executes Raycast processing only when the camera has stopped.
Operation Flow
Camera moving → Skip Raycast processing (lightweight)
↓
Camera stop detected
↓
Wait 30 frames (~0.5 seconds, for stabilization)
↓
Execute Raycast once
↓
No recalculation until camera moves again
Implementation Details
// Performance settings
const CAMERA_MOVE_THRESHOLD = 0.01; // Camera movement threshold (ignore small movements during scrolling)
const IDLE_FRAMES_BEFORE_RAYCAST = 30; // Wait this many frames after stopping before Raycast (~0.5s @ 60fps)
useFrame(() => {
// Check camera movement amount
const cameraMoved = camera.position.distanceTo(prevCameraPosition) > CAMERA_MOVE_THRESHOLD;
prevCameraPosition.copy(camera.position);
if (cameraMoved) {
// Reset counter while camera is moving
idleFrameCountRef.current = 0;
needsRaycastRef.current = true;
return; // Skip Raycast processing
}
// Camera is stopped
idleFrameCountRef.current++;
// Wait a certain number of frames after stopping, then execute Raycast (once only)
if (!needsRaycastRef.current) return;
if (idleFrameCountRef.current < IDLE_FRAMES_BEFORE_RAYCAST) return;
// Execute Raycast (once only)
needsRaycastRef.current = false;
// ... Raycast processing ...
});
Two-Stage Culling Process
The Raycast processing itself is also optimized:
First pass (lightweight): Frustum culling + camera front/back check
- Early elimination of annotations outside the field of view
- Low computational cost
Second pass (heavy): Raycast check
- Only targets annotations that passed the first pass
- Executes mesh intersection testing
Benefits
| Item | Previous Method | After Optimization |
|---|---|---|
| Raycast frequency | Every frame or every 15 frames | Only when camera stops |
| Load during dragging | High | Nearly zero |
| Backface culling accuracy | Always accurate | Accurate after stopping |
Limitations
- During camera movement, backface culling is not updated, so temporarily inaccurate display may occur
- However, since it updates immediately after stopping, this is not a practical issue