Spaces:
Running
Running
| ; | |
| ; | |
| /* ============================================================================ | |
| WORLD MODEL LOADER | |
| ---------------------------------------------------------------------------- | |
| Turns the compact WORLD_MODELS table into InstMesh-ready geometry. Kept | |
| separate from the data file so the data file can be regenerated without | |
| touching runtime logic. | |
| ============================================================================ */ | |
| function loadWorldModel(name,authoredUV){ | |
| const D=WORLD_MODELS[name]; | |
| if(!D){ console.warn('world model not found:',name); return null; } | |
| const vc=D.vertexCount, ic=D.indexCount; | |
| const uvSrc=authoredUV&&D.uv2?D.uv2:D.uv; | |
| /* Curated scenery owns a contact plane. Normalising it here means a Blender | |
| export with its pivot at world origin cannot hover above (or disappear | |
| below) terrain. Existing assets have contactY=0, so old saves do not move. */ | |
| const contactY=Number.isFinite(D.contactY)?D.contactY:0; | |
| const v=new Float32Array(vc*12); | |
| for(let i=0;i<vc;i++){ | |
| const o=i*12; | |
| const p=i*3, u=i*2; | |
| v[o ]=D.pos[p ]; v[o+1]=D.pos[p+1]-contactY; v[o+2]=D.pos[p+2]; // grounded position | |
| v[o+3]=D.nrm[p ]; v[o+4]=D.nrm[p+1]; v[o+5]=D.nrm[p+2]; // normal | |
| v[o+6]=1; v[o+7]=1; v[o+8]=1; // colour (white, atlas multiplies) | |
| v[o+9]=uvSrc[u]; v[o+10]=uvSrc[u+1]; // legacy tiled or authored UV0 | |
| v[o+11]=D.mat[i]; | |
| } | |
| const iArr=new Uint16Array(ic); | |
| for(let k=0;k<ic;k++) iArr[k]=D.idx[k]; | |
| /* Hall pitches sit at ny~0.36–0.41, so |ny|>0.45 left CONC panes on the | |
| roof. Chimney CAPS are RUST/LAMP (+Y quads span<12) — rust + the | |
| industrial amber fill reads as a glowing disc. Windows only on | |
| near-vertical walls. A tessellated dome roof is also many small +Y | |
| tris: treating each as a chimney, then painting an 80-unit TRIM | |
| cylinder under it, turned the whole bowl into unlit metal (solid | |
| black). Cluster first; only an isolated island is a stack. */ | |
| if(typeof MAT!=='undefined'&&D.idx&&D.pos&&D.nrm){ | |
| const roofId=MAT.ROOF+1, trimId=MAT.TRIM+1, litId=MAT.BUILD_OFFICE_LIT+1; | |
| const ymax=D.bounds?D.bounds[1][1]:0; | |
| const bx0=D.bounds?D.bounds[0][0]:-50, bx1=D.bounds?D.bounds[1][0]:50; | |
| const bz0=D.bounds?D.bounds[0][2]:-50, bz1=D.bounds?D.bounds[1][2]:50; | |
| const meshXZ=Math.hypot(bx1-bx0, bz1-bz0); | |
| const seeds=[]; | |
| for(let t=0;t<ic;t+=3){ | |
| const a=iArr[t],b=iArr[t+1],c=iArr[t+2]; | |
| const ny=(D.nrm[a*3+1]+D.nrm[b*3+1]+D.nrm[c*3+1])/3; | |
| if(ny<0.70)continue; | |
| const x=(D.pos[a*3]+D.pos[b*3]+D.pos[c*3])/3; | |
| const y=(D.pos[a*3+1]+D.pos[b*3+1]+D.pos[c*3+1])/3; | |
| const z=(D.pos[a*3+2]+D.pos[b*3+2]+D.pos[c*3+2])/3; | |
| const x0=Math.min(D.pos[a*3],D.pos[b*3],D.pos[c*3]), x1=Math.max(D.pos[a*3],D.pos[b*3],D.pos[c*3]); | |
| const z0=Math.min(D.pos[a*3+2],D.pos[b*3+2],D.pos[c*3+2]), z1=Math.max(D.pos[a*3+2],D.pos[b*3+2],D.pos[c*3+2]); | |
| const span=Math.hypot(x1-x0,z1-z0); | |
| if(span<12&&y>ymax*0.28) seeds.push({x,y,z,span}); | |
| } | |
| const nS=seeds.length, par=new Int32Array(nS); | |
| for(let i=0;i<nS;i++) par[i]=i; | |
| const find=i=>{ while(par[i]!==i) i=par[i]=par[par[i]]; return i; }; | |
| for(let i=0;i<nS;i++) for(let j=i+1;j<nS;j++){ | |
| const A=seeds[i],B=seeds[j]; | |
| if(Math.abs(A.y-B.y)<10&&Math.hypot(A.x-B.x,A.z-B.z)<14){ | |
| const ai=find(i),aj=find(j); if(ai!==aj) par[ai]=aj; | |
| } | |
| } | |
| const groups=new Map(); | |
| for(let i=0;i<nS;i++){ | |
| const r=find(i), S=seeds[i]; | |
| let G=groups.get(r); | |
| if(!G){ G={n:0,x0:1e9,x1:-1e9,z0:1e9,z1:-1e9,y:0,sx:0,sz:0}; groups.set(r,G); } | |
| G.n++; G.y+=S.y; G.sx+=S.x; G.sz+=S.z; | |
| G.x0=Math.min(G.x0,S.x-S.span*0.5); G.x1=Math.max(G.x1,S.x+S.span*0.5); | |
| G.z0=Math.min(G.z0,S.z-S.span*0.5); G.z1=Math.max(G.z1,S.z+S.span*0.5); | |
| } | |
| const caps=[], spanLim=Math.min(9, meshXZ*0.20); | |
| groups.forEach(G=>{ | |
| const span=Math.hypot(G.x1-G.x0, G.z1-G.z0); | |
| if(span<spanLim&&G.n<24) | |
| caps.push({x:G.sx/G.n, y:G.y/G.n, z:G.sz/G.n, r:Math.max(4,span*0.55)}); | |
| }); | |
| /* Tight column under a real stack. Not an 80-unit cylinder — that ate | |
| every dome whose peak happened to be one small +Y triangle. */ | |
| const nearCap=(x,y,z)=>{ | |
| for(let k=0;k<caps.length;k++){ | |
| const C=caps[k]; | |
| if(y>C.y-50&&y<C.y+4&&Math.hypot(x-C.x,z-C.z)<C.r+3) return true; | |
| } | |
| return false; | |
| }; | |
| const winRaw=raw=>raw===MAT.CONC||raw===MAT.BUILD||raw===MAT.GREEBLE||raw===MAT.LAMP | |
| ||raw===MAT.BUILD_OFFICE_LIT||raw===MAT.BUILD_OFFICE_COOL||raw===MAT.BUILD_NIGHT_FLICKER | |
| ||raw===MAT.PRECAST_BAY||raw===MAT.NEON_FACADE; | |
| const uvS=typeof UVS==='number'?UVS:0.055; | |
| for(let i=0;i<vc;i++){ | |
| const o=i*12; | |
| const nx=v[o+3], ny=v[o+4], nz=v[o+5]; | |
| const ax=Math.abs(nx), ay=Math.abs(ny), az=Math.abs(nz); | |
| const facade=ay<0.22&&Math.max(ax,az)>0.85; | |
| const mid=v[o+11]; | |
| const sgn=mid<0?-1:1; | |
| const raw=(Math.floor(Math.abs(mid))-1)|0; | |
| const capVert=nearCap(v[o],v[o+1],v[o+2]); | |
| /* Civic louvres used to stay MAT.LAMP. That tile was a radial | |
| orange blob, so a vertical pane read as a fat orb from the | |
| command camera. Cool office glass keeps the slat and the light. */ | |
| if(capVert&&!(facade&&raw===MAT.LAMP&&ny<=0.55)){ | |
| if(ny>0.45) v[o+11]=roofId*sgn; | |
| else if(winRaw(raw)||raw===MAT.RUST||raw===MAT.TRIM) v[o+11]=trimId*sgn; | |
| }else if(!facade&&(winRaw(raw)||((raw===MAT.RUST||raw===MAT.LAMP)&&ny>0.45))){ | |
| v[o+11]=roofId*sgn; // pitches, ridges, decks, interiors | |
| }else if(facade&&raw===MAT.LAMP){ | |
| v[o+11]=(MAT.BUILD_OFFICE_COOL+1)*sgn; | |
| }else if(facade&&(raw===MAT.CONC||raw===MAT.BUILD)){ | |
| v[o+11]=litId*sgn; // readable window band on walls | |
| } | |
| /* Tiny authored UV islands on a tessellated crown NaN the cotangent | |
| frame and shade black. XZ planar UVs keep ROOF/TRIM sampling. | |
| 0.35 covers hall pitches (ny~0.36) the 0.45 gate missed. */ | |
| if(ay>0.35){ | |
| const fin=(Math.floor(Math.abs(v[o+11]))-1)|0; | |
| if(fin===MAT.ROOF||fin===MAT.TRIM){ v[o+9]=v[o]*uvS; v[o+10]=v[o+2]*uvS; } | |
| } | |
| } | |
| } | |
| return {v:v, i:iArr, count:ic, bones:0, | |
| bounds:D.bounds||null,contactY:0,sourceContactY:contactY, | |
| uvMode:authoredUV&&D.uv2?'authored':(D.uvMode||'legacy')}; | |
| } | |