Files
sunrise/src/scene/summitTrails.ts
T
horoli eb49ea4b84 Improve summit terrain, mobile HUD and multiplayer night equipment
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.
2026-10-31 23:14:44 +09:00

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();
}
}};
}