Nimbin[12]?Games / Entropie / entropie/assets/shaders/ink.frag

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (19 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/assets/shaders/ink.frag 6.3 KB · 162 lines raw
// Rotoscope ink pass: boiling outlines, misregistered fills, paper grain.

in vec2 UV;
out vec4 FragColor;

uniform sampler2D colorTex;
uniform sampler2D normalDepthTex; // xyz = view normal, w = view distance
uniform vec2  texel;
uniform float boilTime;
uniform float time;  // continuous seconds
uniform float drunk; // 0 = sober .. 1 = very drunk
uniform float dark;  // 0 = awake .. 1 = eyes shut

const vec3  INK       = vec3(0.10, 0.07, 0.14);
const float SKY_DEPTH = 1000.0; // written by skybox.frag

float isSky(vec4 normalDepth) { return step(SKY_DEPTH * 0.9, normalDepth.w); }

// Screen-space offset that changes with every animation step.
vec2 jitter(vec2 uv, float seed, float pixels) {
    vec3 q = vec3(uv * 7.0, boilTime * 3.0 + seed);
    return (vec2(valueNoise(q), valueNoise(q + 31.7)) - 0.5) * 2.0 * pixels * texel;
}

float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
float invDepth(vec4 s)  { return 1.0 / max(s.w, 0.01); }

float crease(vec4 a, vec4 b) {
    return (1.0 - dot(a.xyz, b.xyz)) * (1.0 - isSky(a)) * (1.0 - isSky(b));
}

// Edge strength from silhouettes, creases and color band borders.
float edgeAt(vec2 uv, float width) {
    vec2 dx = vec2(texel.x * width, 0.0);
    vec2 dy = vec2(0.0, texel.y * width);
    vec4 c = texture(normalDepthTex, uv);
    vec4 l = texture(normalDepthTex, uv - dx);
    vec4 r = texture(normalDepthTex, uv + dx);
    vec4 d = texture(normalDepthTex, uv - dy);
    vec4 u = texture(normalDepthTex, uv + dy);

    // Inverse depth is linear across flat surfaces, so its second
    // difference only fires at silhouettes and creases.
    float ic  = invDepth(c);
    float lap = abs(invDepth(l) + invDepth(r) - 2.0 * ic)
              + abs(invDepth(d) + invDepth(u) - 2.0 * ic);
    float depthEdge = smoothstep(0.08, 0.2, lap / ic);

    // Creases between surfaces. The sky has no normal (it would count as an
    // edge everywhere), so sky samples are left to the depth test above.
    float bend = max(max(crease(c, l), crease(c, r)), max(crease(c, d), crease(c, u)));
    float normalEdge = smoothstep(0.2, 0.45, bend);

    float lum = abs(luminance(texture(colorTex, uv - dx).rgb) - luminance(texture(colorTex, uv + dx).rgb))
              + abs(luminance(texture(colorTex, uv - dy).rgb) - luminance(texture(colorTex, uv + dy).rgb));
    float toneEdge = smoothstep(0.10, 0.22, lum) * 0.45 * (1.0 - isSky(c)); // calm sky

    return max(max(depthEdge, normalEdge), toneEdge);
}

// Drunk: the picture wavers, and slowly stretches and squeezes like a
// funhouse mirror (more towards the edges of the view).
vec2 waver(vec2 uv) {
    vec2 c = uv - 0.5;
    vec2 stretch = vec2(1.0 + 0.07 * sin(time * 0.47), 1.0 + 0.05 * sin(time * 0.31 + 1.7));
    c *= mix(vec2(1.0), stretch, drunk * (0.4 + dot(c, c) * 2.4));
    c += vec2(sin(uv.y * 7.0 + time * 1.3), cos(uv.x * 5.0 + time * 0.9)) * 0.007 * drunk;
    return c + 0.5;
}

// Drunk: the eyes don't focus. Blur grows towards the edges and pulses.
const int BLUR_TAPS = 12;
vec3 blurred(vec2 uv, float pixels) {
    vec3 sum = texture(colorTex, uv).rgb;
    for (int i = 1; i < BLUR_TAPS; i++) {
        float a = float(i) * 2.39996; // golden angle spiral
        float r = sqrt(float(i) / float(BLUR_TAPS)) * pixels;
        sum += texture(colorTex, uv + vec2(cos(a), sin(a)) * r * texel).rgb;
    }
    return sum / float(BLUR_TAPS);
}

float blurPixels(vec2 uv) {
    vec2 c = uv - 0.5;
    float pulse = 0.6 + 0.4 * sin(time * 0.8);
    return drunk * (2.0 + 16.0 * dot(c, c) * 4.0) * pulse + drunk * drunk * 4.0;
}

// Drunk: a second, faint copy of the picture drifts beside the first.
vec2 doubleOffset() {
    return vec2(sin(time * 0.7), 0.4 * cos(time * 0.5)) * 0.018 * drunk;
}

// Smooth lines: edges looked at in four spots inside the pixel (a rotated
// grid) and averaged - a diagonal line covers a pixel partly, not all or
// nothing, so it doesn't step like a staircase.
const vec2 SUB[4] = vec2[4](vec2(-0.375, -0.125), vec2(0.125, -0.375), vec2(0.375, 0.125), vec2(-0.125, 0.375));

float smoothEdge(vec2 uv, float width) {
    float e = 0.0;
    for (int i = 0; i < 4; i++) e += edgeAt(uv + SUB[i] * texel, width);
    return e * 0.25;
}

// Smooth fills: where neighbouring pixels differ a lot (the border of a roof
// against the sky), the color is averaged over the pixel's corners.
vec3 smoothFill(vec2 uv) {
    vec3 c = texture(colorTex, uv).rgb, sum = vec3(0.0);
    float lo = 1e9, hi = -1e9;
    for (int i = 0; i < 4; i++) {
        vec3 s = texture(colorTex, uv + SUB[i] * 1.6 * texel).rgb;
        float l = luminance(s);
        lo = min(lo, l);
        hi = max(hi, l);
        sum += s;
    }
    return mix(c, sum * 0.25, smoothstep(0.04, 0.14, hi - lo));
}

void main() {
    vec2 uv0 = waver(UV);
    float sky = isSky(texture(normalDepthTex, uv0));

    // Lines and fills are traced separately and don't quite line up.
    // The sky stays still.
    vec2 lineUV = uv0 + jitter(uv0, 0.0, 2.0);
    vec2 fillUV = uv0 + jitter(uv0, 7.0, 1.5) * (1.0 - sky);

    // Pen pressure: width varies, lines thin out and break here and there.
    float width = mix(0.8, 2.2, valueNoise(vec3(UV * 9.0, boilTime * 2.0 + 3.0)));
    float edge  = smoothEdge(lineUV, width);
    edge *= mix(0.45, 1.0, smoothstep(0.3, 0.45, valueNoise(vec3(UV * 30.0, boilTime * 4.0))));

    vec3 color = smoothFill(fillUV);
    float blur = 0.0;
    if (drunk > 0.001) {
        blur  = blurPixels(UV);
        color = blurred(fillUV, blur);
        edge *= 1.0 - clamp(blur / 14.0, 0.0, 0.65); // soft, smeared lines
    }
    vec3 ink = mix(INK, color * 0.35, 0.25);
    color = mix(color, ink, clamp(edge, 0.0, 1.0));

    if (drunk > 0.001) {
        // Double vision: a smeared second copy drifts beside the first
        vec2 ghostUV = lineUV + doubleOffset();
        vec3 ghost = blurred(ghostUV, blur * 1.5 + 2.0);
        ghost = mix(ghost, ink, clamp(edgeAt(ghostUV, width), 0.0, 1.0) * 0.6);
        color = mix(color, ghost, 0.45 * drunk);
    }

    // Paper grain: boils on objects, stays still on the sky
    float grainStep = sky > 0.5 ? 0.0 : floor(boilTime * 12.0);
    color += (hash13(vec3(gl_FragCoord.xy, grainStep)) - 0.5) * mix(0.045, 0.02, sky);

    // Soft vignette
    vec2 v = UV - 0.5;
    color *= 1.0 - dot(v, v) * (0.35 + 0.6 * drunk);
    color = mix(color, INK * 0.6, dark);

    FragColor = vec4(color, 1.0);
}