Spaces:
Running
Running
File size: 11,059 Bytes
b51e03a 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | ;
;
/* ============================================================================
BILLBOARD LAYER
----------------------------------------------------------------------------
Not everything should be geometry. Smoke, fire, muzzle flash, energy glow and
dust have no surface to model β they're volumes of light, and the old
procedural sprite atlas already renders them better than any polygon shell
could. Drawing them as camera-facing textured quads is also several times
cheaper: one quad instead of a 100-triangle sphere, and it stays one draw
call for the whole battlefield.
These are NOT the old fake-3D billboards. They live in the same depth buffer
as everything else, expanded around a real world-space anchor point using the
camera's right/up axes, so a plume behind a hill is correctly occluded by
that hill.
Instance layout: x,y,h, size, rot, r,g,b,a, u0,v0,u1,v1, aspect = 14 floats.
============================================================================ */
const BB_STRIDE=14*4;
const VSBB=`#version 300 es
layout(location=0) in vec2 aCorner;
layout(location=5) in vec4 aPS; // xyz world anchor, w = size
layout(location=6) in float aRot;
layout(location=7) in vec4 aCol;
layout(location=8) in vec4 aUVR; // u0,v0,u1,v1
layout(location=9) in float aAspect; // width / height
uniform mat4 uVP;
uniform vec2 uScale; // world-size to clip-space, x/y
out vec2 vUV; out vec4 vCol;
void main(){
float c=cos(aRot), s=sin(aRot);
vec2 q=vec2(aCorner.x*c - aCorner.y*s, aCorner.x*s + aCorner.y*c)*aPS.w;
q.x*=aAspect;
/* Expand in clip space. The previous world-basis expansion was mathematically
valid but produced driver-dependent horizontal streaks for instanced quads
in very large battles. The anchor is still a genuine 3D point (and keeps
its depth); only the camera-facing corners are screen-space, which is the
conventional stable billboard formulation. */
vec4 p=uVP*vec4(aPS.xyz,1.0);
/* w<=0 is behind/on the near plane. Clip-space expansion then divides by a
tiny or negative w and the quad becomes a full-screen streak β the same
visual as the old instanced TRIANGLE_STRIP join. */
if(p.w<=0.02){ gl_Position=vec4(2.0,2.0,2.0,1.0); vUV=vec2(0.0); vCol=vec4(0.0); return; }
p.xy+=q*uScale*p.w;
vUV=mix(aUVR.xy,aUVR.zw,aCorner+0.5);
vCol=aCol;
gl_Position=p;
}`;
const FSBB=`#version 300 es
precision highp float;
in vec2 vUV; in vec4 vCol;
uniform sampler2D uTex;
out vec4 o;
void main(){
vec4 t=texture(uTex,vUV);
o=vec4(t.rgb*vCol.rgb, t.a*vCol.a);
if(o.a<0.004) discard;
}`;
let progBB=null, UBB={};
class BBBatch{
constructor(gl,cap){
this.gl=gl; this.cap=cap; this.n=0;
this.data=new Float32Array(cap*14);
this.vao=gl.createVertexArray();
gl.bindVertexArray(this.vao);
const qb=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,qb);
/* Two explicit triangles, not an instanced TRIANGLE_STRIP. Some mobile and
browser WebGL drivers incorrectly carry strip connectivity across
instances, joining distant sprites with full-screen streaks. Six corner
vertices keep the same one-draw-call instancing without that ambiguity. */
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([
-.5,-.5, .5,-.5, .5,.5,
-.5,-.5, .5,.5, -.5,.5
]),gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0,2,gl.FLOAT,false,8,0);
/* Indexed triangles, not drawArraysInstanced. Some D3D11 translation
layers still stitch sequential instanced arrays as a strip. The element
buffer is captured on this VAO. */
const ib=gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ib);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array([0,1,2,3,4,5]),gl.STATIC_DRAW);
this.ivb=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,this.ivb);
gl.bufferData(gl.ARRAY_BUFFER,this.data.byteLength,gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(5); gl.vertexAttribPointer(5,4,gl.FLOAT,false,BB_STRIDE,0); gl.vertexAttribDivisor(5,1);
gl.enableVertexAttribArray(6); gl.vertexAttribPointer(6,1,gl.FLOAT,false,BB_STRIDE,16); gl.vertexAttribDivisor(6,1);
gl.enableVertexAttribArray(7); gl.vertexAttribPointer(7,4,gl.FLOAT,false,BB_STRIDE,20); gl.vertexAttribDivisor(7,1);
gl.enableVertexAttribArray(8); gl.vertexAttribPointer(8,4,gl.FLOAT,false,BB_STRIDE,36); gl.vertexAttribDivisor(8,1);
gl.enableVertexAttribArray(9); gl.vertexAttribPointer(9,1,gl.FLOAT,false,BB_STRIDE,52); gl.vertexAttribDivisor(9,1);
gl.bindVertexArray(null);
}
grow(){
/* MEDIUM starts the ring small. HIGH allocates the full fight budget
up front so a flagship barrage never reallocates mid-frame. */
if(this.cap>=90000) return false;
const next=Math.min(90000,this.cap<4096?this.cap*2:Math.ceil(this.cap*1.5));
const data=new Float32Array(next*14); data.set(this.data);
this.data=data; this.cap=next;
const gl=this.gl;
gl.bindBuffer(gl.ARRAY_BUFFER,this.ivb);
gl.bufferData(gl.ARRAY_BUFFER,this.data.byteLength,gl.DYNAMIC_DRAW);
return true;
}
add(uv,x,y,h,size,rot,r,g,b,a){
if(this.n>=this.cap&&!this.grow()) return;
if(!_bbAccept(x,y,size,size,a)) return;
const o=this.n*14, d=this.data;
d[o]=x; d[o+1]=h; d[o+2]=y; d[o+3]=size;
/* Keep particle rotation in a compact positive range. Several effects use
the animation clock when they spin; after a long-running session those
angles used to fall below the negative rectangle sentinel and the
shader mistook a flame/smoke sprite for a kilometre-wide health bar. */
const rawRot=rot||0;
d[o+4]=((rawRot%TAU)+TAU)%TAU;
d[o+5]=r/255; d[o+6]=g/255; d[o+7]=b/255; d[o+8]=(a===undefined?255:a)/255;
d[o+9]=uv[0]; d[o+10]=uv[1]; d[o+11]=uv[2]; d[o+12]=uv[3];
d[o+13]=1;
this.n++;
}
addRect(uv,x,y,h,width,height,r,g,b,a){
this.addOrientedRect(uv,x,y,h,width,height,0,r,g,b,a);
}
addOrientedRect(uv,x,y,h,width,height,rot,r,g,b,a){
if(width<=0||height<=0) return;
if(this.n>=this.cap&&!this.grow()) return;
if(!_bbAccept(x,y,width,height,a)) return;
const o=this.n*14, d=this.data;
d[o]=x; d[o+1]=h; d[o+2]=y; d[o+3]=height;
const rawRot=rot||0;
d[o+4]=((rawRot%TAU)+TAU)%TAU;
d[o+5]=r/255; d[o+6]=g/255; d[o+7]=b/255; d[o+8]=(a===undefined?255:a)/255;
d[o+9]=uv[0]; d[o+10]=uv[1]; d[o+11]=uv[2]; d[o+12]=uv[3];
d[o+13]=width/height;
this.n++;
}
flush(gl){
if(!this.n) return;
gl.bindVertexArray(this.vao);
gl.bindBuffer(gl.ARRAY_BUFFER,this.ivb);
/* Offset/length form β subarray() allocated a view per flush, every frame. */
gl.bufferSubData(gl.ARRAY_BUFFER,0,this.data,0,this.n*14);
gl.drawElementsInstanced(gl.TRIANGLES,6,gl.UNSIGNED_SHORT,0,this.n);
drawCalls++; triCount+=2*this.n;
this.n=0;
}
}
let bbAdd=null, bbAlpha=null;
/* Frustum + screen-footprint gate, refreshed once per beginBB. Local constants
keyed off the existing quality name β meta.js is not the owner of draw cost.
HIGH/CINEMATIC only drop invisible (sub-pixel / offscreen) work. */
let _bbX0=0,_bbX1=0,_bbY0=0,_bbY1=0,_bbMinPx=0.65,_bbWpx=1,_bbReady=0;
let _bbSub=0,_bbBudget=24000,_bbMinPxSoft=1.8;
function _bbGfxKey(){
if(typeof qualityKey==='function') return qualityKey();
const q=typeof META!=='undefined'&&META.settings&&META.settings.quality;
return q==='low'||q==='medium'||q==='cinematic'?q:'high';
}
function _bbSyncCull(){
const asp=VW/Math.max(1,VH);
const hh=orthoSpan*0.5, hw=hh*asp;
const pitch=typeof camPitch==='number'?camPitch:1.19;
const yaw=typeof camYaw==='number'?camYaw:0;
const depth=hh/Math.max(0.30,Math.sin(pitch));
const c=Math.abs(Math.cos(yaw)), s=Math.abs(Math.sin(yaw));
const ex=hw*c+depth*s+90, ey=hw*s+depth*c+90;
_bbX0=cam.x-ex; _bbX1=cam.x+ex; _bbY0=cam.y-ey; _bbY1=cam.y+ey;
_bbWpx=Math.max(1,VH)/Math.max(1,orthoSpan);
const q=_bbGfxKey();
/* MEDIUM drops sparks that cover <2 CSS px. HIGH still submits those; they
are the close-up punch the preset promises. */
_bbMinPx=q==='low'?3.4:q==='medium'?2.1:q==='cinematic'?0.40:0.65;
/* Soft fillrate budget after the hard pixel gate. HIGH/CINEMATIC sit above
any real fight (health bars + smoke). MEDIUM starts dropping extra
sub-6px sparks once ~3.2k quads are already in β bars and icons stay. */
_bbBudget=q==='low'?1400:q==='medium'?3200:q==='cinematic'?1e9:24000;
_bbMinPxSoft=_bbMinPx*2.8;
_bbSub=0;
_bbReady=1;
}
function _bbAccept(x,y,w,h,a){
if(!_bbReady) _bbSyncCull();
const aa=a===undefined?255:a;
if(aa<4) return false;
const pad=w>h?w:h;
if(x<_bbX0-pad||x>_bbX1+pad||y<_bbY0-pad||y>_bbY1+pad) return false;
const px=pad*_bbWpx;
if(px<_bbMinPx) return false;
if(_bbSub>=_bbBudget&&px<_bbMinPxSoft) return false;
_bbSub++;
return true;
}
function initBillboards(){
progBB=mkProg(VSBB,FSBB);
UBB.uVP=gl.getUniformLocation(progBB,'uVP');
UBB.uScale=gl.getUniformLocation(progBB,'uScale');
UBB.uTex=gl.getUniformLocation(progBB,'uTex');
/* HIGH keeps the large ring so a flagship fight never reallocates.
MEDIUM starts smaller β add() already drops offscreen / sub-pixel β
and grow() only if a barrage actually fills it. */
const q=_bbGfxKey();
bbAdd=new BBBatch(gl,q==='low'?2400:q==='medium'?3600:9000);
bbAlpha=new BBBatch(gl,q==='low'?16000:q==='medium'?24000:72000);
}
/* Orthographic world-size to clip-size. X includes the viewport aspect ratio;
the anchor projection already contains camera orbit and terrain depth. */
/* `tex` lets a caller run this same program over a different sheet β the
tactical icons live in their own atlas because the shared one is exactly
full. Omitting it keeps the original behaviour for every existing caller. */
function beginBB(tex){
if(typeof MF_PROG_MODEL!=='undefined') MF_PROG_MODEL=false;
_bbSyncCull();
gl.useProgram(progBB);
gl.uniformMatrix4fv(UBB.uVP,false,matVP);
const asp=VW/Math.max(1,VH);
gl.uniform2f(UBB.uScale,2/(Math.max(1,orthoSpan)*asp),2/Math.max(1,orthoSpan));
gl.uniform1i(UBB.uTex,0);
/* Unit 0 is the material atlas for models. Binding a different sheet here is
required (the icon atlas is a second texture) but MUST stay on 0 only for
this program β post-process lives on 4/5/6 and must never be borrowed.
Callers restore the model atlas with begin3D(S_nA) after the flush. */
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D,tex||atlasTex);
/* InstMesh opaque flushes re-enable CULL. Billboard quads are not closed
meshes β a leftover CULL_FACE ate engine glow and crystal halos. */
gl.disable(gl.CULL_FACE);
gl.enable(gl.BLEND);
/* Additive and alpha batches share this program. A leftover DEPTH_WRITEMASK
from an opaque pass would punch holes through smoke; the icon pass in
render3d already clears it, but every other beginBB caller was relying on
the previous pass having done the same. */
gl.depthMask(false);
}
|