All files / renderer/village UnderwaterAtmosphere.tsx

0% Statements 0/36
0% Branches 0/1
0% Functions 0/1
0% Lines 0/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85                                                                                                                                                                         
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { UNDERWATER_COLOR, UNDERWATER_FOG_DENSITY, isUnderwaterView } from "./sceneConstants";
 
/**
 * Installs / removes scene-level fog and background colour based on
 * whether the camera is below the water line. Also toggles the
 * visibility of a sibling sky/clouds group via the provided ref so that
 * the drei `<Sky>` dome and `<Clouds>` puffs stop occluding the scene
 * when the player is underwater (scene.background alone cannot hide
 * them - they are real rendered geometry).
 *
 * Matches the same `camera.position.y < UNDERWATER_CAMERA_Y` gate that
 * `FishSchool` uses, so the atmosphere switch, the fish reveal and any
 * other underwater-only content toggle on exactly the same frame and
 * the threshold crossing never flickers.
 *
 * Performance: both the fog and the colour are allocated once in a ref
 * and reused - no per-frame allocations. The `useFrame` body does a
 * single float compare per frame, plus two property writes on the
 * threshold-crossing frame.
 */
export interface UnderwaterAtmosphereProps {
  /**
   * Ref to the `<group>` that wraps the sky dome and clouds. Its
   * `.visible` is flipped to false while underwater and true above.
   */
  skyGroupRef: React.MutableRefObject<THREE.Group | null>;
}
 
export function UnderwaterAtmosphere({ skyGroupRef }: UnderwaterAtmosphereProps) {
  const { scene, camera } = useThree();
  const fogRef = useRef<THREE.FogExp2 | null>(null);
  const bgColorRef = useRef<THREE.Color | null>(null);
  // Remember the last state we applied so we only write to scene.* on a
  // threshold crossing instead of every frame. Initialised to `null` so
  // the very first frame always applies the correct state.
  const lastStateRef = useRef<boolean | null>(null);
 
  if (!fogRef.current) {
    fogRef.current = new THREE.FogExp2(UNDERWATER_COLOR, UNDERWATER_FOG_DENSITY);
  }
  if (!bgColorRef.current) {
    bgColorRef.current = new THREE.Color(UNDERWATER_COLOR);
  }
 
  // On unmount, always clear fog + background so a parent remount does
  // not leak underwater state into a fresh scene.
  useEffect(() => {
    return () => {
      scene.fog = null;
      scene.background = null;
    };
  }, [scene]);
 
  useFrame(() => {
    // Underwater state is scoped to "camera below waterline AND over the
    // ocean, not over the main island". A close zoom toward the centre
    // can pull the camera below y = UNDERWATER_CAMERA_Y even though the
    // user is still looking at land - flipping the scene into underwater
    // mode there hides the sky dome while the island fills the view,
    // producing a bright white/green wash (the reported "zoom to white
    // screen" bug).
    const under = isUnderwaterView(camera.position.x, camera.position.y, camera.position.z);
    if (lastStateRef.current === under) return;
    lastStateRef.current = under;
 
    if (under) {
      scene.fog = fogRef.current;
      scene.background = bgColorRef.current;
    } else {
      scene.fog = null;
      scene.background = null;
    }
 
    const skyGroup = skyGroupRef.current;
    if (skyGroup && skyGroup.visible === under) {
      skyGroup.visible = !under;
    }
  });
 
  return null;
}