import { Suspense, useMemo, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { useCubeTexture } from "@react-three/drei"; import { Color, ShaderMaterial, BackSide, Euler } from "three"; import type { TorqueObject } from "../torqueScript"; import { getProperty } from "../mission"; import { useSettings } from "./SettingsProvider"; import { BASE_URL, getUrlForPath, loadDetailMapList } from "../loaders"; import { useThree } from "@react-three/fiber"; const FALLBACK_URL = `${BASE_URL}/black.png`; /** * Load a .dml file, used to list the textures for different faces of a skybox. */ function useDetailMapList(name: string) { return useQuery({ queryKey: ["detailMapList", name], queryFn: () => loadDetailMapList(name), }); } export function SkyBox({ materialList, fogColor, fogDistance, }: { materialList: string; fogColor?: Color; fogDistance?: number; }) { const { data: detailMapList } = useDetailMapList(materialList); const skyBoxFiles = useMemo( () => detailMapList ? [ getUrlForPath(detailMapList[1], FALLBACK_URL), // +x getUrlForPath(detailMapList[3], FALLBACK_URL), // -x getUrlForPath(detailMapList[4], FALLBACK_URL), // +y getUrlForPath(detailMapList[5], FALLBACK_URL), // -y getUrlForPath(detailMapList[0], FALLBACK_URL), // +z getUrlForPath(detailMapList[2], FALLBACK_URL), // -z ] : [ FALLBACK_URL, FALLBACK_URL, FALLBACK_URL, FALLBACK_URL, FALLBACK_URL, FALLBACK_URL, ], [detailMapList], ); const skyBox = useCubeTexture(skyBoxFiles, { path: "" }); // Create a shader material for the skybox with fog const materialRef = useRef(null!); const hasFog = !!fogColor && !!fogDistance; const shaderMaterial = useMemo(() => { if (!hasFog) { return null; } return new ShaderMaterial({ uniforms: { skybox: { value: skyBox }, fogColor: { value: fogColor }, }, vertexShader: ` varying vec3 vDirection; void main() { // Use position directly as direction (no world transform needed) vDirection = position; // Transform position but ignore translation vec4 pos = projectionMatrix * mat4(mat3(modelViewMatrix)) * vec4(position, 1.0); gl_Position = pos.xyww; // Set depth to far plane } `, fragmentShader: ` uniform samplerCube skybox; uniform vec3 fogColor; varying vec3 vDirection; // Convert linear to sRGB vec3 linearToSRGB(vec3 color) { return pow(color, vec3(1.0 / 2.2)); } void main() { vec3 direction = normalize(vDirection); direction.x = -direction.x; vec4 skyColor = textureCube(skybox, direction); // Calculate fog factor based on vertical direction // direction.y: -1 = straight down, 0 = horizon, 1 = straight up // 100% fog from bottom to horizon, then fade from horizon (0) to 0.4 float fogFactor = smoothstep(0.0, 0.4, direction.y); // Mix in sRGB space to match Three.js fog rendering vec3 finalColor = mix(fogColor, skyColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, 1.0); } `, side: BackSide, depthWrite: false, }); }, [skyBox, fogColor, hasFog]); // Update uniforms when fog parameters change useEffect(() => { if (materialRef.current && hasFog && shaderMaterial) { materialRef.current.uniforms.skybox.value = skyBox; materialRef.current.uniforms.fogColor.value = fogColor!; } }, [skyBox, fogColor, hasFog, shaderMaterial]); const { scene } = useThree(); useEffect(() => { scene.backgroundRotation = new Euler(0, Math.PI / 2, 0); }, []); // If fog is disabled, just use the skybox as background if (!hasFog) { return ; } return ( ); } export function Sky({ object }: { object: TorqueObject }) { const { fogEnabled } = useSettings(); // Skybox textures. const materialList = getProperty(object, "materialList"); // Fog parameters. // TODO: There can be multiple fog volumes/layers. Render simple fog for now. const fogDistance = useMemo(() => { return getProperty(object, "fogDistance"); }, [object]); const fogColor = useMemo(() => { const colorString = getProperty(object, "fogColor"); if (colorString) { // `colorString` might specify an alpha value, but three.js doesn't // support opacity on fog or scene backgrounds, so ignore it. // Note: This is a space-separated string, so we split and parse each component. const [r, g, b] = colorString .split(" ") .map((s: string) => parseFloat(s)); return [ new Color().setRGB(r, g, b), new Color().setRGB(r, g, b).convertSRGBToLinear(), ]; } }, [object]); const backgroundColor = fogColor ? ( ) : null; return ( <> {materialList ? ( // If there's a skybox, its textures will need to load. Render just the // fog color as the background in the meantime. ) : ( // If there's no skybox, just render the fog color as the background. backgroundColor )} {fogEnabled && fogDistance && fogColor ? ( ) : null} ); }