---
title: "Building Real-Time Global Illumination: Part 2"
scripts:
  - https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js
#  - https://cdn.jsdelivr.net/npm/spectorjs@0.9.30/dist/spector.bundle.js
head: '
  <link rel="stylesheet" href="../css/mdxish.css">
  <script src="../js/three.js"></script>
  <script src="../js/prism.js"></script>
  <style>
  button { border: none; cursor: pointer; }
  .color { max-width: 20px; width: 20px; height: 20px; }
#  canvas { cursor: none; image-rendering: pixelated; }
  .iconButton {
    margin-left: -1px;
    padding: 0;
    width: 24px;
    height: 24px;
    padding-bottom: 2px;
  }
  </style>
'
---

[//]: # (Note to markdown source readers - I tend to put a bunch of code up front - Just scroll down to the first `#` for the title / start of the post.)

```javascript
// @run
// var spector = new SPECTOR.Spector();
// spector.displayUI();

class GPUTimer {
  constructor(renderer, disabled = false) {
    this.gl = renderer.getContext();
    this.ext = !disabled && this.gl.getExtension('EXT_disjoint_timer_query_webgl2');
    if (!this.ext) {
      console.warn('EXT_disjoint_timer_query_webgl2 not available');
    }
    this.queries = new Map();
    this.results = new Map();
    this.lastPrintTime = Date.now();
    this.printInterval = 1000; // 10 seconds
  }

  start(id) {
    if (!this.ext) return;
    const query = this.gl.createQuery();
    this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);
    if (!this.queries.has(id)) {
      this.queries.set(id, []);
    }
    this.queries.get(id).push(query);
  }

  end(id) {
    if (!this.ext) return;
    this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
  }

  update() {
    if (!this.ext) return;
    for (const [id, queryList] of this.queries) {
      const completedQueries = [];
      for (let i = queryList.length - 1; i >= 0; i--) {
        const query = queryList[i];
        const available = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE);
        const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT);

        if (available && !disjoint) {
          const timeElapsed = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT);
          const timeMs = timeElapsed / 1000000; // Convert nanoseconds to milliseconds

          if (!this.results.has(id)) {
            this.results.set(id, []);
          }
          this.results.get(id).push(timeMs);

          completedQueries.push(query);
          queryList.splice(i, 1);
        }
      }

      // Clean up completed queries
      completedQueries.forEach(query => this.gl.deleteQuery(query));
    }

    // Check if it's time to print results
    const now = Date.now();
    if (now - this.lastPrintTime > this.printInterval) {
      this.printAverages();
      this.lastPrintTime = now;
    }
  }

  printAverages() {
    if (!this.ext) return;
    console.log('--- GPU Timing Averages ---');
    for (const [id, times] of this.results) {
      if (times.length > 0) {
        const avg = times.reduce((a, b) => a + b, 0) / times.length;
        console.log(`${id}: ${avg.toFixed(2)}ms (${times.length} samples)`);
      }
    }
    console.log('---------------------------');
  }
}

const isMobile = (() => {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
    || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
})();

const vertexShader = `
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`;

const resetSvg = `<svg  xmlns="http://www.w3.org/2000/svg"  width="16"  height="16"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="1"  stroke-linecap="round"  stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /></svg>`;

const clearSvg = `<svg  xmlns="http://www.w3.org/2000/svg"  width="16"  height="16"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="1"  stroke-linecap="round"  stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 20h-10.5l-4.21 -4.3a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9.2 9.3" /><path d="M18 13.3l-6.3 -6.3" /></svg>`;

const sunMoonSvg = `<svg  xmlns="http://www.w3.org/2000/svg"  width="16"  height="16"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9.173 14.83a4 4 0 1 1 5.657 -5.657" /><path d="M11.294 12.707l.174 .247a7.5 7.5 0 0 0 8.845 2.492a9 9 0 0 1 -14.671 2.914" /><path d="M3 12h1" /><path d="M12 3v1" /><path d="M5.6 5.6l.7 .7" /><path d="M3 21l18 -18" /></svg>`

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}

function rgbToHex(r, g, b) {
  return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

// This is the html plumbing / structure / controls for little canvases
function intializeCanvas({
 id, canvas, onSetColor, startDrawing, onMouseMove, stopDrawing, clear, reset, toggleSun
}) {
  const clearDom = clear ? `<button id="${id}-clear" class="iconButton">${clearSvg}</button>` : "";
  const resetDom = reset ? `<button id="${id}-reset" class="iconButton">${resetSvg}</button>` : "";
  const sunMoonDom = toggleSun ? `<button id="${id}-sun" class="iconButton">${sunMoonSvg}</button>` : "";
  document.querySelector(`#${id}`).innerHTML = `
  <div style="display: flex; gap: 20px;">
    <div id="${id}-canvas-container"></div>
    
    <div style="display: flex; flex-direction: column; justify-content: space-between;">
        <div id="${id}-color-picker" style="display: flex; flex-direction: column;  border: solid 1px white; margin: 1px;">
          <input type="color" id="${id}-color-input" value="#eb6b6f" style="display: none; width: 0px" >
      </div>
      <div style="display: flex; flex-direction: column; gap: 2px">
      ${sunMoonDom}
      ${clearDom}
      ${resetDom}
      </div>
    </div>
</div>`;
  const colorInput = document.getElementById(`${id}-color-input`);

  function setColor(r, g, b) {
    colorInput.value = rgbToHex(r, g, b);
    onSetColor({r, g, b});
  }

  function setHex(hex) {
    const rgb = hexToRgb(hex);
    setColor(rgb.r, rgb.g, rgb.b);
  }

  function updateColor(event) {
    const hex = event.target.value;
    setHex(hex);
  }

  colorInput.addEventListener('input', updateColor);

  const colorPicker = document.querySelector(`#${id}-color-picker`);
  
  ["#03C4A1", "#fff6d3", "#f9a875", "#eb6b6f", "#7c3f58", "#3d9eff", "#ff2859", "#000000"].forEach((color, i) => {
    const colorButton = document.createElement("button");
    colorButton.className = "color";
    colorButton.style.backgroundColor = color;
    colorPicker.appendChild(colorButton);
    colorButton.addEventListener('click', () => setHex(color));
  });
  const container = document.querySelector(`#${id}-canvas-container`);
  container.appendChild(canvas);

  canvas.addEventListener('touchstart', startDrawing);
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', onMouseMove);
  canvas.addEventListener('touchmove', onMouseMove);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('touchend', stopDrawing);
  canvas.addEventListener('touchcancel', stopDrawing);
  canvas.addEventListener('mouseleave', stopDrawing);

  if (clear) {
    document.querySelector(`#${id}-clear`).addEventListener("click", () => {
      clear();
    });
  }

  if (reset) {
    document.querySelector(`#${id}-reset`).addEventListener("click", () => {
      reset();
    });
  }

  if (toggleSun) {
    document.querySelector(`#${id}-sun`).addEventListener("click", () => {
      toggleSun();
    });
  }

  return {container, setHex};
}

// This is the JS side that connects our canvas to three.js, and adds drawing on mobile
// Also deals with interaction (mouse / touch) logic
class PaintableCanvas {
  constructor({width, height, initialColor = 'transparent', radius = 6, friction = 0.1}) {

    this.isDrawing = false;
    this.lastPoint = null;
    this.currentPoint = null;
    this.mouseMoved = false;
    this.currentColor = {r: 255, g: 255, b: 255};
    this.RADIUS = radius;
    this.FRICTION = friction;
    this.width = width;
    this.height = height;

    this.initialColor = initialColor;

    if (this.useFallbackCanvas()) {
      [this.canvas, this.context] = this.createCanvas(width, height, initialColor);
      this.texture = new THREE.CanvasTexture(this.canvas);
      this.setupTexture(this.texture);
      this.currentImageData = new ImageData(this.canvas.width, this.canvas.height);
    }
    this.onUpdateTextures = () => {
    };

    this.drawSmoothLine = (from, to) => {
      throw new Error("Missing implementation");
    }
  }

  useFallbackCanvas() {
    return false;
  }

  // Mobile breaks in all kinds of ways
  // Drawing on cpu fixes most of the issues
  drawSmoothLineFallback(from, to) {
    this.drawLine(from, to, this.currentColor, this.context);
    this.updateTexture();
  }

  drawLine(from, to, color, context) {
    const radius = this.RADIUS;

    // Ensure we're within canvas boundaries
    const left = 0;
    const top = 0;
    const right = context.canvas.width - 1;
    const bottom = context.canvas.height - 1;

    let width = right - left + 1;
    let height = bottom - top + 1;

    let imageData = this.currentImageData;
    let data = imageData.data;

    // Bresenham's line algorithm
    let x0 = Math.round(from.x - left);
    let y0 = Math.round(from.y - top);
    let x1 = Math.round(to.x - left);
    let y1 = Math.round(to.y - top);

    let dx = Math.abs(x1 - x0);
    let dy = Math.abs(y1 - y0);
    let sx = (x0 < x1) ? 1 : -1;
    let sy = (y0 < y1) ? 1 : -1;
    let err = dx - dy;

    while (true) {
      // Draw the pixel and its surrounding pixels
      this.drawCircle(x0, y0, color, radius);

      if (x0 === x1 && y0 === y1) break;
      let e2 = 2 * err;
      if (e2 > -dy) {
        err -= dy;
        x0 += sx;
      }
      if (e2 < dx) {
        err += dx;
        y0 += sy;
      }
    }

    // Put the modified image data back to the canvas
    context.putImageData(imageData, left, top);
  }

  drawCircle(x0, y0, color, radius) {
    for (let ry = -radius; ry <= radius; ry++) {
      for (let rx = -radius; rx <= radius; rx++) {
        if (rx * rx + ry * ry <= radius * radius) {
          let x = x0 + rx;
          let y = y0 + ry;
          if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
            this.setPixel(x, y, color);
          }
        }
      }
    }
  }

  setPixel(x, y, color) {
    let index = (y * this.width + x) * 4;
    this.currentImageData.data[index] = color.r;     // Red
    this.currentImageData.data[index + 1] = color.g; // Green
    this.currentImageData.data[index + 2] = color.b; // Blue
    this.currentImageData.data[index + 3] = 255.0;   // Alpha
  }

  createCanvas(width, height, initialColor) {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext('2d');
    context.fillStyle = initialColor;
    context.fillRect(0, 0, canvas.width, canvas.height);
    // canvas.style.width = `${width / 2}px`;
    // canvas.style.height = `${height / 2}px`;
    return [canvas, context];
  }

  setupTexture(texture) {
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;
    texture.format = THREE.RGBAFormat;
    texture.type = true ? THREE.HalfFloatType : THREE.FloatType;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping
    // texture.generateMipmaps = true;
  }

  updateTexture() {
    this.texture.needsUpdate = true;
    this.onUpdateTextures();
  }

  startDrawing(e) {
    this.isDrawing = true;
    this.currentMousePosition = this.lastPoint = this.currentPoint = this.getMousePos(e);
    try {
      this.onMouseMove(e);
    } catch(e) {
      console.error(e);
    }
    this.mouseMoved = false;
  }

  stopDrawing(e) {
    const wasDrawing = this.isDrawing;
    if (!wasDrawing) {
      return false;
    }
    if (!this.mouseMoved) {
      this.drawSmoothLine(this.currentPoint, this.currentPoint);
    } else {
      this.drawSmoothLine(this.currentPoint, this.getMousePos(e));
    }
    this.isDrawing = false;
    this.mouseMoved = false;
    return true;
  }

  onMouseMove(event) {
    if (!this.isDrawing) return false;
    this.mouseMoved = true;

    this.currentMousePosition = this.getMousePos(event);
    this.doDraw();

    return true;
  }

  doDraw() {
    const newPoint = this.currentMousePosition;

    // Some smoothing...
    let dist = this.distance(this.currentPoint, newPoint);

    if (dist > 0) {
      let dir = {
        x: (newPoint.x - this.currentPoint.x) / dist,
        y: (newPoint.y - this.currentPoint.y) / dist
      };
      let len = Math.max(dist - this.RADIUS, 0);
      let ease = 1 - Math.pow(this.FRICTION, 1 / 60 * 10);
      this.currentPoint = {
        x: this.currentPoint.x + dir.x * len * ease,
        y: this.currentPoint.y + dir.y * len * ease
      };
    } else {
      this.currentPoint = newPoint;
    }

    this.drawSmoothLine(this.lastPoint, this.currentPoint);
    this.lastPoint = this.currentPoint;
  }

  // I'll be honest - not sure why I can't just use `clientX` and `clientY`
  // Must have made a weird mistake somewhere.
  getMousePos(e) {
    e.preventDefault();

    const {width, height} = e.target.style;
    const [dx, dy] = [
      (width ? this.width / parseInt(width) : 1.0),
      (height ? this.height / parseInt(height) : 1.0),
    ];

    if (e.touches) {
      return {
        x: (e.touches[0].clientX - (e.touches[0].target.offsetLeft - window.scrollX)) * dx,
        y: (e.touches[0].clientY - (e.touches[0].target.offsetTop - window.scrollY)) * dy
      };
    }

    return {
      x: (e.clientX - (e.target.offsetLeft - window.scrollX)) * dx,
      y: (e.clientY - (e.target.offsetTop - window.scrollY)) * dy
    };
  }

  distance(p1, p2) {
    return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  }

  setColor(r, g, b) {
    this.currentColor = {r, g, b};
  }

  clear() {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.currentImageData = new ImageData(this.canvas.width, this.canvas.height);
    this.updateTexture();
  }
}

function createMipmapRenderTarget(width, height, renderer, cascadeCount, props) {
  const mipmaps = [];
  let mipWidth = width;
  let mipHeight = height;

  for (let i = 0; i < cascadeCount; i++) {
    const renderTarget = new THREE.WebGLRenderTarget(mipWidth, mipHeight, props);
    mipmaps.push(renderTarget);
    mipWidth = Math.max(1, Math.floor(mipWidth / 2));
    mipHeight = Math.max(1, Math.floor(mipHeight / 2));
  }

  return mipmaps;
}

function threeJSInit(width, height, materialProperties, renderer = null, renderTargetOverrides = {}, makeRenderTargets = undefined, extra = {}) {
  const scene = new THREE.Scene();
  const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
  const dpr = 1.0; // extra.dpr || window.devicePixelRatio || 1;
  
  if (!renderer) {
    renderer = new THREE.WebGLRenderer({
      antialiasing: true,
      powerPreference: "high-performance"
      // powerPreference: "low-power",
    });
    renderer.setPixelRatio(dpr);
  }
  renderer.setSize(width, height);
  const renderTargetProps = {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    type: true ? THREE.HalfFloatType : THREE.FloatType,
    format: THREE.RGBAFormat,
    wrapS: THREE.ClampToEdgeWrapping,
    wrapT: THREE.ClampToEdgeWrapping,
    ...renderTargetOverrides,
  };

  const geometry = new THREE.PlaneGeometry(2, 2);
  const material = new THREE.ShaderMaterial({
    depthTest: false,
    depthWrite: false,
    glslVersion: THREE.GLSL3,
    ...materialProperties,
  });
  plane = new THREE.Mesh(geometry, material);
  scene.add(plane);

  return {
    plane,
    canvas: renderer.domElement,
    render: () => {
      renderer.render(scene, camera)
    },
    renderTargets: makeRenderTargets ? makeRenderTargets(
      { width, height, renderer, renderTargetProps}
    ) : (() => {
      const renderTargetA = new THREE.WebGLRenderTarget(width * dpr, height * dpr, renderTargetProps);
      const renderTargetB = renderTargetA.clone();
      return [renderTargetA, renderTargetB];
    })(),
    renderer
  }
}
```

```javascript
// @run
// Let's instrument the post with this so we can disable animations while editing.
const disableAnimation = false;
// Draw animations very fast, with a huge loss in accuracy (for testing)
const instantMode = false;
const getFrame = disableAnimation
  ? (fn) => { fn() }
  : requestAnimationFrame;
let rayCount = 32.0;
```

```javascript
// @run
class BaseSurface {
  constructor({ id, width, height, radius = 5 }) {
    // Create PaintableCanvas instances
    this.createSurface(width, height, radius);
    this.dpr = window.devicePixelRatio || 1;
    this.width = width;
    this.height = height;
    this.id = id;
    this.initialized = false;
    this.initialize();
  }

  createSurface(width, height, radius) {
    this.surface = new PaintableCanvas({ width, height, radius });
  }

  initialize() {
    // Child class should fill this out
  }

  load() {
    // Child class should fill this out
  }

  clear() {
    // Child class should fill this out
  }

  renderPass() {
    // Child class should fill this out
  }

  reset() {
    this.clear();
    this.setHex("#fff6d3");
    new Promise((resolve) => {
      getFrame(() => this.draw(0.0, null, resolve));
    });
  }

  draw(t, last, resolve) {
    if (t >= 10.0) {
      resolve();
      return;
    }

    const angle = (t * 0.05) * Math.PI * 2;

    const {x, y} = {
      x: 100 + 100 * Math.sin(angle + 0.25) * Math.cos(angle * 0.15),
      y: 50 + 100 * Math.sin(angle * 0.7)
    };

    last ??= {x, y};

    this.surface.drawSmoothLine(last, {x, y});
    last = {x, y};

    const step = instantMode ? 5.0 : 0.2;
    getFrame(() => this.draw(t + step, last, resolve));
  }

  buildCanvas() {
    return intializeCanvas({
      id: this.id,
      canvas: this.canvas,
      onSetColor: ({r, g, b}) => {
        this.surface.currentColor = {r, g, b};
        this.plane.material.uniforms.color.value = new THREE.Color(
          this.surface.currentColor.r / 255.0,
          this.surface.currentColor.g / 255.0,
          this.surface.currentColor.b / 255.0
        );
      },
      startDrawing: (e) => this.surface.startDrawing(e),
      onMouseMove: (e) => this.surface.onMouseMove(e),
      stopDrawing: (e) => this.surface.stopDrawing(e),
      clear: () => this.clear(),
      reset: () => this.reset(),
      ...this.canvasModifications()
    });
  }

  canvasModifications() {
    return {}
  }

  observe() {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting === true) {
        this.load();
        observer.disconnect(this.container);
      }
    });

    observer.observe(this.container);
  }

  initThreeJS({ uniforms, fragmentShader, renderTargetOverrides, makeRenderTargets, ...rest }) {
    return threeJSInit(this.width, this.height, {
      uniforms,
      fragmentShader,
      vertexShader,
      transparent: false,
    }, this.renderer, renderTargetOverrides ?? {}, makeRenderTargets, rest)
  }
}

class Drawing extends BaseSurface {
  initializeSmoothSurface() {
    const props = this.initThreeJS({
      uniforms: {
        inputTexture: { value: this.surface.texture },
        color: {value: new THREE.Color(1, 1, 1)},
        from: {value: new THREE.Vector2(0, 0)},
        to: {value: new THREE.Vector2(0, 0)},
        radiusSquared: {value: Math.pow(this.surface.RADIUS, 2.0)},
        resolution: {value: new THREE.Vector2(this.width, this.height)},
        drawing: { value: false },
      },
      fragmentShader: `
uniform sampler2D inputTexture;
uniform vec3 color;
uniform vec2 from;
uniform vec2 to;
uniform float radiusSquared;
uniform vec2 resolution;
uniform bool drawing;
varying vec2 vUv;

out vec4 FragColor;

float sdfLineSquared(vec2 p, vec2 from, vec2 to) {
vec2 toStart = p - from;
vec2 line = to - from;
float lineLengthSquared = dot(line, line);
float t = clamp(dot(toStart, line) / lineLengthSquared, 0.0, 1.0);
vec2 closestVector = toStart - line * t;
return dot(closestVector, closestVector);
}

void main() {
vec4 current = texture(inputTexture, vUv, 0.0);
if (drawing) {
  vec2 coord = vUv * resolution;
  if (sdfLineSquared(coord, from, to) <= radiusSquared) {
    current = vec4(color, 1.0);
  }
}
FragColor = current;
}`,
    });

    if (this.surface.useFallbackCanvas()) {
      this.surface.drawSmoothLine = (from, to) => {
        this.surface.drawSmoothLineFallback(from, to);
      }
      this.surface.onUpdateTextures = () => {
        this.renderPass();
      }
    } else {
      this.surface.drawSmoothLine = (from, to) => {
        props.plane.material.uniforms.drawing.value = true;
        props.plane.material.uniforms.from.value = {
          ...from, y: this.height - from.y
        };
        props.plane.material.uniforms.to.value = {
          ...to, y: this.height - to.y
        };
        this.renderPass();
        props.plane.material.uniforms.drawing.value = false;
      }
    }

    return props;
  }

  clear() {
    if (this.surface.useFallbackCanvas()) {
      this.surface.clear();
      return;
    }
    if (this.initialized) {
      this.renderTargets.forEach((target) => {
        this.renderer.setRenderTarget(target);
        this.renderer.clearColor();
      });
    }
    this.renderer.setRenderTarget(null);
    this.renderer.clearColor();
  }

  initialize() {
    const {
      plane, canvas, render, renderer, renderTargets
    } = this.initializeSmoothSurface();
    this.canvas = canvas;
    this.plane = plane;
    this.render = render;
    this.renderer = renderer;
    this.renderTargets = renderTargets;
    const { container, setHex } = this.buildCanvas();
    this.container = container;
    this.setHex = setHex;
    this.renderIndex = 0;

    this.innerInitialize();

    this.observe();
  }

  innerInitialize() {

  }

  load() {
    this.reset();
    this.initialized = true;
  }

  drawPass() {
    if (this.surface.useFallbackCanvas()) {
      return this.surface.texture;
    } else {
      this.plane.material.uniforms.inputTexture.value = this.renderTargets[this.renderIndex].texture;
      this.renderIndex = 1 - this.renderIndex;
      this.renderer.setRenderTarget(this.renderTargets[this.renderIndex]);
      this.render();
      return this.renderTargets[this.renderIndex].texture;
    }
  }

  renderPass() {
    this.drawPass()
    this.renderer.setRenderTarget(null);
    this.render();
  }
}

```

```javascript
// @run
class JFA extends Drawing {
  innerInitialize() {
    this.passes = Math.ceil(Math.log2(Math.max(this.width, this.height)));

    const {plane: seedPlane, render: seedRender, renderTargets: seedRenderTargets} = this.initThreeJS({
      uniforms: {
        surfaceTexture: {value: this.surface.texture},
      },
      fragmentShader: `
        precision highp float;
        uniform sampler2D surfaceTexture;
        out vec4 FragColor;

        in vec2 vUv;
        
        void main() {
          float alpha = texture(surfaceTexture, vUv).a;
          FragColor = vec4(vUv * alpha, vUv * (1.0 - alpha));
        }`,
    });

    const {plane: jfaPlane, render: jfaRender, renderTargets: jfaRenderTargets} = this.initThreeJS({
      uniforms: {
        inputTexture: {value: this.surface.texture},
        oneOverSize: {value: new THREE.Vector2(2.0 / this.width, 2.0 / this.height)},
        uOffset: {value: Math.pow(2, this.passes - 1)},
        skip: {value: true},
      },
      fragmentShader: `
uniform vec2 oneOverSize;
uniform sampler2D inputTexture;
uniform float uOffset;
uniform bool skip;

in vec2 vUv;
out vec4 FragColor;

void main() {
  if (skip) {
    FragColor = vec4(vUv, 0.0, 1.0);
  } else {
    vec4 nearestSeed = vec4(0.0);
    float nearestDist = 999999.9;
    float nearestDistInside = 999999.9;
    
    for (float y = -1.0; y <= 1.0; y += 1.0) {
      for (float x = -1.0; x <= 1.0; x += 1.0) {
        vec2 sampleUV = vUv + vec2(x, y) * uOffset * oneOverSize;
        
        // Check if the sample is within bounds
        if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) { continue; }
        
          vec4 sampleValue = texture(inputTexture, sampleUV);
          vec2 sampleSeed = sampleValue.xy;
          
          if (sampleSeed.x != 0.0 || sampleSeed.y != 0.0) {
            vec2 diff = sampleSeed - vUv;
            float dist = dot(diff, diff);
            if (dist < nearestDist) {
              nearestDist = dist;
              nearestSeed.xy = sampleValue.xy;
            }
          }
          
          vec2 sampleSeedInside = sampleValue.zw;
          
          if (sampleSeedInside.x != 0.0 || sampleSeedInside.y != 0.0) {
            vec2 diff = sampleSeedInside - vUv;
            float dist = dot(diff, diff);
            if (dist < nearestDistInside) {
              nearestDistInside = dist;
              nearestSeed.zw = sampleValue.zw;
            }
          }
      }
    }
    
    FragColor = nearestSeed;
  }
}`
    });

    this.seedPlane = seedPlane;
    this.seedRender = seedRender;
    this.seedRenderTargets = seedRenderTargets;

    this.jfaPlane = jfaPlane;
    this.jfaRender = jfaRender;
    this.jfaRenderTargets = jfaRenderTargets;
  }

  seedPass(inputTexture) {
    this.seedPlane.material.uniforms.surfaceTexture.value = inputTexture;
    this.renderer.setRenderTarget(this.seedRenderTargets[0]);
    this.seedRender();
    return this.seedRenderTargets[0].texture;
  }

  jfaPassesCount() {
    return parseInt(jfaSlider.value);
  }

  jfaPass(inputTexture) {
    let currentInput = inputTexture;
    let [renderA, renderB] = this.jfaRenderTargets;
    let currentOutput = renderA;
    this.jfaPlane.material.uniforms.skip.value = true;
    let passes = this.jfaPassesCount();

    for (let i = 0; i < passes || (passes === 0 && i === 0); i++) {

      this.jfaPlane.material.uniforms.skip.value = passes === 0;
      this.jfaPlane.material.uniforms.inputTexture.value = currentInput;
      // This intentionally uses `this.passes` which is the true value
      // In order to properly show stages using the JFA slider.
      this.jfaPlane.material.uniforms.uOffset.value = Math.pow(2, this.passes - i - 1);

      this.renderer.setRenderTarget(currentOutput);
      this.jfaRender();

      currentInput = currentOutput.texture;
      currentOutput = (currentOutput === renderA) ? renderB : renderA;
    }

    return currentInput;
  }

  draw(last, t, isShadow, resolve) {
    if (t >= 10.0) {
      resolve();
      return;
    }

    const angle = (t * 0.05) * Math.PI * 2;

    const {x, y} = isShadow
      ? {
        x: 90 + 12 * t,
        y: 200 + 1 * t,
      }
      : {
        x: 100 + 100 * Math.sin(angle + 0.25) * Math.cos(angle * 0.15),
        y: 50 + 100 * Math.sin(angle * 0.7)
      };

    last ??= {x, y};

    this.surface.drawSmoothLine(last, {x, y});
    last = {x, y};

    const step = instantMode ? 5.0 : (isShadow ? 0.5 : 0.3);
    getFrame(() => this.draw(last, t + step, isShadow, resolve));
  }

  clear() {
    if (this.initialized) {
      this.seedRenderTargets.concat(this.jfaRenderTargets).forEach((target) => {
        this.renderer.setRenderTarget(target);
        this.renderer.clearColor();
      });
    }
    super.clear();
  }

  load() {
    super.load();
    jfaSlider.addEventListener("input", () => {
      this.renderPass();
      // Save the value
      window.mdxishState.jfaSlider = jfaSlider.value;
    });
    getFrame(() => this.reset());
  }

  renderPass() {
    let out = this.drawPass();
    out = this.seedPass(out);
    out = this.jfaPass(out);
    this.renderer.setRenderTarget(null);
    this.jfaRender();
  }

  reset() {
    this.clear();
    let last = undefined;
    return new Promise((resolve) => {
      this.setHex("#f9a875");
      getFrame(() => this.draw(last, 0, false, resolve));
    }).then(() => new Promise((resolve) => {
      last = undefined;
      getFrame(() => {
        this.setHex("#000000");
        getFrame(() => this.draw(last, 0, true, resolve));
      });
    }))
      .then(() => {
        this.renderPass();
        getFrame(() => this.setHex("#fff6d3"));
      });
  }
}

```

```javascript
// @run
class DistanceField extends JFA {
  jfaPassesCount() {
    return this.passes;
  }

  innerInitialize() {
    super.innerInitialize();

    const {plane: dfPlane, render: dfRender, renderTargets: dfRenderTargets} = this.initThreeJS({
      uniforms: {
        jfaTexture: {value: this.surface.texture},
        surfaceTexture: {value: this.surface.texture},
        size: {value: new THREE.Vector2(this.width, this.height)},
      },
      fragmentShader: `
        uniform sampler2D jfaTexture;
        uniform vec2 size;
        uniform sampler2D surfaceTexture;
        
        in vec2 vUv;
        out vec4 FragColor;
        
        void main() {
          vec4 nearestSeed = texture(jfaTexture, vUv);
          // Clamp by the size of our texture (1.0 in uv space).
          vec2 distance = vec2(
            clamp(distance(vUv, nearestSeed.xy), 0.0, 1.0),
            clamp(distance(vUv, nearestSeed.zw), 0.0, 1.0)
          );
          
          // Normalize and visualize the distance
          FragColor = vec4(distance, 0.0, 1.0);
        }`,
    });

    this.dfPlane = dfPlane;
    this.dfRender = dfRender;
    this.dfRenderTargets = dfRenderTargets;
  }

  load() {
    this.reset();
    this.initialized = true;
  }

  clear() {
    if (this.initialized) {
      this.dfRenderTargets.forEach((target) => {
        this.renderer.setRenderTarget(target);
        this.renderer.clearColor();
      });
    }
    super.clear();
  }

  dfPass(inputTexture) {
    this.renderer.setRenderTarget(this.dfRenderTargets[0]);
    this.dfPlane.material.uniforms.jfaTexture.value = inputTexture;
    this.dfPlane.material.uniforms.surfaceTexture.value = this.surface.texture;
    this.dfRender();
    return this.dfRenderTargets[0].texture;
  }

  renderPass() {
    let out = this.drawPass();
    out = this.seedPass(out);
    out = this.jfaPass(out);
    out = this.dfPass(out);
    this.renderer.setRenderTarget(null);
    this.dfRender();
  }
}

```

```javascript
// @run
class Particle {
  constructor(color, empty = false) {
    this.color = color;
    this.empty = empty;
    this.modified = true;
  }

  update () {
    this.modified = false;
  }

  getUpdateCount() {
    return 0;
  }

  resetVelocity() {
    this.velocity = 0;
  }
}

class MovingParticle extends Particle {
  constructor(color, empty = false) {
    super(color, empty);
    this.color = color;
    this.empty = empty;
    this.maxSpeed = 8;
    this.acceleration = 0.4;
    this.velocity = 0;
    this.modified = true;
  }

  update() {
    if (this.maxSpeed === 0) {
      this.modified = false;
      return;
    }
    this.updateVelocity();
    this.modified = this.velocity > 0.5;
  }

  updateVelocity() {
    let newVelocity = this.velocity + this.acceleration;
    if (Math.abs(newVelocity) > this.maxSpeed) {
      newVelocity = Math.sign(newVelocity) * this.maxSpeed;
    }
    this.velocity = newVelocity;
  }

  resetVelocity() {
    this.velocity = 0;
  }

  getUpdateCount() {
    const abs = Math.abs(this.velocity);
    const floored = Math.floor(abs);
    const mod = abs - floored;
    return floored + (Math.random() < mod ? 1 : 0);
  }
}

class Sand extends MovingParticle {
  constructor(color) {
    super(color);
  }
}

class Solid extends Particle {
  constructor(color) {
    super(color);
    this.maxSpeed = 0;
  }
}

class Empty extends Particle {
  constructor() {
    super({ r: 0, g: 0, b: 0 }, true);
    this.maxSpeed = 0;
  }
}

class FallingSandSurface extends PaintableCanvas {
  constructor(options) {
    super(options);
    this.updateRequired = true;
    this.grid = new Array(this.width * this.height).fill(null).map(() => new Empty());
    this.tempGrid = new Array(this.width * this.height).fill(null).map(() => new Empty());
    this.colorGrid = new Array(this.width * this.height * 3).fill(0);
    this.modifiedIndices = new Set();
    this.cleared = false;
    this.rowCount = Math.floor(this.grid.length / this.width);
    requestAnimationFrame(() => this.updateSand());
    this.mode = Sand;

    document.querySelector("#sand-mode-button").addEventListener("click", () => {
      this.mode = Sand;
    });

    document.querySelector("#solid-mode-button").addEventListener("click", () => {
      this.mode = Solid;
    });

    document.querySelector("#empty-mode-button").addEventListener("click", () => {
      this.mode = Empty;
    });
  }

  onMouseMove(event) {
    if (!this.isDrawing) return false;
    this.mouseMoved = true;
    this.currentMousePosition = this.getMousePos(event);
    return true;
  }

  varyColor(color) {
    const hue = color.h;
    let saturation = color.s + Math.floor(Math.random() * 20) - 20;
    saturation = Math.max(0, Math.min(100, saturation));
    let lightness = color.l + Math.floor(Math.random() * 10) - 5;
    lightness = Math.max(0, Math.min(100, lightness));
    return this.hslToRgb(hue, saturation, lightness);
  }

  hslToRgb(h, s, l) {
    s /= 100;
    l /= 100;
    const k = n => (n + h / 30) % 12;
    const a = s * Math.min(l, 1 - l);
    const f = n =>
      l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
    return {
      r: Math.round(255 * f(0)),
      g: Math.round(255 * f(8)),
      b: Math.round(255 * f(4))
    };
  }

  rgbToHsl(rgb) {
    const r = rgb.r / 255;
    const g = rgb.g / 255;
    const b = rgb.b / 255;
    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    let h, s, l = (max + min) / 2;

    if (max === min) {
      h = s = 0; // achromatic
    } else {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }

    return { h: h * 360, s: s * 100, l: l * 100 };
  }

  drawSmoothLineFallback(from, to) {
    this.drawParticleLine(from, to, this.mode);
    requestAnimationFrame(() => this.updateSand());
  }

  startDrawing(e) {
    super.startDrawing(e);
    this.isDrawing = true;
    requestAnimationFrame(() => this.updateSand());
  }

  drawParticleLine(from, to, ParticleType) {
    const radius = this.RADIUS;
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    const steps = Math.max(Math.abs(dx), Math.abs(dy));

    for (let i = 0; i <= steps; i++) {
      const t = (steps === 0) ? 0 : i / steps;
      const x = Math.round(from.x + dx * t);
      const y = Math.round(from.y + dy * t);

      for (let ry = -radius; ry <= radius; ry++) {
        for (let rx = -radius; rx <= radius; rx++) {
          if (rx * rx + ry * ry <= radius * radius) {
            const px = x + rx;
            const py = y + ry;
            if (px >= 0 && px < this.width && py >= 0 && py < this.height) {
              const index = py * this.width + px;
              const variedColor = this.varyColor(this.rgbToHsl(this.currentColor));
              this.setParticle(index, new ParticleType(variedColor));
              this.modifiedIndices.add(index);
            }
          }
        }
      }
    }
  }

  updateSand() {
    if (this.updating) return;

    if (!this.needsUpdate()) {
      return;
    }

    this.updating = true;
    this.updateRequired = false;

    if (this.isDrawing) {
      this.doDraw();
    }

    this.cleared = false;

    for (let row = this.rowCount - 1; row >= 0; row--) {
      const rowOffset = row * this.width;
      const leftToRight = Math.random() > 0.5;
      for (let i = 0; i < this.width; i++) {
        const columnOffset = leftToRight ? i : -i - 1 + this.width;
        let index = rowOffset + columnOffset;
        const particle = this.grid[index];

        particle.update();

        if (!particle.modified) {
          continue;
        }
        
        // SHOULDN'T NEED THIS IT CAUSES CONSTANT RERENDERS
        // this.modifiedIndices.add(index);
        
        if (particle.getUpdateCount() === 0) {
          // this.modifiedIndices.add(index);
        }

        for (let v = 0; v < particle.getUpdateCount(); v++) {
          const newIndex = this.updatePixel(index);

          if (newIndex !== index) {
            index = newIndex;
          } else {
            particle.resetVelocity();
            break;
          }
        }
      }
    }

    this.updateCanvasFromGrid();
    this.updateTexture();
    requestAnimationFrame(() => {
      this.updating = false;
      this.updateSand()
    });
  }

  updatePixel(i) {
    const particle = this.grid[i];
    if (particle instanceof Empty) return i;

    const below = i + this.width;
    const belowLeft = below - 1;
    const belowRight = below + 1;
    const column = i % this.width;

    if (this.isEmpty(below)) {
      this.swap(i, below);
      return below;
    } else if (this.isEmpty(belowLeft) && belowLeft % this.width < column) {
      this.swap(i, belowLeft);
      return belowLeft;
    } else if (this.isEmpty(belowRight) && belowRight % this.width > column) {
      this.swap(i, belowRight);
      return belowRight;
    }

    return i;
  }

  swap(a, b) {
    if (this.grid[a] instanceof Empty && this.grid[b] instanceof Empty) {
      return;
    }
    [this.grid[a], this.grid[b]] = [this.grid[b], this.grid[a]];
    this.modifiedIndices.add(a);
    this.modifiedIndices.add(b);
  }

  setParticle(i, particle) {
    this.grid[i] = particle;
  }

  isEmpty(i) {
    return this.grid[i] instanceof Empty;
  }

  updateCanvasFromGrid() {
    const imageData = this.currentImageData;
    this.modifiedIndices.forEach((i) => {
      const index = i * 4;
      const particle = this.grid[i];
      if (particle instanceof Empty) {
        imageData.data[index] = 0; // Set alpha to 0 for empty spaces
        imageData.data[index + 1] = 0; // Set alpha to 0 for empty spaces
        imageData.data[index + 2] = 0; // Set alpha to 0 for empty spaces
        imageData.data[index + 3] = 0; // Set alpha to 0 for empty spaces
      } else {
        imageData.data[index] = particle.color.r;
        imageData.data[index + 1] = particle.color.g;
        imageData.data[index + 2] = particle.color.b;
        imageData.data[index + 3] = 255; // Full opacity
      }
    });
    this.updateRequired = this.modifiedIndices.size > 0;
    this.modifiedIndices = new Set();

    this.context.putImageData(imageData, 0, 0);
  }

  clear() {
    super.clear();
    this.grid.fill(new Empty());
    this.tempGrid.fill(new Empty());
    this.colorGrid.fill(0);
    this.cleared = true;
  }
  
  load() {
    this.clear();
    this.reset();
  }

  setColor(r, g, b) {
    super.setColor(r, g, b);
  }

  needsUpdate() {
    return this.cleared || this.modifiedIndices.size > 0 || this.isDrawing || this.updateRequired;
  }

  useFallbackCanvas() {
    return true;
  }
}

```

```javascript
// @run
function create2DRangeInput(width = 300, height = 300) {
  // Create main container
  const range2d = document.createElement('div');
  range2d.style.width = `${width}px`;
  range2d.style.minHeight = `${height}px`;
  range2d.style.border = '1px solid var(--article-text-color)';
  range2d.style.position = 'relative';
  range2d.style.touchAction = 'none';

  // Create horizontal axis
  const horizontalAxis = document.createElement('div');
  horizontalAxis.style.position = 'absolute';
  horizontalAxis.style.width = '100%';
  horizontalAxis.style.height = '1px';
  horizontalAxis.style.backgroundColor = 'var(--pre-background)';
  horizontalAxis.style.top = '50%';
  range2d.appendChild(horizontalAxis);

  // Create vertical axis
  const verticalAxis = document.createElement('div');
  verticalAxis.style.position = 'absolute';
  verticalAxis.style.width = '1px';
  verticalAxis.style.height = '100%';
  verticalAxis.style.backgroundColor = 'var(--pre-background)';
  verticalAxis.style.left = '50%';
  range2d.appendChild(verticalAxis);

  // Create dot
  const dot = document.createElement('div');
  dot.style.width = '6px';
  dot.style.height = '6px';
  dot.style.backgroundColor = 'var(--article-text-color)';
  dot.style.borderRadius = '50%';
  dot.style.position = 'absolute';
  dot.style.top = '50%';
  dot.style.left = '50%';
  dot.style.transform = 'translate(-50%, -50%)';
  range2d.appendChild(dot);

  function updateDotPosition(event) {
    const rect = range2d.getBoundingClientRect();
    const x = (event.clientX - rect.left) / rect.width * 200 - 100;
    const y = -((event.clientY - rect.top) / rect.height * 200 - 100);

    dot.style.left = `${(x + 100) / 2}%`;
    dot.style.top = `${(-y + 100) / 2}%`;

    range2d.x = x;
    range2d.y = y;
    // Dispatch a custom 'input' event
    range2d.dispatchEvent(
      new CustomEvent('input', { detail: { x: x / 100, y: y / 100 } })
    );
  }

  function handleMove(event) {
    event.preventDefault();
    updateDotPosition(event.type === 'touchmove' ? event.touches[0] : event);
  }
  
  function handleMouseMove(event) {
    if (event.buttons !== 1) return; // Only move if left mouse button is pressed
    handleMove(event);
  }

  range2d.addEventListener('mousedown', handleMouseMove);
  range2d.addEventListener('touchstart', handleMove);
  range2d.addEventListener('mousemove', handleMouseMove);
  range2d.addEventListener('mousemove', handleMouseMove);
  range2d.addEventListener('touchmove', handleMove, { passive: false });

  const rect = range2d.getBoundingClientRect();
  const x = (rect.left + (rect.width * 0.5)) / rect.width * 200 - 100;
  const y = -((rect.top + (rect.height * 0.5)) / rect.height * 200 - 100);
  range2d.x = x;
  range2d.y = y;

  return range2d;
}
```


## Radiance Cascades

```html
// @run

<div style="display: flex; align-items: center; gap: 4px">
<input type="checkbox" id="exaggerate-rays">
<label for="exaggerate-rays">Exaggerate Intervals</label>
</div>

<br />

<div style="display: flex; align-items: center; gap: 8px">
interval Amount
<input id="ray-interval-slider" class="slider" type="range" min="0" max="1.1" step="0.01" value="0.37" />
</div>

Value <span id="ray-interval-slider-value"></span>

<div style="display: flex; align-items: center; gap: 8px">
Sun Angle
<input id="rc-sun-angle-slider" class="slider" type="range" min="0" max="6.2" step="0.1" value="2.0" />
</div>

<br />

<div id="rc-sliders">
</div>

<br />

<div id="radiance-cascades-canvas"></div>

<br /><br /><br /><br />
```

```glsl
// @run id="rc-fragment" type="x-shader/x-fragment"
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform vec2 resolution;
uniform sampler2D sceneTexture;
uniform sampler2D distanceTexture;
uniform sampler2D lastTexture;
uniform vec2 cascadeExtent;
uniform float cascadeCount;
uniform float cascadeIndex;
uniform float basePixelsBetweenProbes;
uniform float cascadeInterval;
uniform float mipBias;
uniform float rayinterval;
uniform bool exaggerateRays;
uniform float sunAngle;
uniform float sunDistance;
uniform float attenuationDepth;
uniform float attenuationBrightness;
uniform float time;

in vec2 vUv;
out vec4 FragColor;

const float PI = 3.14159265;
const float TAU = 2.0 * PI;
const float EPS = 0.00001;
const float goldenAngle = PI * 0.7639320225;
const float srgb = 2.2;

const vec3 skyColor = vec3(0.2, 0.24, 0.35) * 12.0;
const vec3 sunColor = vec3(0.95, 0.9, 0.3) * 10.0;

vec3 sunAndSky(float rayAngle) {
    // Get the sun / ray relative angle
    float angleToSun = mod(rayAngle - sunAngle, TAU);

    // Sun falloff
    float sunIntensity = pow(max(0.0, cos(angleToSun)), sunDistance * 20.0);

    // Adjust sky contribution based on sun distance
    float skyFactor = mix(0.2, 0.6, sunDistance / 4.0);

    return mix(sunColor * sunIntensity, skyColor, skyFactor);
}

float rand(vec2 co) {
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

vec4 safeTextureSample(sampler2D tex, vec2 uv, float lod) {
    vec4 color = textureLod(tex, uv, lod);
    return vec4(color.rgb, color.a);
}

vec4 colorSample(sampler2D tex, vec2 uv, float lod) {
    vec4 color = textureLod(tex, uv, lod);
    return vec4(pow(color.rgb, vec3(srgb)), color.a);
}

struct ProbeInfo {
    float rayCount;
    vec2 pixelsBetweenProbes;
    vec2 size;
    vec2 position;
    float index;
    vec2 interval;
    float range;
    float base;
};

ProbeInfo getProbeInfo(vec2 coord) {
    float base = 4.0;
    float baseEx = pow(base, cascadeIndex);
    float rayCount = pow(2.0, cascadeIndex);
    vec2 pixelsBetweenProbes = vec2(basePixelsBetweenProbes * pow(2.0, cascadeIndex));
    vec2 size = cascadeExtent / rayCount;
    vec2 position = mod(floor(coord), size);
    vec2 rayPos = floor(vUv * rayCount);
    float index = rayPos.x + (rayCount * rayPos.y);

    float coef = exaggerateRays ? 10.0 : 1.0;
    float modifiedInterval = coef * cascadeInterval;
    
    float range = modifiedInterval * baseEx + length(pixelsBetweenProbes * 2.0);

    float term = 5.0 + cascadeIndex;

    vec2 interval = (cascadeInterval * pow(base, cascadeIndex - 1.0)) / resolution;

    return ProbeInfo(rayCount * rayCount, pixelsBetweenProbes, size, position, index, interval, range, base);
}

vec4 raymarch(vec2 normalizedPoint, vec2 delta, float scale, vec2 oneOverSize, ProbeInfo info, float baseMipLevel) {
    vec2 rayUv = normalizedPoint + delta * info.interval;
    if (floor(rayUv) != vec2(0.0)) return vec4(0.0, 0.0, 0.0, 1.0);

    vec2 pre = delta * scale * oneOverSize;
    bool inside = false;
    float depth = 0.0;
    vec4 outerColor = vec4(0.0);

    for (float dist = 0.0; dist < info.range; ) {
        float df = 0.0;

        if (inside) {
            df = safeTextureSample(distanceTexture, rayUv, 0.0).g;
        } else {
            df = safeTextureSample(distanceTexture, rayUv, 0.0).r;
        }
        
        if (df <= EPS) {
            if (!inside) {
                inside = true;
//                float prevCascadeRange = cascadeInterval * pow(4.0, cascadeIndex + 1.0);
//                float t = (dist - prevCascadeRange) / (info.range - prevCascadeRange);
//                t = smoothstep(0.0, 1.0, t);
                outerColor = vec4(
                    colorSample(sceneTexture, rayUv, 0.0).rgb, 
                    0.0
                );
                return outerColor;
            } else {
                return vec4(0.0, 0.0, 0.0, 1.0);
            }
        }

        if (inside) {
            if (attenuationDepth < 0.0001) {
                return vec4(outerColor.rgb, 0.0);
            }
            depth += df * scale;
            float attenuation = exp(-depth * (length(outerColor) < 0.1 ? 4.0 : 1.0)); // Adjust 0.2 for desired attenuation rate
            if (attenuation < attenuationDepth) { // Stop if light is mostly attenuated
                                                  return vec4(vec3(pow(outerColor.rgb, vec3(1.0))), 0.0);
            }
        }

        dist += df * scale;

        if (dist >= info.range) break;

        rayUv += pre * df;
        if (floor(rayUv) != vec2(0.0)) break;
    }

    return vec4(0.0, 0.0, 0.0, 1.0);
}

vec4 merge(vec4 currentRadiance, float index, ProbeInfo info) {
    // Occluders / Last cascade
    if (currentRadiance.a == 0.0 || cascadeIndex >= cascadeCount - 1.0)
    return vec4(currentRadiance.rgb, 1.0 - currentRadiance.a);

    float nextRayCount = pow(2.0, cascadeIndex + 1.0);
    vec2 nextSize = info.size * 0.5;
    vec2 nextProbe = vec2(mod(index, nextRayCount), floor(index / nextRayCount)) * nextSize;

    vec2 interpUV = (info.position * 0.5) + 0.25;
    vec2 clampedUV = clamp(interpUV, vec2(0.5), nextSize - 0.5);
    vec2 probeUV = (nextProbe + clampedUV) / cascadeExtent;
//    float coef = 40.0;
//    vec2 r = (
//        vec2(
//            snoise(coef * vec3(vUv, 0.2 + cascadeIndex)), 
//            snoise(coef * vec3(vUv, 0.1 + cascadeIndex))
//        ) * 2.0
//    ) / cascadeExtent * 1.0;
    
//     painterly offset
//    vec2 r = pow(1.4, cascadeIndex) * (
//      vec2(
//          rand(vUv * (0.2 + cascadeIndex)),
//          rand(vUv * (0.1 + cascadeIndex))
//      ) * 2.0 - 1.0
//    ) / cascadeExtent * 1.0 * 0.5;
    
//    if (cascadeIndex == 0.0) {
//        float coef = 1.0;
//        vec4 nextCascade1 = colorSample(lastTexture, clamp(probeUV + (vec2(-1.0, 1.0) * coef) / cascadeExtent, vec2(0.0), vec2(1.0)), (0.0));
//        vec4 nextCascade2 = colorSample(lastTexture, clamp(probeUV + (vec2(1.0, -1.0) * coef) / cascadeExtent, vec2(0.0), vec2(1.0)), (0.0));
//        vec4 nextCascade3 = colorSample(lastTexture, clamp(probeUV + (vec2(-1.0, -1.0) * coef) / cascadeExtent, vec2(0.0), vec2(1.0)), (0.0));
//        vec4 nextCascade4 = colorSample(lastTexture, clamp(probeUV + (vec2(1.0, 1.0) * coef) / cascadeExtent, vec2(0.0), vec2(1.0)), (0.0));
//        vec4 mix1 = mix(nextCascade1, nextCascade2, 0.5);
//        vec4 mix2 = mix(nextCascade3, nextCascade4, 0.5);
//        return currentRadiance + mix(mix1, mix2, 0.5);
//    }
    
    vec4 nextCascade = colorSample(
        lastTexture,
        probeUV,
        0.0
    );
    return currentRadiance + nextCascade;
}

void main() {
    vec2 coord = floor(vUv * cascadeExtent);
    ProbeInfo info = getProbeInfo(coord);
    vec2 probeCenter = (info.position + 0.5) * info.pixelsBetweenProbes;
    float preAvgAmt = 4.0;
    float baseIndex = info.index * preAvgAmt;
    float thetaStep = TAU / (info.rayCount * preAvgAmt);

    // Can we do this instead of length?
    float scale = min(resolution.x, resolution.y);
    vec2 oneOverSize = 1.0 / resolution;
    float avgRecip = 1.0 / preAvgAmt;

    float baseMipLevel = cascadeIndex;
    vec2 normalizedProbeCenter = probeCenter * oneOverSize;
    bool gotColor = false;
    
    vec4 totalRadiance = vec4(0.0);

    for (int i = 0; i < 4; i++) {
        float index = baseIndex + float(i);
        float theta = (index + 0.5) * thetaStep;
        vec2 rayDir = vec2(cos(theta), -sin(theta));

        vec4 radiance = raymarch(normalizedProbeCenter, rayDir, scale, oneOverSize, info, baseMipLevel);

        vec4 deltaRadiance = merge(radiance, index, info);

//        if (deltaRadiance.a < 0.1) {
//            vec3 color = sunAndSky(theta);
//            deltaRadiance += vec4(color, length(color));
//        }

        totalRadiance += deltaRadiance * avgRecip;
    }
    
//    totalRadiance = max(totalRadiance, merge(totalRadiance, baseIndex, info));

    FragColor = vec4(pow(totalRadiance.rgb, vec3(1.0 / srgb)), totalRadiance.a);
}
```

```javascript
// @run
function onBuildReload(self, instance) {
  return (event) => {
    document.querySelectorAll("iframe").forEach((o) => {
      o.parentNode.removeChild(o);
    });
    const iframe = document.createElement('iframe');
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    const htmlContent = event.html;
    iframe.srcdoc = htmlContent;
    iframe.onload = () => {
      const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
      const shaders = Object.keys(iframe.contentWindow[instance]).filter((a) => a.toLowerCase().includes("plane")).forEach((p) => {
        if (iframe.contentWindow[instance][p]?.material) {
          const shader = iframe.contentWindow[instance][p].material.fragmentShader;
          self[p].material.fragmentShader = shader;
          self[p].material.needsUpdate = true;
        }
      });

      self.renderPass();
      document.querySelectorAll("iframe").forEach((o) => {
        o.parentNode.removeChild(o);
      });
    };
    return false;
  };
}

function addSlider({
   id,
   name,
   onUpdate,
   options = {},
 }) {
  const div = document.createElement("div");
  div.style = "display: flex; align-items: center; gap: 8px"
  document.querySelector(`#${id}`).appendChild(div);
  div.append(`${name}`);
  const input = document.createElement("input");
  input.id = `${id}-${name.replace(" ", "-").toLowerCase()}-slider`;
  input.className = "slider";
  input.type = "range";
  Object.entries(options).forEach(([key, value]) => {
    input.setAttribute(key, `${value}`);
  });
  input.addEventListener("input", () => {
    onUpdate(input.value);
  });
  div.appendChild(input);
  return input;
}

class RC extends DistanceField {
  innerInitialize() {
    this.lastAnimationRequest = null;
    super.innerInitialize();
    this.gpuTimer = new GPUTimer(this.renderer);
    this.activelyDrawing = false;
    this.rawBasePixelsBetweenProbes = 1.0;
    this.renderWidth = this.width;
    this.renderHeight = this.height;

    const thisNode = document.querySelector(`#rc-sliders`);
    const myRangeInput = create2DRangeInput(100, 100);
    thisNode.parentNode.insertBefore(myRangeInput, thisNode);
    thisNode.parentNode.insertBefore(document.createElement("br"), thisNode);

    this.exaggerateRays = document.querySelector("#exaggerate-rays");
    this.rayintervalSlider = document.querySelector("#ray-interval-slider");
    this.rayintervalSliderValue = document.querySelector("#ray-interval-slider-value");
    this.rayintervalSliderValue.innerHTML = this.rayintervalSlider.value;
    this.sunAngleSlider = document.querySelector("#rc-sun-angle-slider");
    
    this.attenuationDepth = addSlider({
      id: "rc-sliders", 
      name: "Depth", 
      onUpdate: (value) => {
        this.rcPlane.material.uniforms.attenuationDepth.value = Math.pow(10, value);
        this.renderPass();
      }, 
      options: { min: -5, max: 0, value: -2, step: 0.1 }
    });

    this.attenuationBrightness = addSlider({ id: "rc-sliders", name: "Brightness", onUpdate: (value) => {
      this.rcPlane.material.uniforms.attenuationBrightness.value = value;
        this.renderPass();
    }, options: { min: 0.1, max: 1.0, step: 0.1, value: 0.3 }});

    // Calculate radiance cascades
    const angularSize = Math.sqrt(
      this.renderWidth * this.renderWidth + this.renderHeight * this.renderHeight
    );
    this.radianceCascades = Math.ceil(Math.log(angularSize) / Math.log(4));
    this.basePixelsBetweenProbes = this.rawBasePixelsBetweenProbes * this.rawBasePixelsBetweenProbes;
    this.radianceInterval = Math.sqrt(
      2.0 * (this.rawBasePixelsBetweenProbes * this.rawBasePixelsBetweenProbes)
    );
    this.radianceWidth = Math.floor(this.renderWidth / this.basePixelsBetweenProbes);
    this.radianceHeight = Math.floor(this.renderHeight / this.basePixelsBetweenProbes);
    this.cascadeIndex = 0.0;

    const fragmentShader = document.querySelector("#rc-fragment").innerHTML;

    console.log("Cascade count: ", this.radianceCascades);
    const {plane: rcPlane, render: rcRender, renderTargets: rcRenderTargets} = this.initThreeJS({
      renderTargetOverrides: {
        minFilter: THREE.LinearMipMapLinearFilter,
        magFilter: THREE.LinearFilter,
        generateMipmaps: true,
      },
      uniforms: {
        resolution: {value: new THREE.Vector2(this.width, this.height)},
        sceneTexture: {value: this.surface.texture},
        distanceTexture: {value: null},
        lastTexture: {value: null},
        cascadeExtent: {value: new THREE.Vector2(this.radianceWidth, this.radianceHeight)},
        cascadeCount: {value: this.radianceCascades},
        cascadeIndex: {value: this.cascadeIndex},
        basePixelsBetweenProbes: {value: this.basePixelsBetweenProbes},
        cascadeInterval: {value: this.radianceInterval},
        rayinterval: {value: this.rayintervalSlider.value},
        exaggerateRays: {value: this.exaggerateRays.checked},
        sunAngle: { value: this.sunAngleSlider },
        sunDistance: { value: 1.0 },
        attenuationDepth: { value: this.attenuationDepth.value },
        attenuationBrightness: { value: this.attenuationBrightness.value },
        time: { value: 0.1 }
      },
      fragmentShader,
    });

    myRangeInput.addEventListener('input', (event) => {
      const {x, y} = event.detail;
      this.rcPlane.material.uniforms.sunAngle.value = Math.atan2(-y, x);
      this.rcPlane.material.uniforms.sunDistance.value = (
        2.0 - Math.sqrt(x * x + y * y)
      );
      this.renderPass();
    });

    this.rcPlane = rcPlane;
    this.rcRender = rcRender;
    this.rcRenderTargets = rcRenderTargets;
    this.prev = 0;
  }

  rcPass(distanceFieldTexture, drawPassTexture) {
    this.rcPlane.material.uniforms.distanceTexture.value = distanceFieldTexture;
    this.rcPlane.material.uniforms.sceneTexture.value = drawPassTexture;
    this.rcPlane.material.uniforms.lastTexture.value = null;

    for (let i = this.radianceCascades - 1; i >= 0; i--) {
      this.rcPlane.material.uniforms.cascadeIndex.value = this.cascadeIndex + i;

      if (i > 0) {
        this.renderer.setRenderTarget(this.rcRenderTargets[this.prev]);
        this.rcRender();
        this.rcPlane.material.uniforms.lastTexture.value = this.rcRenderTargets[this.prev].texture;
        this.prev = 1 - this.prev;
      }
    }
    
    return this.rcRenderTargets[1 - this.prev].texture;
  }
  
  doRenderPass() {
    let drawPassTexture, out;

    this.gpuTimer.start('drawPass');
    drawPassTexture = this.drawPass();
    this.gpuTimer.end('drawPass');
    
    this.gpuTimer.start('seedPass');
    out = this.seedPass(drawPassTexture);
    this.gpuTimer.end('seedPass');

    this.gpuTimer.start('jfaPass');
    out = this.jfaPass(out);
    this.gpuTimer.end('jfaPass');

    this.gpuTimer.start('dfPass');
    out = this.dfPass(out);
    this.gpuTimer.end('dfPass');

    this.gpuTimer.start('rcPass');
    out = this.rcPass(out, drawPassTexture);
    this.gpuTimer.end('rcPass');

    this.renderer.setRenderTarget(null);
    this.gpuTimer.start('rcRender');
    this.rcRender();
    this.gpuTimer.end('rcRender');
 
    // Update timer and potentially print results
    this.gpuTimer.update();
  }

  // foo bar baz!!
  renderPass() {
    this.drawPass();
    if (!this.animating) {
      this.animating = true;
      requestAnimationFrame(() => {
        this.animate();
      });
    }
    
    // console.log("ask for render pass");
    // const now = (new Date()).getTime();
    // let doAnimate = false;
    // if (!this.lastAnimationRequest || (now - this.lastAnimationRequest) > 2000) {
    //   doAnimate = true;
    // }
    // this.lastAnimationRequest = now;
    // if (this.animating || !doAnimate) {
    //   return;
    // }
    // requestAnimationFrame(() => {
    //   this.updating = false;
    //   this.animate();
    // });
  }
  
  animate() {
    this.animating = true;
    
    // this.rcPlane.material.uniforms.time.value += 0.0005;
    
    this.doRenderPass();
    this.desiredRenderPass = false;
    
    // if (!this.lastAnimationRequest || this.updating) {
    //   return;
    // }
    // this.updating = true;
    // const now = (new Date()).getTime();
    
    // if (this.lastAnimationRequest && (now - this.lastAnimationRequest) > 2000) {
    //   this.lastAnimationRequest = null;
    //   return;
    // }
    
    requestAnimationFrame(() => {
      this.animate()
    });
  }

  clear() {
    this.lastFrame = null;
    if (this.initialized) {
      this.rcRenderTargets.forEach((target) => {
        this.renderer.setRenderTarget(target);
        this.renderer.clearColor();
      });
    }
    super.clear();
    this.renderPass();
  }

  //foo bar baz!!
  load() {
    this.rayintervalSlider.addEventListener("input", () => {
      this.rcPlane.material.uniforms.rayinterval.value = this.rayintervalSlider.value;
      this.rayintervalSliderValue.innerHTML = this.rayintervalSlider.value;
      this.renderPass();
    });
    this.exaggerateRays.addEventListener("input", () => {
      this.rcPlane.material.uniforms.exaggerateRays.value = this.exaggerateRays.checked;
      this.renderPass();
    });
    this.sunAngleSlider.addEventListener("input", () => {
      this.rcPlane.material.uniforms.sunAngle.value = this.sunAngleSlider.value;
      this.renderPass();
    })
    window.mdxishState.onReload = onBuildReload(this, "radianceCascades");
    this.reset();
    this.initialized = true;
  }

  reset() {
    this.clear();
    let point = { x: this.width * 0.1, y: this.height * 0.1 };
    this.surface.drawSmoothLine(point, { x: this.width * 0.9, y: this.height * 0.1 });
    point = { x: this.width * 0.25, y: this.height * 0.25 };
    this.surface.drawSmoothLine(point, point);
    point = { x: this.width * 0.75, y: this.height * 0.75 };
    this.surface.drawSmoothLine(point, point);
    point = { x: this.width * 0.25, y: this.height * 0.75 };
    this.surface.drawSmoothLine(point, point);
    point = { x: this.width * 0.75, y: this.height * 0.25 };
    this.surface.drawSmoothLine(point, point);

    this.setHex("#000000");
    this.surface.drawSmoothLine({ x: this.width * 0.4, y: this.height * 0.5 }, { x: this.width * 0.6, y: this.height * 0.5 });
    this.setHex("#fff6d3");

    this.renderPass();
  }
}

let [width, height] = [1024, 512];
window.radianceCascades = new RC({id: "radiance-cascades-canvas", width, height, radius: 8});

const rcCanvas = document.querySelector("#radiance-cascades-canvas").querySelector("canvas");
rcCanvas.style.width = `512px`;
rcCanvas.style.height = `256px`;
```

```html
// @run
<!--<div style="display: flex; align-items: center; gap: 4px">-->
<!--  <button id="sand-mode-button">Sand Mode</button>-->
<!--  <button id="solid-mode-button">Solid Mode</button>-->
<!--  <button id="empty-mode-button">Empty Mode</button>-->
<!--</div>-->

<br/>

<div id="falling-sand-rc-canvas"></div>
```


```javascript
// @run
class FallingSandDrawingRC extends RC {
  createSurface(width, height, radius) {
    this.surface = new FallingSandSurface({ width, height, radius });
  }

  reset() {
    this.clear();
    let last = undefined;
    return new Promise((resolve) => {
      this.setHex("#f9a875");
      getFrame(() => this.draw(last, 0, false, resolve));
    }).then(() => new Promise((resolve) => {
      last = undefined;
      getFrame(() => {
        this.surface.mode = Solid;
        this.setHex("#000000");
        getFrame(() => this.draw(last, 0, true, resolve));
      });
    }))
      .then(() => {
        this.renderPass();
        getFrame(() => this.setHex("#fff6d3"));
        this.surface.mode = Sand;
      });

  }
}

// window.radianceCascades = new FallingSandDrawingRC({id: "falling-sand-rc-canvas", width, height});
//
// const fallingSandRcCanvas = document.querySelector("#falling-sand-rc-canvas").querySelector("canvas");
// //
// //
// fallingSandRcCanvas.style.width = `1024px`;
// fallingSandRcCanvas.style.height = `1024px`;
```

<div id="rc-2-canvas"></div>

```glsl
// @run id="rc-dda-shader" type="x-shader/x-fragment"
uniform vec2 resolution;
uniform sampler2D sceneTexture;
uniform sampler2D previousCascadeTexture;
uniform int level;
uniform int maxLevel;
uniform float probeRadius;
uniform int probeRayCount;
uniform float intervalRadius;
uniform int branchingFactor;

in vec2 vUv;

const float PI = 3.14159265;
const float TAU = 2.0 * PI;
const float srgb = 2.2;
const vec3 skyColor = vec3(0.2, 0.24, 0.35) * 12.0;
const vec3 sunColor = vec3(0.95, 0.9, 0.3) * 10.0;
const float sunAngle = 2.2;
const float sunDistance = 0.5;

vec4 colorSample(sampler2D tex, vec2 uv, float lod) {
    vec4 color = textureLod(tex, uv, lod);
    return vec4(pow(color.rgb, vec3(srgb)), color.a);
}

vec4 accumulateSample(vec4 acc, vec4 sampleColor) {
    float transparency = 1.0 - sampleColor.a;
    return vec4(
        acc.rgb + acc.a * sampleColor.rgb,
        transparency * acc.a
    );
}

vec3 sunAndSky(float rayAngle) {
    // Get the sun / ray relative angle
    float angleToSun = mod(rayAngle - sunAngle, TAU);

    // Sun falloff
    float sunIntensity = pow(max(0.0, cos(angleToSun)), sunDistance * 20.0);

    // Adjust sky contribution based on sun distance
    float skyFactor = mix(0.2, 0.6, sunDistance / 4.0);

    return mix(sunColor * sunIntensity, skyColor, skyFactor);
}

vec4 raymarchFixed(
    vec2 probeCenter, float rayAngle, vec2 interval, float stepSize, vec2 oneOverResolution
) {
    vec4 radiance = vec4(0.0, 0.0, 0.0, 1.0);
    float throughput = 1.0;
    vec2 rayDirection = vec2(cos(rayAngle), -sin(rayAngle));
    vec2 pos = probeCenter;

    for (float dist = interval.x; dist <= interval.y; dist += stepSize) {
        if (pos.x < 0.0 || pos.y < 0.0 || pos.x >= resolution.x || pos.y >= resolution.y) {
            break;
        }

        vec4 sampleColor = colorSample(sceneTexture, pos * oneOverResolution, float(level));
        radiance = accumulateSample(radiance, sampleColor);
        if (sampleColor.a > 0.1) {
            radiance = sampleColor;
            break;
        }
        pos += rayDirection * stepSize;
    }

    // Sample from upper level in the same direction
    if (level < maxLevel - 1) {
        vec2 upperRayOrigin = (rayDirection * interval.y) * oneOverResolution;
        vec2 upperLevelSamplePos = (probeCenter * oneOverResolution + upperRayOrigin);
        vec4 upperLevelSample = colorSample(
            previousCascadeTexture,
            upperLevelSamplePos,
            float(level + 1)
        );
        if (length(radiance.rgb) > 0.01) {
            radiance += upperLevelSample;
        }
    } else {
//        radiance += vec4(sunAndSky(rayAngle), radiance.a);
        radiance.a = 1.0 - radiance.a;
    }
    
    return radiance;
}

vec2 getCenter(float stepSize) {
    vec2 gridSize = resolution / (2.0 * probeRadius * stepSize);
    vec2 cellIndex = floor(vUv * gridSize);
    return (cellIndex + 0.5) / gridSize;
}

void main() {
    vec2 coord = vUv * resolution;
    float stepSize = pow(2.0, float(level));
    vec2 probeCenterUv = getCenter(stepSize);
    vec2 oneOverResolution = 1.0 / resolution;

    vec2 probeCenter = probeCenterUv * resolution;
    vec2 relativeOffset = coord - probeCenter;

    float angleStep = TAU / (float(probeRayCount) * stepSize);

    vec2 interval = 10.0 * pow(
        vec2(2), vec2(level - 1, level) * float(branchingFactor)
    );

    int pixelIndex = int(relativeOffset.x + relativeOffset.y * stepSize);
    int angleIndex = pixelIndex * probeRayCount;

    vec4 totalRadiance = vec4(0.0);

    vec4 color = colorSample(sceneTexture, vUv, float(level));
    for (int i = 0; i < probeRayCount; i++) {
        // Rotate 45 degrees (0.5 = half an `angleStep`)
        int index = angleIndex + i;
        float rayAngle = angleStep * (float(index) + 0.5);

        totalRadiance += raymarchFixed(
            probeCenter, rayAngle, interval, stepSize, oneOverResolution
        );
    }

    totalRadiance /= float(probeRayCount);
    
    gl_FragColor = vec4(pow(totalRadiance.rgb, vec3(1.0 / srgb)), totalRadiance.a);
}
```

```javascript
// @run

class RC2 extends Drawing {

  innerInitialize() {
    this.gpuTimer = new GPUTimer(this.renderer);

    const diagonal = Math.sqrt(
      this.width * this.width + this.height * this.height
    );
    this.cascadeLevels = Math.ceil(Math.log(diagonal) / Math.log(2));
    const {plane: rcPlane, render: rcRender, renderTargets: rcRenderTargets} = this.initThreeJS({
      renderTargetOverrides: {
        // minFilter: THREE.NearestMipMapLinearFilter,
        minFilter: THREE.LinearMipMapLinearFilter,
        magFilter: THREE.LinearFilter,
        generateMipmaps: true,
      },
      uniforms: {
        resolution: { value: new THREE.Vector2(width, height) },
        sceneTexture: { value: this.surface.texture },
        previousCascadeTexture: { value: null },
        level: { value: 0 },
        maxLevel: { value: this.cascadeLevels },
        probeRadius: { value: 0.5 },
        probeRayCount: { value: 4 },
        intervalRadius: { value: Math.sqrt(4.84) },
        branchingFactor: { value: 2 },
      },
      fragmentShader: document.querySelector("#rc-dda-shader").innerHTML,
    });
    
    this.rcPlane = rcPlane;
    this.rcRender = rcRender;
    this.rcRenderTargets = rcRenderTargets;
    this.prev = 0;
    this.baseLevel = 0;
  }

  rcPass(drawPassTexture) {
    this.rcPlane.material.uniforms.sceneTexture.value = drawPassTexture;
    this.rcPlane.material.uniforms.previousCascadeTexture.value = null;

    for (let i = this.cascadeLevels - 1; i >= 0; i--) {
      this.rcPlane.material.uniforms.level.value = this.baseLevel + i;

      if (i > 0) {
        this.renderer.setRenderTarget(this.rcRenderTargets[this.prev]);
        this.rcRender();
        this.rcPlane.material.uniforms.previousCascadeTexture.value = this.rcRenderTargets[this.prev].texture;
        this.prev = 1 - this.prev;
      }
    }

    return this.rcRenderTargets[1 - this.prev].texture;
  }

  renderPass() {
    this.gpuTimer.start('drawPass');
    let out = this.drawPass();
    this.gpuTimer.end('drawPass');

    this.gpuTimer.start('rcPass');
    out = this.rcPass(out);
    this.gpuTimer.end('rcPass');

    this.renderer.setRenderTarget(null);
    this.gpuTimer.start('rcRender');
    this.rcRender();
    this.gpuTimer.end('rcRender');

    // Update timer and potentially print results
    this.gpuTimer.update();
  }

  clear() {
    this.lastFrame = null;
    if (this.initialized) {
      this.rcRenderTargets.forEach((target) => {
        this.renderer.setRenderTarget(target);
        this.renderer.clearColor();
      });
    }
    super.clear();
    this.renderPass();
  }
  
  load() {
    window.mdxishState.onReload = onBuildReload(this, "radianceCascades");
    this.reset();
    this.initialized = true;
  }

  reset() {
    this.clear();
    // let point = { x: this.width * 0.1, y: this.height * 0.1 };
    // this.surface.drawSmoothLine(point, { x: this.width * 0.9, y: this.height * 0.1 });
    // point = { x: this.width * 0.25, y: this.height * 0.25 };
    // this.surface.drawSmoothLine(point, point);
    // point = { x: this.width * 0.75, y: this.height * 0.75 };
    // this.surface.drawSmoothLine(point, point);
    // point = { x: this.width * 0.25, y: this.height * 0.75 };
    // this.surface.drawSmoothLine(point, point);
    // point = { x: this.width * 0.75, y: this.height * 0.25 };
    // this.surface.drawSmoothLine(point, point);
    //
    // this.setHex("#000000");
    // this.surface.drawSmoothLine({ x: this.width * 0.4, y: this.height * 0.5 }, { x: this.width * 0.6, y: this.height * 0.5 });
    // this.setHex("#fff6d3");
    //
    // this.renderPass();
  }
}

// let [width, height] = [1024, 512];
// window.radianceCascades = new RC2({id: "rc-2-canvas", width, height, radius: 8});
//
// const rc2Canvas = document.querySelector("#rc-2-canvas").querySelector("canvas");
// rc2Canvas.style.width = `${width / 2}px`;
// rc2Canvas.style.height = `${height / 2}px`;
```