8.4 KiB
Surface reflection implementation report
Status and scope
Implemented Task 1 from .superpowers/sdd/surface-reflection/task-1-brief.md.
The change is limited to the final non-REGION_ATLAS reflection composition in
src/shaders/mosaic.frag plus one source/math regression script. Existing
uniforms were sufficient, so src/main.ts was not changed.
The exact pre-edit shader is preserved at
docs/qa-surface-reflection/baseline-mosaic.frag.
Baseline SHA-256:
CEE5F75C7A62A9146CBE451C661C2E0066EE57EB8963A7B0F56C281873F37E35
Implementation
Changed files:
src/shaders/mosaic.fragscripts/test-surface-reflection.mjsdocs/qa-surface-reflection/baseline-mosaic.frag(pre-edit snapshot)docs/qa-surface-reflection/implementation-report.md
The existing rectangular reflection remains intact and is now named
specGloss. A separate broad reflection is added after color-plane
simplification and before #include <colorspace_fragment>.
Final coefficients and rationale:
- Surface reflection edge weight:
mix(0.35, 1.0, edgeWeight). This keeps the existing refractionedgeWeightandstrengthpath unchanged while reducing reflection at pane edges without removing it entirely. - Existing rectangular reflection strength:
.12, unchanged. - Broad reflection strength:
.035, intentionally low relative to the rectangular lobe. - Broad spatial footprint:
smoothstep(vec2(.15), vec2(1.75), windowCoord), wider and softer than the existing.45to1.15rectangular mask. - View/surface response:
pow(max(dot(nCurved, v), 0.), 2.), using the same signed curved normal that drives refraction and the existing reflection. - Both reflection amplitudes use the same
clamp(uGlossStrength, 0., 1.)gate, so gloss0makes both contributions exactly zero.
No environment texture, render pass, render target, dependency, palette input, history input, UI, camera, placement, framing, or other glass-mode change was added.
TDD evidence
Focused regression: scripts/test-surface-reflection.mjs.
The assertions are source/math-only and do not claim to execute GLSL or prove
browser visuals.
RED
Command:
node scripts/test-surface-reflection.mjs
Exact result against the pre-edit shader:
exit_code=1
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: both surface reflection terms must share a clamped gloss gate
at file:///D:/project/behind_glass/scripts/test-surface-reflection.mjs:11:8 {
generatedMessage: false,
actual: '#ifndef REGION_ATLAS\n' +
' color.a=1.;\n' +
' vec3 v=normalize(uContentCameraPosition-gh),reflected=reflect(-v,nCurved); float lightT=(uLightPosition.z-gh.z)/safeZ(reflected.z); vec2 lightHit=gh.xy+reflected.xy*lightT; vec2 halfSize=vec2(.32,.42)*min(uPaneSize.x,uPaneSize.y); vec2 windowCoord=abs(lightHit-uLightPosition.xy)/halfSize; vec2 windowMask=1.-smoothstep(vec2(.45),vec2(1.15),windowCoord); float spec=windowMask.x*windowMask.y*step(.0001,reflected.z)*step(0.,lightT); float gloss=spec*clamp(uGlossStrength,0.,1.)*.12*edgeWeight*clamp(uStrength,0.,1.); color.rgb=mix(color.rgb,vec3(1.),gloss); gl_FragColor=color;\n' +
' #include <colorspace_fragment>\n',
expected: /float glossStrength=clamp\(uGlossStrength,0\.,1\.\);/,
operator: 'match'
}
This failed for the intended reason: the pre-edit shader had one reflection term, no shared gloss gate, no broad reflection, and no separate surface edge weight.
GREEN
Command:
node scripts/test-surface-reflection.mjs
Exact result after the minimal shader edit:
exit_code=0
surface reflection source/math regression passed
The final regression checks zero-gloss gating, .35 to 1.0 surface-weight
bounds, preservation of the refraction edge weight, normal/view and light-hit
spatial dependence, non-atlas placement, and ordering before color-space
conversion.
Automated verification
All six existing scripts from docs/qa-window-immersion/verification.md were
run with node --experimental-strip-types after implementation.
=== node --experimental-strip-types scripts/test-camera-history.mjs ===
Camera/history checks passed: default/max clamp, model reset keys, ping-pong and disposal, premultiplied color, cadence 0.999955/0.999955/0.999955, 70ms settling.
=== node --experimental-strip-types scripts/test-region-axis-split.mjs ===
Region axis-split checks passed: SSE axis/cut/pair scoring, vertical/horizontal cuts 2/6, observed side-label pairs, checkerboard model, single-label fallback, metadata round-trip, confidence blending, dark eye/nose protection, and edge-independent color strength.
=== node --experimental-strip-types scripts/test-mosaic-depth-distance.mjs ===
Mosaic depth-region projection checks passed: 21 perspective reconstructions, 12 original-depth UV mappings across camera/zoom-pan/depthScale, near-depth gate, distance-independent color application, and invalid-surface guards.
=== node --experimental-strip-types scripts/test-mosaic-continuity.mjs ===
Mosaic projected-UV continuity passed: 493760 Jacobians, 87200 seam convergence checks, center/edge anchors, and view-dependent within-cell displacement.
=== node --experimental-strip-types scripts/test-region-atlas-dark-sampling.mjs ===
Region atlas dark sampling source/math-only regression passed: phase-offset narrow feature, far/invalid/uniform invariants, and four-probe bounded scoring contract.
=== node --experimental-strip-types scripts/test-window-reveal-coverage.mjs ===
Window reveal coverage passed: 146 previous-frame misses sealed across 2 aspects, 3 depth scales, 17 view offsets (including neutral) and 49 aperture samples; geometry reuse/disposal verified.
Build command:
npm.cmd run build
Result: exit code 0; TypeScript and Vite production build succeeded. Vite
reported the existing large-chunk advisory for the main and AI-depth assets.
Visual comparison and limitations
No browser or GPU visual capture was performed in this implementation task.
The preserved shader baseline is available at
docs/qa-surface-reflection/baseline-mosaic.frag; use it for the
same-viewport dog1 comparison with the brief's fixed values, including gloss
0, 0.30, and 0.40, curvature -1/0/+1, camera 0.90, portrait, and indoor
checks.
The automated regression does not prove grass highlight appearance, adjacent cell coherence, edge leakage, eye/nose readability, mobile behavior, or GPU shader compilation. Those remain visual/browser verification items. No claim of GPU, mobile, or visual success is made here.
Round 1/5 regression hardening
Addressed both Astra findings without changing production scope. The focused
script now extracts individual GLSL statements instead of using unrestricted
cross-statement matching, compares edgeWeight, strength, and sceneHit
against the preserved pre-edit shader, inspects the effective atlas branch,
and verifies simplification before final composition and composition before
color-space conversion. It evaluates the extracted specGloss,
broadReflection, and final mix expressions for gloss-zero output plus
representative spatial/view variation. It also applies an in-memory constant
50% final-mix mutation and requires the covering assertions to reject it.
Exact covering command:
node scripts/test-surface-reflection.mjs
Exact output:
exit_code=0
surface reflection source/math regression passed; constant-output mutation rejected
Only the focused regression was rerun because this round changed no production file; the six existing checks and build remain covered by the prior verification record above.
Round 2/5 atlas exclusion hardening
Addressed the remaining atlas-exclusion finding without changing production
code. scripts/test-surface-reflection.mjs now walks all REGION_ATLAS
preprocessor branches, retains the complete effective source for the macro
defined, and compares that source with the preserved baseline. It also inserts
an in-memory gloss-dependent white reflection immediately after the earlier
atlas atlasCenterColor sample and requires the covering assertions to reject
that mutation. This remains a source/math regression and is not GPU visual
verification.
Exact covering command:
node scripts/test-surface-reflection.mjs
Exact output:
exit_code=0
surface reflection source/math regression passed; constant-output and atlas-reflection mutations rejected
No full suite or build was rerun because the production shader remained unchanged.