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.
204 lines
9.9 KiB
JavaScript
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');
|