Add blade signs so names read while walking along walls
Flat wall signs vanish at grazing angles. Walls facing a walker now also carry blade signs projecting perpendicular to the wall, repeated along long walls and offset from the flat signs. Each blade is two back-to-back faces; the sign shader discards whichever face would read mirrored from the viewer's side. Blades shorten to the free space in front of the wall and are skipped when a neighbouring building is closer than 1.2 m of sign.
This commit is contained in:
+12
-2
@@ -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<SIGN_LIMITS.wallRadius&&walls<SIGN_LIMITS.walls){for(const quad of wallSigns(b,eye,entry.aspect))add(quad,entry);walls++;}
|
||||
if(!heli&&d<SIGN_LIMITS.wallRadius&&walls<SIGN_LIMITS.walls){
|
||||
for(const quad of wallSigns(b,eye,entry.aspect))add(quad,entry);
|
||||
for(const quad of bladeSigns(b,eye,entry.aspect,bladeClearance))add(quad,entry);
|
||||
walls++;
|
||||
}
|
||||
}
|
||||
// Street names on the northern kerb of nearby blocks.
|
||||
for(const block of cityModel.blocks){
|
||||
@@ -810,6 +814,12 @@ function buildSignQuads(signs,focus,radius,eye,heli,yaw){
|
||||
}
|
||||
}
|
||||
|
||||
// Free distance from a wall point outward at sign height, so blades stop short of neighbours.
|
||||
function bladeClearance(x,y,[nx,ny]){
|
||||
const origin=[x+nx*.05,y+ny*.05,4],hit=pickRay(origin,[nx,ny,0],[...cityIndex.near(x,y,5)],5);
|
||||
return hit?hit.distance:5;
|
||||
}
|
||||
|
||||
// Source code on the wall of the building in the crosshair, once you are close enough to read it.
|
||||
let facadeState={entry:null,texture:null};
|
||||
function updateFacade(){
|
||||
|
||||
+31
@@ -306,3 +306,34 @@ export function joystick(dx, dy, radius = 56) {
|
||||
const scale = Math.min(1, length / radius) / length;
|
||||
return { forward: -dy * scale, right: dx * scale };
|
||||
}
|
||||
|
||||
// A quad's text reads correctly only from the side where (u x v) points away from the viewer
|
||||
// (the world is left-handed). The sign shader applies the same test to skip mirrored faces.
|
||||
export function readableFrom(quad, eye) {
|
||||
const [ux, uy, uz] = quad.u, [vx, vy, vz] = quad.v;
|
||||
const n = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
|
||||
return n[0] * (eye[0] - quad.c[0]) + n[1] * (eye[1] - quad.c[1]) + n[2] * (eye[2] - quad.c[2]) < 0;
|
||||
}
|
||||
|
||||
// Blade signs stick out from the wall like shop signs, so names read while walking alongside.
|
||||
// Each blade is two back-to-back faces; only the one readable from the viewer's side is drawn.
|
||||
// clearance(x, y, direction) returns the free distance outward, so blades never pierce a neighbour.
|
||||
export function bladeSigns(b, eye, aspect, clearance, height = .7, z = 4.2) {
|
||||
const out = [], inset = .15;
|
||||
for (const wall of walls(b)) {
|
||||
if ((eye[0] - wall.c[0]) * wall.n[0] + (eye[1] - wall.c[1]) * wall.n[1] <= 0) continue;
|
||||
const count = Math.max(1, Math.floor(wall.width / 24)), segment = wall.width / count, along = [wall.n[1], -wall.n[0]];
|
||||
const signZ = Math.max(2.6, Math.min(z, b.height - height));
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Blades sit a third of the way into each segment so they don't overlap the flat wall sign.
|
||||
const offset = (i + .3) * segment - wall.width / 2, x = wall.c[0] + along[0] * offset, y = wall.c[1] + along[1] * offset;
|
||||
const room = clearance ? clearance(x, y, wall.n) - inset - .5 : Infinity;
|
||||
const width = Math.min(3.2, aspect * height, room);
|
||||
if (width < 1.2) continue;
|
||||
const c = [x + wall.n[0] * (inset + width / 2), y + wall.n[1] * (inset + width / 2), signZ];
|
||||
const u = [wall.n[0] * width / 2, wall.n[1] * width / 2, 0], v = [0, 0, width / aspect / 2];
|
||||
out.push({ c, u, v }, { c, u: u.map(value => -value), v });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
+7
-1
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user