// Renders Starlock's glow shaders the way the cutscene does, in real WebGL (headless Chromium), // and saves the raw pixels: the 8-bit intermediate target (linear light) and the canvas after the // painterly pass (tone-mapped, sRGB). Uses the game's own three.js (public/ideas/starlock/vendor) // and copies the painterly pass's shader out of reveal3d.js, so it renders what ships. // // node --input-type=module < webgl.mjs.txt // // (named .txt because the site serves code/ as text; see README.md) // spec.json: { W, H, radius, stride (taps apart: the pixel ratio; default 1), quads: [{ frag, uniforms: {name: number|[r,g,b]}, x, y, w, h }], // noise: { size, data: [bytes] } (optional: a sampler2D uNoise, nearest, repeating) } // writes .target.rgba and .canvas.rgba (W*H*4 bytes, rows bottom-up as GL reads them) import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; const require = createRequire(path.join(process.cwd(), 'x.js')); const { chromium } = (() => { for (const id of [process.env.PLAYWRIGHT, 'playwright', '/opt/node-tools/node_modules/playwright'].filter(Boolean)) { try { return require(id); } catch {} } throw new Error('Playwright not found: set PLAYWRIGHT to its path.'); })(); const [specFile, outPrefix] = process.argv.slice(2); const starlock = process.env.STARLOCK || path.resolve(process.cwd(), '../../../public/ideas/starlock'); const three = fs.readFileSync(path.join(starlock, 'vendor/three.min.js'), 'utf8'); const reveal = fs.readFileSync(path.join(starlock, 'reveal3d.js'), 'utf8'); const kuwahara = reveal.slice(reveal.indexOf('fragmentShader: `uniform sampler2D tScene')); const paintFrag = kuwahara.slice(kuwahara.indexOf('`') + 1, kuwahara.indexOf('`,\n}')); const spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); const browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const page = await browser.newPage(); await page.route('http://local/**', (r) => r.fulfill({ contentType: r.request().url().endsWith('.js') ? 'text/javascript' : 'text/html', body: r.request().url().endsWith('.js') ? three : '' })); await page.goto('http://local/index.html'); const out = await page.evaluate(async ({ spec, paintFrag }) => { const T = await import('http://local/three.min.js'); const { W, H } = spec; const canvas = document.getElementById('c'); canvas.width = W; canvas.height = H; const renderer = new T.WebGLRenderer({ canvas, antialias: false, preserveDrawingBuffer: true }); renderer.setPixelRatio(1); renderer.setSize(W, H, false); renderer.toneMapping = T.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.15; const scene = new T.Scene(), cam = new T.OrthographicCamera(0, W, H, 0, -1, 1); const QUAD_V = 'varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); }'; let noise = null; if (spec.noise) { const n = spec.noise.size, c = document.createElement('canvas'); c.width = c.height = n; const g = c.getContext('2d'), img = g.createImageData(n, n); for (let i = 0; i < n * n; i++) { const v = spec.noise.data[i]; img.data.set([v, v, v, 255], i * 4); } g.putImageData(img, 0, 0); noise = new T.CanvasTexture(c); noise.minFilter = noise.magFilter = T.NearestFilter; noise.wrapS = noise.wrapT = T.RepeatWrapping; noise.generateMipmaps = false; } for (const q of spec.quads) { const uniforms = {}; for (const [k, v] of Object.entries(q.uniforms)) uniforms[k] = { value: Array.isArray(v) ? new T.Color(...v) : v }; if (noise) uniforms.uNoise = { value: noise }; const m = new T.Mesh(new T.PlaneGeometry(1, 1), new T.ShaderMaterial({ vertexShader: QUAD_V, fragmentShader: q.frag, uniforms, transparent: true, blending: T.AdditiveBlending, depthWrite: false, side: T.DoubleSide, toneMapped: false })); m.position.set(q.x, q.y, 0); m.scale.set(q.w, q.h, 1); scene.add(m); } // the painterly pass, as reveal3d.js sets it up const target = new T.WebGLRenderTarget(W, H, { samples: 4 }); const mat = new T.ShaderMaterial({ uniforms: { tScene: { value: target.texture }, uTexel: { value: new T.Vector2((spec.stride || 1) / W, (spec.stride || 1) / H) } }, vertexShader: 'varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }', fragmentShader: paintFrag, defines: { RADIUS: spec.radius }, depthTest: false, depthWrite: false }); const post = new T.Scene(); post.add(new T.Mesh(new T.PlaneGeometry(2, 2), mat)); const pcam = new T.OrthographicCamera(-1, 1, 1, -1, 0, 1); renderer.setRenderTarget(target); renderer.setClearColor(0x000000, 1); renderer.clear(); renderer.render(scene, cam); const tgt = new Uint8Array(W * H * 4); renderer.readRenderTargetPixels(target, 0, 0, W, H, tgt); renderer.setRenderTarget(null); renderer.render(post, pcam); const gl = renderer.getContext(), cv = new Uint8Array(W * H * 4); gl.readPixels(0, 0, W, H, gl.RGBA, gl.UNSIGNED_BYTE, cv); const info = { renderer: gl.getParameter(gl.RENDERER), highp: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision }; const b64 = (a) => { let s = ''; for (let i = 0; i < a.length; i += 0x8000) s += String.fromCharCode(...a.subarray(i, i + 0x8000)); return btoa(s); }; return { tgt: b64(tgt), cv: b64(cv), info }; }, { spec, paintFrag }); await browser.close(); fs.writeFileSync(outPrefix + '.target.rgba', Buffer.from(out.tgt, 'base64')); fs.writeFileSync(outPrefix + '.canvas.rgba', Buffer.from(out.cv, 'base64')); console.error(JSON.stringify(out.info));