Volumetric Radiance Cascades 2D

by Cody Bennett

HTML

<script id="sceneShader" type="x-shader/x-fragment">
  precision mediump float;

  out vec4 radiance;

  uniform vec2 resolution;

  // https://iquilezles.org/articles/distfunctions2d
  void sdCircle(inout vec4 radiance, vec2 position, float radius, vec4 color) {
    if (length(position) - radius < radius) radiance = color;
  }

  void main() {
    vec2 center = (resolution * 0.5) - gl_FragCoord.xy;
    radiance = vec4(0.0); // undefined on Apple OpenGL
    sdCircle(radiance, center + vec2(0, 0), 15.0, vec4(1, 1, 1, 1));
    sdCircle(radiance, center + vec2(40, 0), 15.0, vec4(0, 0, 0, 1));
  }
</script>
<script id="rcShader" type="x-shader/x-fragment">
  precision mediump float;

  out vec4 radiance;

  uniform sampler2D sceneTexture;
  uniform sampler2D cascadeTexture;
  uniform int baseProbeSize;
  uniform float baseIntervalLength;
  uniform int cascadeIndex;
  uniform vec2 resolution;

  const float PI = acos(-1.0);

  // https://m4xc.dev/articles/fundamental-rc

  float getIntervalScale(int cascadeIndex) {
    if (cascadeIndex <= 0) return 0.0;
    return float(1 << (2 * cascadeIndex));
  }

  vec2 getIntervalRange(int cascadeIndex, float baseIntervalLength) {
    return baseIntervalLength * vec2(getIntervalScale(cascadeIndex), getIntervalScale(cascadeIndex + 1));
  }

  vec4 castInterval(vec2 intervalStart, vec2 intervalEnd, int cascadeIndex) {
    vec2 dir = intervalEnd - intervalStart;
    int steps = 16 << cascadeIndex;
    vec2 stepSize = dir / float(steps);

    vec3 radiance = vec3(0);
    float transmittance = 1.0;

    for (int i = 0; i < steps; i++) {
      vec2 coord = intervalStart + (stepSize * float(i));
      vec4 scene = texelFetch(sceneTexture, ivec2(coord), 0);
      radiance += scene.rgb * transmittance * scene.a;
      transmittance *= 1.0 - scene.a;
    }

    return vec4(radiance, transmittance);
  }

  vec4 mergeIntervals(vec4 near, vec4 far) {
    vec3 radiance = near.rgb + (far.rgb * near.a);
    return vec4(radiance, near.a *...

CSS

body {
  margin: 0;
}

canvas {
  display: block;
}

JavaScript

const NUM_CASCADES = 8
const baseProbeSize = 1
const baseIntervalLength = 0.2

const canvas = document.createElement('canvas')
document.body.appendChild(canvas)

const gl = canvas.getContext('webgl2')

const vertex = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(
  vertex,
  /* glsl */ `#version 300 es
  out vec2 vUv;

  void main() {
    vUv = vec2(gl_VertexID << 1 & 2, gl_VertexID & 2);
    gl_Position = vec4(vUv * 2.0 - 1.0, 0, 1);
  }  
`,
)
gl.compileShader(vertex)

const lineNumbers = (source, offset = 0) => source.replace(/^/gm, () => `${offset++}:`)

function createProgram(fragmentShader) {
  const fragment = gl.createShader(gl.FRAGMENT_SHADER)
  gl.shaderSource(fragment, '#version 300 es\n' + fragmentShader)
  gl.compileShader(fragment)

  const program = gl.createProgram()
  gl.attachShader(program, vertex)
  gl.attachShader(program, fragment)
  gl.linkProgram(program)
  gl.useProgram(program)

  for (const shader of [vertex, fragment]) {
    const error = gl.getShaderInfoLog(shader)
    if (error) throw `Error compiling shader: ${error}\n${lineNumbers(gl.getShaderSource(shader))}`
  }
  const error = gl.getProgramInfoLog(program)
  if (error) throw `Error compiling program: ${gl.getProgramInfoLog(program)}`

  return program
}

const sceneProgram = createProgram(sceneShader.textContent)
const rcProgram = createProgram(rcShader.textContent)

const sceneTexture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, sceneTexture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

const sceneTarget = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, sceneTarget)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, sceneTexture, 0)

const cascadeTextures = []
const cascadeTargets = []

for (let i = 0; i < 2;...