import { BufferGeometry, Color, Group, Mesh, MeshStandardMaterial, RepeatWrapping, ShaderChunk, SRGBColorSpace, TextureLoader, Vector2 } from 'three'; import { SUMMIT_MATERIALS } from '../constants/summitMaterials.ts'; import { SUMMIT_PLATEAU } from '../constants/summitPlateau.ts'; /** Illustrative land cover, not photogrammetry. Uses the same corridors as walking. */ export function createSummitTrails(geometry: BufferGeometry, onTextureLoaded: ()=>void = ()=>{}, start={x:0,z:0}) { const group=new Group(); group.name='Summit trails and estimated land cover (200m)'; const loader=new TextureLoader(); const extraTextures: import('three').Texture[]=[]; let disposed=false; let rockMaterial: MeshStandardMaterial; function createMesh() { const name='forest'; const settings=SUMMIT_MATERIALS.forest; const load=(kind:string,surface=name)=>{ const texture=loader.load(`${SUMMIT_MATERIALS.baseUrl}/${surface}-${kind}.jpg`,()=>{if(!disposed)onTextureLoaded();}); texture.wrapS=texture.wrapT=RepeatWrapping; texture.anisotropy=SUMMIT_MATERIALS.anisotropy; if(kind==='color')texture.colorSpace=SRGBColorSpace; return texture; }; const material=new MeshStandardMaterial({color:new Color(settings.tint),vertexColors:true, map:load('color'),normalMap:load('normal'),roughnessMap:load('roughness'), normalScale:new Vector2(settings.normalStrength,settings.normalStrength),roughness:1, polygonOffset:true,polygonOffsetFactor:-1,polygonOffsetUnits:-1}); { material.color.set('#ffffff'); const pathColor=load('color','path'),pathNormal=load('normal','path'),pathRoughness=load('roughness','path'); extraTextures.push(pathColor,pathNormal,pathRoughness); const rockColor=load('color','rock'),rockNormal=load('normal','rock'),rockRoughness=load('roughness','rock'); extraTextures.push(rockColor,rockNormal,rockRoughness); const plateauColor=loader.load(`${SUMMIT_MATERIALS.baseUrl}/plateau-color.png`,()=>{if(!disposed)onTextureLoaded();}); plateauColor.colorSpace=SRGBColorSpace;plateauColor.anisotropy=SUMMIT_MATERIALS.anisotropy;extraTextures.push(plateauColor); rockMaterial=new MeshStandardMaterial({map:rockColor,normalMap:rockNormal,roughnessMap:rockRoughness, normalScale:new Vector2(SUMMIT_MATERIALS.rock.normalStrength,SUMMIT_MATERIALS.rock.normalStrength),roughness:1}); const detail=SUMMIT_MATERIALS.rock; const secondaryUv=`uv*${detail.secondaryScale}+vec2(${detail.secondaryOffset.join(',')})`; const secondaryBlend=`${detail.secondaryBlend}+${detail.secondaryVariation}*sin(uv.x*0.71+sin(uv.y*0.53))`; rockMaterial.onBeforeCompile=shader=>{ shader.fragmentShader=`vec4 rockDetail(sampler2D tex,vec2 uv) { return mix(texture2D(tex,uv),texture2D(tex,${secondaryUv}),${secondaryBlend}); }\n`+shader.fragmentShader; for(const [chunk,map,uv] of [['map_fragment','map','vMapUv'],['normal_fragment_maps','normalMap','vNormalMapUv'],['roughnessmap_fragment','roughnessMap','vRoughnessMapUv']] as const) shader.fragmentShader=shader.fragmentShader.replace(`#include <${chunk}>`,ShaderChunk[chunk].replaceAll(`texture2D( ${map}, ${uv} )`,`rockDetail(${map},${uv})`)); shader.fragmentShader=shader.fragmentShader.replace('#include ', `roughnessFactor=max(roughnessFactor,${SUMMIT_MATERIALS.minimumRoughness});\n#include `); }; rockMaterial.customProgramCacheKey=()=>`route-rock-detail-${detail.secondaryScale}-${secondaryBlend}`; // Keep leaf detail near the observer; avoid obvious repeated bands across distant slopes. material.onBeforeCompile=shader=>{ shader.uniforms.farGroundColor={value:new Color(SUMMIT_MATERIALS.forest.farColor)}; shader.uniforms.forestTint={value:new Color(SUMMIT_MATERIALS.forest.tint)}; shader.uniforms.trailColor={value:pathColor};shader.uniforms.trailNormal={value:pathNormal};shader.uniforms.trailRoughness={value:pathRoughness}; shader.uniforms.rockColor={value:rockColor};shader.uniforms.rockNormal={value:rockNormal};shader.uniforms.rockRoughness={value:rockRoughness}; shader.uniforms.plateauColor={value:plateauColor}; shader.vertexShader='attribute float trailDistance,rockDistance; varying float vTrailDistance,vRockDistance;\n'+shader.vertexShader; shader.vertexShader=shader.vertexShader.replace('#include ','#include \nvTrailDistance=trailDistance;vRockDistance=rockDistance;'); const edge=SUMMIT_MATERIALS.edgeBlend; const rockEdge=SUMMIT_MATERIALS.rockBlend; shader.fragmentShader=`uniform vec3 farGroundColor,forestTint; uniform sampler2D trailColor,trailNormal,trailRoughness; uniform sampler2D rockColor,rockNormal,rockRoughness; uniform sampler2D plateauColor; varying float vTrailDistance,vRockDistance; vec2 summitDx,summitDy; float summitRock,summitTrail,summitFade; vec2 rockUv(vec2 uv) { return uv*${(SUMMIT_MATERIALS.forest.tileMeters/SUMMIT_MATERIALS.rock.tileMeters).toFixed(6)}; } float rockWeight(vec2 uv) { float scatter=sin(uv.x*9.7+sin(uv.y*11.3))*sin(uv.y*7.1+uv.x*4.3); return 1.0-smoothstep(${rockEdge.innerMeters},${rockEdge.outerMeters},vRockDistance+scatter*${rockEdge.irregularityMeters}); } float trailWeight(vec2 uv) { float scatter=sin(uv.x*13.7+sin(uv.y*8.3))*sin(uv.y*17.1+uv.x*3.1); return 1.0-smoothstep(${edge.innerMeters},${edge.outerMeters},vTrailDistance+scatter*${edge.irregularityMeters}); } vec4 summitTex(sampler2D tex,vec2 uv,float scale) { return textureGrad(tex,uv,summitDx*scale,summitDy*scale); } vec4 summitGroundSample(sampler2D tex,vec2 uv) { float blend=0.5+0.5*sin(uv.x*0.71+sin(uv.y*0.53)); return mix(summitTex(tex,uv,1.0),summitTex(tex,uv*1.37+vec2(0.31,0.73),1.37),blend); } vec4 summitRockSample(sampler2D tex,vec2 uv) { float scale=${(SUMMIT_MATERIALS.forest.tileMeters/SUMMIT_MATERIALS.rock.tileMeters).toFixed(6)}; return mix(summitTex(tex,uv,scale),summitTex(tex,${secondaryUv},scale*${detail.secondaryScale}),${secondaryBlend}); } // Uniform interiors sample one material; only transition fragments sample neighbours. vec4 summitSample(sampler2D groundMap,sampler2D dirtMap,sampler2D stoneMap,vec2 uv,int kind) { vec4 stone=vec4(0.0),dirt=vec4(0.0); if(summitRock>0.0) { stone=summitRockSample(stoneMap,rockUv(uv)); vec2 local=vec2(uv.x,-uv.y)*${SUMMIT_MATERIALS.forest.tileMeters.toFixed(1)}-vec2(${start.x.toFixed(6)},${start.z.toFixed(6)}); vec2 atlas=(local-vec2(${SUMMIT_PLATEAU.atlasBounds.slice(0,2).join(',')}))/vec2(${SUMMIT_PLATEAU.atlasBounds[2]-SUMMIT_PLATEAU.atlasBounds[0]},${SUMMIT_PLATEAU.atlasBounds[3]-SUMMIT_PLATEAU.atlasBounds[1]}); float atlasMask=smoothstep(0.0,0.035,min(min(atlas.x,atlas.y),min(1.0-atlas.x,1.0-atlas.y))); if(kind==0)stone.rgb=mix(stone.rgb,texture2D(plateauColor,vec2(atlas.x,1.0-atlas.y)).rgb,atlasMask*0.95); if(kind==1)stone.xy=mix(stone.xy,vec2(0.5)+(stone.xy-0.5)*0.32,atlasMask); if(kind==1)stone.xy=(stone.xy-0.5)*${SUMMIT_MATERIALS.rock.normalStrength/SUMMIT_MATERIALS.forest.normalStrength}+0.5; if(summitRock>=1.0)return stone; } if(summitTrail>0.0) { dirt=summitTex(dirtMap,uv,1.0); if(kind==1)dirt.xy=(dirt.xy-0.5)*${SUMMIT_MATERIALS.path.normalStrength/SUMMIT_MATERIALS.forest.normalStrength}+0.5; if(summitTrail>=1.0)return mix(dirt,stone,summitRock); } vec4 ground=summitGroundSample(groundMap,uv); if(kind==0)ground.rgb=mix(ground.rgb*forestTint,farGroundColor,summitFade); return mix(mix(ground,dirt,summitTrail),stone,summitRock); }\n`+shader.fragmentShader; shader.fragmentShader=shader.fragmentShader.replace('#include ', `// Derivatives are computed before divergent branches to preserve mip selection at edges. summitDx=dFdx(vMapUv);summitDy=dFdy(vMapUv); summitRock=rockWeight(vMapUv);summitTrail=trailWeight(vMapUv); summitFade=smoothstep(${SUMMIT_MATERIALS.forest.fadeNear.toFixed(1)},${SUMMIT_MATERIALS.forest.fadeFar.toFixed(1)},length(vViewPosition))*0.9; ${ShaderChunk.map_fragment.replace('texture2D( map, vMapUv )','summitSample(map,trailColor,rockColor,vMapUv,0)')}`); shader.fragmentShader=shader.fragmentShader.replace('#include ', ShaderChunk.normal_fragment_maps.replaceAll('texture2D( normalMap, vNormalMapUv )','summitSample(normalMap,trailNormal,rockNormal,vNormalMapUv,1)')); shader.fragmentShader=shader.fragmentShader.replace('#include ', ShaderChunk.roughnessmap_fragment.replace('texture2D( roughnessMap, vRoughnessMapUv )','summitSample(roughnessMap,trailRoughness,rockRoughness,vRoughnessMapUv,2)') + `\nroughnessFactor=max(roughnessFactor,${SUMMIT_MATERIALS.minimumRoughness});`); }; } const mesh=new Mesh(geometry,material);mesh.name=`Summit ${name} PBR surface`;group.add(mesh); return mesh; } const meshes=[createMesh()]; return {group,rockMaterial:rockMaterial!,dispose(){ disposed=true; rockMaterial.dispose(); extraTextures.forEach(texture=>texture.dispose()); for(const {geometry,material} of meshes) { geometry.dispose();material.map?.dispose();material.normalMap?.dispose();material.roughnessMap?.dispose();material.dispose(); } }}; }