137 lines
10 KiB
JavaScript
137 lines
10 KiB
JavaScript
import * as THREE from 'three';
|
|
import {RoomEnvironment} from 'three/addons/environments/RoomEnvironment.js';
|
|
|
|
// Runtime-only material treatment. No geometry, hydraulic data or source GLB edits.
|
|
// Surface grains are illustrative, not measured site textures. World-space detail
|
|
// avoids inventing UVs for the CAD meshes, and fades below the pixel footprint.
|
|
const noiseGLSL=`
|
|
varying vec3 vSurfaceWorld;
|
|
float grainHash(vec3 p){p=fract(p*.1031);p+=dot(p,p.yzx+33.33);return fract((p.x+p.y)*p.z);}
|
|
float grainNoise(vec3 p){vec3 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);
|
|
return mix(mix(mix(grainHash(i),grainHash(i+vec3(1,0,0)),f.x),mix(grainHash(i+vec3(0,1,0)),grainHash(i+vec3(1,1,0)),f.x),f.y),mix(mix(grainHash(i+vec3(0,0,1)),grainHash(i+vec3(1,0,1)),f.x),mix(grainHash(i+vec3(0,1,1)),grainHash(i+vec3(1,1,1)),f.x),f.y),f.z);}
|
|
`;
|
|
export function grain(material,{frequency=180,amplitude=.00008,variation=.04,colorVariation=0}={}){
|
|
material.onBeforeCompile=shader=>{
|
|
shader.vertexShader=shader.vertexShader.replace('#include <common>','#include <common>\nvarying vec3 vSurfaceWorld;').replace('#include <project_vertex>','#include <project_vertex>\nvec4 surfacePosition=vec4(transformed,1.0);\n#ifdef USE_INSTANCING\nsurfacePosition=instanceMatrix*surfacePosition;\n#endif\nvSurfaceWorld=(modelMatrix*surfacePosition).xyz;');
|
|
shader.fragmentShader=shader.fragmentShader.replace('#include <common>','#include <common>\n'+noiseGLSL)
|
|
.replace('#include <color_fragment>',`#include <color_fragment>
|
|
float macroFade=1.-smoothstep(.2,1.2,max(length(dFdx(vSurfaceWorld)),length(dFdy(vSurfaceWorld)))*2.7);\ndiffuseColor.rgb*=1.+(grainNoise(vSurfaceWorld*2.7)-.5)*macroFade*${colorVariation.toFixed(4)};`)
|
|
.replace('#include <roughnessmap_fragment>',`#include <roughnessmap_fragment>
|
|
float grainFoot=max(length(dFdx(vSurfaceWorld)),length(dFdy(vSurfaceWorld)))*${frequency.toFixed(1)};
|
|
float grainFade=1.-smoothstep(.25,1.5,grainFoot);
|
|
float surfaceGrain=(grainNoise(vSurfaceWorld*${frequency.toFixed(1)})-.5)*grainFade;
|
|
roughnessFactor=clamp(roughnessFactor+surfaceGrain*${variation.toFixed(4)},.08,1.);`)
|
|
.replace('#include <normal_fragment_maps>',`#include <normal_fragment_maps>
|
|
float grainHeight=surfaceGrain*${amplitude.toFixed(6)};
|
|
vec3 gx=dFdx(-vViewPosition),gy=dFdy(-vViewPosition);
|
|
vec3 rx=cross(gy,normal),ry=cross(normal,gx);float gd=dot(gx,rx);
|
|
normal=normalize(abs(gd)*normal-sign(gd)*(dFdx(grainHeight)*rx+dFdy(grainHeight)*ry));`);
|
|
};
|
|
material.customProgramCacheKey=()=>`zjb-grain-${frequency}-${amplitude}-${variation}-${colorVariation}`;
|
|
}
|
|
|
|
export function enhanceMaterials(object){
|
|
const cache=new Map();
|
|
object.traverse(mesh=>{
|
|
if(!mesh.isMesh)return;
|
|
mesh.receiveShadow=true;mesh.castShadow=true;
|
|
const convert=source=>{
|
|
if(cache.has(source))return cache.get(source);
|
|
const m=new THREE.MeshPhysicalMaterial();THREE.MeshStandardMaterial.prototype.copy.call(m,source);
|
|
m.name=source.name;m.envMapIntensity=.85;
|
|
const name=m.name.toLowerCase();let surface='paint';
|
|
// Convert display paint swatches from sRGB; the source palette was very pale
|
|
// under environment illumination. Preserve blue / red / green identities.
|
|
if(/v11_blue/.test(name))m.color.set(0x176184);
|
|
if(/v11_red/.test(name))m.color.set(0xad322c);
|
|
if(/v11_out/.test(name))m.color.set(0x267f74);
|
|
if(/v11_white|v11_wall/.test(name))m.color.set(0xcbd0ce);
|
|
if(/v11_floor/.test(name))m.color.set(0x8c9692);
|
|
if(/v3_roofinlay/.test(name))m.color.set(0x59646a);
|
|
else if(/v3_roof|v3_white/.test(name))m.color.set(0xd8dcda);
|
|
if(/v3_v9_road/.test(name))m.color.set(0x3e4648);
|
|
if(/v3.*concrete/.test(name))m.color.set(0x929b96);
|
|
if(/roof/.test(name)){surface='roof-metal';m.metalness=.5;m.roughness=.46;m.clearcoat=.12;m.envMapIntensity=.65;grain(m,{frequency:90,amplitude:.00002,variation:.045});}else if(/glass|water/.test(name)){
|
|
surface='glass';m.metalness=.05;m.roughness=.19;m.clearcoat=1;m.clearcoatRoughness=.09;m.envMapIntensity=1.2;
|
|
// Retain alpha visibility and avoid transmission rendering through inspection cutaways.
|
|
}else if(/steel|lattice|perforated/.test(name)){
|
|
surface='metal';m.metalness=.88;m.roughness=.3;m.envMapIntensity=1.1;grain(m,{frequency:260,amplitude:.00002,variation:.13});
|
|
}else if(/concrete|floor|wall|road/.test(name)){
|
|
surface='mineral';m.metalness=0;m.roughness=/floor/.test(name)?.64:.91;m.envMapIntensity=.3;
|
|
grain(m,{frequency:/road/.test(name)?25:75,amplitude:.001,variation:.16,colorVariation:.1});
|
|
}else if(/seat/.test(name)){
|
|
surface='seat';m.metalness=.04;m.roughness=.66;grain(m,{frequency:360,amplitude:.00012,variation:.15});
|
|
}else if(/light|diffuser|screen/.test(name)){
|
|
surface='luminaire';m.metalness=0;m.roughness=.38;m.emissive.set(0xffe5b7);m.emissiveIntensity=.55;
|
|
}else{
|
|
m.metalness=/dark|black/.test(name)?.58:.23;m.roughness=/roof/.test(name)?.36:.3;
|
|
m.clearcoat=.4;m.clearcoatRoughness=.25;grain(m);
|
|
}
|
|
// Facade coatings need their own optical treatment. Keep generic glass
|
|
// (instruments, water and interiors) separate from the station curtain wall.
|
|
if(/v3_glass|coatedcurtainglass/.test(name)){
|
|
surface='glass';const coated=/coatedcurtain/.test(name);
|
|
m.color.set(coated?0x466b82:0x577e90);m.metalness=coated?.42:.18;
|
|
m.roughness=coated?.1:.13;m.opacity=coated?.78:.58;
|
|
m.transparent=true;m.depthWrite=false;m.clearcoat=.65;
|
|
m.clearcoatRoughness=.08;m.envMapIntensity=1.6;
|
|
}
|
|
m.userData={...source.userData,surfaceTreatment:surface,appearanceRevision:29};cache.set(source,m);return m;
|
|
};
|
|
mesh.material=Array.isArray(mesh.material)?mesh.material.map(convert):convert(mesh.material);
|
|
mesh.castShadow=!(Array.isArray(mesh.material)?mesh.material:[mesh.material]).every(m=>m.userData.surfaceTreatment==='glass');mesh.receiveShadow=mesh.castShadow;
|
|
});
|
|
return cache;
|
|
}
|
|
|
|
// Restore the authored appearance on every mode change. In network overview,
|
|
// opaque buildings fade more than glass so the curtain wall remains legible.
|
|
export function applyBuildingContext(material,fade){
|
|
const original=material.userData.original??{opacity:material.opacity,transparent:material.transparent,depthWrite:material.depthWrite};
|
|
material.userData.original=original;
|
|
const facade=/v3_glass|coatedcurtainglass/i.test(material.name);
|
|
material.opacity=fade?(facade?Math.min(original.opacity,.38):.12):original.opacity;
|
|
material.transparent=fade||original.transparent;
|
|
material.depthWrite=fade?false:original.depthWrite;material.needsUpdate=true;
|
|
}
|
|
|
|
const presets={
|
|
day:{background:0xe2e8e9,ground:0xb9c1bd,key:0xffefdc,keyPower:3.1,fill:.32,env:.45,rim:.85,exposure:.95},
|
|
studio:{background:0x242d34,ground:0x303a40,key:0xfff5e9,keyPower:2.6,fill:.24,env:.8,rim:1.3,exposure:.95},
|
|
evening:{background:0x555d69,ground:0x707574,key:0xffc185,keyPower:3.0,fill:.22,env:.5,rim:.9,exposure:.95},
|
|
};
|
|
export function createAppearance(renderer,scene){
|
|
renderer.outputColorSpace=THREE.SRGBColorSpace;
|
|
renderer.toneMapping=THREE.ACESFilmicToneMapping;
|
|
renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFShadowMap;renderer.shadowMap.autoUpdate=false;
|
|
const pmrem=new THREE.PMREMGenerator(renderer),room=new RoomEnvironment();
|
|
const environment=pmrem.fromScene(room,.04);room.dispose();pmrem.dispose();scene.environment=environment.texture;
|
|
const fill=new THREE.HemisphereLight(0xcde3ff,0x716658,.75),sun=new THREE.DirectionalLight(0xffefdc,3.2),rim=new THREE.DirectionalLight(0xc1dbff,1.);
|
|
sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);sun.shadow.radius=3;sun.shadow.bias=-.00008;sun.shadow.normalBias=.018;
|
|
scene.add(fill,sun,sun.target,rim,rim.target);
|
|
const floorMat=new THREE.MeshStandardMaterial({color:0xb9c1bd,roughness:.94,metalness:0});grain(floorMat,{frequency:45,amplitude:.0006,variation:.12,colorVariation:.035});
|
|
const floor=new THREE.Mesh(new THREE.PlaneGeometry(1,1),floorMat);floor.rotation.x=-Math.PI/2;floor.receiveShadow=true;floor.userData.displayOnly=true;floor.name='Presentation ground (not CAD)';scene.add(floor);
|
|
let current='day',shadowEnabled=true,lastBox,lastMode;
|
|
const state={revision:27,preset:current,environment:'offline studio PMREM',shadows:true,proceduralSurfaces:true,groundIsPresentationOnly:true};
|
|
function refresh(){if(lastMode==='hydraulic'){let y=Infinity;for(const o of scene.children)if(o.visible&&o.isGroup){const b=new THREE.Box3().setFromObject(o);if(!b.isEmpty())y=Math.min(y,b.min.y-.3);}if(Number.isFinite(y))floor.position.y=y;}renderer.shadowMap.needsUpdate=true;}
|
|
function frame(box,mode){
|
|
if(box.isEmpty())return;lastBox=box.clone();lastMode=mode;
|
|
const c=box.getCenter(new THREE.Vector3()),size=box.getSize(new THREE.Vector3()),radius=Math.max(size.length()/2,2),extent=Math.max(size.x,size.z,4)*.7;
|
|
const p=presets[current];
|
|
sun.target.position.copy(c);sun.position.copy(c).add(new THREE.Vector3(-.8,current==='evening'?.7:1.8,.95).multiplyScalar(radius*2));
|
|
rim.target.position.copy(c);rim.position.copy(c).add(new THREE.Vector3(1,.5,-1).multiplyScalar(radius*2));
|
|
Object.assign(sun.shadow.camera,{left:-extent,right:extent,top:extent,bottom:-extent,near:.1,far:radius*9});sun.shadow.camera.updateProjectionMatrix();sun.shadow.normalBias=radius>100?.15:.055;
|
|
floor.visible=mode!=='network';
|
|
let groundY=box.min.y-.06;if(mode==='hydraulic'){for(const o of scene.children)if(o.visible&&o.isGroup){const b=new THREE.Box3().setFromObject(o);if(!b.isEmpty())groundY=Math.min(groundY,b.min.y-.3);}}floor.position.set(c.x,groundY,c.z);floor.scale.setScalar(Math.max(size.x,size.z,4)*12);
|
|
scene.fog=['network','hydraulic'].includes(mode)?null:new THREE.Fog(p.background,radius*8,radius*25);
|
|
sun.castShadow=shadowEnabled&&mode!=='network';state.activeShadows=sun.castShadow;state.presentationGroundVisible=floor.visible;refresh();
|
|
}
|
|
function preset(name){current=presets[name]?name:'day';const p=presets[current];scene.background=new THREE.Color(p.background);floorMat.color.set(p.ground);sun.color.set(p.key);sun.intensity=p.keyPower;fill.intensity=p.fill;rim.intensity=p.rim;scene.environmentIntensity=p.env;renderer.toneMappingExposure=p.exposure;state.preset=current;state.exposure=p.exposure;if(lastBox)frame(lastBox,lastMode);refresh();}
|
|
preset('day');
|
|
return {state,frame,preset,refresh,exposure(value){renderer.toneMappingExposure=value;state.exposure=value;},shadows(enabled){shadowEnabled=enabled;state.shadows=enabled;if(lastBox)frame(lastBox,lastMode);},dispose(){environment.dispose();floor.geometry.dispose();floorMat.dispose();sun.shadow.dispose();scene.remove(fill,sun,sun.target,rim,rim.target,floor);}};
|
|
}
|
|
|
|
|
|
|
|
|