// 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); }