Entropie git · main
Entropie festival game; first steps
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.gzentropie/assets/shaders/ink.frag 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);
}