Compile the city's shader features only into the city program

Landscape 2D/3D shared one fragment shader with the city's fog, window
and beacon code. Building two variants from the same source with a
CITY define brought 3D frame cost back near its pre-city level in the
profiling setup (native 708 -> ~300 ms/frame under software GL, JS and
canvas 71 -> ~33 ms/frame).
This commit is contained in:
Andy Dixon
2026-06-06 12:21:04 +01:00
parent dd8976b13c
commit 4468afec8e
+42 -36
View File
@@ -64,44 +64,50 @@ void main(){
vColor = vec4(aColorHeight.rgb, 1.0); vColor = vec4(aColorHeight.rgb, 1.0);
vUnit = aUnit; vUnit = aUnit;
vAtlas = aAtlas.xy + aUnit.xy * aAtlas.zw; vAtlas = aAtlas.xy + aUnit.xy * aAtlas.zw;
vSize = vec3(aRect.zw, height);
vFace = gl_VertexID / 6; vFace = gl_VertexID / 6;
vKind = ivec2(kind, int(aMeta.w + .5)); vKind = ivec2(kind, int(aMeta.w + .5));
vSize = vec3(aRect.zw, height);
vLit = aMeta.yz; vLit = aMeta.yz;
gl_Position = projectWorld(world); gl_Position = projectWorld(world);
vDepth = gl_Position.w; vDepth = gl_Position.w;
}`; }`;
const FS = `#version 300 es // Two variants from one source: the landscape build compiles none of the city's fog, window
// and beacon code, which otherwise tripled fragment cost in software renderers.
const fragmentShader = city => `#version 300 es
precision highp float; precision highp float;
${city ? '#define CITY 1' : ''}
in vec4 vColor; in vec4 vColor;
in vec3 vUnit; in vec3 vUnit;
in vec2 vAtlas; in vec2 vAtlas;
in vec3 vSize;
in float vDepth;
flat in int vFace; flat in int vFace;
flat in ivec2 vKind; flat in ivec2 vKind;
flat in vec2 vLit;
uniform float uMode; uniform float uMode;
uniform float uAlpha; uniform float uAlpha;
uniform sampler2D uCode; uniform sampler2D uCode;
uniform float uCodeOn; uniform float uCodeOn;
out vec4 outColor;
#ifdef CITY
in vec3 vSize;
in float vDepth;
flat in vec2 vLit;
uniform float uFogDensity; uniform float uFogDensity;
uniform vec3 uFogColor; uniform vec3 uFogColor;
out vec4 outColor;
float hash(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); } float hash(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }
vec3 fog(vec3 color){ vec3 fog(vec3 color){ return mix(color, uFogColor, 1.0 - exp(-pow(vDepth * uFogDensity, 2.0))); }
float amount = 1.0 - exp(-pow(vDepth * uFogDensity, 2.0)); #else
return mix(color, uFogColor, amount); vec3 fog(vec3 color){ return color; }
} #endif
void main(){ void main(){
#ifdef CITY
if(vKind.x == 3){ if(vKind.x == 3){
// Search beacon: a light column fading upward, open at the top. // Search beacon: a light column fading upward, open at the top.
if(vFace == 0 || vFace == 5) discard; if(vFace == 0 || vFace == 5) discard;
float fade = pow(1.0 - vUnit.z, 1.6); outColor = vec4(vColor.rgb * pow(1.0 - vUnit.z, 1.6) * .55, 0.0);
outColor = vec4(vColor.rgb * fade * .55, 0.0);
return; return;
} }
if(vKind.x == 2){ outColor = vec4(fog(vec3(.03, .036, .052)), uAlpha); return; } // asphalt
#endif
if(uMode < .5 && vFace > 0) discard; if(uMode < .5 && vFace > 0) discard;
vec2 face = vFace == 0 || vFace == 5 ? vUnit.xy : vFace < 3 ? vUnit.xz : vUnit.yz; vec2 face = vFace == 0 || vFace == 5 ? vUnit.xy : vFace < 3 ? vUnit.xz : vUnit.yz;
float edge = min(min(face.x, 1.0-face.x), min(face.y, 1.0-face.y)); float edge = min(min(face.x, 1.0-face.x), min(face.y, 1.0-face.y));
@@ -109,13 +115,11 @@ void main(){
// Tile tops match TILE_TOP in app.js so source drawn over a roof blends in; 2D gets a faint layer tint. // Tile tops match TILE_TOP in app.js so source drawn over a roof blends in; 2D gets a faint layer tint.
vec3 top = vec3(.055, .067, .094); vec3 top = vec3(.055, .067, .094);
vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top; vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top;
#ifdef CITY
vec3 stroke = mix(vColor.rgb, vec3(1.0), .3) * 1.2; // neon edges at night
#else
vec3 stroke = mix(vColor.rgb, vec3(1.0), .12); vec3 stroke = mix(vColor.rgb, vec3(1.0), .12);
if(vKind.x == 2){ #endif
outColor = vec4(fog(vec3(.03, .036, .052)), uAlpha); // asphalt
return;
}
bool city = uMode > 1.5;
if(city) stroke = mix(vColor.rgb, vec3(1.0), .3) * 1.2; // neon edges at night
if(vKind.x == 1){ if(vKind.x == 1){
// Blocks are raised pavements tinted by their dominant layer. // Blocks are raised pavements tinted by their dominant layer.
base = mix(vec3(.075, .085, .11), vColor.rgb, .07); base = mix(vec3(.075, .085, .11), vColor.rgb, .07);
@@ -124,26 +128,27 @@ void main(){
// Walls: a fixed key light per side and a darker foot make buildings read as solids. // Walls: a fixed key light per side and a darker foot make buildings read as solids.
float light = vFace == 1 ? .34 : vFace == 2 ? .2 : vFace == 3 ? .26 : .42; float light = vFace == 1 ? .34 : vFace == 2 ? .2 : vFace == 3 ? .26 : .42;
base = mix(top, vColor.rgb, light) * (.55 + .45 * vUnit.z); base = mix(top, vColor.rgb, light) * (.55 + .45 * vUnit.z);
stroke = mix(stroke, base, city ? .25 : .45); #ifdef CITY
if(city){ stroke = mix(stroke, base, .25);
// Windows: a grid in metres; the lit share follows the file's definition count. // Windows: a grid in metres; the lit share follows the file's definition count.
float wide = vFace < 3 ? vSize.x : vSize.y; float wide = vFace < 3 ? vSize.x : vSize.y;
vec2 metres = vec2((vFace < 3 ? vUnit.x : vUnit.y) * wide, vUnit.z * vSize.z); vec2 metres = vec2((vFace < 3 ? vUnit.x : vUnit.y) * wide, vUnit.z * vSize.z);
vec2 cell = floor(metres / vec2(3.2, 3.5)), local = fract(metres / vec2(3.2, 3.5)); vec2 cell = floor(metres / vec2(3.2, 3.5)), local = fract(metres / vec2(3.2, 3.5));
float pane = step(.22, local.x) * step(local.x, .78) * step(.3, local.y) * step(local.y, .78); float pane = step(.22, local.x) * step(local.x, .78) * step(.3, local.y) * step(local.y, .78);
pane *= step(1.0, cell.y) * step(metres.y, vSize.z - 2.0) * step(1.6, metres.x) * step(metres.x, wide - 1.6); pane *= step(1.0, cell.y) * step(metres.y, vSize.z - 2.0) * step(1.6, metres.x) * step(metres.x, wide - 1.6);
bool lit = hash(vec3(cell, vLit.y + float(vFace) * 17.0)) < vLit.x; bool lit = hash(vec3(cell, vLit.y + float(vFace) * 17.0)) < vLit.x;
vec3 glass = lit ? mix(vec3(1.0, .82, .52), vColor.rgb, .35) * (.8 + .4 * hash(vec3(cell.yx, vLit.y))) : base * .55; vec3 glass = lit ? mix(vec3(1.0, .82, .52), vColor.rgb, .35) * (.8 + .4 * hash(vec3(cell.yx, vLit.y))) : base * .55;
base = mix(base, glass, pane); base = mix(base, glass, pane);
} #else
stroke = mix(stroke, base, .45);
#endif
} else if(vFace == 0 && uCodeOn > .5){ } else if(vFace == 0 && uCodeOn > .5){
// Roofs sample the code overview canvas at this file's rectangle. // Roofs sample the code overview canvas at this file's rectangle.
vec4 code = texture(uCode, vAtlas); vec4 code = texture(uCode, vAtlas);
base = mix(base, code.rgb / max(code.a, .001), code.a * .92); base = mix(base, code.rgb / max(code.a, .001), code.a * .92);
} }
if(vKind.y == 1) stroke = vec3(1.0, .94, .54); // selected if(vKind.y == 1) stroke = vec3(1.0, .94, .54); // selected
vec3 color = mix(stroke, base, border); outColor = vec4(fog(mix(stroke, base, border)), uAlpha);
outColor = vec4(city ? fog(color) : color, uAlpha);
}`; }`;
const GRID_VS = `#version 300 es const GRID_VS = `#version 300 es
@@ -250,7 +255,8 @@ export class LandscapeRenderer {
const gl = canvas.getContext('webgl2', { antialias: true, alpha: true, depth: true, powerPreference: 'high-performance' }); const gl = canvas.getContext('webgl2', { antialias: true, alpha: true, depth: true, powerPreference: 'high-performance' });
if (!gl) throw new Error('WebGL2 is required'); if (!gl) throw new Error('WebGL2 is required');
this.gl = gl; this.gl = gl;
this.program = program(gl, VS, FS); this.uniforms = uniformsOf(gl, this.program); this.program = program(gl, VS, fragmentShader(false)); this.uniforms = uniformsOf(gl, this.program);
this.cityProgram = program(gl, VS, fragmentShader(true)); this.cityUniforms = uniformsOf(gl, this.cityProgram);
this.gridProgram = program(gl, GRID_VS, GRID_FS); this.gridUniforms = uniformsOf(gl, this.gridProgram); this.gridProgram = program(gl, GRID_VS, GRID_FS); this.gridUniforms = uniformsOf(gl, this.gridProgram);
const grid = []; const grid = [];
for (let x = -200; x <= 1200; x += 100) grid.push(x, -200, x, 900); for (let x = -200; x <= 1200; x += 100) grid.push(x, -200, x, 900);
@@ -499,12 +505,12 @@ export class LandscapeRenderer {
gl.useProgram(this.gridProgram); gl.bindVertexArray(this.gridVao); this.setCameraUniforms(this.gridUniforms); gl.useProgram(this.gridProgram); gl.bindVertexArray(this.gridVao); this.setCameraUniforms(this.gridUniforms);
gl.depthMask(false); gl.drawArrays(gl.LINES, 0, this.gridCount); gl.depthMask(true); gl.depthMask(false); gl.drawArrays(gl.LINES, 0, this.gridCount); gl.depthMask(true);
} }
const u = this.uniforms, layer = this.mode === 'city' ? this.cityLayer : this.landscape; const city = this.mode === 'city', u = city ? this.cityUniforms : this.uniforms, layer = city ? this.cityLayer : this.landscape;
gl.useProgram(this.program); gl.bindVertexArray(layer.vao); this.setCameraUniforms(u); gl.useProgram(city ? this.cityProgram : this.program); gl.bindVertexArray(layer.vao); this.setCameraUniforms(u);
gl.uniform1f(u.uAlpha, alpha); gl.uniform1f(u.uAlpha, alpha);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.codeTexture); gl.uniform1i(u.uCode, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.codeTexture); gl.uniform1i(u.uCode, 0);
gl.uniform1f(u.uCodeOn, this.codeOn && this.mode !== '2d' ? 1 : 0); gl.uniform1f(u.uCodeOn, this.codeOn && this.mode !== '2d' ? 1 : 0);
gl.uniform1f(u.uFogDensity, fog); gl.uniform3f(u.uFogColor, ...fogColor); if (city) { gl.uniform1f(u.uFogDensity, fog); gl.uniform3f(u.uFogColor, ...fogColor); }
gl.drawArraysInstanced(gl.TRIANGLES, 0, 36, layer.count); gl.drawArraysInstanced(gl.TRIANGLES, 0, 36, layer.count);
if (this.mode === 'city') { if (this.mode === 'city') {
// Light effects add colour and never occlude: additive blending without depth writes. // Light effects add colour and never occlude: additive blending without depth writes.