import { Euler, Quaternion, Vector3 } from "three"; import type { FirstPersonTelemetry } from "./navigation-controller"; import type { RuntimeSceneDefinition, RuntimeWaterVolume } from "./runtime-scene-build"; export interface UnderwaterFogState { colorHex: string; density: number; } const MIN_UNDERWATER_FOG_DENSITY = 0.018; const MAX_UNDERWATER_FOG_DENSITY = 0.12; function clampNumber(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } function getWaterVolumeLocalPoint(point: { x: number; y: number; z: number }, volume: RuntimeWaterVolume) { const offset = new Vector3(point.x - volume.center.x, point.y - volume.center.y, point.z - volume.center.z); const inverseRotation = new Quaternion() .setFromEuler( new Euler( (volume.rotationDegrees.x * Math.PI) / 180, (volume.rotationDegrees.y * Math.PI) / 180, (volume.rotationDegrees.z * Math.PI) / 180, "XYZ" ) ) .invert(); offset.applyQuaternion(inverseRotation); return offset; } function isPointInsideWaterVolume(point: { x: number; y: number; z: number }, volume: RuntimeWaterVolume) { const offset = getWaterVolumeLocalPoint(point, volume); return ( Math.abs(offset.x) <= volume.size.x * 0.5 && Math.abs(offset.y) <= volume.size.y * 0.5 && Math.abs(offset.z) <= volume.size.z * 0.5 ); } function resolveUnderwaterFogDensity(volume: RuntimeWaterVolume, point: { x: number; y: number; z: number }) { const localPoint = getWaterVolumeLocalPoint(point, volume); const halfHeight = Math.max(volume.size.y * 0.5, 0.0001); const submersionDepth = clampNumber((halfHeight - localPoint.y) / (halfHeight * 2), 0, 1); return clampNumber( 0.045 + volume.surfaceOpacity * 0.035 + Math.max(volume.waveStrength, 0) * 0.015 + submersionDepth * 0.03, MIN_UNDERWATER_FOG_DENSITY, MAX_UNDERWATER_FOG_DENSITY ); } export function resolveUnderwaterFogState( runtimeScene: Pick | null, telemetry: Pick | null ): UnderwaterFogState | null { if (runtimeScene === null || telemetry === null || telemetry.cameraSubmerged !== true) { return null; } const containingVolume = runtimeScene.volumes.water.find((volume) => isPointInsideWaterVolume(telemetry.eyePosition, volume)); if (containingVolume === undefined) { return null; } return { colorHex: containingVolume.colorHex, density: resolveUnderwaterFogDensity(containingVolume, telemetry.eyePosition) }; }