Buildings cast no shadows: daylight passes player-built pieces (t37)
The lighting finds its own roof per column, skipping walls, belts, tubes, machines and molds, and lets light through them like air, in both looks. Natural rock still shades; the simulation's roof (steam condensing under a built roof) is unchanged. New test/t_light.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -352,9 +352,9 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
|||||||
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
||||||
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
|
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
|
||||||
|
|
||||||
### src/render/light.js (79 lines)
|
### src/render/light.js (95 lines)
|
||||||
Propagated coloured light from glowing and burning cells.
|
Propagated coloured light from glowing and burning cells.
|
||||||
- functions: computeLight
|
- functions: builtForLight, computeLight
|
||||||
|
|
||||||
### src/render/world.js (308 lines)
|
### src/render/world.js (308 lines)
|
||||||
Scale colour without changing the smooth-look class.
|
Scale colour without changing the smooth-look class.
|
||||||
@@ -509,6 +509,7 @@ The frame loop and boot.
|
|||||||
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
||||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
|
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||||
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
|
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
|||||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||||
open or not verified. Keep each entry short; the git history has the details.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-25: t37: buildings cast no shadows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `computeLight()` (`src/render/light.js`): a per-view-column light roof that skips player-built
|
||||||
|
cells (`builtForLight`: unnatural solids, block, machine or part cells), and built cells pass
|
||||||
|
light like air. `drawWorld()` stops marking built cells as shading terrain for the smooth look.
|
||||||
|
The simulation's `roofY` is untouched (built roofs still condense steam).
|
||||||
|
- `test/t_light.js`; screenshots `shots/t37-before-*.png`, `shots/t37-after-*.png`.
|
||||||
|
`run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
## 2026-10-25: t34: the nozzle blows no further than it sucks (Claude Code, Codex)
|
## 2026-10-25: t34: the nozzle blows no further than it sucks (Claude Code, Codex)
|
||||||
|
|
||||||
- `toolBlow()` (`src/sim/vacuum.js`) aims at the capped point from `nozzle()` when the cursor is
|
- `toolBlow()` (`src/sim/vacuum.js`) aims at the capped point from `nozzle()` when the cursor is
|
||||||
|
|||||||
+18
-2
@@ -2,6 +2,9 @@
|
|||||||
// ---------- Lighting ----------
|
// ---------- Lighting ----------
|
||||||
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
|
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
|
||||||
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
|
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
|
||||||
|
function builtForLight(i, x, y) {
|
||||||
|
return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i];
|
||||||
|
}
|
||||||
function computeLight() {
|
function computeLight() {
|
||||||
const res = SET.lighting === 'fast' ? 4 : 2;
|
const res = SET.lighting === 'fast' ? 4 : 2;
|
||||||
const M = Math.ceil(28 / res);
|
const M = Math.ceil(28 / res);
|
||||||
@@ -13,6 +16,7 @@ function computeLight() {
|
|||||||
R.lightC.width = lw; R.lightC.height = lh;
|
R.lightC.width = lw; R.lightC.height = lh;
|
||||||
R.lightImg = R.lightX.createImageData(lw, lh);
|
R.lightImg = R.lightX.createImageData(lw, lh);
|
||||||
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
|
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
|
||||||
|
R.lightRoof = new Int32Array(lw);
|
||||||
}
|
}
|
||||||
R.lx0 = lx0; R.ly0 = ly0;
|
R.lx0 = lx0; R.ly0 = ly0;
|
||||||
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
|
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
|
||||||
@@ -20,15 +24,27 @@ function computeLight() {
|
|||||||
const P = res;
|
const P = res;
|
||||||
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
|
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
|
||||||
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
|
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
|
||||||
|
// Only sampled view columns, starting at the simulation roof; nothing below the view matters.
|
||||||
|
const roofEnd = Math.min(H, (ly0 + lh) * res), lightRoof = R.lightRoof;
|
||||||
|
for (let lx = 0; lx < lw; lx++) {
|
||||||
|
const x = (lx0 + lx) * res;
|
||||||
|
if (x < 0 || x >= W) continue;
|
||||||
|
let y = roofY[x];
|
||||||
|
for (; y < roofEnd; y++) {
|
||||||
|
const i = y * W + x, ph = PH[mat[i]];
|
||||||
|
if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y)) break;
|
||||||
|
}
|
||||||
|
lightRoof[lx] = y;
|
||||||
|
}
|
||||||
for (let ly = 0; ly < lh; ly++) {
|
for (let ly = 0; ly < lh; ly++) {
|
||||||
const wy = (ly0 + ly) * res;
|
const wy = (ly0 + ly) * res;
|
||||||
for (let lx = 0; lx < lw; lx++) {
|
for (let lx = 0; lx < lw; lx++) {
|
||||||
const wx = (lx0 + lx) * res, k = ly * lw + lx;
|
const wx = (lx0 + lx) * res, k = ly * lw + lx;
|
||||||
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
|
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
|
||||||
const i = wy * W + wx, m = mat[i], ph = PH[m];
|
const i = wy * W + wx, m = mat[i], ph = PH[m];
|
||||||
la[k] = ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
|
la[k] = builtForLight(i, wx, wy) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
|
||||||
let r = 0, g = 0, b = 0;
|
let r = 0, g = 0, b = 0;
|
||||||
if (wy < roofY[wx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
|
if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
|
||||||
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
|
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
|
||||||
// emission from heat: hottest grain in the block
|
// emission from heat: hottest grain in the block
|
||||||
let T = 0;
|
let T = 0;
|
||||||
|
|||||||
+1
-1
@@ -219,7 +219,7 @@ function drawWorld() {
|
|||||||
else cl = 5;
|
else cl = 5;
|
||||||
}
|
}
|
||||||
top = cl << 24;
|
top = cl << 24;
|
||||||
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 || cl === 4 || cl === 6 ? 0xff : 0);
|
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0);
|
||||||
}
|
}
|
||||||
if (fv < 255) {
|
if (fv < 255) {
|
||||||
const k = fv / 255; r *= k; g *= k; b *= k;
|
const k = fv / 255; r *= k; g *= k; b *= k;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
for (const f of ['setup.js', 'light.js']) run(fs.readFileSync(path.join(__dirname, '../src/render', f), 'utf8'));
|
||||||
|
run(`
|
||||||
|
const SET = { lighting: 'full', daylight: 1, ambient: 0.12 };
|
||||||
|
R.lightC = {};
|
||||||
|
R.lightX = { createImageData: (w, h) => ({ data: new Uint8ClampedArray(w * h * 4) }), putImageData() {} };
|
||||||
|
R.vx0 = 0; R.vy0 = 100; R.bw = 320; R.bh = 80;
|
||||||
|
function lightAt(a, x, y) { return a[(Math.floor(y / R.lres) - R.ly0) * R.lw + Math.floor(x / R.lres) - R.lx0]; }
|
||||||
|
`);
|
||||||
|
flatWorld(run, 320, 200, 160);
|
||||||
|
run(`
|
||||||
|
bot.lamp = false;
|
||||||
|
for (let bx = 8; bx < 30; bx++) placeBlock(BP_WALL, RSTONE, bx, 20, true);
|
||||||
|
for (let by = 21; by < 39; by++) {
|
||||||
|
placeBlock(BP_WALL, RSTONE, 8, by, true);
|
||||||
|
placeBlock(BP_WALL, RSTONE, 29, by, true);
|
||||||
|
}
|
||||||
|
placeBlock(BP_BELT, 1, 12, 30, true);
|
||||||
|
placeBlock(BP_TUBE, 0, 14, 30, true);
|
||||||
|
addMachine('silo', 18, 30, 0);
|
||||||
|
addMold('bit', 24, 30, RSTONE, true);
|
||||||
|
for (let y = 80; y < 88; y++) for (let x = 180; x < 280; x++) setCell(y * W + x, STONE, AMB[y], F_NAT);
|
||||||
|
updateRoof();
|
||||||
|
const simRoof = Array.from(roofY);
|
||||||
|
`);
|
||||||
|
for (const mode of ['full', 'fast']) {
|
||||||
|
run(`SET.lighting = '${mode}'; computeLight();`);
|
||||||
|
for (const channel of ['lr', 'lg']) {
|
||||||
|
const [inside, open, rock, ground, openGround] = run(`[lightAt(R.${channel}, 80, 144), lightAt(R.${channel}, 152, 144), lightAt(R.${channel}, 232, 144), lightAt(R.${channel}, 80, 160), lightAt(R.${channel}, 152, 160)]`);
|
||||||
|
check(`${mode} ${channel}: under a built roof matches open daylight`, Math.abs(inside / open - 1) < 0.03, `${inside} / ${open}`);
|
||||||
|
check(`${mode} ${channel}: ground under the base matches outside`, Math.abs(ground / openGround - 1) < 0.03, `${ground} / ${openGround}`);
|
||||||
|
check(`${mode} ${channel}: natural overhang still shades`, rock < open * 0.95, `${rock} / ${open}`);
|
||||||
|
}
|
||||||
|
// Directly check transmission as daylight can hide an opaque piece in the open.
|
||||||
|
for (const [name, x, y] of [['wall', 32, 120], ['belt', 48, 120], ['tube', 56, 120], ['machine', 72, 120], ['mold', 96, 120]]) {
|
||||||
|
const a = run(`lightAt(R.latt, ${x}, ${y})`);
|
||||||
|
check(`${mode}: ${name} transmits like air`, Math.abs(a - Math.pow(0.978, mode === 'fast' ? 4 : 2)) < 1e-6, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check('lighting leaves the simulation roof unchanged', run('simRoof.every((y, x) => roofY[x] === y) && roofY[80] === 80'));
|
||||||
|
for (const material of ['STONE', 'SAND', 'WATER']) {
|
||||||
|
run(`setCell(96 * W + 80, ${material}, AMB[96], F_NAT); updateRoof(); computeLight();`);
|
||||||
|
check(`${material} above the view still blocks daylight`, run('lightAt(R.lr, 80, 144) < 0.95'));
|
||||||
|
run('setCell(96 * W + 80, EMPTY, AMB[96], 0);');
|
||||||
|
}
|
||||||
|
run('updateRoof(); computeLight();');
|
||||||
|
check('removing an obstruction restores daylight', run('lightAt(R.lr, 80, 144) === 1'));
|
||||||
|
for (const field of ['blockP[24 * BW + 20]', 'mach[96 * W + 80]', 'part[96 * W + 80]']) {
|
||||||
|
run(`setCell(96 * W + 80, STONE, AMB[96], F_NAT); ${field} = 1; updateRoof(); computeLight();`);
|
||||||
|
check(`${field}: built contents do not shade or absorb light`, run('lightAt(R.lr, 80, 144) === 1 && Math.abs(lightAt(R.latt, 80, 96) - Math.pow(0.978, 4)) < 1e-6'));
|
||||||
|
run(`${field} = 0; setCell(96 * W + 80, EMPTY, AMB[96], 0);`);
|
||||||
|
}
|
||||||
|
run('updateRoof();');
|
||||||
|
run(`SET.daylight = 0; temp[140 * W + 80] = 1500; computeLight();`);
|
||||||
|
check('hot material inside the base lights its surroundings', run('lightAt(R.lr, 88, 140) > 0.2'));
|
||||||
|
run(`temp[140 * W + 80] = AMB[140]; bot.x = 72; bot.y = 130; bot.lamp = true; computeLight();`);
|
||||||
|
check('the bot lamp still lights the base', run('lightAt(R.lr, 80, 140) > 0.2'));
|
||||||
|
done();
|
||||||
Reference in New Issue
Block a user