/* ============================================================================ MATERIAL SYSTEM V2 — MAP STRUCTURES ---------------------------------------------------------------------------- The five map-generated civilian/military structures use the same authored BaseAO / normal-roughness-emissive / material-mask contract proven by the V2 tank and factory. They are packed into three shared 1024 maps because a battlefield may contain hundreds of these objects; loading their individual 2048 showcase maps would spend roughly 320 MB before mipmaps. HIGH and CINEMATIC use this battle material LOD. LOW, failed texture loads, and `?worldv2=0` retain the old InstMesh path, which makes the conversion reversible and prevents a missing optional asset from erasing a city. ============================================================================ */ const MFWORLD2_QUERY=typeof location!=='undefined'?new URLSearchParams(location.search):new URLSearchParams(); const MFWORLD2_DISABLED=MFWORLD2_QUERY.get('worldv2')==='0'; const MFWORLD2_DEFS=[ {kind:0,key:'mdlCityTower',family:0,role:0,rect:[.0234375,.0234375,.296875,.296875]}, {kind:1,key:'mdlCityDome', family:0,role:1,rect:[.3515625,.0234375,.296875,.296875]}, {kind:2,key:'mdlCityHall', family:1,role:2,rect:[.6796875,.0234375,.296875,.296875]}, {kind:3,key:'mdlCityTank', family:1,role:3,rect:[.0234375,.3515625,.296875,.296875]}, {kind:4,key:'mdlCivicBlock',family:0,role:4,rect:[.3515625,.3515625,.296875,.296875]}, ]; let mfWorld2Prog=null,mfWorld2U={},mfWorld2BaseAO=null,mfWorld2NRE=null,mfWorld2Masks=null,mfWorld2DamageTex=null,mfWorld2Detail=null; let mfWorld2Meshes={},mfWorld2Ready=false,mfWorld2Loaded=0,mfWorld2Error=false,mfWorld2Epoch=-1; const MFWORLD2_VS=`#version 300 es layout(location=0) in vec3 aPos; layout(location=1) in vec3 aNrm; layout(location=2) in vec3 aCol; layout(location=3) in vec2 aUV; layout(location=4) in float aMat; layout(location=5) in vec4 aInst; layout(location=6) in float aYaw; layout(location=7) in vec4 aTint; layout(location=8) in float aWide; layout(location=9) in float aAnim; uniform mat4 uVP; uniform vec3 uEye; uniform float uHazeQ; uniform vec4 uRect; out vec3 vNrm;out vec3 vObjNrm;out vec3 vWorld;out vec3 vObj;out vec2 vUV;out vec3 vTint; out float vFog;out float vDamage;out vec2 vFowUV; void main(){ float c=cos(aYaw),s=sin(aYaw); vec3 sp=vec3(aPos.x*aInst.w,aPos.y*aInst.w,aPos.z*aWide); vec3 p=vec3(sp.x*c-sp.z*s,sp.y,sp.x*s+sp.z*c)+aInst.xyz; vNrm=normalize(vec3(aNrm.x*c-aNrm.z*s,aNrm.y,aNrm.x*s+aNrm.z*c)); vObjNrm=aNrm;vWorld=p;vObj=aPos; /* Inset authored UVs so mip/atlas bleed cannot sample a neighbour tile's emissive into a roof. 0.02 stays inside the 24px gutter of each 304px cell. */ vUV=uRect.xy+clamp(aUV,0.02,0.98)*uRect.zw;vTint=aTint.rgb; vDamage=clamp(aAnim,0.0,1.0); vFowUV=p.xz/MAPSIZE_CONST; float d=length(p-uEye); float planar=length(vec2(p.x-uEye.x, p.z-uEye.z)); float hq=uHazeQ>0.01?uHazeQ:1.0; vFog=clamp((planar-1380.0)/1900.0,0.0,0.40)*hq; float bd=min(min(p.x, MAPSIZE_CONST-p.x), min(p.z, MAPSIZE_CONST-p.z)); vFog=max(vFog, clamp((BFOG_CONST-bd)/BFOG_CONST,0.0,1.0)); gl_Position=uVP*vec4(p,1.0); }`; const MFWORLD2_FS=`#version 300 es precision highp float; in vec3 vNrm;in vec3 vObjNrm;in vec3 vWorld;in vec3 vObj;in vec2 vUV;in vec3 vTint; in float vFog;in float vDamage;in vec2 vFowUV; uniform sampler2D uBaseAO;uniform sampler2D uNRE;uniform sampler2D uMasks;uniform sampler2D uDamageTex;uniform sampler2D uDetail; uniform sampler2D uFowMap;uniform float uFowOn; uniform vec3 uEye,uSun,uSunC,uAmbSky,uAmbGnd,uFogC;uniform float uNight,uTime,uFamily,uRole; uniform int uLightCount;uniform vec4 uLightPosR[8];uniform vec4 uLightColI[8]; out vec4 o; float hash21(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);} vec2 mfUvGradClamp(vec2 g){ float l=length(g); return (l>0.25)?g*(0.25/l):g; } mat3 cotangent(vec3 N,vec3 p,vec2 uv){ vec3 dp1=dFdx(p),dp2=dFdy(p);vec2 du1=mfUvGradClamp(dFdx(uv)),du2=mfUvGradClamp(dFdy(uv)); vec3 p2=cross(dp2,N),p1=cross(N,dp1); vec3 T=p2*du1.x+p1*du2.x,B=p2*du1.y+p1*du2.y; float t2=dot(T,T),b2=dot(B,B); if(max(t2,b2)<1e-10){ vec3 Tf=normalize(abs(N.y)<0.999?cross(N,vec3(0.0,1.0,0.0)):cross(N,vec3(1.0,0.0,0.0))); return mat3(Tf,cross(N,Tf),N); } vec3 To=T-N*dot(N,T); if(dot(To,To)<1e-10){ vec3 Tf=normalize(abs(N.y)<0.999?cross(N,vec3(0.0,1.0,0.0)):cross(N,vec3(1.0,0.0,0.0))); return mat3(Tf,cross(N,Tf),N); } T=normalize(To); vec3 Bn=cross(N,T); B=normalize(dot(Bn,B)<0.0?-Bn:Bn); return mat3(T,B,N); } void main(){ vec2 dxa=mfUvGradClamp(dFdx(vUV)),dya=mfUvGradClamp(dFdy(vUV)); vec4 ba=textureGrad(uBaseAO,vUV,dxa,dya),nr=textureGrad(uNRE,vUV,dxa,dya),mk=textureGrad(uMasks,vUV,dxa,dya); vec3 dan=pow(abs(normalize(vObjNrm))+vec3(.0001),vec3(5.0));dan/=dan.x+dan.y+dan.z; float damageData=texture(uDamageTex,vObj.zy*.078).r*dan.x+ texture(uDamageTex,vObj.xz*.078).r*dan.y+texture(uDamageTex,vObj.xy*.078).r*dan.z; /* Shared neutral micro-surface detail gives civilian concrete, ceramic and military alloy the same restrained age/roughness breakup as live units. It is intentionally far weaker than the authored maps: it should support roof seams and facade normals, never turn a city into visual static. */ float detailData=texture(uDetail,vObj.zy*.115).r*dan.x+ texture(uDetail,vObj.xz*.115).r*dan.y+texture(uDetail,vObj.xy*.115).r*dan.z; float damageCrust=smoothstep(.055,.38,damageData),damageCrack=smoothstep(.23,.49,damageData); vec2 xy=(nr.rg*2.0-1.0)*1.18;float nz=sqrt(max(.03,1.0-dot(xy,xy))); vec3 N=normalize(cotangent(normalize(vNrm),vWorld,vUV)*vec3(xy,nz)); /* SRGB8_ALPHA8 yields LINEAR albedo (AO in alpha stays linear). FS3D and terrain light DISPLAY albedo — RGBA8 atlas/canvas * linearized sun, then 1-exp(-lit*1.55) with no gamma encode. Feeding linear alb into that curve parked hulls in the filmic toe (near-black silhouettes) while procedural window RGB, already display values, still punched. Encode into the production lighting space; keep the shared filmic write so SSAO and multiply-shadows share one curve with units and ground. */ vec3 alb=pow(max(ba.rgb,vec3(0.0)),vec3(1.0/2.2)); float ao=pow(clamp(ba.a,.06,1.0),1.42),rough=clamp(nr.b,.10,1.0),metal=mk.r,emis=nr.a; float micro=detailData-.5;ao=clamp(ao-micro*.035,0.0,1.0);rough=clamp(rough-micro*.075,.08,1.0); /* Civilian blocks are weathered warm concrete and ceramic; military plant is cold steel with denser soot. This is family grading, not a paint wash, so authored roof/window/material separation survives. */ float grime=mk.a*(uFamily<.5?.42:.72); alb*=uFamily<.5?vec3(.92,.88,.82):vec3(.82,.86,.90); alb=mix(alb,alb*vec3(.32,.29,.25),grime); /* Battle-scale V2 still needs authored age when the structure is healthy. Reuse the shared object-space surface tile at low strength: broad faces stay quiet, while raised normal detail and sparse rubbed edges keep the building from reading as a clean grey cut-out against weathered terrain. */ float relief=clamp(length(xy),0.0,1.0),weather=smoothstep(.46,.80,damageData); float aluma=dot(alb,vec3(.2126,.7152,.0722)); float authoredEdge=smoothstep(.018,.085,length(vec2(dFdx(aluma),dFdy(aluma)))); float edgeWear=max(relief*weather,authoredEdge*.72)*(1.0-grime); alb*=.96+ao*.08; alb=mix(alb,uFamily<.5?vec3(.43,.44,.42):vec3(.38,.42,.46),edgeWear*.31); rough=clamp(rough+grime*.18,0.0,1.0); rough=clamp(rough+(weather-.5)*.10-edgeWear*.17-(uFamily<.5?0.0:1.0)*metal*.08,0.0,1.0); /* Authored emissive maps are mostly black on the legacy rebuilds. Walls get occupied windows; caps keep roof beacons — a command camera never sees the facade, so facade-only lights made every HQ a silhouette. */ vec3 on=abs(vObjNrm.y)<.22&&max(abs(vObjNrm.x),abs(vObjNrm.z))>.85?vec3(1.0):vec3(0.0); float roofOn=step(.55,vObjNrm.y); vec2 face=abs(vObjNrm.x)>abs(vObjNrm.z)?vec2(vObj.z,vObj.y):vec2(vObj.x,vObj.y); vec2 cell=face*vec2(.072,.098),f=fract(cell),id=floor(cell); float pane=step(.24,f.x)*step(f.x,.76)*step(.24,f.y)*step(f.y,.69); float occupied=step(.78,hash21(id+vec2(uRole*19.0,7.0))); float civilian=(uFamily<.5?1.0:0.0); /* CivicBlock already has authored window/emissive strips. Procedural panes are reserved for the otherwise-unlit tower, and only above its service plinth. Applying them to every civil mesh produced the blue circuit-board wash that the V2 material system was specifically meant to replace. */ float tower=float(uRole<.5),window=on.x*pane*occupied*civilian*tower*step(8.0,vObj.y); float signBand=on.x*step(.46,f.y)*step(f.y,.55)*step(.18,f.x)*step(f.x,.82); float signGate=step(.90,hash21(floor(face*vec2(.025,.041))+vec2(uRole*3.1,2.7))); float sign=signBand*signGate*float(uRole<.5||uRole>3.5); float warning=on.x*(1.0-civilian)*step(.80,f.x)*step(.78,f.y)*step(.955,hash21(id+9.4)); /* Industrial halls and tank farms need powered service detail as much as towers need windows, but cool cyan strips made every faction read Nova. Sparse amber loading-bay lamps retain the brutalist military character and give the facade a useful scale cue without becoming neon trim. */ float industrial=float(uRole>1.5&&uRole<3.5); float service=on.x*industrial*step(.35,f.x)*step(f.x,.65)*step(.70,f.y)*step(.91,hash21(id+vec2(4.2,11.7))); /* Night-only occupancy flicker. A noon sin() on every pane read as shader strobe; daylight windows stay static. */ float winPulse=mix(1.0,0.93+0.07*sin(uTime*1.15+id.x*1.7+id.y),uNight); vec3 emissive=vec3(.30,.74,.92)*((window*winPulse)*.30+sign*.42)+vec3(1.0,.20,.025)*warning*.86; emissive+=vec3(1.0,.42,.085)*service*.56; /* Roof lamps are DISCS, not hash cells. floor(xz*0.07) lit a ~14-unit tile and bloomed into a white halo on every command-camera cap. */ vec2 roofCell=fract(vObj.xz*0.34); float roofId=hash21(floor(vObj.xz*0.34)+vec2(uRole*5.0,4.2)); float roofLite=roofOn*step(.96,roofId)*step(length(roofCell-vec2(.5)),.10); float lampPulse=mix(1.0,0.90+0.10*sin(uTime*1.35+roofId*8.0),uNight); emissive+=vec3(.32,.78,.95)*roofLite*.40*lampPulse; /* Authored emis on facades and the tiny roof discs — never a whole cap. Sampling NRE.a across roofOn turned neighbouring atlas texels into glowing tiles. */ emissive+=mix(vec3(.22,.63,.84),vec3(1.0,.18,.018),uFamily)*emis*.42*(on.x+roofLite); /* Damage carbonises the authored material, destroys polish and turns off facade systems. Wreck particles are a separate layer; the surface itself must stop looking like intact glossy paint before flames appear. */ float chip=smoothstep(.36,.84,hash21(floor(face*.31)+floor(vObj.y*.17))); float charMask=clamp(vDamage*(.62+.38*damageCrust+.18*chip)+grime*vDamage*.50,0.0,1.0); alb=mix(alb,vec3(.018,.015,.013),charMask*.94); rough=mix(rough,.985,charMask);metal=mix(metal,0.0,charMask*.94); emissive*=1.0-smoothstep(.10,.58,vDamage); /* Towers must scorch as soon as they are under fire. A crack-only mask stayed black when the carbon tile was quiet, so civic/defense blocks looked intact until they popped. */ float crack=mix(damageCrack,max(damageCrack,.55*tower),.80); /* Dead towers used to lose the hot term at vDamage=1, so a collapsed civic block read as intact grey. Keep embers on the wreck. */ float wreck=smoothstep(.88,1.0,vDamage); float hot=smoothstep(.10,.46,vDamage)*crack*(1.0-wreck*0.22)+wreck*crack*0.62; alb=mix(alb,vec3(.28,.020,.002),hot*.78); emissive+=vec3(1.0,.075,.003)*hot*2.25+vec3(1.0,.42,.035)*smoothstep(.42,.80,damageData)*hot*1.05; vec3 V=normalize(uEye-vWorld),L=normalize(uSun),H=normalize(V+L); float ndl=max(dot(N,L),0.0),ndv=max(dot(N,V),.001),ndh=max(dot(N,H),.001),vdh=max(dot(V,H),0.0); float a=rough*rough,a2=a*a,den=ndh*ndh*(a2-1.0)+1.0; float D=a2/max(3.14159265*den*den,.001),k=(rough+1.0)*(rough+1.0)*.125; float gv=ndv/(ndv*(1.0-k)+k),gl=ndl/(ndl*(1.0-k)+k); vec3 f0=mix(vec3(.04),alb,metal),F=f0+(1.0-f0)*pow(1.0-vdh,5.0); vec3 spec=D*gv*gl*F/max(4.0*ndl*ndv,.001); /* Same command-camera rule as FS3D: (1-metal) kd made steel civic blocks ambient-only grey. Do not divide Lambert by PI — production exposure was authored without it (the lab shader does, for one showcase object). */ vec3 kd=(1.0-F)*mix(1.0,0.55,metal),amb=mix(uAmbGnd,uAmbSky,N.y*.5+.5); float wrap=dot(N,uSun)*0.5+0.5; float sunLen=max(length(vec2(uSun.x,uSun.z)),1e-5); float sunSide=dot(vec2(N.x,N.z),vec2(uSun.x,uSun.z)/sunLen); ndl=min(1.0,ndl+max(sunSide,0.0)*0.32*(N.y*0.5+0.5)); /* Lambert sun, not GGX sheet-spec: overhead ndv≈1 made civic roofs one pale value. Fade spec on camera-facing caps; keep ndl for day/night. */ float specAtten=mix(1.0,0.18,smoothstep(0.60,0.94,ndv)); /* Same sun/ambient weights as FS3D. The 0.68 ambient scale parked civic blocks in the filmic toe (black hulls) next to correctly exposed units. Overhead ndv lift matches FS3D so HIGH civic HQs are not 0.62-crushed. */ float metalLift=mix(1.0, mix(0.70, 0.90, smoothstep(0.50, 0.94, ndv)), metal); vec3 lit=alb*metalLift*(amb*ao*0.82+uSunC*(ndl*0.92+max(wrap,0.0)*0.10*ao)) +spec*specAtten*uSunC*ndl; /* Eight camera-relevant sources are already selected for the production forward renderer. Reusing that list gives powered windows/signage actual environmental influence without turning every window into a GPU light. */ for(int i=0;i<8;i++){ if(i>=uLightCount)break; vec3 d=uLightPosR[i].xyz-vWorld;float dist=length(d),att=pow(clamp(1.0-dist/uLightPosR[i].w,0.0,1.0),2.0); vec3 ll=d/max(dist,.001);float nll=max(dot(N,ll),0.0); lit+=(alb*(1.0-metal)*.34+f0*.20)*uLightColI[i].rgb*uLightColI[i].a*nll*att; } vec3 env=mix(uAmbGnd,uAmbSky,clamp(N.y*.5+.5,0.0,1.0)); lit+=env*(f0+(1.0-f0)*pow(1.0-ndv,5.0))*(1.0-rough)*.28*ao*(1.0-charMask); /* Sharp window/engine lights at noon. 0.34 assumed bloom would carry them and they vanished at thresh 0.86. 0.70 keeps charcoal facades and lets the panes punch without a bloom soup. */ float fowA=texture(uFowMap,vFowUV).a*uFowOn; /* Civic blocks used to skip the sensor map, so a city on fogged ground kept daylight hulls — the same glowing-pieces bug the model shader already fixed. Dim lamps with the shroud, then veil only un-fogged faces. */ lit+=emissive*(.95+uNight*.50)*(1.0-fowA); lit=mix(lit,mix(uAmbGnd*0.10,uFogC*0.20,0.5),fowA); lit=mix(lit,uFogC,vFog*(1.0-fowA)); /* Same display contract as FS3D / terrain: filmic roll-off, no extra pow(1/2.2) on the output. Albedo was lifted into that space above. */ lit=vec3(1.0)-exp(-max(lit,vec3(0.0))*1.55); o=vec4(clamp(lit,vec3(0.0),vec3(1.0)),1.0); }`; class MFWorldV2InstMesh extends InstMesh{ constructor(gl,geo,cap,def){super(gl,geo,cap);this.def=def;} flushWorld(){ if(!this.n)return; const g=this.gl,D=this.def; g.uniform4fv(mfWorld2U.uRect,D.rect);g.uniform1f(mfWorld2U.uFamily,D.family);g.uniform1f(mfWorld2U.uRole,D.role); g.bindVertexArray(this.vao);g.bindBuffer(g.ARRAY_BUFFER,this.ivb); g.bufferSubData(g.ARRAY_BUFFER,0,this.data,0,this.n*INST_FLOATS); g.drawElementsInstanced(g.TRIANGLES,this.count,g.UNSIGNED_SHORT,0,this.n); drawCalls++;triCount+=this.count/3*this.n;this.n=0; } } function mfWorld2TexScratch(fn){ /* Civic maps decode after boot, often mid-match. Binding on the active unit (TEXTURE0 / atlas) then leaving it there made every hull sample a 1x1 placeholder for a frame. Unit 7 is the shared scratch (ads, detail); restore the previous 7-binding, never bindTexture(null), never unit 0. */ const was=gl.getParameter(gl.ACTIVE_TEXTURE); gl.activeTexture(gl.TEXTURE7); const prev=gl.getParameter(gl.TEXTURE_BINDING_2D); fn(); gl.bindTexture(gl.TEXTURE_2D,prev); gl.activeTexture(was); } function mfWorld2Placeholder(r,g,b,a){ const t=gl.createTexture(); mfWorld2TexScratch(()=>{ gl.bindTexture(gl.TEXTURE_2D,t); gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA8,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([r,g,b,a])); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR); }); return t; } function mfWorld2Load(tex,file,srgb,epoch,repeat){ const img=new Image();img.onload=()=>{ if(epoch!==glEpoch)return; const align=gl.getParameter(gl.UNPACK_ALIGNMENT),flip=gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL); mfWorld2TexScratch(()=>{ gl.bindTexture(gl.TEXTURE_2D,tex);gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,false);gl.pixelStorei(gl.UNPACK_ALIGNMENT,1); gl.texImage2D(gl.TEXTURE_2D,0,srgb?gl.SRGB8_ALPHA8:gl.RGBA8,gl.RGBA,gl.UNSIGNED_BYTE,img); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR_MIPMAP_LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,repeat?gl.REPEAT:gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,repeat?gl.REPEAT:gl.CLAMP_TO_EDGE); gl.generateMipmap(gl.TEXTURE_2D); if(typeof mfAniso==='function') mfAniso(); else { const an=gl.getExtension('EXT_texture_filter_anisotropic'); if(an)gl.texParameterf(gl.TEXTURE_2D,an.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(4,gl.getParameter(an.MAX_TEXTURE_MAX_ANISOTROPY_EXT))); } gl.pixelStorei(gl.UNPACK_ALIGNMENT,align);gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,flip); }); mfWorld2Loaded++;mfWorld2Ready=mfWorld2Loaded===5&&!mfWorld2Error; }; img.onerror=()=>{mfWorld2Error=true;mfWorld2Ready=false;console.warn('[WorldV2] texture unavailable:',file);}; img.src='./assets/textures/materials/'+file; } function mfWorld2Init(){ mfWorld2Ready=false;mfWorld2Loaded=0;mfWorld2Error=false;mfWorld2Epoch=glEpoch; mfWorld2Prog=mkProg( MFWORLD2_VS.replace(/MAPSIZE_CONST/g,MAP.toFixed(1)).replace(/BFOG_CONST/g,'430.0'), MFWORLD2_FS,'world-structures-v2');if(!mfWorld2Prog){mfWorld2Error=true;return;} for(const k of ['uVP','uEye','uHazeQ','uRect','uBaseAO','uNRE','uMasks','uDamageTex','uDetail','uFowMap','uFowOn','uSun','uSunC','uAmbSky','uAmbGnd','uFogC','uNight','uTime','uFamily','uRole','uLightCount']) mfWorld2U[k]=gl.getUniformLocation(mfWorld2Prog,k); mfWorld2U.uLightPosR=gl.getUniformLocation(mfWorld2Prog,'uLightPosR[0]');mfWorld2U.uLightColI=gl.getUniformLocation(mfWorld2Prog,'uLightColI[0]'); mfWorld2BaseAO=mfWorld2Placeholder(128,128,128,255);mfWorld2NRE=mfWorld2Placeholder(128,128,220,0);mfWorld2Masks=mfWorld2Placeholder(0,0,0,0); mfWorld2DamageTex=mfWorld2Placeholder(22,22,22,255); mfWorld2Detail=mfWorld2Placeholder(128,128,128,255); /* The atlas PNG stores display/sRGB albedo in RGB and linear AO in alpha. Loading it as plain RGBA treated encoded mid-grey as linear light, then linearToSrgb() lifted it a second time: roofs clipped pale and baked AO disappeared. SRGB8_ALPHA8 decodes RGB only; alpha remains linear, which is exactly this packed map's contract. */ mfWorld2Load(mfWorld2BaseAO,'mf-world-structures-v2-baseao.png',true,mfWorld2Epoch); mfWorld2Load(mfWorld2NRE,'mf-world-structures-v2-nre.png',false,mfWorld2Epoch); mfWorld2Load(mfWorld2Masks,'mf-world-structures-v2-masks.png',false,mfWorld2Epoch); mfWorld2Load(mfWorld2DamageTex,'mf2-carbon-cracks-v1.png',false,mfWorld2Epoch,true); mfWorld2Load(mfWorld2Detail,'mf_mechanical_microdetail_v2.webp',false,mfWorld2Epoch,true); } function mfWorldV2Enabled(){ /* HIGH/CINEMATIC only. Mid used to keep this path so civic hulls would not "look like a missing shader", but the PBR civic pass is HIGH-class work (extra maps, 8-light loop, detail on unit 7). MEDIUM falls back to the production atlas — still a building, much cheaper. */ const G=typeof GFX!=='undefined'?GFX:{}; if(G.worldV2===false) return false; const q=typeof qualityKey==='function'?qualityKey():'high'; return !MFWORLD2_DISABLED&&q!=='low'&&mfWorld2Ready&&mfWorld2Epoch===glEpoch; } function mfWorldV2Queue(R,scale,height,lod){ /* V2 is the tactical/showcase material. At strategic range its normal, mask, damage and eight-light reads collapse into a few pixels, so the caller deliberately falls back to the legacy one-atlas material. That is material LOD, not an asset failure, and keeps city density scalable. */ if(!mfWorldV2Enabled()||(lod||2)<2)return false; const M=mfWorld2Meshes[R.kind];if(!M)return false; const health=R.hpm>0?clamp(R.hp/Math.max(1,R.hpm),0,1):0; M.add(R.x,R.y,height,scale*(R.kind===0?.9:1),R.a+(R.lean||0)+(R.alive?0:0.42),255,255,255,255,undefined,R.alive?1-health:1); return true; } function mfWorldV2Flush(S,nA,time){ if(!mfWorldV2Enabled())return false; const wasBlend=gl.isEnabled(gl.BLEND),wasCull=gl.isEnabled(gl.CULL_FACE),wasDepth=gl.isEnabled(gl.DEPTH_TEST),wasMask=gl.getParameter(gl.DEPTH_WRITEMASK); gl.useProgram(mfWorld2Prog);gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.disable(gl.BLEND);gl.depthMask(true); gl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D,mfWorld2BaseAO); gl.activeTexture(gl.TEXTURE2);gl.bindTexture(gl.TEXTURE_2D,mfWorld2NRE); gl.activeTexture(gl.TEXTURE3);gl.bindTexture(gl.TEXTURE_2D,mfWorld2Masks); gl.activeTexture(gl.TEXTURE1);gl.bindTexture(gl.TEXTURE_2D,mfWorld2DamageTex); /* Unit 7 is outside the 4/5/6 post chain and is rebound by begin3D after the world pass, so detailed civic materials cannot corrupt AO or bloom. */ gl.activeTexture(gl.TEXTURE7);gl.bindTexture(gl.TEXTURE_2D,mfWorld2Detail); gl.uniform1i(mfWorld2U.uBaseAO,0);gl.uniform1i(mfWorld2U.uNRE,2);gl.uniform1i(mfWorld2U.uMasks,3);gl.uniform1i(mfWorld2U.uDamageTex,1);gl.uniform1i(mfWorld2U.uDetail,7); /* Unit 8 is the live sensor map in the model pass. Terrain rebinds it to splat art, so this flush must put fogTex back before sampling. */ const fowReady=typeof fogGameplayActive==='function'&&fogGameplayActive()&&!demoMode&&typeof fogTex!=='undefined'&&fogTex; gl.activeTexture(gl.TEXTURE8);gl.bindTexture(gl.TEXTURE_2D,fowReady?fogTex:mfWorld2BaseAO); if(mfWorld2U.uFowMap!=null){gl.uniform1i(mfWorld2U.uFowMap,8);gl.uniform1f(mfWorld2U.uFowOn,fowReady?1:0);} gl.uniformMatrix4fv(mfWorld2U.uVP,false,matVP);gl.uniform3f(mfWorld2U.uEye,eyeX,eyeY,eyeZ); if(mfWorld2U.uHazeQ) gl.uniform1f(mfWorld2U.uHazeQ, typeof mfHazeQ==='function'?mfHazeQ():1); gl.uniform3f(mfWorld2U.uSun,S.dir[0],S.dir[1],S.dir[2]); let c=_lin(S.col);gl.uniform3f(mfWorld2U.uSunC,c[0],c[1],c[2]);c=_lin(S.sky);gl.uniform3f(mfWorld2U.uAmbSky,c[0],c[1],c[2]); c=_lin(S.gnd);gl.uniform3f(mfWorld2U.uAmbGnd,c[0],c[1],c[2]);c=_lin(S.fog);gl.uniform3f(mfWorld2U.uFogC,c[0],c[1],c[2]); gl.uniform1f(mfWorld2U.uNight,nA);gl.uniform1f(mfWorld2U.uTime,time);gl.uniform1i(mfWorld2U.uLightCount,_sceneLightN); if(_sceneLightN){gl.uniform4fv(mfWorld2U.uLightPosR,_sceneLightPR);gl.uniform4fv(mfWorld2U.uLightColI,_sceneLightCI);} for(const D of MFWORLD2_DEFS){const M=mfWorld2Meshes[D.kind];if(M)M.flushWorld();} if(wasBlend)gl.enable(gl.BLEND);else gl.disable(gl.BLEND);if(wasCull)gl.enable(gl.CULL_FACE);else gl.disable(gl.CULL_FACE); if(wasDepth)gl.enable(gl.DEPTH_TEST);else gl.disable(gl.DEPTH_TEST);gl.depthMask(wasMask);gl.activeTexture(gl.TEXTURE0); begin3D(nA);return true; } /* Take over after models-civic so the old world meshes remain present as a runtime fallback. V2 owns separate authored-UV VAOs and shared textures. */ const mfWorld2BaseInitModels=initModels; initModels=function(){ mfWorld2BaseInitModels();mfWorld2Init();mfWorld2Meshes={}; for(const D of MFWORLD2_DEFS){const geo=loadWorldModel(D.key,true);if(geo)mfWorld2Meshes[D.kind]=new MFWorldV2InstMesh(gl,geo,420,D);} }; if(typeof window!=='undefined')window.MFWorldStructuresV2={ status:()=>({enabled:mfWorldV2Enabled(),ready:mfWorld2Ready,error:mfWorld2Error,loaded:mfWorld2Loaded,epoch:mfWorld2Epoch}), definitions:MFWORLD2_DEFS };