mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-03-23 14:21:01 +00:00
244 lines
6.5 KiB
TypeScript
244 lines
6.5 KiB
TypeScript
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<number, number> = {
|
|
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<MeshLambertMaterial>(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<string, number>;
|
|
};
|
|
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 (
|
|
<meshLambertMaterial
|
|
ref={materialRef}
|
|
key={materialKey}
|
|
map={displacementMap}
|
|
depthWrite
|
|
side={FrontSide}
|
|
defines={{ DEBUG_MODE: debugMode ? 1 : 0 }}
|
|
onBeforeCompile={onBeforeCompile}
|
|
/>
|
|
);
|
|
});
|
|
|
|
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 (
|
|
<Suspense
|
|
fallback={
|
|
// Geometry is already CPU-displaced, so no displacementMap needed
|
|
<meshLambertMaterial color="rgb(0, 109, 56)" wireframe />
|
|
}
|
|
>
|
|
<BlendedTerrainTextures
|
|
displacementMap={displacementMap}
|
|
visibilityMask={visibilityMask}
|
|
textureNames={textureNames}
|
|
alphaTextures={alphaTextures}
|
|
detailTextureName={detailTextureName}
|
|
lightmap={lightmap}
|
|
/>
|
|
</Suspense>
|
|
);
|
|
});
|
|
|
|
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 (
|
|
<mesh
|
|
position={position}
|
|
geometry={geometry}
|
|
castShadow
|
|
receiveShadow
|
|
visible={visible}
|
|
>
|
|
<TerrainMaterial
|
|
displacementMap={displacementMap}
|
|
visibilityMask={visibilityMask}
|
|
textureNames={textureNames}
|
|
alphaTextures={alphaTextures}
|
|
detailTextureName={detailTextureName}
|
|
lightmap={lightmap}
|
|
/>
|
|
</mesh>
|
|
);
|
|
});
|