115 lines
8.2 KiB
JavaScript
115 lines
8.2 KiB
JavaScript
/** Standard Three.js backend. UI and catalogue never depend on THREE internals. */
|
|
export async function createThreeRenderer({canvas,gltf,binary,bytes,config}) {
|
|
let timer;
|
|
const modules=Promise.all([import(config.threeModule),import(config.threeLoader)]);
|
|
const [T,{GLTFLoader}]=await Promise.race([modules,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('Three.js CDN timeout')),config.cdnTimeoutMs||6500);})]).finally(()=>clearTimeout(timer));
|
|
const loader=new GLTFLoader();
|
|
const documentModel=await loader.parseAsync(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength),'');
|
|
const scene=new T.Scene();scene.add(documentModel.scene);
|
|
const renderer=new T.WebGLRenderer({canvas,antialias:true,alpha:false,preserveDrawingBuffer:true,powerPreference:'high-performance'});
|
|
renderer.outputColorSpace=T.SRGBColorSpace;
|
|
renderer.toneMapping=T.ACESFilmicToneMapping;renderer.toneMappingExposure=1.05;
|
|
renderer.shadowMap.enabled=true;renderer.shadowMap.type=T.PCFSoftShadowMap;renderer.shadowMap.autoUpdate=false;
|
|
const ambient=new T.AmbientLight(0xffffff,.7);
|
|
const skyLight=new T.HemisphereLight(0xf5f8ff,0x67604e,1.7);
|
|
const sun=new T.DirectionalLight(0xffffff,3.0);
|
|
const fill=new T.HemisphereLight(0xfff5df,0x77716a,0);
|
|
scene.add(ambient,skyLight,sun,sun.target,fill);
|
|
sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);sun.shadow.bias=-.00008;sun.shadow.normalBias=.025;
|
|
const persp=new T.PerspectiveCamera(),ortho=new T.OrthographicCamera();
|
|
const textures=await Promise.all((gltf.textures||[]).map((_,i)=>documentModel.parser.getDependency('texture',i)));
|
|
const images=[];
|
|
for (let i=0;i<textures.length;i++) {
|
|
const imageSpec=gltf.images[gltf.textures[i].source];
|
|
if(imageSpec.bufferView!==undefined){const b=gltf.bufferViews[imageSpec.bufferView];const src=URL.createObjectURL(new Blob([binary.subarray(b.byteOffset||0,(b.byteOffset||0)+b.byteLength)],{type:imageSpec.mimeType}));images[i]={src};}
|
|
textures[i].anisotropy=Math.min(8,renderer.capabilities.getMaxAnisotropy());
|
|
}
|
|
const entries=[],matCopies=new Map(),associations=documentModel.parser.associations;
|
|
documentModel.scene.updateMatrixWorld(true);
|
|
documentModel.scene.traverse(obj=>{
|
|
if(!obj.isMesh)return;
|
|
const association=associations.get(obj)||{};
|
|
let parent=obj,nodeIndex=association.nodes;
|
|
while(nodeIndex===undefined&&parent){const a=associations.get(parent);if(a?.nodes!==undefined)nodeIndex=a.nodes;parent=parent.parent;}
|
|
if(nodeIndex===undefined)return;
|
|
const node=gltf.nodes[nodeIndex],meshSpec=gltf.meshes[node.mesh];
|
|
const primitiveIndex=association.primitives||0;
|
|
const primitive=meshSpec.primitives[primitiveIndex]||meshSpec.primitives[0];
|
|
const materialIndex=primitive.material;
|
|
if(materialIndex===undefined)return;
|
|
const spec=gltf.materials[materialIndex],name=node.name||meshSpec.name||obj.name;
|
|
// Separate copies prevent a material variant from changing unrelated objects.
|
|
let material=matCopies.get(materialIndex);
|
|
if(!material){material=obj.material.clone();material.side=T.DoubleSide;matCopies.set(materialIndex,material);}
|
|
obj.material=material;
|
|
const box=new T.Box3().setFromObject(obj);
|
|
const pbr=spec.pbrMetallicRoughness||{};
|
|
const desc={object:obj,std:node.extras||{},transparent:spec.alphaMode==='BLEND',material:materialIndex,name,nodeIndex,
|
|
count:obj.geometry.index?.count||obj.geometry.attributes.position.count,context:/^(90_|91_|28_|23_)/.test(name),
|
|
legacySite:!!node.extras?.legacySite,siteLayer:node.extras?.siteLayer||null,castShadow:node.extras?.castShadow!==false,
|
|
bounds:{min:box.min.toArray(),max:box.max.toArray()},glass:(spec.name||'').startsWith('glass'),
|
|
interiorLayer:node.extras?.interiorLayer||null,interiorMaterial:!!spec.extras?.interiorMaterial,
|
|
interiorStyle:spec.extras?.interiorStyle||0,pbr};
|
|
obj.castShadow=desc.castShadow&&!desc.glass&&!desc.transparent;obj.receiveShadow=!desc.transparent;
|
|
if(desc.transparent){material.transparent=true;material.depthWrite=false;}
|
|
entries.push(desc);
|
|
});
|
|
const stamps=new Map(),textureCopies=new Map();
|
|
function localTexture(index,role,materialIndex,scale) {
|
|
const key=materialIndex+':'+role+':'+index+':'+scale.join(',');
|
|
if(!textureCopies.has(key)){
|
|
const tx=textures[index]?.clone();if(!tx)return null;
|
|
tx.colorSpace=role==='map'?T.SRGBColorSpace:T.NoColorSpace;
|
|
tx.repeat.set(...scale);tx.needsUpdate=true;textureCopies.set(key,tx);
|
|
}
|
|
return textureCopies.get(key);
|
|
}
|
|
function syncMaterials(){
|
|
for(const [index,mat] of matCopies){
|
|
const spec=gltf.materials[index],p=spec.pbrMetallicRoughness||{};
|
|
const stamp=JSON.stringify([p,spec.normalTexture,spec.emissiveFactor,spec.name]);if(stamps.get(index)===stamp)continue;stamps.set(index,stamp);
|
|
const rgba=p.baseColorFactor||[.7,.7,.7,1];mat.color.setRGB(...rgba.slice(0,3));
|
|
// Legacy exterior factors were sRGB; tagged PBR / interior factors are already linear.
|
|
if(!spec.extras?.interiorMaterial&&!spec.extras?.linearColor&&!spec.extras?.hardieRole)mat.color.convertSRGBToLinear();
|
|
mat.opacity=rgba[3]??1;mat.roughness=p.roughnessFactor??.8;mat.metalness=p.metallicFactor??0;
|
|
const tex=p.baseColorTexture;
|
|
mat.map=tex?localTexture(tex.index,'map',index,tex.extensions?.KHR_texture_transform?.scale||[1,1]):null;
|
|
mat.normalMap=spec.normalTexture?localTexture(spec.normalTexture.index,'normalMap',index,[1,1]):null;
|
|
if(mat.normalScale&&spec.normalTexture)mat.normalScale.setScalar(spec.normalTexture.scale??1);
|
|
if(spec.extras?.interiorStyle===1){mat.emissive.copy(mat.color);mat.emissiveIntensity=.75;}
|
|
mat.needsUpdate=true;
|
|
}
|
|
}
|
|
const temp=new T.Vector3();let oldVisible='',oldInterior='';
|
|
function shadowFit(frame){
|
|
const low=[Infinity,Infinity,Infinity],high=[-Infinity,-Infinity,-Infinity];
|
|
for(const m of entries){if(!m.object.visible||m.glass)continue;for(let a=0;a<3;a++){low[a]=Math.min(low[a],m.bounds.min[a]);high[a]=Math.max(high[a],m.bounds.max[a]);}}
|
|
if(!Number.isFinite(low[0]))return;
|
|
if(frame.interior.enabled){low.splice(0,3,-9,-1,-6);high.splice(0,3,11,6,6);}
|
|
const center=low.map((v,i)=>(v+high[i])/2);
|
|
sun.target.position.fromArray(center);sun.position.fromArray(center).addScaledVector(temp.fromArray(frame.solar.light),120);
|
|
sun.target.updateMatrixWorld();sun.updateMatrixWorld();
|
|
const c=sun.shadow.camera;c.near=.1;c.far=320;c.position.copy(sun.position);c.up.set(0,1,0);c.lookAt(sun.target.position);c.updateMatrixWorld();
|
|
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
|
|
for(let a=0;a<8;a++){temp.set(a&1?high[0]:low[0],a&2?high[1]:low[1],a&4?high[2]:low[2]).applyMatrix4(c.matrixWorldInverse);minX=Math.min(minX,temp.x);maxX=Math.max(maxX,temp.x);minY=Math.min(minY,temp.y);maxY=Math.max(maxY,temp.y);}
|
|
c.left=minX-2;c.right=maxX+2;c.bottom=minY-2;c.top=maxY+2;c.updateProjectionMatrix();renderer.shadowMap.needsUpdate=true;
|
|
}
|
|
return {backend:'three',label:'Three.js r'+T.REVISION,meshes:entries,images,
|
|
resize(w,h,dpr){renderer.setPixelRatio(dpr);renderer.setSize(w,h,false);},
|
|
draw(frame){
|
|
syncMaterials();
|
|
let key='';for(const m of entries){m.object.visible=!!frame.visible(m);key+=m.object.visible?'1':'0';}
|
|
if(frame.solar.shadowDirty||key!==oldVisible||oldInterior!==frame.interior.mode){shadowFit(frame);oldVisible=key;oldInterior=frame.interior.mode;}
|
|
sun.intensity=Math.max(0,frame.solar.direct)*3.0;sun.color.fromArray(frame.solar.sunColor);sun.castShadow=frame.solar.shadows;
|
|
const day=frame.solar.dayFactor;skyLight.intensity=.18+1.5*day;ambient.intensity=.12+.45*day;
|
|
fill.intensity=frame.interior.enabled&&frame.interior.fill?1.3:0;
|
|
scene.background=new T.Color().setRGB(...frame.solar.sky).convertSRGBToLinear();
|
|
const camera=frame.orthographic?ortho:persp;
|
|
camera.position.fromArray(frame.eye);camera.up.set(0,1,0);camera.lookAt(temp.fromArray(frame.lookTarget));camera.updateMatrixWorld();
|
|
camera.projectionMatrix.fromArray(frame.vp).multiply(camera.matrixWorld);camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
|
|
renderer.render(scene,camera);
|
|
},
|
|
dispose(){renderer.dispose();images.forEach(i=>i?.src&&URL.revokeObjectURL(i.src));textureCopies.forEach(t=>t.dispose());}
|
|
};
|
|
}
|