Files
behind_glass/scripts/test-surface-reflection.mjs
horoli 71c8d3b7ff Implement patterned glass and maximum-depth distance controls
Add gobang surface refraction and shared photo/depth distance adjustment. Compare V2 Small and Base at input 770 with maximum-depth masks and pixel diagnostics. Record browser QA and update runtime dependencies and model notices.
2026-10-06 14:33:47 +09:00

204 lines
9.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const shader = (await readFile(new URL('../src/shaders/mosaic.frag', import.meta.url), 'utf8')).replace(/\r\n/g, '\n');
const baseline = (await readFile(new URL('../docs/qa-surface-reflection/baseline-mosaic.frag', import.meta.url), 'utf8')).replace(/\r\n/g, '\n');
function statement(source, type, name) {
const match = source.match(new RegExp(`\\b${type} ${name}=([^;]+);`));
assert.ok(match, `complete ${type} ${name} statement exists`);
return match[1];
}
function effectiveRegionAtlasSource(source) {
let active = true;
const stack = [];
const effective = [];
for (const line of source.split('\n')) {
const directive = line.trim();
if (directive === '#ifdef REGION_ATLAS') {
stack.push({ parentActive: active, ifdef: true });
continue;
}
if (directive === '#ifndef REGION_ATLAS') {
stack.push({ parentActive: active, ifdef: false });
active = false;
continue;
}
if (directive === '#else') {
const frame = stack.at(-1);
assert.ok(frame, 'REGION_ATLAS preprocessor else has a matching branch');
active = frame.parentActive && !frame.ifdef;
continue;
}
if (directive === '#endif') {
const frame = stack.pop();
assert.ok(frame, 'REGION_ATLAS preprocessor endif has a matching branch');
active = frame.parentActive;
continue;
}
if (active) effective.push(line);
}
assert.equal(stack.length, 0, 'REGION_ATLAS preprocessor branches are balanced');
return effective.join('\n');
}
function finalOutputBranches(source) {
const reflectionStart = source.indexOf('#ifndef REGION_ATLAS\n color.a=1.;');
const outputEnd = source.indexOf('\n#endif\n}', reflectionStart);
assert.ok(reflectionStart >= 0 && outputEnd > reflectionStart,
'surface reflections must be in the final non-atlas display pass');
const output = source.slice(reflectionStart, outputEnd);
const atlasMarker = '\n#else\n';
const atlasIndex = output.indexOf(atlasMarker);
assert.ok(atlasIndex >= 0, 'the final output must expose an effective atlas branch');
return {
reflectionStart,
output,
nonAtlas: output.slice(0, atlasIndex),
atlas: output.slice(atlasIndex + atlasMarker.length),
};
}
function finalWeightExpression(nonAtlas) {
const match = nonAtlas.match(/color\.rgb=mix\(color\.rgb,vec3\(1\.\),([^;]+)\);/);
assert.ok(match, 'final surface composition must mix the color with white');
return match[1];
}
function expressions(source) {
const { reflectionStart, output, nonAtlas, atlas } = finalOutputBranches(source);
// Atlas now shares the depth-aware sampling correction. It must still contain
// no surface reflection, rather than remain byte-identical to the old optics.
assert.doesNotMatch(effectiveRegionAtlasSource(source), /\breflect\(|color\.rgb=mix\(color\.rgb,vec3\(1\.\)/,
'complete effective REGION_ATLAS source must exclude surface reflection');
const finalWeight = finalWeightExpression(nonAtlas);
assert.equal(finalWeight, 'specGloss+broadReflection',
'final composition must use the complete existing and broad amplitudes');
assert.equal(statement(source, 'float', 'surfaceReflectionWeight'), 'mix(.35,1.,edgeWeight)',
'surface reflection must use a separate bounded edge weight');
assert.equal(statement(source, 'float', 'glossStrength'), 'clamp(uGlossStrength,0.,1.)',
'both surface reflection terms must share the actual clamped gloss gate');
assert.equal(statement(source, 'float', 'specGloss'),
'spec*glossStrength*.12*surfaceReflectionWeight*clamp(uStrength,0.,1.)',
'the existing rectangular reflection must use its complete amplitude expression');
assert.equal(statement(source, 'vec2', 'broadWindowMask'),
'1.-smoothstep(vec2(.15),vec2(1.75),windowCoord)',
'the broad reflection must use its complete soft spatial mask');
assert.equal(statement(source, 'float', 'broadFacing'), 'pow(max(dot(nCurved,v),0.),2.)',
'the broad reflection factor must use the curved normal and view direction');
assert.equal(statement(source, 'float', 'broadReflection'),
'broadWindowMask.x*broadWindowMask.y*broadFacing*glossStrength*.035*surfaceReflectionWeight*clamp(uStrength,0.,1.)',
'the broad reflection must use its complete spatial/view-dependent amplitude expression');
for (const name of ['edgeWeight', 'strength', 'sceneHit']) {
assert.equal(statement(source, name === 'sceneHit' ? 'vec3' : 'float', name),
statement(baseline, name === 'sceneHit' ? 'vec3' : 'float', name),
`${name} refraction expression remains unchanged from the preserved baseline`);
}
assert.ok(!nonAtlas.slice('#ifndef REGION_ATLAS\n'.length).includes('REGION_ATLAS'),
'reflection composition must stay outside the atlas branch');
assert.match(atlas, /color\.a=atlasEvidence;\s+gl_FragColor=color;/,
'the effective atlas branch must preserve atlas evidence output');
assert.doesNotMatch(atlas, /specGloss|broadReflection|surfaceReflectionWeight|color\.rgb=mix/,
'the effective atlas branch must not receive final surface reflection');
const simplification = source.indexOf('color.rgb=mix(color.rgb,candidateColor,');
assert.ok(simplification >= 0 && simplification < reflectionStart,
'color-plane simplification must precede final reflection composition');
const finalMix = source.indexOf('color.rgb=mix', reflectionStart);
const colorSpace = source.indexOf('#include <colorspace_fragment>', finalMix);
assert.ok(finalMix >= 0 && colorSpace > finalMix,
'reflection composition must happen before color-space conversion');
return {
surfaceWeight: statement(source, 'float', 'surfaceReflectionWeight'),
gloss: statement(source, 'float', 'glossStrength'),
spec: statement(source, 'float', 'specGloss'),
broadMask: statement(source, 'vec2', 'broadWindowMask'),
broadFacing: statement(source, 'float', 'broadFacing'),
broad: statement(source, 'float', 'broadReflection'),
finalWeight,
output,
};
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function mix(left, right, factor) {
return left * (1 - factor) + right * factor;
}
function evaluate(expression, variables) {
const names = Object.keys(variables);
return Function(...names, 'clamp', 'max', 'pow', 'mix', `return ${expression};`)(
...names.map(name => variables[name]), clamp, Math.max, Math.pow, mix);
}
function evaluateOutput(source, sample) {
const expr = expressions(source);
const surfaceReflectionWeight = evaluate(expr.surfaceWeight, { edgeWeight: sample.edgeWeight });
const glossStrength = evaluate(expr.gloss, { uGlossStrength: sample.uGlossStrength });
const variables = {
...sample,
surfaceReflectionWeight,
glossStrength,
};
const specGloss = evaluate(expr.spec, variables);
const broadReflection = evaluate(expr.broad, variables);
const weight = evaluate(expr.finalWeight, { ...variables, specGloss, broadReflection });
const output = sample.baseColor * (1 - weight) + weight;
return { specGloss, broadReflection, weight, output };
}
function assertBehavior(source) {
const center = {
spec: .82, broadWindowMask: { x: .95, y: .88 }, broadFacing: .92,
edgeWeight: .9, uGlossStrength: .3, uStrength: 1, baseColor: .22,
};
const viewTilt = { ...center, broadFacing: .25 };
const spatialEdge = {
...center, spec: .18, broadWindowMask: { x: .40, y: .55 },
edgeWeight: .1, baseColor: .31,
};
const glossZero = evaluateOutput(source, { ...center, uGlossStrength: 0 });
const centerOutput = evaluateOutput(source, center);
const viewTiltOutput = evaluateOutput(source, viewTilt);
const spatialEdgeOutput = evaluateOutput(source, spatialEdge);
assert.equal(glossZero.specGloss, 0, 'gloss 0 removes the existing reflection amplitude');
assert.equal(glossZero.broadReflection, 0, 'gloss 0 removes the broad reflection amplitude');
assert.equal(glossZero.weight, 0, 'gloss 0 removes the actual final reflection mix weight');
assert.equal(glossZero.output, center.baseColor, 'gloss 0 preserves the pre-reflection color');
assert.ok(centerOutput.specGloss > 0 && centerOutput.broadReflection > 0,
'representative positive-gloss input produces both actual reflection amplitudes');
assert.notEqual(centerOutput.broadReflection, viewTiltOutput.broadReflection,
'the broad amplitude varies with representative view/normal input');
assert.notEqual(centerOutput.broadReflection, spatialEdgeOutput.broadReflection,
'the broad amplitude varies with representative spatial input');
assert.notEqual(centerOutput.output, viewTiltOutput.output,
'the actual final output varies with representative view input');
assert.notEqual(centerOutput.output, spatialEdgeOutput.output,
'the actual final output varies with representative spatial input');
}
expressions(shader);
assertBehavior(shader);
const constantOutputMutation = shader.replace(
'color.rgb=mix(color.rgb,vec3(1.),specGloss+broadReflection);',
'color.rgb=mix(color.rgb,vec3(1.),.5);');
assert.notEqual(constantOutputMutation, shader, 'constant-output mutation must change the shader');
assert.throws(() => {
expressions(constantOutputMutation);
assertBehavior(constantOutputMutation);
}, /complete existing and broad amplitudes|actual final output varies/,
'constant 50% white output mutation must fail the covering assertions');
const atlasReflectionMutation = shader.replace(
' vec3 atlasCenterColor=sampleScene(finalSceneUv).rgb;',
' vec3 atlasCenterColor=sampleScene(finalSceneUv).rgb; float atlasGloss=clamp(uGlossStrength,0.,1.)*.5; color.rgb=mix(color.rgb,vec3(1.),atlasGloss);');
assert.notEqual(atlasReflectionMutation, shader, 'atlas-reflection mutation must change the shader');
assert.throws(() => expressions(atlasReflectionMutation), /complete effective REGION_ATLAS source/,
'an earlier REGION_ATLAS reflection mutation must fail the covering assertions');
console.log('surface reflection source/math regression passed; constant-output and atlas-reflection mutations rejected');