proof-engine 0.2.3

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation
// bloom.frag — separable Gaussian bloom with a soft-knee threshold and an
// anamorphic option.
//
// Three things were missing.
//
// There was no threshold: the whole emission buffer was blurred and added
// back, so dim matter glowed exactly as readily as bright matter and the
// result was a general haze rather than light coming off the things that are
// actually lit. A soft knee is what makes bloom look like an optical effect
// instead of a blur layer.
//
// The kernel was thirteen taps at a fixed radius with no use of bilinear
// filtering, which is the standard trick: sampling *between* two texels gets
// two taps for one fetch, so the same nine fetches cover a much wider kernel.
//
// And it was isotropic. Real anamorphic glass streaks light horizontally, and
// a horizontal pass with a stretched radius costs exactly what the ordinary
// horizontal pass costs.
#version 330 core

in vec2 f_uv;
uniform sampler2D u_texture;
uniform bool  u_horizontal;
uniform float u_radius;
uniform float u_threshold;
uniform float u_knee;
uniform bool  u_prefilter;
uniform float u_stretch;

out vec4 o_color;

// Linear-sampled 9-tap: offsets fall between texels so the hardware's
// bilinear filter does half the work.
const float weight[3] = float[](0.2270270270, 0.3162162162, 0.0702702703);
const float offset[3] = float[](0.0, 1.3846153846, 3.2307692308);

// Soft knee: nothing under the threshold blooms, everything well over it
// blooms fully, and the region between is a quadratic rather than a cliff.
// A hard cut-off makes bright edges crawl as the frame changes.
vec3 prefilter(vec3 c) {
    float lum = max(c.r, max(c.g, c.b));
    float knee = max(u_knee, 1e-4);
    float soft = clamp(lum - u_threshold + knee, 0.0, 2.0 * knee);
    soft = soft * soft / (4.0 * knee);
    float contribution = max(soft, lum - u_threshold) / max(lum, 1e-4);
    return c * contribution;
}

void main() {
    vec2 texel = 1.0 / vec2(textureSize(u_texture, 0));
    vec3 result = texture(u_texture, f_uv).rgb * weight[0];

    if (u_horizontal) {
        float r = u_radius * u_stretch;
        for (int i = 1; i < 3; ++i) {
            vec2 o = vec2(texel.x * offset[i] * r, 0.0);
            result += texture(u_texture, f_uv + o).rgb * weight[i];
            result += texture(u_texture, f_uv - o).rgb * weight[i];
        }
    } else {
        for (int i = 1; i < 3; ++i) {
            vec2 o = vec2(0.0, texel.y * offset[i] * u_radius);
            result += texture(u_texture, f_uv + o).rgb * weight[i];
            result += texture(u_texture, f_uv - o).rgb * weight[i];
        }
    }

    // Only the first pass thresholds. Doing it on every pass would eat the
    // blur it just produced.
    if (u_prefilter) {
        result = prefilter(result);
    }
    o_color = vec4(result, 1.0);
}