PROCITY/pipeline/dump_bird.mjs
m3ultra a569f08b14 Lane E R39 (1/n): THE MAGPIE, SETTLED ON A PICTURE — B's own birdGeometry() dumped vertex-for-vertex (182 tris, not 154), rendered against E's tinted GLB at 64/48/32 px, still and under motion blur
- pipeline/dump_bird.mjs: imports web/js/world/magpie.js UNMODIFIED (bare 'three' resolved to the
  repo's vendored build through a node resolve hook) and dumps birdGeometry() — so the A/B is
  against Lane B's actual bird, not a port. MEASURED 182 tris / 216 verts. --sim walks a player
  past a territory on magpie.js's own clock: drawn 54.1% of frames, and OF THOSE perched 80.5% /
  swooping 7.7% / returning 11.8%.
- pipeline/bird_to_glb.py: wraps the dump as a GLB in E's frame (head +Z, +Y up) so the identical
  render_views.py rig shoots both; --fold applies the perch pose (x x 0.42). glb_stat re-measures 182.
- pipeline/render_views.py --noemit: strips emission from BOTH candidates. normalize.py's
  emissiveFactor 0.28 is albedo-MODULATED; a vertex-coloured mesh cannot express that in glTF and
  Blender writes a flat 0.28 that lifts a black bird to grey. Off both sides or the picture lies.
- pipeline/view_sheet.py --ab: the strip as a GRID (one line per candidate, stacked) plus two
  linear motion-blur blocks at 25% and 100% of the bird's width, and a wrapped notes footer.
- docs/shots/laneE/r39_magpie_ab.png — the sheet Fable rules on.

Recommendation in LANE_E_NOTES; picture first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:35:00 +10:00

96 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// PROCITY Lane E — dump_bird.mjs (R39, the magpie A/B)
//
// Dump Lane B's PROCEDURAL magpie geometry, by RUNNING LANE B'S OWN MODULE — not by re-implementing
// it. `web/js/world/magpie.js` is imported unmodified; the bare `three` / `three/addons/` specifiers
// its imports use are resolved to the repo's OWN vendored build through a node resolve hook, so the
// vertices dumped here are byte-for-byte the vertices the browser gets. That matters: an A/B render
// against a hand-ported strawman proves nothing, and the whole point of R39 item 1 is that Fable
// rules on the picture.
//
// node pipeline/dump_bird.mjs [OUT.json]
//
// Writes { tris, verts, position[], normal[], color[], index[]|null } in the mesh's own metric frame
// (metres, three's +Y up, nose along Z). `pipeline/bird_to_glb.py` turns it into a GLB so the same
// `render_views.py` rig that shot E's tinted GLB can shoot B's bird from the same cameras.
import { registerHooks } from 'node:module';
import { pathToFileURL } from 'node:url';
import { writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(HERE, '..');
const VENDOR = pathToFileURL(resolve(ROOT, 'web/vendor/three.module.js')).href;
const ADDONS = pathToFileURL(resolve(ROOT, 'web/vendor/addons')).href + '/';
// The repo's importmap, as a node resolver: "three" and "three/addons/*" only.
registerHooks({
resolve(spec, ctx, next) {
if (spec === 'three') return { url: VENDOR, shortCircuit: true };
if (spec.startsWith('three/addons/')) {
return { url: ADDONS + spec.slice('three/addons/'.length), shortCircuit: true };
}
return next(spec, ctx);
},
});
const { generatePlan } = await import(pathToFileURL(resolve(ROOT, 'web/js/citygen/plan.js')).href);
const { createMagpie } = await import(pathToFileURL(resolve(ROOT, 'web/js/world/magpie.js')).href);
const plan = generatePlan(20261990);
const scene = { add() {}, remove() {} };
const camera = { position: { x: 0, y: 1.6, z: 0 } };
const m = createMagpie({ scene, plan, citySeed: 20261990, townKey: null, camera, chunks: null, lighting: null, force: true });
const mesh = m.group.children.find((c) => c.isInstancedMesh);
const g = mesh.geometry;
const idx = g.index ? Array.from(g.index.array) : null;
const pos = Array.from(g.attributes.position.array);
const nrm = g.attributes.normal ? Array.from(g.attributes.normal.array) : null;
const col = g.attributes.color ? Array.from(g.attributes.color.array) : null;
const tris = idx ? idx.length / 3 : pos.length / 9;
const out = {
source: 'web/js/world/magpie.js :: birdGeometry() via createMagpie()',
tris, verts: pos.length / 3,
material: { vertexColors: true, roughness: 0.62, metalness: 0, side: 'DoubleSide', wind: 'wing' },
bbox: (() => {
g.computeBoundingBox();
const b = g.boundingBox;
return { min: [b.min.x, b.min.y, b.min.z], max: [b.max.x, b.max.y, b.max.z],
size: [b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z] };
})(),
// the perch pose is the same geometry squashed in X (magpie.js `place(..., folded)`)
foldedScaleX: 0.42,
index: idx, position: pos, normal: nrm, color: col,
};
const dst = process.argv.find((a) => a.endsWith('.json')) || resolve(HERE, '_bird_b.json');
writeFileSync(dst, JSON.stringify(out));
console.log(`bird: ${tris} triangles, ${out.verts} verts, bbox size ${out.bbox.size.map((v) => v.toFixed(3)).join(' × ')} m → ${dst}`);
// ── --sim: WHICH POSE IS THE PLAYER ACTUALLY LOOKING AT? ────────────────────────────────────────
// The A/B is usually argued as "the swoop is the whole point", but magpie.js's own clock says the
// swoop is the minority state: COOLDOWN 5.5 s perched against SWOOP_T 1.25 + RETURN_T 1.9 = 3.15 s
// in the air, and the mesh is drawn out to DEFEND_R × 2.2 = 74.8 m where it can only be perched.
// So walk a player down the street past a territory at WALK speed and COUNT the frames.
if (process.argv.includes('--sim')) {
const t = m.territories[0] || { x: 0, z: 0 };
const DT = 1 / 60, SPEED = 4.6, OFFSET = 4.0; // WALK m/s, and how far off the perch you pass
globalThis.window = { PROCITY: { game: { day: 1 } } };
const tally = {};
let frames = 0;
for (let i = 0; i < 60 * 60; i++) { // 60 s of walking
const s = -140 + i * DT * SPEED; // straight past the perch, 140 m either side
camera.position.x = t.x + s; camera.position.z = t.z + OFFSET;
m.update(DT);
tally[m.state.mode] = (tally[m.state.mode] || 0) + 1;
if (m.count > 0) frames++;
}
const drawn = Object.entries(tally).filter(([k]) => k === 'perched' || k === 'swooping' || k === 'returning');
const total = drawn.reduce((a, [, v]) => a + v, 0);
console.log(`sim: 60 s walk at ${SPEED} m/s, ${OFFSET} m off the perch — modes ${JSON.stringify(tally)}`);
console.log(`sim: bird DRAWN in ${frames} of 3600 frames (${(frames / 36).toFixed(1)}%)`);
for (const [k, v] of drawn) console.log(`sim: ${k.padEnd(10)} ${v} frames = ${(100 * v / total).toFixed(1)}% of the frames it is on screen`);
}