import { ClampToEdgeWrapping, Color, DataTexture, LinearFilter, Material, MeshBasicMaterial, MeshStandardMaterial, RGBAFormat, SRGBColorSpace, UnsignedByteType, type Texture } from "three"; import type { MaterialDef } from "../materials/starter-material-library"; interface TerrainLayerBlendMaterialOptions { layerTextures: readonly [Texture, Texture, Texture, Texture]; emissiveHex?: number; emissiveIntensity?: number; wireframe?: boolean; } let fallbackTerrainLayerTexture: Texture | null = null; function createFallbackTerrainLayerTexture(): Texture { const texture = new DataTexture( new Uint8Array([174, 167, 154, 255]), 1, 1, RGBAFormat, UnsignedByteType ); texture.colorSpace = SRGBColorSpace; texture.minFilter = LinearFilter; texture.magFilter = LinearFilter; texture.wrapS = ClampToEdgeWrapping; texture.wrapT = ClampToEdgeWrapping; texture.needsUpdate = true; return texture; } export function getFallbackTerrainLayerTexture(): Texture { if (fallbackTerrainLayerTexture === null) { fallbackTerrainLayerTexture = createFallbackTerrainLayerTexture(); } return fallbackTerrainLayerTexture; } export function getTerrainLayerTexture( material: MaterialDef | null, textureLookup: (material: MaterialDef) => Texture ): Texture { return material === null ? getFallbackTerrainLayerTexture() : textureLookup(material); } export function getTerrainLayerPreviewColor(material: MaterialDef | null): number { return material === null ? new Color("#aea79a").getHex() : new Color(material.swatchColorHex).getHex(); } export function createTerrainLayerBlendMaterial( options: TerrainLayerBlendMaterialOptions ): Material { if (options.wireframe === true) { return new MeshBasicMaterial({ color: 0xf2ece2, wireframe: true }); } const material = new MeshStandardMaterial({ color: 0xffffff, map: options.layerTextures[0], emissive: options.emissiveHex ?? 0x000000, emissiveIntensity: options.emissiveIntensity ?? 0, roughness: 1, metalness: 0 }); material.onBeforeCompile = (shader) => { shader.uniforms.terrainLayerMap0 = { value: options.layerTextures[0] }; shader.uniforms.terrainLayerMap1 = { value: options.layerTextures[1] }; shader.uniforms.terrainLayerMap2 = { value: options.layerTextures[2] }; shader.uniforms.terrainLayerMap3 = { value: options.layerTextures[3] }; shader.vertexShader = shader.vertexShader .replace( "#include ", `#include attribute vec4 terrainLayerWeights; varying vec4 vTerrainLayerWeights; varying vec2 vTerrainUv;` ) .replace( "#include ", `#include vTerrainUv = uv; vTerrainLayerWeights = terrainLayerWeights;` ); shader.fragmentShader = shader.fragmentShader .replace( "#include ", `#include varying vec4 vTerrainLayerWeights; varying vec2 vTerrainUv; uniform sampler2D terrainLayerMap0; uniform sampler2D terrainLayerMap1; uniform sampler2D terrainLayerMap2; uniform sampler2D terrainLayerMap3;` ) .replace( "#include ", `vec4 terrainWeights = max(vTerrainLayerWeights, 0.0); float terrainWeightSum = terrainWeights.x + terrainWeights.y + terrainWeights.z + terrainWeights.w; if (terrainWeightSum <= 0.0) { terrainWeights = vec4(1.0, 0.0, 0.0, 0.0); } else { terrainWeights /= terrainWeightSum; } vec4 terrainLayerColor = texture2D(terrainLayerMap0, vTerrainUv) * terrainWeights.x + texture2D(terrainLayerMap1, vTerrainUv) * terrainWeights.y + texture2D(terrainLayerMap2, vTerrainUv) * terrainWeights.z + texture2D(terrainLayerMap3, vTerrainUv) * terrainWeights.w; diffuseColor *= terrainLayerColor;` ); }; material.customProgramCacheKey = () => "terrain-layer-blend-v1"; material.needsUpdate = true; return material; }