| 1 | #version 300 es |
| 2 | precision highp float; |
| 3 | |
| 4 | in vec2 v_texcoord; |
| 5 | uniform sampler2D tex; |
| 6 | out vec4 fragColor; |
| 7 | |
| 8 | // --- CONFIGURATION --- |
| 9 | const float edge_threshold = 0.15; // Sensitivity (0.05-0.5) - lower = more edges |
| 10 | const float edge_softness = 0.08; // Anti-aliasing amount |
| 11 | const float line_thickness = 1.0; // 1.0 = normal, 2.0 = thicker |
| 12 | const float line_darkness = 0.05; // 0.0 = pure black, higher = lighter |
| 13 | const float paper_brightness = 0.98; // Paper color |
| 14 | const float paper_grain = 0.03; // Paper texture intensity |
| 15 | const float noise_reduction = 0.5; // Reduces speckles in smooth areas |
| 16 | // --------------------- |
| 17 | |
| 18 | float luminance(vec3 color) { |
| 19 | return dot(color, vec3(0.2126, 0.7152, 0.0722)); |
| 20 | } |
| 21 | |
| 22 | float hash12(vec2 p) { |
| 23 | vec3 p3 = fract(vec3(p.xyx) * 0.1031); |
| 24 | p3 += dot(p3, p3.yzx + 33.33); |
| 25 | return fract((p3.x + p3.y) * p3.z); |
| 26 | } |
| 27 | |
| 28 | // Sample luminance with bounds checking |
| 29 | float sampleLum(vec2 uv) { |
| 30 | vec2 safe_uv = clamp(uv, 0.0, 1.0); |
| 31 | return luminance(texture(tex, safe_uv).rgb); |
| 32 | } |
| 33 | |
| 34 | void main() { |
| 35 | vec2 screen_res = vec2(textureSize(tex, 0)); |
| 36 | vec2 pixel_size = line_thickness / screen_res; |
| 37 | vec2 pixel_coords = v_texcoord * screen_res; |
| 38 | |
| 39 | // Sobel kernel sampling with proper edge clamping |
| 40 | float tl = sampleLum(v_texcoord + vec2(-pixel_size.x, -pixel_size.y)); |
| 41 | float t = sampleLum(v_texcoord + vec2( 0.0, -pixel_size.y)); |
| 42 | float tr = sampleLum(v_texcoord + vec2( pixel_size.x, -pixel_size.y)); |
| 43 | float l = sampleLum(v_texcoord + vec2(-pixel_size.x, 0.0)); |
| 44 | float c = sampleLum(v_texcoord); // Center pixel |
| 45 | float r = sampleLum(v_texcoord + vec2( pixel_size.x, 0.0)); |
| 46 | float bl = sampleLum(v_texcoord + vec2(-pixel_size.x, pixel_size.y)); |
| 47 | float b = sampleLum(v_texcoord + vec2( 0.0, pixel_size.y)); |
| 48 | float br = sampleLum(v_texcoord + vec2( pixel_size.x, pixel_size.y)); |
| 49 | |
| 50 | // Sobel operators |
| 51 | float Gx = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl); |
| 52 | float Gy = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr); |
| 53 | float gradient = sqrt(Gx * Gx + Gy * Gy); |
| 54 | |
| 55 | // Calculate local variance for noise reduction |
| 56 | // (reduces speckles in smooth areas while preserving real edges) |
| 57 | float mean = (tl + t + tr + l + c + r + bl + b + br) / 9.0; |
| 58 | float variance = 0.0; |
| 59 | variance += (tl - mean) * (tl - mean); |
| 60 | variance += (t - mean) * (t - mean); |
| 61 | variance += (tr - mean) * (tr - mean); |
| 62 | variance += (l - mean) * (l - mean); |
| 63 | variance += (c - mean) * (c - mean); |
| 64 | variance += (r - mean) * (r - mean); |
| 65 | variance += (bl - mean) * (bl - mean); |
| 66 | variance += (b - mean) * (b - mean); |
| 67 | variance += (br - mean) * (br - mean); |
| 68 | variance = sqrt(variance / 9.0); |
| 69 | |
| 70 | // Adaptive threshold: require stronger edges in noisy areas |
| 71 | float adaptive_threshold = edge_threshold + variance * noise_reduction; |
| 72 | |
| 73 | // Smooth edge detection (anti-aliased) |
| 74 | float edge = smoothstep( |
| 75 | adaptive_threshold - edge_softness, |
| 76 | adaptive_threshold + edge_softness, |
| 77 | gradient |
| 78 | ); |
| 79 | |
| 80 | // Paper texture |
| 81 | float paper = paper_brightness; |
| 82 | paper -= hash12(pixel_coords * 0.4) * paper_grain; |
| 83 | paper -= hash12(pixel_coords * 2.1) * paper_grain * 0.4; |
| 84 | |
| 85 | // Final blend |
| 86 | float final_value = mix(paper, line_darkness, edge); |
| 87 | |
| 88 | fragColor = vec4(vec3(final_value), 1.0); |
| 89 | } |