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:

  1. First pass (lightweight): Frustum culling + camera front/back check

    • Early elimination of annotations outside the field of view
    • Low computational cost
  2. Second pass (heavy): Raycast check

    • Only targets annotations that passed the first pass
    • Executes mesh intersection testing

Benefits

ItemPrevious MethodAfter Optimization
Raycast frequencyEvery frame or every 15 framesOnly when camera stops
Load during draggingHighNearly zero
Backface culling accuracyAlways accurateAccurate 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