<!-- Radiosity demo — simplified Cornell box with 5 patches -->
<div class="controls">
<label>Left wall
<input type="color" id="leftColor" value="#cc2222" title="Pick the color of the left wall">
</label>
<label>Right wall
<input type="color" id="rightColor" value="#2244cc" title="Pick the color of the right wall">
</label>
<label>Bounces
<input type="range" id="bounces" min="1" max="8" value="4" step="1">
<span id="bounceVal">4</span>
</label>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="scene" width="340" height="260" aria-label="Radiosity scene showing color bleeding between patches"></canvas>
<div id="status" class="status" aria-live="polite"></div>
/* Radiosity demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #f5f7fa; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; margin-bottom: .7rem; }
label { display: flex; align-items: center; gap: .35rem; font-size: .88rem; font-weight: 500; }
input[type=color] { width: 36px; height: 28px; border: 1px solid #ccc; border-radius: 5px; padding: 1px; cursor: pointer; }
input[type=range] { width: 90px; accent-color: #1d3557; }
#bounceVal { font-size: .85rem; min-width: 1.2em; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 8px; box-shadow: 0 1px 6px #0002; }
.status { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; color: #1d3557; }
// Radiosity demo — iterative energy gathering between surface patches.
// Each iteration gathers reflected light from all visible neighbors using form factors.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Convert hex color string to normalized RGB array.
function hexToRgb(hex) {
const r = parseInt(hex.slice(1,3),16);
const g = parseInt(hex.slice(3,5),16);
const b = parseInt(hex.slice(5,7),16);
return [r/255, g/255, b/255];
}
// Scene patches and their approximate form factors (hand-tuned for a Cornell-like box).
// Patch indices: 0=ceiling-light, 1=left-wall, 2=right-wall, 3=floor, 4=back-wall
// Form factors F[i][j]: fraction of patch i's hemisphere subtended by patch j.
// Each row sums to roughly 1 (small deviations are fine for a demo).
const F = [
[0, 0.2, 0.2, 0.4, 0.2], // ceiling sees all walls + floor
[0.15, 0, 0.05, 0.4, 0.4], // left wall sees ceiling, right, floor, back
[0.15, 0.05, 0, 0.4, 0.4], // right wall sees ceiling, left, floor, back
[0.35, 0.2, 0.2, 0, 0.25], // floor sees ceiling, left, right, back
[0.2, 0.25, 0.25, 0.35, 0] // back wall sees ceiling, left, right, floor
];
// Diffuse reflectivity rho[i] per patch (0=absorbs all, 1=perfect mirror). Light emits, not reflects.
const RHO = [0.0, 0.85, 0.85, 0.88, 0.88];
// Emission color per patch. Walls emit their chosen color as the energy source for bouncing.
// Emission: only the ceiling-light emits; rest are zero.
const BASE_EMISSION = [[1,1,1], [0,0,0], [0,0,0], [0,0,0], [0,0,0]];
// Run iterative radiosity (gathering pass). Returns final radiosity B[i] per patch.
function runRadiosity(leftRgb, rightRgb, bounces) {
// Emission: light is white; walls emit their user-chosen color.
const E = [
[1, 1, 1],
leftRgb.slice(),
rightRgb.slice(),
[0, 0, 0],
[0, 0, 0]
];
// B[i] = current radiosity (RGB) of patch i; start with emission only.
// B[i] = current radiosity (RGB) of patch i; start with emission only.
let B = E.map(e => e.slice());
// Each iteration gathers incoming energy from all other patches.
for (let iter = 0; iter < bounces; iter++) {
const newB = E.map(e => e.slice()); // Always restart from emission before accumulating.
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (i === j) continue;
// Gather from j into i newB[i] += rho[i] * F[j][i] * B[j]
for (let ch = 0; ch < 3; ch++) {
newB[i][ch] += RHO[i] * F[j][i] * B[j][ch];
}
}
}
B = newB;
}
return B;
}
// Draw the 5 patches on the canvas as colored rectangles.
function rgbCss(arr) {
// Clamp to [0,1] before converting to 8-bit.
const r = Math.round(Math.min(1, arr[0]) * 255);
const g = Math.round(Math.min(1, arr[1]) * 255);
const b = Math.round(Math.min(1, arr[2]) * 255);
return `rgb(${r},${g},${b})`;
}
function drawScene(B) {
ctx.clearRect(0, 0, W, H);
// ceiling-light strip at top center
ctx.fillStyle = rgbCss(B[0]);
ctx.fillRect(60, 0, 220, 50);
// left wall
ctx.fillStyle = rgbCss(B[1]);
ctx.fillRect(0, 0, 60, H);
// right wall
ctx.fillStyle = rgbCss(B[2]);
ctx.fillRect(280, 0, 60, H);
// floor strip at bottom center
ctx.fillStyle = rgbCss(B[3]);
ctx.fillRect(60, H - 50, 220, 50);
// back wall (the large center panel)
ctx.fillStyle = rgbCss(B[4]);
ctx.fillRect(60, 50, 220, H - 100);
// Patch labels in white
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#ffffffcc';
ctx.textAlign = 'center';
ctx.fillText('Light', W/2, 30);
ctx.textAlign = 'center';
ctx.fillText('Floor', W/2, H - 15);
ctx.save();
ctx.translate(30, H/2); ctx.rotate(-Math.PI/2);
ctx.fillText('Left', 0, 0); ctx.restore();
ctx.save();
ctx.translate(W - 30, H/2); ctx.rotate(Math.PI/2);
ctx.fillText('Right', 0, 0); ctx.restore();
ctx.fillStyle = '#333c';
ctx.textAlign = 'center';
ctx.fillText('Back wall', W/2, H/2);
}
function run() {
const leftHex = document.getElementById('leftColor').value;
const rightHex = document.getElementById('rightColor').value;
const bounces = parseInt(document.getElementById('bounces').value, 10);
const B = runRadiosity(hexToRgb(leftHex), hexToRgb(rightHex), bounces);
drawScene(B);
document.getElementById('status').textContent =
i18nFmt('Simulated %n% bounce(s) of indirect light.', { n: bounces });
}
function reset() {
document.getElementById('leftColor').value = '#cc2222';
document.getElementById('rightColor').value = '#2244cc';
document.getElementById('bounces').value = '4';
document.getElementById('bounceVal').textContent = '4';
run();
}
document.getElementById('bounces').addEventListener('input', function() {
document.getElementById('bounceVal').textContent = this.value;
});
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
// Run on load so the canvas is not blank.
run();
<!-- Radiosity demo — simplified Cornell box with 5 patches -->
<div class="controls">
<label>Left wall
<input type="color" id="leftColor" value="#cc2222" title="Pick the color of the left wall">
</label>
<label>Right wall
<input type="color" id="rightColor" value="#2244cc" title="Pick the color of the right wall">
</label>
<label>Bounces
<input type="range" id="bounces" min="1" max="8" value="4" step="1">
<span id="bounceVal">4</span>
</label>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="scene" width="340" height="260" aria-label="Radiosity scene showing color bleeding between patches"></canvas>
<div id="status" class="status" aria-live="polite"></div>
/* Radiosity demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #f5f7fa; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; margin-bottom: .7rem; }
label { display: flex; align-items: center; gap: .35rem; font-size: .88rem; font-weight: 500; }
input[type=color] { width: 36px; height: 28px; border: 1px solid #ccc; border-radius: 5px; padding: 1px; cursor: pointer; }
input[type=range] { width: 90px; accent-color: #1d3557; }
#bounceVal { font-size: .85rem; min-width: 1.2em; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 8px; box-shadow: 0 1px 6px #0002; }
.status { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; color: #1d3557; }
// Radiosity demo — iterative energy gathering between surface patches.
// Each iteration gathers reflected light from all visible neighbors using form factors.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Convert hex color string to normalized RGB array.
function hexToRgb(hex) {
const r = parseInt(hex.slice(1,3),16);
const g = parseInt(hex.slice(3,5),16);
const b = parseInt(hex.slice(5,7),16);
return [r/255, g/255, b/255];
}
// Scene patches and their approximate form factors (hand-tuned for a Cornell-like box).
// Patch indices: 0=ceiling-light, 1=left-wall, 2=right-wall, 3=floor, 4=back-wall
// Form factors F[i][j]: fraction of patch i's hemisphere subtended by patch j.
// Each row sums to roughly 1 (small deviations are fine for a demo).
const F = [
[0, 0.2, 0.2, 0.4, 0.2], // ceiling sees all walls + floor
[0.15, 0, 0.05, 0.4, 0.4], // left wall sees ceiling, right, floor, back
[0.15, 0.05, 0, 0.4, 0.4], // right wall sees ceiling, left, floor, back
[0.35, 0.2, 0.2, 0, 0.25], // floor sees ceiling, left, right, back
[0.2, 0.25, 0.25, 0.35, 0] // back wall sees ceiling, left, right, floor
];
// Diffuse reflectivity rho[i] per patch (0=absorbs all, 1=perfect mirror). Light emits, not reflects.
const RHO = [0.0, 0.85, 0.85, 0.88, 0.88];
// Emission color per patch. Walls emit their chosen color as the energy source for bouncing.
// Emission: only the ceiling-light emits; rest are zero.
const BASE_EMISSION = [[1,1,1], [0,0,0], [0,0,0], [0,0,0], [0,0,0]];
// Run iterative radiosity (gathering pass). Returns final radiosity B[i] per patch.
function runRadiosity(leftRgb, rightRgb, bounces) {
// Emission: light is white; walls emit their user-chosen color.
const E = [
[1, 1, 1],
leftRgb.slice(),
rightRgb.slice(),
[0, 0, 0],
[0, 0, 0]
];
// B[i] = current radiosity (RGB) of patch i; start with emission only.
// B[i] = current radiosity (RGB) of patch i; start with emission only.
let B = E.map(e => e.slice());
// Each iteration gathers incoming energy from all other patches.
for (let iter = 0; iter < bounces; iter++) {
const newB = E.map(e => e.slice()); // Always restart from emission before accumulating.
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (i === j) continue;
// Gather from j into i newB[i] += rho[i] * F[j][i] * B[j]
for (let ch = 0; ch < 3; ch++) {
newB[i][ch] += RHO[i] * F[j][i] * B[j][ch];
}
}
}
B = newB;
}
return B;
}
// Draw the 5 patches on the canvas as colored rectangles.
function rgbCss(arr) {
// Clamp to [0,1] before converting to 8-bit.
const r = Math.round(Math.min(1, arr[0]) * 255);
const g = Math.round(Math.min(1, arr[1]) * 255);
const b = Math.round(Math.min(1, arr[2]) * 255);
return `rgb(${r},${g},${b})`;
}
function drawScene(B) {
ctx.clearRect(0, 0, W, H);
// ceiling-light strip at top center
ctx.fillStyle = rgbCss(B[0]);
ctx.fillRect(60, 0, 220, 50);
// left wall
ctx.fillStyle = rgbCss(B[1]);
ctx.fillRect(0, 0, 60, H);
// right wall
ctx.fillStyle = rgbCss(B[2]);
ctx.fillRect(280, 0, 60, H);
// floor strip at bottom center
ctx.fillStyle = rgbCss(B[3]);
ctx.fillRect(60, H - 50, 220, 50);
// back wall (the large center panel)
ctx.fillStyle = rgbCss(B[4]);
ctx.fillRect(60, 50, 220, H - 100);
// Patch labels in white
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#ffffffcc';
ctx.textAlign = 'center';
ctx.fillText('Light', W/2, 30);
ctx.textAlign = 'center';
ctx.fillText('Floor', W/2, H - 15);
ctx.save();
ctx.translate(30, H/2); ctx.rotate(-Math.PI/2);
ctx.fillText('Left', 0, 0); ctx.restore();
ctx.save();
ctx.translate(W - 30, H/2); ctx.rotate(Math.PI/2);
ctx.fillText('Right', 0, 0); ctx.restore();
ctx.fillStyle = '#333c';
ctx.textAlign = 'center';
ctx.fillText('Back wall', W/2, H/2);
}
function run() {
const leftHex = document.getElementById('leftColor').value;
const rightHex = document.getElementById('rightColor').value;
const bounces = parseInt(document.getElementById('bounces').value, 10);
const B = runRadiosity(hexToRgb(leftHex), hexToRgb(rightHex), bounces);
drawScene(B);
document.getElementById('status').textContent =
i18nFmt('Simulated %n% bounce(s) of indirect light.', { n: bounces });
}
function reset() {
document.getElementById('leftColor').value = '#cc2222';
document.getElementById('rightColor').value = '#2244cc';
document.getElementById('bounces').value = '4';
document.getElementById('bounceVal').textContent = '4';
run();
}
document.getElementById('bounces').addEventListener('input', function() {
document.getElementById('bounceVal').textContent = this.value;
});
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
// Run on load so the canvas is not blank.
run();
Comments
Loading comments...