import { useFrame, useThree } from "@react-three/fiber"; import { useGLTF } from "@react-three/drei"; import { Component, useEffect, useMemo, useRef, type ReactNode } from "react"; import * as THREE from "three"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; interface SkyModelProps { fallbackModelScale?: number | undefined; fallbackModelPath?: string | undefined; modelPath: string; fallbackColor?: string | undefined; scale?: number | undefined; } interface SkyModelContentProps { modelPath: string; scale: number; } interface SkyModelErrorBoundaryProps { children: ReactNode; fallback: ReactNode; } interface SkyModelErrorBoundaryState { hasError: boolean; } const SKY_MODEL_SCALE = 1; const SKY_MODEL_RENDER_ORDER = -1000; const SKYBOX_MODEL_PATH = "/models/skybox/model.gltf"; class SkyModelErrorBoundary extends Component< SkyModelErrorBoundaryProps, SkyModelErrorBoundaryState > { constructor(props: SkyModelErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): SkyModelErrorBoundaryState { return { hasError: true }; } render(): ReactNode { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } export function SkyModel({ fallbackColor, fallbackModelScale = SKY_MODEL_SCALE, fallbackModelPath, modelPath, scale = SKY_MODEL_SCALE, }: SkyModelProps): React.JSX.Element { const colorFallback = fallbackColor ? ( ) : null; const fallback = fallbackModelPath ? ( ) : ( colorFallback ); return ( ); } function SkyModelContent({ modelPath, scale, }: SkyModelContentProps): React.JSX.Element { const camera = useThree((state) => state.camera); const groupRef = useRef(null); const { scene } = useLoggedGLTF(modelPath, { scope: "SkyModel", scale, }); const model = useMemo(() => createSkyModel(scene), [scene]); useEffect(() => { return () => { disposeSkyModelMaterials(model); }; }, [model]); useFrame(() => { groupRef.current?.position.copy(camera.position); }); return ( ); } function createSkyModel(scene: THREE.Object3D): THREE.Object3D { const model = scene.clone(true); model.traverse((object) => { object.frustumCulled = false; object.renderOrder = SKY_MODEL_RENDER_ORDER; if (!(object instanceof THREE.Mesh)) return; object.material = Array.isArray(object.material) ? object.material.map(createSkyMaterial) : createSkyMaterial(object.material); }); return model; } function createSkyMaterial(material: T): T { const skyMaterial = material.clone(); skyMaterial.side = THREE.BackSide; skyMaterial.depthTest = false; skyMaterial.depthWrite = false; return skyMaterial as T; } function disposeSkyModelMaterials(model: THREE.Object3D): void { model.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; if (Array.isArray(object.material)) { for (const material of object.material) { material.dispose(); } return; } object.material.dispose(); }); } useGLTF.preload(SKYBOX_MODEL_PATH);