Texture 3D roofs on the GPU and draw the grid in WebGL
The overlay used to clip and affine-map a code texture onto every visible roof and punch every roof out of a 2D ground grid each frame (about 13,600 saves and 6,800 clips per frame on a 1,676-file repo). The existing code overview canvas already matches the world layout, so it is uploaded once as a mipmapped texture and sampled by the roof shader. The overlay now only draws sharper text for the few roofs close enough to magnify that texture, and the grid is a WebGL line pass. The vertex shader emits a real clip-space w so the texture is perspective- correct; screen positions are unchanged. JS and canvas time per 3D frame fell from 284 ms to 27 ms in the same profile.
This commit is contained in:
+106
-38
@@ -13,68 +13,104 @@ const VERTICES = new Float32Array([
|
||||
0,1,0, 0,0,0, 1,0,0, 0,1,0, 1,0,0, 1,1,0,
|
||||
]);
|
||||
|
||||
const VS = `#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec3 aUnit;
|
||||
layout(location=1) in vec4 aRect;
|
||||
layout(location=2) in vec4 aColorHeight;
|
||||
// Shared by the tile and grid programs so both use one camera. The 3D branch returns a real
|
||||
// clip-space w (the view depth), which keeps textures perspective-correct on roofs while
|
||||
// producing exactly the same screen positions as project() in JS.
|
||||
const PROJECT = `
|
||||
uniform vec2 uCenter;
|
||||
uniform vec2 uViewport;
|
||||
uniform float uZoom;
|
||||
uniform float uMode;
|
||||
uniform vec2 uOrbit;
|
||||
uniform float uDistance;
|
||||
vec4 projectWorld(vec3 world){
|
||||
if(uMode < .5){
|
||||
vec2 p = (world.xy - uCenter) * uZoom;
|
||||
return vec4(p.x * 2.0 / uViewport.x, -p.y * 2.0 / uViewport.y, min(world.z, 1.0) * .001, 1.0);
|
||||
}
|
||||
vec3 p = vec3((world.xy - uCenter) / 420.0, world.z / 170.0);
|
||||
float cy=cos(uOrbit.x), sy=sin(uOrbit.x), cp=cos(uOrbit.y), sp=sin(uOrbit.y);
|
||||
p = vec3(cy*p.x-sy*p.y, sy*p.x+cy*p.y, p.z);
|
||||
// Positive pitch places the camera above the ground plane.
|
||||
p = vec3(p.x, cp*p.y+sp*p.z, -sp*p.y+cp*p.z);
|
||||
float depth = max(.5, uDistance - p.y);
|
||||
float aspect = uViewport.x / uViewport.y;
|
||||
return vec4(p.x/aspect*2.7, (p.z-.35)*2.7, (depth-1.0)/10.0*depth, depth);
|
||||
}`;
|
||||
|
||||
const VS = `#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec3 aUnit;
|
||||
layout(location=1) in vec4 aRect;
|
||||
layout(location=2) in vec4 aColorHeight;
|
||||
${PROJECT}
|
||||
out vec4 vColor;
|
||||
out vec3 vUnit;
|
||||
out float vFace;
|
||||
out vec2 vWorld;
|
||||
flat out int vFace;
|
||||
void main(){
|
||||
float height = max(1.0, aColorHeight.a);
|
||||
vec3 world = vec3(aRect.xy + aUnit.xy * aRect.zw, aUnit.z * height);
|
||||
vColor = vec4(aColorHeight.rgb, 1.0);
|
||||
vUnit = aUnit;
|
||||
vFace = float(gl_VertexID / 6);
|
||||
if(uMode < .5){
|
||||
vec2 p = (world.xy - uCenter) * uZoom;
|
||||
gl_Position = vec4(p.x * 2.0 / uViewport.x, -p.y * 2.0 / uViewport.y, aUnit.z * .001, 1.0);
|
||||
} else {
|
||||
vec3 p = vec3((world.xy - uCenter) / 420.0, world.z / 170.0);
|
||||
float cy=cos(uOrbit.x), sy=sin(uOrbit.x), cp=cos(uOrbit.y), sp=sin(uOrbit.y);
|
||||
p = vec3(cy*p.x-sy*p.y, sy*p.x+cy*p.y, p.z);
|
||||
// Positive pitch places the camera above the ground plane. The previous
|
||||
// signs put the eye below it, making every useful orbit an underside view.
|
||||
p = vec3(p.x, cp*p.y+sp*p.z, -sp*p.y+cp*p.z);
|
||||
float depth = max(.5, uDistance - p.y);
|
||||
float aspect = uViewport.x / uViewport.y;
|
||||
gl_Position = vec4(p.x/(depth*aspect)*2.7, (p.z-.35)/depth*2.7, (depth-1.0)/10.0, 1.0);
|
||||
}
|
||||
vWorld = world.xy;
|
||||
vFace = gl_VertexID / 6;
|
||||
gl_Position = projectWorld(world);
|
||||
}`;
|
||||
|
||||
const FS = `#version 300 es
|
||||
precision highp float;
|
||||
in vec4 vColor;
|
||||
in vec3 vUnit;
|
||||
in float vFace;
|
||||
in vec2 vWorld;
|
||||
flat in int vFace;
|
||||
uniform float uMode;
|
||||
uniform float uAlpha;
|
||||
uniform sampler2D uCode;
|
||||
uniform float uCodeOn;
|
||||
out vec4 outColor;
|
||||
void main(){
|
||||
if(uMode < .5 && vFace > .5) discard;
|
||||
vec2 face = vFace < .5 || vFace > 4.5 ? vUnit.xy : vFace < 2.5 ? vUnit.xz : vUnit.yz;
|
||||
if(uMode < .5 && vFace > 0) discard;
|
||||
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 border = smoothstep(0.0, fwidth(edge)*1.35 + .009, edge);
|
||||
// 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 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top;
|
||||
vec3 stroke = mix(vColor.rgb, vec3(1.0), .12);
|
||||
if(vFace > .5 && vFace < 4.5){
|
||||
if(vFace > 0 && vFace < 5){
|
||||
// Walls: a fixed key light per side and a darker foot make buildings read as solids.
|
||||
float light = vFace < 1.5 ? .34 : vFace < 2.5 ? .2 : vFace < 3.5 ? .26 : .42;
|
||||
float light = vFace == 1 ? .34 : vFace == 2 ? .2 : vFace == 3 ? .26 : .42;
|
||||
base = mix(top, vColor.rgb, light) * (.55 + .45 * vUnit.z);
|
||||
stroke = mix(stroke, base, .45);
|
||||
} else if(vFace == 0 && uCodeOn > .5){
|
||||
// Roofs sample the code overview, which is laid out exactly like the 1000x680 world.
|
||||
vec4 code = texture(uCode, vWorld / vec2(1000.0, 680.0));
|
||||
base = mix(base, code.rgb / max(code.a, .001), code.a * .92);
|
||||
}
|
||||
outColor = vec4(mix(stroke, base, border), uAlpha);
|
||||
}`;
|
||||
|
||||
const GRID_VS = `#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec2 aPoint;
|
||||
${PROJECT}
|
||||
void main(){ gl_Position = projectWorld(vec3(aPoint, 0.0)); }`;
|
||||
|
||||
const GRID_FS = `#version 300 es
|
||||
precision highp float;
|
||||
out vec4 outColor;
|
||||
void main(){ outColor = vec4(.59, .63, 1.0, .11); }`;
|
||||
|
||||
function program(gl, vertex, fragment) {
|
||||
const value = gl.createProgram();
|
||||
gl.attachShader(value, shader(gl, gl.VERTEX_SHADER, vertex));
|
||||
gl.attachShader(value, shader(gl, gl.FRAGMENT_SHADER, fragment));
|
||||
gl.linkProgram(value);
|
||||
if (!gl.getProgramParameter(value, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
function shader(gl, type, source) {
|
||||
const value = gl.createShader(type); gl.shaderSource(value, source); gl.compileShader(value);
|
||||
if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(value));
|
||||
@@ -87,11 +123,17 @@ export class LandscapeRenderer {
|
||||
const gl = canvas.getContext('webgl2', { antialias: true, alpha: true, depth: true, powerPreference: 'high-performance' });
|
||||
if (!gl) throw new Error('WebGL2 is required');
|
||||
this.gl = gl;
|
||||
this.program = gl.createProgram();
|
||||
gl.attachShader(this.program, shader(gl, gl.VERTEX_SHADER, VS));
|
||||
gl.attachShader(this.program, shader(gl, gl.FRAGMENT_SHADER, FS));
|
||||
gl.linkProgram(this.program);
|
||||
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(this.program));
|
||||
this.program = program(gl, VS, FS);
|
||||
this.gridProgram = program(gl, GRID_VS, GRID_FS);
|
||||
const grid = [];
|
||||
for (let x = -200; x <= 1200; x += 100) grid.push(x, -200, x, 900);
|
||||
for (let y = -200; y <= 900; y += 100) grid.push(-200, y, 1200, y);
|
||||
this.gridVao = gl.createVertexArray(); gl.bindVertexArray(this.gridVao);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(grid), gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||
this.gridCount = grid.length / 2;
|
||||
this.codeTexture = gl.createTexture(); this.codeOn = false;
|
||||
this.anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
||||
this.vao = gl.createVertexArray(); gl.bindVertexArray(this.vao);
|
||||
const vertices = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertices); gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
|
||||
@@ -123,20 +165,46 @@ export class LandscapeRenderer {
|
||||
this.dpr=dpr; this.gl.viewport(0,0,w,h);
|
||||
}
|
||||
|
||||
render(alpha=1) {
|
||||
const gl=this.gl; this.resize();
|
||||
gl.clearColor(0,0,0,0); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
|
||||
gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.useProgram(this.program); gl.bindVertexArray(this.vao);
|
||||
this.uniforms||=Object.fromEntries(['uCenter','uViewport','uZoom','uMode','uOrbit','uDistance','uAlpha'].map(n=>[n,gl.getUniformLocation(this.program,n)]));
|
||||
const u=this.uniforms;
|
||||
// Uploads the code overview canvas as the roof texture; call again after it changes.
|
||||
setCodeTexture(source) {
|
||||
const gl=this.gl; gl.bindTexture(gl.TEXTURE_2D,this.codeTexture);
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,true); // mipmaps of straight alpha get dark fringes around glyphs
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,source);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
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,gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
|
||||
if(this.anisotropy)gl.texParameterf(gl.TEXTURE_2D,this.anisotropy.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(8,gl.getParameter(this.anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT)));
|
||||
}
|
||||
|
||||
setCameraUniforms(u) {
|
||||
const gl=this.gl;
|
||||
gl.uniform2f(u.uCenter,this.camera.x,this.camera.y);
|
||||
gl.uniform2f(u.uViewport,this.width,this.height);
|
||||
gl.uniform1f(u.uZoom,this.camera.zoom);
|
||||
gl.uniform1f(u.uMode,this.mode==='3d'?1:0);
|
||||
gl.uniform2f(u.uOrbit,this.camera.yaw,this.camera.pitch);
|
||||
gl.uniform1f(u.uDistance,this.camera.distance);
|
||||
}
|
||||
|
||||
render(alpha=1) {
|
||||
const gl=this.gl; this.resize();
|
||||
gl.clearColor(0,0,0,0); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
|
||||
gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
|
||||
const names=['uCenter','uViewport','uZoom','uMode','uOrbit','uDistance','uAlpha','uCode','uCodeOn'];
|
||||
this.uniforms||=Object.fromEntries(names.map(n=>[n,gl.getUniformLocation(this.program,n)]));
|
||||
this.gridUniforms||=Object.fromEntries(names.map(n=>[n,gl.getUniformLocation(this.gridProgram,n)]));
|
||||
if(this.mode==='3d'){
|
||||
// Ground grid first without writing depth; buildings then cover it where they stand.
|
||||
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);
|
||||
}
|
||||
const u=this.uniforms;
|
||||
gl.useProgram(this.program); gl.bindVertexArray(this.vao); this.setCameraUniforms(u);
|
||||
gl.uniform1f(u.uAlpha,alpha);
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,this.codeTexture); gl.uniform1i(u.uCode,0);
|
||||
gl.uniform1f(u.uCodeOn,this.codeOn&&this.mode==='3d'?1:0);
|
||||
gl.drawArraysInstanced(gl.TRIANGLES,0,36,this.count);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user