diff --git a/web/app.js b/web/app.js index 2cde2f1..6ea4091 100644 --- a/web/app.js +++ b/web/app.js @@ -1,5 +1,5 @@ import { LandscapeRenderer, KIND } from './renderer.js'; -import { layoutCity, squarifiedLayout, spatialIndex, pickRay, stepCamera, collide, blockAt, groundHit, flatQuad, wallSigns, facadeQuad, heading, tourStops, encodePlace, decodePlace, joystick, MOVE } from './city.js'; +import { layoutCity, squarifiedLayout, spatialIndex, pickRay, stepCamera, collide, blockAt, groundHit, flatQuad, wallSigns, bladeSigns, facadeQuad, heading, tourStops, encodePlace, decodePlace, joystick, MOVE } from './city.js'; import { LabelAtlas } from './labels.js'; import { apiFetch, progressEvents } from './api.mjs'; @@ -799,7 +799,11 @@ function buildSignQuads(signs,focus,radius,eye,heli,yaw){ const entry=label(b.file.name,isSelected?'selected':'plate');if(!entry)break; const height=Math.max(1.2,Math.min(Math.min(b.w,b.h)*.22,heli?12:5)),width=Math.min(b.w*.92,height*entry.aspect); add(flatQuad(b.x+b.w/2,b.y+b.h/2,b.height+.08,width,width/entry.aspect),entry);roofs++; - if(!heli&&d -value), v }); + } + } + return out; +} diff --git a/web/renderer.js b/web/renderer.js index 26c9557..beb34da 100644 --- a/web/renderer.js +++ b/web/renderer.js @@ -171,9 +171,13 @@ layout(location=2) in vec3 aAxisU; layout(location=3) in vec3 aAxisV; layout(location=4) in vec4 aUv; uniform mat4 uViewProj; +uniform vec3 uEye; out vec2 vUv; out float vDepth; +flat out int vMirrored; void main(){ + // Text reads correctly only when (u x v) faces away from the viewer; back-to-back blade faces rely on this. + vMirrored = dot(cross(aAxisU, aAxisV), uEye - aCenter) >= 0.0 ? 1 : 0; vec3 world = aCenter + aAxisU * (aCorner.x * 2.0 - 1.0) + aAxisV * (aCorner.y * 2.0 - 1.0); vUv = aUv.xy + vec2(aCorner.x, 1.0 - aCorner.y) * aUv.zw; gl_Position = uViewProj * vec4(world, 1.0); @@ -219,10 +223,12 @@ const SIGN_FS = `#version 300 es precision highp float; in vec2 vUv; in float vDepth; +flat in int vMirrored; uniform sampler2D uTexture; uniform float uFogDensity; out vec4 outColor; void main(){ + if(vMirrored == 1) discard; vec4 color = texture(uTexture, vUv); if(color.a < .02) discard; outColor = color * exp(-pow(vDepth * uFogDensity, 2.0)); // premultiplied, so fading toward clear @@ -389,7 +395,7 @@ export class LandscapeRenderer { if (!layer.count) return; const gl = this.gl, u = this.signUniforms; gl.useProgram(this.signProgram); gl.bindVertexArray(layer.vao); - gl.uniformMatrix4fv(u.uViewProj, false, this.viewProj); gl.uniform1f(u.uFogDensity, this.fogDensity()); + gl.uniformMatrix4fv(u.uViewProj, false, this.viewProj); gl.uniform1f(u.uFogDensity, this.fogDensity()); gl.uniform3f(u.uEye, ...this.eye); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(u.uTexture, 0); gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, layer.count); } diff --git a/webtest/labels.test.mjs b/webtest/labels.test.mjs index 8ab0c9b..ddee603 100644 --- a/webtest/labels.test.mjs +++ b/webtest/labels.test.mjs @@ -40,3 +40,24 @@ test('facade covers the most squarely facing wall within the building', () => { assert.ok(Math.abs(width / height - .5) < 1e-9 && quad.c[2] + height / 2 <= building.height); assert.deepEqual(flatQuad(1, 2, 3, 4, 2), { c: [1, 2, 3], u: [2, 0, 0], v: [0, -1, 0] }); }); + +import { bladeSigns, readableFrom } from '../web/city.js'; + +test('blade signs project from the wall and exactly one face reads from either side', () => { + const south = { x: 0, y: 0, w: 20, h: 10, height: 30 }; + const blades = bladeSigns(south, [5, 14, 1.7], 4); + assert.equal(blades.length, 2, 'one blade (two faces) on the south wall only'); + const [a, b] = blades; + assert.ok(Math.abs(a.u[0]) < 1e-9 && a.u[1] > 0 && a.c[1] > 10, 'perpendicular to the wall, outside it'); + for (const eye of [[-5, 14, 1.7], [25, 14, 1.7]]) { + assert.equal(readableFrom(a, eye) + readableFrom(b, eye), 1, `one readable face from ${eye}`); + } + assert.ok(readableFrom(flatQuad(0, 0, 0, 4, 1), [0, 0, 50]), 'ground labels read from above'); +}); + +test('blades shrink to the free space and are skipped when a neighbour is too close', () => { + const b = { x: 0, y: 0, w: 20, h: 10, height: 30 }; + assert.ok(bladeSigns(b, [5, 30, 1.7], 10, () => 2.5)[0].u[1] * 2 <= 2.5 - .65 + 1e-9); + assert.equal(bladeSigns(b, [5, 30, 1.7], 10, () => 1).length, 0); + assert.equal(bladeSigns({ x: 0, y: 0, w: 100, h: 10, height: 30 }, [50, 30, 1.7], 4).length, 8, 'repeats along long walls'); +});