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; //...