5.4 KiB · text History ca16122
// The moire wallpaper's whole picture: ground, bokeh, halftone dots and
// moire rings, the patterns fading out into the ground away from a
// drifting hub.
//
// Everything that moves is computed in Field.qml, in double precision,
// and arrives here as positions and phases; the shader holds no clock,
// so it never loses precision however long the session runs.
//
// Compiled into ../theme/contents/ui/field.frag.qsb by tools/shaders.sh.
#version 440

layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;

layout(std140, binding = 0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    vec2 size;        // pixels
    // Positions and lengths below are in units of the height, so every
    // aspect ratio shows the same pattern at the same apparent scale.
    vec2 ringA;       // centres of the two ring families
    vec2 ringB;
    vec2 hub;         // where the pattern is strongest
    float reach;      // distance at which it has faded out entirely
    float period;     // ring spacing
    float moire;      // 0-1, how much of the rings to show
    float halftone;   // 0-1, how much of the dots to show
    float cell;       // halftone grid spacing
    float angle;      // halftone grid rotation, radians
    float wave;       // halftone wave phase, radians, kept in [0, 2pi)
    float bokeh;      // 0-1, how much of the bokeh to show
    float drift;      // bokeh phase, 0-1 over its whole cycle

    vec4 base;        // the ground
    vec4 deep;        // the ground at the edges
    vec4 ink;         // the moire
    vec4 tone;        // the halftone
};

float fade(vec2 p)
{
    float t = clamp(1.0 - length(p - hub) / reach, 0.0, 1.0);
    return t * t * (3.0 - 2.0 * t);
}

float rings(vec2 p, vec2 centre, float px)
{
    float r = length(p - centre);
    float d = abs(fract(r / period + 0.5) - 0.5) * period;
    float halfWidth = max(0.9 * px, period * 0.09);
    return clamp(1.0 - (d - halfWidth) / px, 0.0, 1.0);
}

// A repeatable pseudo-random number in [0, 1) per disc and purpose.
float hash(float n)
{
    return fract(sin(n * 127.1 + 311.7) * 43758.5453);
}

// Out-of-focus lights drifting upward behind the pattern, as if the
// moire were in focus in front of them. Each disc moves at a whole
// multiple of drift, so when drift wraps from 1 back to 0 every disc
// is exactly where it started; each fades in rising from the bottom
// and out before the top, so none ever appears or vanishes abruptly.
const int DISCS = 14;

float bokehLight(vec2 p, float aspect, float px, out float tint)
{
    float total = 0.0;
    tint = 0.0;
    for (int i = 0; i < DISCS; i++) {
        float n = float(i);
        float climb = 1.0 + floor(hash(n + 1.0) * 3.0);       // 1-3 laps a cycle
        float sway = 1.0 + floor(hash(n + 2.0) * 2.0);
        float life = fract(hash(n + 3.0) + climb * drift);    // 0 at bottom, 1 at top
        vec2 c = vec2(hash(n + 4.0) * aspect
                        + 0.04 * sin(6.2831853 * (sway * drift + hash(n + 5.0))),
                      1.15 - 1.3 * life);
        float radius = 0.035 + 0.075 * hash(n + 6.0);
        float d = length(p - c);
        float disc = clamp((radius - d) / (2.5 * px) + 0.5, 0.0, 1.0);
        // A lens renders a defocused point with a slightly brighter rim.
        float rim = smoothstep(radius * 0.7, radius, d) * disc;
        float shape = disc * 0.65 + rim * 0.35;
        float strength = shape * sin(3.1415927 * life) * (0.45 + 0.55 * hash(n + 7.0));
        total += strength;
        tint += strength * step(0.5, hash(n + 8.0));
    }
    tint = total > 0.0 ? tint / total : 0.0;
    return min(total, 1.0);
}

void main()
{
    float px = 1.0 / size.y;
    vec2 p = qt_TexCoord0 * size * px;
    vec2 centre = vec2(0.5 * size.x * px, 0.5);

    // Ground: base, darkening towards deep at the corners.
    float v = smoothstep(0.35, 1.25, length(p - centre));
    vec3 colour = mix(base.rgb, deep.rgb, v);

    // Bokeh sits behind everything else: it is the out-of-focus scene.
    if (bokeh > 0.0) {
        float tint;
        float light = bokehLight(p, size.x * px, px, tint);
        colour = mix(colour, mix(ink.rgb, tone.rgb, tint), light * 0.14 * bokeh);
    }

    // Halftone: dots on a rotated grid, each sized by the fade and a
    // travelling wave sampled at its cell's centre, so a dot is round
    // and uniform rather than cut by the field. Area, not radius, is
    // proportional to the field, as in print.
    if (halftone > 0.0) {
        mat2 rot = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
        vec2 q = rot * p;
        vec2 index = floor(q / cell);
        vec2 local = (q / cell - index - 0.5) * cell;
        vec2 cellCentre = transpose(rot) * ((index + 0.5) * cell);
        float field = fade(cellCentre)
                    * (0.55 + 0.45 * sin(dot(cellCentre, vec2(0.8, 0.6)) * 9.0 - wave));
        float radius = 0.5 * cell * sqrt(field);
        float dotCover = clamp((radius - length(local)) / px + 0.5, 0.0, 1.0);
        colour = mix(colour, tone.rgb, dotCover * halftone * 0.55);
    }

    // Moire: two ring families slightly apart. Where their rings
    // interleave the light doubles up, where they coincide it thins
    // out, and that beat is the moire.
    if (moire > 0.0) {
        float a = rings(p, ringA, px);
        float b = rings(p, ringB, px);
        float light = 1.0 - (1.0 - 0.85 * a) * (1.0 - 0.85 * b);
        colour = mix(colour, ink.rgb, light * 0.6 * fade(p) * moire);
    }

    fragColor = vec4(colour, 1.0) * qt_Opacity;
}