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.`);