Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
137 lines
9.7 KiB
TypeScript
137 lines
9.7 KiB
TypeScript
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 <metalnessmap_fragment>',
|
|
`roughnessFactor=max(roughnessFactor,${SUMMIT_MATERIALS.minimumRoughness});\n#include <metalnessmap_fragment>`);
|
|
};
|
|
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 <begin_vertex>','#include <begin_vertex>\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 <map_fragment>',
|
|
`// 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 <normal_fragment_maps>',
|
|
ShaderChunk.normal_fragment_maps.replaceAll('texture2D( normalMap, vNormalMapUv )','summitSample(normalMap,trailNormal,rockNormal,vNormalMapUv,1)'));
|
|
shader.fragmentShader=shader.fragmentShader.replace('#include <roughnessmap_fragment>',
|
|
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();
|
|
}
|
|
}};
|
|
}
|
|
|