概要

3Dビューワーでアノテーションが多数ある場合、背面判定(Raycast)処理がパフォーマンスのボトルネックになります。本ドキュメントでは、採用した改善手法について説明します。

問題

アノテーションの背面判定には、各アノテーションに対してRaycast(光線とメッシュの衝突判定)を実行する必要があります。この処理は以下の理由で重くなります:

  • メッシュの全頂点との衝突判定が必要
  • アノテーション数に比例して計算量が増加
  • 毎フレーム実行すると60FPSの維持が困難

解決策: Idle時のみRaycast実行

カメラが停止した時のみRaycast処理を実行 する方式を採用しました。

動作フロー

カメラ移動中 → Raycast処理をスキップ(軽量)
カメラ停止検出
30フレーム待機(約0.5秒、安定化)
1回だけRaycast実行
次にカメラが動くまで再計算しない

実装詳細

// パフォーマンス設定
const CAMERA_MOVE_THRESHOLD = 0.01; // カメラ移動の閾値(スクロール時の微小な動きを無視)
const IDLE_FRAMES_BEFORE_RAYCAST = 30; // 停止後このフレーム数待ってからRaycast(約0.5秒 @ 60fps)

useFrame(() => {
  // カメラの移動量をチェック
  const cameraMoved = camera.position.distanceTo(prevCameraPosition) > CAMERA_MOVE_THRESHOLD;
  prevCameraPosition.copy(camera.position);

  if (cameraMoved) {
    // カメラが動いている間はカウンタをリセット
    idleFrameCountRef.current = 0;
    needsRaycastRef.current = true;
    return; // Raycast処理をスキップ
  }

  // カメラが停止している
  idleFrameCountRef.current++;

  // 停止後、一定フレーム待ってからRaycast実行(1回のみ)
  if (!needsRaycastRef.current) return;
  if (idleFrameCountRef.current < IDLE_FRAMES_BEFORE_RAYCAST) return;

  // Raycast実行(1回のみ)
  needsRaycastRef.current = false;

  // ... Raycast処理 ...
});

2段階の判定処理

Raycast処理自体も最適化しています:

  1. 第1パス(軽量) : Frustum判定 + カメラ前後判定

    • 視野外のアノテーションを早期に除外
    • 計算コストが低い
  2. 第2パス(重い) : Raycast判定

    • 第1パスを通過したアノテーションのみ対象
    • メッシュとの衝突判定を実行

メリット

項目従来方式最適化後
Raycast頻度毎フレーム or 15フレームごとカメラ停止時のみ
ドラッグ中の負荷高いほぼゼロ
背面判定の正確性常に正確停止後に正確

制限事項

  • カメラ移動中は背面判定が更新されないため、一時的に不正確な表示になる可能性があります
  • ただし、停止後すぐに更新されるため、実用上は問題ありません