mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
A deterministic WebGL film of God's Eye View coming online: a seed on Null Island grows into a culture that draws the planet, indexes the surface tile by tile, ingests the live feeds and outgrows its dish, ending on the logo. Renders 16:9 and 9:16 frame by frame in headless Chromium; the procedural score is locked to the same clock. Data and imagery are fetched by the scripts in tools/ and are not committed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqP857pippaUs6konSDfe8
92 lines
3.9 KiB
JavaScript
92 lines
3.9 KiB
JavaScript
// Deterministic frame renderer: drives src/main.js frame by frame in headless
|
|
// Chromium and either writes stills or pipes straight into ffmpeg.
|
|
//
|
|
// node tools/render.mjs --w 1920 --h 1080 --frames 480,900,1500 --out out/stills
|
|
// node tools/render.mjs --w 1920 --h 1080 --from 0 --to 2160 --video out/video_16x9.mp4
|
|
import { chromium } from 'playwright-core';
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const args = Object.fromEntries(
|
|
process.argv
|
|
.slice(2)
|
|
.join(' ')
|
|
.split('--')
|
|
.filter(Boolean)
|
|
.map((s) => {
|
|
const [k, ...v] = s.trim().split(' ');
|
|
return [k, v.join(' ') || true];
|
|
}),
|
|
);
|
|
const W = +args.w || 1920;
|
|
const H = +args.h || 1080;
|
|
const URL = `http://127.0.0.1:${args.port || 8765}/index.html?w=${W}&h=${H}`;
|
|
const CHROME = '/opt/pw-browsers/chromium-1194/chrome-linux/chrome';
|
|
|
|
const browser = await chromium.launch({
|
|
executablePath: CHROME,
|
|
args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--disable-gpu-vsync', '--js-flags=--max-old-space-size=8192'],
|
|
});
|
|
const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
|
|
page.on('console', (m) => {
|
|
const t = m.text();
|
|
if (/error|warn|missing|\[gev\]/i.test(t)) console.log(' page:', t.slice(0, 300));
|
|
});
|
|
page.on('pageerror', (e) => console.log(' PAGE ERROR:', e.message));
|
|
await page.goto(URL);
|
|
await page.waitForFunction(() => window.__status === 'ready' || String(window.__status).startsWith('error'), null, { timeout: 600000, polling: 500 });
|
|
const st = await page.evaluate(() => window.__status);
|
|
if (st !== 'ready') {
|
|
console.error('init failed:', st);
|
|
process.exit(1);
|
|
}
|
|
const info = await page.evaluate(() => ({ counts: GEV.counts, heroes: GEV.heroes, cams: GEV.cams, frames: GEV.frames }));
|
|
console.log('ready', JSON.stringify(info));
|
|
|
|
const cdp = await page.context().newCDPSession(page);
|
|
async function grab(format = 'png') {
|
|
const r = await cdp.send('Page.captureScreenshot', { format, quality: format === 'jpeg' ? 96 : undefined, fromSurface: true, captureBeyondViewport: false });
|
|
return Buffer.from(r.data, 'base64');
|
|
}
|
|
|
|
let frames = [];
|
|
if (args.frames) frames = String(args.frames).split(',').map((x) => (x.includes('s') ? Math.round(parseFloat(x) * 30) : +x));
|
|
else {
|
|
const from = +(args.from || 0);
|
|
const to = +(args.to || info.frames);
|
|
const step = +(args.step || 1);
|
|
for (let f = from; f < to; f += step) frames.push(f);
|
|
}
|
|
|
|
let ff = null;
|
|
if (args.video) {
|
|
fs.mkdirSync(path.dirname(args.video), { recursive: true });
|
|
ff = spawn('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'image2pipe', '-framerate', '30', '-c:v', 'png', '-i', '-', '-c:v', 'libx264', '-preset', args.preset || 'slow', '-crf', String(args.crf || 14), '-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-color_primaries', 'bt709', '-color_trc', 'bt709', '-movflags', '+faststart', args.video], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
} else {
|
|
fs.mkdirSync(args.out || 'out/stills', { recursive: true });
|
|
}
|
|
|
|
const t0 = Date.now();
|
|
for (let i = 0; i < frames.length; i++) {
|
|
const f = frames[i];
|
|
await page.evaluate((fr) => window.GEV.renderFrame(fr), f);
|
|
const buf = await grab('png');
|
|
if (ff) {
|
|
if (!ff.stdin.write(buf)) await new Promise((r) => ff.stdin.once('drain', r));
|
|
} else {
|
|
fs.writeFileSync(path.join(args.out || 'out/stills', `${args.prefix || 'f'}_${String(f).padStart(5, '0')}.png`), buf);
|
|
}
|
|
if (i % 30 === 0 || i === frames.length - 1) {
|
|
const el = (Date.now() - t0) / 1000;
|
|
const per = el / (i + 1);
|
|
console.log(`frame ${f} (${i + 1}/${frames.length}) ${per.toFixed(2)} s/frame, eta ${((frames.length - i - 1) * per / 60).toFixed(1)} min`);
|
|
}
|
|
}
|
|
if (ff) {
|
|
ff.stdin.end();
|
|
await new Promise((r) => ff.on('close', r));
|
|
}
|
|
await browser.close();
|
|
console.log('done in', ((Date.now() - t0) / 60000).toFixed(1), 'min');
|