proof-engine 0.2.1

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation
// light.frag — the screen-space light map, with shadows.
//
// Every pixel of the scene is lit by an ambient level plus each light in
// the frame, and each light is shadowed by the matter between it and the
// pixel. The matter is the occluder buffer the glyph pass wrote: coverage
// for particles, nothing for floors and panel fills. Twenty-four samples
// are taken along the line from the pixel to the light, and each one that
// lands on matter takes a share of the light away.
//
// So a figure standing beside a brazier is lit on the side that faces it
// and dark on the side that does not, and casts its shape across the floor
// behind it, because the pixels back there have to look through the figure
// to see the fire. Nobody wrote a shadow; the geometry did.
#version 330 core

in vec2 f_uv;
uniform sampler2D u_occluder;
uniform vec2  u_screen;
uniform vec3  u_ambient;
uniform int   u_count;
const int MAX_LIGHTS = 32;
// (u, v, radius_px, casts_shadow)
uniform vec4  u_light_pos[MAX_LIGHTS];
// (r, g, b, intensity)
uniform vec4  u_light_color[MAX_LIGHTS];
// How much light one fully covered sample removes.
uniform float u_shadow_density;

out vec4 o_light;

const int STEPS = 24;

void main() {
    vec3 light = u_ambient;
    for (int i = 0; i < MAX_LIGHTS; ++i) {
        if (i >= u_count) break;
        vec4 lp = u_light_pos[i];
        vec4 lc = u_light_color[i];
        vec2 d_px = (lp.xy - f_uv) * u_screen;
        float dist = length(d_px);
        float radius = max(lp.z, 1.0);
        if (dist >= radius) continue;
        // Inverse-square-ish with a smooth end at the radius.
        float x = dist / radius;
        float atten = (1.0 - x) * (1.0 - x) / (0.15 + x * x * 2.0);

        float transmit = 1.0;
        if (lp.w > 0.5 && u_shadow_density > 0.0) {
            // Start a little way off the pixel itself, so a surface is not
            // shadowed by its own coverage; stop a little short of the
            // light, which sits inside its own emitter.
            vec2 step_uv = (lp.xy - f_uv) / float(STEPS);
            vec2 p = f_uv + step_uv * 1.5;
            float density = u_shadow_density / float(STEPS);
            for (int s = 1; s < STEPS - 1; ++s) {
                float occ = texture(u_occluder, p).r;
                transmit *= 1.0 - clamp(occ * density, 0.0, 1.0);
                p += step_uv;
                if (transmit < 0.02) break;
            }
        }
        light += lc.rgb * lc.a * atten * transmit;
    }
    o_light = vec4(light, 1.0);
}