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