Files
behind_glass/scripts/test-mosaic-depth-distance.mjs
T
horoli e358e24436 Replace project with current server release and Windows setup
Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
2026-10-09 16:43:35 +09:00

129 lines
8.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import * as THREE from 'three';
const shader = await readFile(new URL('../src/shaders/mosaic.frag', import.meta.url), 'utf8');
const main = await readFile(new URL('../src/main.ts', import.meta.url), 'utf8') + await readFile(new URL('../src/rendering/OriginalGlassRenderer.ts',import.meta.url),'utf8');
assert.match(shader, /uniform sampler2D uSceneTexture,uSceneDepth/);
assert.match(shader, /color=sampleScene\(finalSceneUv\)/);
assert.match(shader, /regionSurfaceValid\(finalSceneUv,gh\)/);
assert.match(shader, /float depth=texture2D\(uSceneDepth,sceneUv\)\.x/);
assert.doesNotMatch(shader, /smoothstep\(\.045,\.14,gap\)/);
assert.match(shader, /uRegionStrength\*clamp\(uStrength,0\.,1\.\)\*regionSurfaceValid\(finalSceneUv,gh\)/);
assert.match(shader, /uniform sampler2D uProtectionDepthMap/);
assert.match(shader, /uniform mat4 uPetWorldToLocal/);
assert.match(shader, /uProtectionDepthMap,sourceUv\)\.r/);
assert.match(shader, /return smoothstep\(uDepthEvidenceRange.x,uDepthEvidenceRange.y,sourceDepth\)/);
assert.match(main, /depthEvidenceUniform.value.fromArray\(DepthProcessor.evidenceRange\(texture\)\)/);
assert.match(shader, /if\(!reconstructScenePoint\(sceneUv,worldPoint\)\)return 0\./);
assert.match(shader, /if\(localPoint\.x<-.5\|\|localPoint\.x>\.5\|\|localPoint\.y<-.5\|\|localPoint\.y>\.5\)return 0\./);
assert.match(shader, /atlasEvidence=regionProtectionWeight\(finalSceneUv\)\*\(1\.-smoothstep\(\.10,\.20,regionLuminance\(atlasCenterColor\)\)\)/);
assert.match(shader, /color\.a=atlasEvidence/);
assert.match(shader, /gl_FragColor=vec4\(sampleScene\(screenUv\)\.rgb,0\.\)/);
assert.match(shader, /gl_FragColor=vec4\(uBackgroundColor,0\.\)/);
assert.doesNotMatch(shader, /protectDark|protectedColor|ownPalette|darkestPalette/);
assert.match(main, /uProtectionDepthMap: depthUniforms\.uDepthMap/);
assert.match(main, /uPetWorldToLocal: \{ value: new THREE\.Matrix4\(\) \}/);
assert.match(main, /uPetWorldToLocal\.value\.copy\(petDepth\.matrixWorld\)\.invert\(\)/);
assert.match(main, /renderTarget\.depthTexture\s*=\s*new THREE\.DepthTexture/);
function reconstructPoint(uv, depth, inverseViewProjection) {
const world = new THREE.Vector4(uv.x * 2 - 1, uv.y * 2 - 1, depth * 2 - 1, 1)
.applyMatrix4(inverseViewProjection);
return new THREE.Vector3(world.x / world.w, world.y / world.w, world.z / world.w);
}
function validAtDepth(depth, uv, glassHit, inverseViewProjection, worldW = 1) {
if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1) return 0;
if (depth >= 0.999999 || Math.abs(worldW) < 0.0001) return 0;
const subject = reconstructPoint(uv, depth, inverseViewProjection);
const axialGap = glassHit.z - subject.z;
if (axialGap <= 0.0001) return 0;
return 1;
}
function protectionWeight(sourceDepth) {
const t = Math.max(0, Math.min(1, (sourceDepth - 0.8) / (0.95 - 0.8)));
return t * t * (3 - 2 * t);
}
function projectSourceUv(sourceUv, sourceDepth, depthScale, photoZoom, photoPan, cameraPosition) {
const baseW = 1.65, baseH = 1.2, worldScale = 0.48, baseZ = 0.42, depthBias = 0.1;
const position = new THREE.Vector3(
baseW * (photoZoom - 1) * 0.5 * photoPan.x * worldScale,
baseH * (photoZoom - 1) * 0.5 * photoPan.y * worldScale,
baseZ
);
const scale = new THREE.Vector3(baseW * photoZoom * worldScale, baseH * photoZoom * worldScale, 1);
const petMatrix = new THREE.Matrix4().compose(position, new THREE.Quaternion(), scale);
const local = new THREE.Vector3(sourceUv.x - 0.5, sourceUv.y - 0.5, (sourceDepth - depthBias) * depthScale);
const worldPoint = local.clone().applyMatrix4(petMatrix);
const camera = new THREE.PerspectiveCamera(40, 1.4, 0.1, 100);
camera.position.copy(cameraPosition);
camera.lookAt(0, 0, 0);
camera.updateMatrixWorld(true);
const viewProjection = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
const inverseViewProjection = viewProjection.clone().invert();
const clip = new THREE.Vector4(worldPoint.x, worldPoint.y, worldPoint.z, 1).applyMatrix4(viewProjection);
const ndc = new THREE.Vector3(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
const screenUv = new THREE.Vector2(ndc.x * 0.5 + 0.5, ndc.y * 0.5 + 0.5);
const reconstructed = reconstructPoint(screenUv, ndc.z * 0.5 + 0.5, inverseViewProjection);
const recoveredLocal = reconstructed.applyMatrix4(petMatrix.clone().invert());
return { screenUv, recoveredUv: new THREE.Vector2(recoveredLocal.x + 0.5, recoveredLocal.y + 0.5) };
}
for (const [sourceDepth, expected] of [[0.79, 0], [0.8, 0], [0.875, 0.5], [0.95, 1], [1, 1]]) {
assert.ok(Math.abs(protectionWeight(sourceDepth) - expected) < 1e-9,
`near-depth gate at ${sourceDepth} should be ${expected}`);
}
assert.equal(protectionWeight(0), 0, 'flat black depth disables the protection gate');
let mapped = 0;
for (const depthScale of [0, 0.25, 0.4]) for (const photo of [
{ zoom: 1, pan: new THREE.Vector2(0, 0) },
{ zoom: 1.6, pan: new THREE.Vector2(0.6, -0.35) },
]) for (const cameraPosition of [new THREE.Vector3(0, 0, 3), new THREE.Vector3(0.35, -0.2, 3.2)]) {
const sourceUv = new THREE.Vector2(0.12, 0.74);
const result = projectSourceUv(sourceUv, 0.92, depthScale, photo.zoom, photo.pan, cameraPosition);
assert.ok(result.recoveredUv.distanceTo(sourceUv) < 1e-5,
`source depth lookup must survive depthScale ${depthScale}, zoom ${photo.zoom}, camera ${cameraPosition.toArray()}`);
mapped++;
}
let reconstructed = 0;
for (const position of [[0, 0, 3], [1.8, 1.2, 3.4], [-1.8, -1.2, 3.4]]) {
const camera = new THREE.PerspectiveCamera(40, 1.4, 0.1, 100);
camera.position.set(...position);
camera.lookAt(0, 0, 0);
camera.updateMatrixWorld(true);
const viewProjection = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
const inverseViewProjection = viewProjection.clone().invert();
const rayDirection = camera.getWorldDirection(new THREE.Vector3());
const rayToGlass = (0.6 - camera.position.z) / rayDirection.z;
const glassHit = camera.position.clone().addScaledVector(rayDirection, rayToGlass);
for (const gap of [0.0002, 0.04, 0.045, 0.09, 0.14, 0.16, 1.2]) {
const subject = glassHit.clone().addScaledVector(rayDirection, gap);
const clip = new THREE.Vector4(subject.x, subject.y, subject.z, 1).applyMatrix4(viewProjection);
const ndc = new THREE.Vector3(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
const uv = new THREE.Vector2(ndc.x * 0.5 + 0.5, ndc.y * 0.5 + 0.5);
const depth = ndc.z * 0.5 + 0.5;
const recovered = reconstructPoint(uv, depth, inverseViewProjection);
assert.ok(recovered.distanceTo(subject) < 1e-5, `world reconstruction failed at camera ${position}, gap ${gap}`);
reconstructed++;
assert.equal(validAtDepth(depth, uv, glassHit, inverseViewProjection), 1, `valid surface at gap ${gap} should receive full configured region strength`);
}
assert.equal(validAtDepth(1, new THREE.Vector2(0.5, 0.5), glassHit, inverseViewProjection), 0, 'background depth must bypass simplification');
assert.equal(validAtDepth(0.5, new THREE.Vector2(-0.01, 0.5), glassHit, inverseViewProjection), 0, 'out-of-range UV must bypass simplification');
assert.equal(validAtDepth(0.5, new THREE.Vector2(0.5, 0.5), glassHit, inverseViewProjection, 0.00001), 0, 'invalid homogeneous world coordinate must bypass simplification');
const frontPoint = glassHit.clone().addScaledVector(rayDirection, -0.04);
const frontClip = new THREE.Vector4(frontPoint.x, frontPoint.y, frontPoint.z, 1).applyMatrix4(viewProjection);
const frontNdc = new THREE.Vector3(frontClip.x / frontClip.w, frontClip.y / frontClip.w, frontClip.z / frontClip.w);
assert.equal(validAtDepth(frontNdc.z * 0.5 + 0.5, new THREE.Vector2(frontNdc.x * 0.5 + 0.5, frontNdc.y * 0.5 + 0.5), glassHit, inverseViewProjection), 0, 'surface in front of glass must bypass simplification');
const contactPoint = glassHit.clone().addScaledVector(rayDirection, 0.00005);
const contactClip = new THREE.Vector4(contactPoint.x, contactPoint.y, contactPoint.z, 1).applyMatrix4(viewProjection);
const contactNdc = new THREE.Vector3(contactClip.x / contactClip.w, contactClip.y / contactClip.w, contactClip.z / contactClip.w);
assert.equal(validAtDepth(contactNdc.z * 0.5 + 0.5, new THREE.Vector2(contactNdc.x * 0.5 + 0.5, contactNdc.y * 0.5 + 0.5), glassHit, inverseViewProjection), 0, 'same or nearly coincident glass surface must bypass simplification');
}
console.log(`Mosaic depth-region projection checks passed: ${reconstructed} perspective reconstructions, ${mapped} original-depth UV mappings across camera/zoom-pan/depthScale, near-depth gate, distance-independent color application, and invalid-surface guards.`);