Distance field shadows
Simulation of dynamic lighting using a distance field
by i_e_b
HTML
<div id="container">
<canvas id="canvas" class="subcanv" width="512" height="512"></canvas>
<canvas id="overlay" class="subcanv" width="512" height="512"></canvas>
</div>
<h4>Click to add 'solid' points; Move cursor to change lighting</h4>
<input type="checkbox" id="blur" />
<label for="blur">Blur shadow map</label>
<input type="checkbox" id="jitterStart" checked />
<label for="jitterStart">Jitter ray offset</label>
<input type="checkbox" id="surfaceCheck" checked />
<label for="surfaceCheck">Near-surface checks</label>
<input type="checkbox" id="useCone" checked />
<label for="useCone">Soften by area</label>
<input type="checkbox" id="jitterStep" />
<label for="jitterStep">Jitter ray steps</label>
<p id="frameRate"></p>
<p>Distance map: <span id="stats"></span></p>
<button onclick="resetPoints()">Clear</button>
CSS
#container {
position: relative;
border: 1px solid #000;
width: 512px;
height: 512px;
}
.subcanv {
position: absolute;
width: 100%;
height: 100%;
}
JavaScript
let c = document.getElementById('canvas');
let overlay = document.getElementById('overlay');
let outer = document.getElementById('container');
let ctx = c.getContext('2d');
let ctxo = overlay.getContext('2d');
let doBlur = document.getElementById('blur');
let doJitterStart = document.getElementById('jitterStart');
let doJitterStep = document.getElementById('jitterStep');
let doSurfaceCheck = document.getElementById('surfaceCheck');
let doUseCone = document.getElementById('useCone');
let height = c.height;
let width = c.width;
let pxCount = height * width;
let floor = Math.floor;
ctx.globalAlpha = 1.0;
ctx.fillStyle = "#000";
let lightMap = []; // 0..1 lightness values
let solidMap = []; // areas drawn solid
let distanceMap = []; // distance to nearest edge, or zero if in solid area
let xPosMap = []; // X position of nearest edge
let yPosMap = []; // Y position of nearest edge
let xNormMap = []; // normalised dx vector toward nearest edge
let yNormMap = []; // normalised dy vector toward nearest edge
// Core of the shadow generation
//
// Mouse point (x,y) acts as the light source
// for each point in the distance map,
// 1. If 'solid', mark black and stop
// 2. Step toward mouse position, recording smallest step size
// - if we get to a solid, mark dark and stop
// - mark light based on min step size
function updateLightAndShadow(x, y) {
if (x < 1) x = 1;
if (x >= width) x = width - 1;
if (y < 1) y = 1;
if (y >= height) y = height - 1;
let idx = (y * width) + x;
if (solidMap[idx] > 0) return;
const useCone = doUseCone.checked;
const lightRadius = 600; // Light fades to 0.0 at this radius
const edgeDistance = 25; // how close to solid objects should we fade out soft shadows
let lightDist = distanceMap[(y * width) + x]; // free space at light source
// scan through all map points
for (let sy = 0; sy < height; sy++) {
for (let sx = 0; sx < width; sx++) {
let idx = (sy * width) + sx;
lightMap[idx] = 0; //...