scene: Day sky bound was 1.2e-6 low; refine to 1e-6 degrees and check both directions
Some checks failed
Build pull request / Build pull request (pull_request) Failing after 44s

Day lightest 0.92805 -> 0.928052 (true terrain peak 0.92805117...). The compass
refinement now halves to 5e-7 degrees; the drift test checks the constant against
the refined computation and an independent golden-section sweep, exact to
[extreme, extreme + 1e-6).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nibu 2026-11-09 23:28:55 +00:00
commit eafa3eda79
3 changed files with 63 additions and 7 deletions

View file

@ -130,26 +130,82 @@ describe('both scenes of a state lie inside skyBackdrop', () => {
});
describe('skyBackdrop holds the union of what the scenes draw', () => {
// An independent extremum: not terrainDrawn's grid and compass search but alternating golden-section
// line searches in elevation and azimuth from 60 starts (a golden-angle spiral and scatter about the sun),
// the bracket shrinking to 1e-3 degrees. A plateau (the shader clamps) cannot stall it: each line search
// always moves to the better end of its bracket.
const extremes = (state: 'day' | 'dusk' | 'night') => {
const sky = chooseSky(terrainSkyFor(state));
const rad = Math.PI / 180;
const f = (el: number, az: number) => {
const e = Math.min(90, Math.max(0, el)) * rad;
const a = az * rad;
const [r, g, b] = skyColour(sky, [
Math.cos(e) * Math.sin(a),
Math.sin(e),
-Math.cos(e) * Math.cos(a),
]).map((v) => Math.min(1, Math.max(0, v)));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const P = 0.6180339887;
const line = (g: (x: number) => number, mid: number, w: number) => {
let a = mid - w;
let b = mid + w;
for (let k = 0; k < 40; k += 1) {
const c = b - P * (b - a);
const d = a + P * (b - a);
if (g(c) > g(d)) b = d;
else a = c;
}
return (a + b) / 2;
};
const search = (sign: 1 | -1) => {
let best = -Infinity;
for (let i = 0; i < 60; i += 1) {
let el =
i % 2
? sky.elevation + (((i * 7919) % 1600) - 800) / 100
: Math.asin((i + 0.5) / 60) / rad;
let az =
i % 2 ? sky.azimuth + (((i * 104729) % 1600) - 800) / 100 : (i * 137.50776405) % 360;
for (let w = 8; w > 1e-3; w /= 2) {
const [el0, az0] = [el, az];
el = line((x) => sign * f(Math.max(0, x), az0), el0, w);
const el1 = el;
az = line((x) => sign * f(el1, x), az0, w);
best = Math.max(best, sign * f(el, az));
}
}
return sign * best;
};
return { darkest: search(-1), lightest: search(1) };
};
const computed = (state: 'day' | 'dusk' | 'night') => {
const r = [ridgeDrawn(state), terrainDrawn(state)].filter((x) => x !== undefined) as {
darkest: number;
lightest: number;
}[];
const ind = extremes(state);
const ridge = ridgeDrawn(state);
return {
darkest: Math.min(...r.map((x) => x.darkest)),
lightest: Math.max(...r.map((x) => x.lightest)),
darkest: Math.min(...r.map((x) => x.darkest), ind.darkest, ridge?.darkest ?? 1),
lightest: Math.max(...r.map((x) => x.lightest), ind.lightest, ridge?.lightest ?? 0),
};
};
// The constants are the swept extremes rounded outward to six places, so each lies in [extreme, extreme + 1e-6):
// a constant 1e-6 low is under the extreme, one 2e-5 high is over the bound. SEARCH is the searches' own error.
const SEARCH = 1e-8;
it.each(['day', 'dusk', 'night'] as const)(
'%s: the constant bounds the computed range and sits within 1e-5 of it',
'%s: the constant is the swept extreme rounded outward, within 1e-6 above it',
(state) => {
const c = computed(state);
const b = skyBackdrop(state);
expect(b.darkest).toBeLessThanOrEqual(c.darkest);
expect(b.lightest).toBeGreaterThanOrEqual(c.lightest);
expect(c.darkest - b.darkest).toBeLessThan(1e-5);
expect(b.lightest - c.lightest).toBeLessThan(1e-5);
expect(c.darkest - b.darkest).toBeLessThan(1e-6 + SEARCH);
expect(b.lightest - c.lightest).toBeLessThan(1e-6 + SEARCH);
}
);

View file

@ -121,7 +121,7 @@ export function terrainDrawn(state: SkyState): Range {
});
const refine = (from: Point, sign: 1 | -1) => {
let best = from;
for (let step = 1; step > 0.0005; step /= 2) {
for (let step = 1; step > 5e-7; step /= 2) {
let improved = true;
while (improved) {
const cur = best;

View file

@ -63,7 +63,7 @@ export interface SkyBackdrop {
// bounds all three fixed states.
const BACKDROPS: Record<SkyState, SkyBackdrop> = {
auto: { darkest: 0.016727, lightest: 0.967912 },
day: { darkest: 0.237309, lightest: 0.92805 },
day: { darkest: 0.237309, lightest: 0.928052 },
dusk: { darkest: 0.016727, lightest: 0.967912 },
night: { darkest: 0.058099, lightest: 0.077467 },
};