#!/usr/bin/env node /** * Independent CPU counterpart of MeshRefraction.ts's bRevealTriangle traversal. * Compares its hit/miss and world-distance results against Three's Raycaster on * the actual WindowReveal geometry. This does not execute GLSL or validate GPU * rendering, texture color, or the photo/reveal nearest-hit selection. * * Run from the repository root: * node scripts/test-reveal-intersections.mjs * Or supply the repository root: * node scripts/test-reveal-intersections.mjs /path/to/behind_glass */ import assert from 'node:assert/strict'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; const root = path.resolve(process.argv[2] ?? process.cwd()); const THREE = await import(pathToFileURL(path.join(root, 'node_modules/three/build/three.module.js')).href); const { WindowReveal } = await import(pathToFileURL(path.join(root, 'src/objects/WindowReveal.ts')).href); const material = new THREE.MeshStandardMaterial({ side: THREE.DoubleSide }); const reveal = new WindowReveal(material); reveal.update(.928, 1.16, 1.45 * .8 * (3 - .2204) / 3, 1.45 * (3 - .2204) / 3, .6, .2204); const vertices = reveal.meshes.flatMap(mesh => Array.from({ length: 4 }, (_, i) => new THREE.Vector3().fromBufferAttribute(mesh.geometry.attributes.position, i))); // Independent arithmetic equivalent to the bounded DoubleSide GLSL triangle test. function triangleDistance(origin, ray, a, b, c) { const e1 = b.clone().sub(a); const e2 = c.clone().sub(a); const p = ray.clone().cross(e2); const determinant = e1.dot(p); if (Math.abs(determinant) < 1e-9) return 1e20; const delta = origin.clone().sub(a); const u = delta.dot(p) / determinant; const q = delta.clone().cross(e1); const v = ray.dot(q) / determinant; if (u < -.00001 || v < -.00001 || u + v > 1.00001) return 1e20; const t = e2.dot(q) / determinant; return t < .000001 ? 1e20 : t; } let seed = 17; const random = () => ((seed = Math.imul(seed, 1664525) + 1013904223 | 0) >>> 0) / 4294967296; let matched = 0; let hits = 0; let maxError = 0; try { for (let i = 0; i < 10000; i++) { const origin = new THREE.Vector3((random() - .5) * .928, (random() - .5) * 1.16, .588); const ray = new THREE.Vector3(random() - .5, random() - .5, -random() - .1).normalize(); let distance = 1e20; for (let wall = 0; wall < 4; wall++) { const a = wall * 4; distance = Math.min(distance, triangleDistance(origin, ray, vertices[a], vertices[a + 1], vertices[a + 2]), triangleDistance(origin, ray, vertices[a], vertices[a + 2], vertices[a + 3])); } const expected = new THREE.Raycaster(origin, ray).intersectObjects(reveal.meshes, false)[0]; assert.equal(distance < 1e19, Boolean(expected), `Hit/miss mismatch at deterministic ray ${i}`); if (expected) { hits++; const error = Math.abs(distance - expected.distance); maxError = Math.max(maxError, error); assert.ok(error <= 1e-9, `Distance mismatch ${error} at deterministic ray ${i}`); } matched++; } console.log(JSON.stringify({ matched, hits, misses: matched - hits, maxDistanceError: maxError })); } finally { reveal.dispose(); material.dispose(); }