import { memo, Suspense, useCallback, useEffect, useMemo, useRef } from "react"; import { type BufferGeometry, DataTexture, FrontSide, type MeshLambertMaterial, } from "three"; import { useTexture } from "@react-three/drei"; import { FALLBACK_TEXTURE_URL, terrainTextureToUrl, textureToUrl, } from "../loaders"; import { setupTexture } from "../textureUtils"; import { updateTerrainTextureShader } from "../terrainMaterial"; import { useDebug } from "./SettingsProvider"; import { useAnisotropy } from "./useAnisotropy"; import { injectCustomFog } from "../fogShader"; import { globalFogUniforms } from "../globalFogUniforms"; // Texture tiling factors for each terrain layer const TILING: Record = { 0: 32, 1: 32, 2: 32, 3: 32, 4: 32, 5: 32, }; interface TerrainTileProps { tileX: number; tileZ: number; blockSize: number; basePosition: { x: number; z: number }; textureNames: string[]; geometry: BufferGeometry; displacementMap: DataTexture; visibilityMask: DataTexture; alphaTextures: DataTexture[]; detailTextureName?: string; lightmap?: DataTexture; visible?: boolean; } const BlendedTerrainTextures = memo(function BlendedTerrainTextures({ displacementMap, visibilityMask, textureNames, alphaTextures, detailTextureName, lightmap, }: { displacementMap: DataTexture; visibilityMask: DataTexture; textureNames: string[]; alphaTextures: DataTexture[]; detailTextureName?: string; lightmap?: DataTexture; }) { const { debugMode } = useDebug(); const anisotropy = useAnisotropy(); const baseTextures = useTexture( textureNames.map((name) => terrainTextureToUrl(name)), (textures) => { textures.forEach((tex) => setupTexture(tex, { anisotropy })); }, ); // Load detail texture if specified const detailTextureUrl = detailTextureName ? textureToUrl(detailTextureName) : null; const detailTexture = useTexture( detailTextureUrl ?? FALLBACK_TEXTURE_URL, (tex) => { setupTexture(tex, { anisotropy }); }, ); const onBeforeCompile = useCallback( (shader: any) => { updateTerrainTextureShader({ shader, baseTextures, alphaTextures, visibilityMask, tiling: TILING, detailTexture: detailTextureUrl ? detailTexture : null, lightmap, }); // Inject volumetric fog using global uniforms injectCustomFog(shader, globalFogUniforms); }, [ baseTextures, alphaTextures, visibilityMask, detailTexture, detailTextureUrl, lightmap, ], ); // Build a unique program cache key so Three.js recompiles the shader when // textures change between maps. Without this, onBeforeCompile may not be // called because the program cache hash (from toString()) is identical. const programCacheKey = useMemo(() => { const parts = [ textureNames.join(","), detailTextureUrl ?? "none", lightmap ? lightmap.id : "nolm", baseTextures.map((t: any) => t.id).join(","), ]; return parts.join("|"); }, [textureNames, detailTextureUrl, lightmap, baseTextures]); // Ref for forcing shader recompilation const materialRef = useRef(null); // Force shader recompilation when debugMode changes // r3f doesn't sync defines prop changes, so we update the material directly useEffect(() => { const mat = materialRef.current as MeshLambertMaterial & { defines?: Record; }; if (mat) { mat.defines ??= {}; mat.defines.DEBUG_MODE = debugMode ? 1 : 0; mat.needsUpdate = true; } }, [debugMode]); // Set customProgramCacheKey so Three.js distinguishes terrain shaders // across different maps even when the shader structure is identical. useEffect(() => { const mat = materialRef.current; if (mat) { mat.customProgramCacheKey = () => programCacheKey; mat.needsUpdate = true; } }, [programCacheKey]); // Key for shader structure changes (detail texture, lightmap) const materialKey = `${detailTextureUrl ? "detail" : "nodetail"}-${lightmap ? "lightmap" : "nolightmap"}`; // Displacement is done on CPU, so no displacementMap needed // We keep 'map' to provide UV coordinates for shader (vMapUv) // Use MeshLambertMaterial for compatibility with shadow maps return ( ); }); export const TerrainMaterial = memo(function TerrainMaterial({ displacementMap, visibilityMask, textureNames, alphaTextures, detailTextureName, lightmap, }: { displacementMap: DataTexture; visibilityMask: DataTexture; textureNames: string[]; alphaTextures: DataTexture[]; detailTextureName?: string; lightmap?: DataTexture; }) { return ( } > ); }); export const TerrainTile = memo(function TerrainTile({ tileX, tileZ, blockSize, basePosition, textureNames, geometry, displacementMap, visibilityMask, alphaTextures, detailTextureName, lightmap, visible = true, }: TerrainTileProps) { const position = useMemo(() => { // Terrain geometry is centered at origin. Torque's terrain position formula // is -squareSize * 128, which equals -blockSize / 2. Since our geometry is // already centered, basePosition + geometryOffset cancels to 0 for single tiles. const geometryOffset = blockSize / 2; return [ basePosition.x + tileX * blockSize + geometryOffset, 0, basePosition.z + tileZ * blockSize + geometryOffset, ] as [number, number, number]; }, [tileX, tileZ, blockSize, basePosition]); return ( ); });