pub const COMPOSITE_FRAG: &str = "// composite.frag \u{2014} scene + light + grade, in the order a film pipeline uses.\r\n//\r\n// The scene arrives in linear HDR: a sixteen-bit float buffer that a few\r\n// hundred thousand overlapping emissive particles can push well past white\r\n// without clipping. Everything here works on those real values, and the\r\n// tonemap is the one place the range is brought down.\r\n//\r\n// Order: shockwave refraction, lens shape, chromatic sample, sharpen,\r\n// exposure, indirect light, bloom, halation, light shafts, lens flare,\r\n// flash, tonemap, grade, vignette, grain, dither, scanlines.\r\n#version 330 core\r\n\r\nin vec2 f_uv;\r\nuniform sampler2D u_scene;\r\nuniform sampler2D u_bloom;\r\nuniform float u_bloom_intensity;\r\nuniform float u_exposure;\r\nuniform vec3 u_tint;\r\nuniform float u_saturation;\r\nuniform float u_contrast;\r\nuniform float u_brightness;\r\nuniform float u_vignette;\r\nuniform float u_vignette_softness;\r\nuniform float u_grain_intensity;\r\nuniform float u_grain_seed;\r\nuniform float u_chromatic;\r\nuniform float u_scanline_intensity;\r\nuniform bool u_scanlines_enabled;\r\nuniform float u_tonemap;\r\nuniform vec3 u_lift;\r\nuniform vec3 u_gain;\r\nuniform float u_halation;\r\nuniform float u_sharpen;\r\nuniform float u_dither;\r\nuniform float u_barrel;\r\n\r\n// \u{2500}\u{2500} New: light and moments \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n// The finished frame\'s size in pixels. Shockwave geometry is in pixels so a\r\n// blow looks the same at any resolution.\r\nuniform vec2 u_screen;\r\n// Screen-space indirect light: matter near something bright is lit by it.\r\nuniform float u_indirect;\r\n// Radial light shafts streaming from a point.\r\nuniform float u_light_shafts;\r\nuniform vec3 u_shaft; // (u, v, strength)\r\nuniform vec3 u_shaft_tint;\r\n// Lens flare ghosts and halo off anything that blooms.\r\nuniform float u_lens_flare;\r\n// A colour added to the whole frame, already scaled. Decays on the CPU.\r\nuniform vec3 u_flash;\r\n// A glossy floor: (line_v, strength, fade_v, ripple_px), and its blur in px.\r\nuniform vec4 u_reflection;\r\nuniform float u_reflection_blur;\r\n// Heat shimmer, 0 to about 1, and a clock for it.\r\nuniform float u_haze;\r\nuniform float u_time;\r\n// The light map, and the emission buffer that says what lights itself.\r\nuniform sampler2D u_light;\r\nuniform sampler2D u_emission;\r\nuniform bool u_lighting;\r\n// Expanding rings of refraction: (u, v, radius_px, width_px) and strength_px.\r\nconst int MAX_SHOCK = 12;\r\nuniform vec4 u_shock[MAX_SHOCK];\r\nuniform float u_shock_strength[MAX_SHOCK];\r\nuniform int u_shock_count;\r\n\r\nout vec4 o_color;\r\n\r\nfloat rand(vec2 co) {\r\n return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);\r\n}\r\n\r\n// ACES filmic, the fitted curve. Rolls highlights off instead of clipping\r\n// them, which is the single thing that separates a rendered image from a\r\n// photographed one.\r\nvec3 aces(vec3 x) {\r\n const float a = 2.51;\r\n const float b = 0.03;\r\n const float c = 2.43;\r\n const float d = 0.59;\r\n const float e = 0.14;\r\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);\r\n}\r\n\r\nvoid main() {\r\n vec2 uv0 = f_uv;\r\n\r\n // \u{2500}\u{2500} Shockwaves \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // Each one is a Gaussian ring in pixel space. Inside the ring the image\r\n // is pulled toward the origin, outside it is pushed away, which is what\r\n // a pressure front does to the air in front of a lens. The ring also\r\n // brightens a little, because compressed air refracts more light into\r\n // the eye than the calm air either side of it.\r\n float shock_light = 0.0;\r\n for (int i = 0; i < MAX_SHOCK; ++i) {\r\n if (i >= u_shock_count) break;\r\n vec4 s = u_shock[i];\r\n vec2 d_px = (uv0 - s.xy) * u_screen;\r\n float dist = length(d_px);\r\n float ring = exp(-pow((dist - s.z) / s.w, 2.0));\r\n // Signed: the leading edge pushes out, the trailing edge pulls in.\r\n float side = clamp((dist - s.z) / s.w, -1.0, 1.0);\r\n vec2 dir = dist > 0.5 ? d_px / dist : vec2(0.0);\r\n uv0 -= dir * ring * side * u_shock_strength[i] / u_screen;\r\n shock_light += ring * u_shock_strength[i] * 0.012;\r\n }\r\n\r\n // \u{2500}\u{2500} Heat haze \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // Two sine fields at different scales, rising, strongest at the bottom\r\n // of the frame where the hot air is. A fraction of a pixel is enough:\r\n // real shimmer is seen as things wavering, not as things moving.\r\n if (u_haze > 0.0) {\r\n float rise = u_time * 0.9;\r\n float band = 1.0 - smoothstep(0.0, 0.9, uv0.y);\r\n vec2 wobble = vec2(\r\n sin(uv0.y * 61.0 + rise * 7.0 + sin(uv0.x * 23.0 + rise * 3.0)),\r\n sin(uv0.x * 47.0 - rise * 5.0 + cos(uv0.y * 31.0 + rise * 2.0)) * 0.5\r\n );\r\n uv0 += wobble * (0.9 / u_screen) * u_haze * 2.2 * band;\r\n }\r\n\r\n // \u{2500}\u{2500} Lens shape \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n // A very slight barrel, applied before anything is sampled so\r\n // everything downstream inherits it.\r\n vec2 centred = uv0 - 0.5;\r\n if (u_barrel != 0.0) {\r\n centred *= 1.0 + u_barrel * dot(centred, centred);\r\n }\r\n vec2 uv = centred + 0.5;\r\n\r\n // Chromatic aberration, scaled by distance from the centre rather than\r\n // applied flat, because a real lens is sharp in the middle.\r\n float radial = dot(centred, centred);\r\n vec2 offset = centred * u_chromatic * (0.35 + radial * 2.4);\r\n float r = texture(u_scene, uv + offset).r;\r\n float g = texture(u_scene, uv).g;\r\n float b = texture(u_scene, uv - offset).b;\r\n vec3 color = vec3(r, g, b);\r\n\r\n // An unsharp mask on the scene before anything is added to it. Bloom and\r\n // a tonemap both soften an image, and this picture is built out of\r\n // overlapping particles whose detail is all in the edges.\r\n if (u_sharpen > 0.0) {\r\n vec2 t = 1.0 / vec2(textureSize(u_scene, 0));\r\n vec3 blur = texture(u_scene, uv + vec2( t.x, 0.0)).rgb\r\n + texture(u_scene, uv + vec2(-t.x, 0.0)).rgb\r\n + texture(u_scene, uv + vec2(0.0, t.y)).rgb\r\n + texture(u_scene, uv + vec2(0.0, -t.y)).rgb;\r\n color += (color - blur * 0.25) * u_sharpen;\r\n }\r\n\r\n // \u{2500}\u{2500} Floor reflection \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // Below the line, the picture above it, mirrored, blurred a little and\r\n // laid over the floor, fading out with distance from the line. What is\r\n // standing on the floor is what shows in it, because the whole scene is\r\n // in the buffer by now. Sampled after the lens so the mirror inherits\r\n // the same glass.\r\n if (u_reflection.y > 0.0 && uv.y < u_reflection.x) {\r\n float below = (u_reflection.x - uv.y) / max(u_reflection.z, 1e-4);\r\n float weight = u_reflection.y * (1.0 - smoothstep(0.0, 1.0, below));\r\n if (weight > 0.001) {\r\n vec2 t = 1.0 / vec2(textureSize(u_scene, 0));\r\n float ripple = sin(uv.y * 380.0 + u_time * 2.4) * u_reflection.w * t.x;\r\n vec2 m = vec2(uv.x + ripple, 2.0 * u_reflection.x - uv.y);\r\n float b = u_reflection_blur * (1.0 + below * 2.0);\r\n vec3 refl = texture(u_scene, m).rgb * 2.0\r\n + texture(u_scene, m + vec2( t.x * b, 0.0)).rgb\r\n + texture(u_scene, m + vec2(-t.x * b, 0.0)).rgb\r\n + texture(u_scene, m + vec2(0.0, t.y * b)).rgb\r\n + texture(u_scene, m + vec2(0.0, -t.y * b)).rgb;\r\n refl /= 6.0;\r\n // Screen rather than add: a mirror never makes a floor darker,\r\n // and adding on top of a lit floor blows it out.\r\n color = color + refl * weight * (1.0 - clamp(color, 0.0, 1.0) * 0.5);\r\n }\r\n }\r\n\r\n // \u{2500}\u{2500} The light map \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // Ambient plus every light, shadowed. Emissive matter lights itself:\r\n // a brazier is not dimmed by the dark it is standing in.\r\n if (u_lighting) {\r\n vec3 light = texture(u_light, uv).rgb;\r\n vec3 em = texture(u_emission, uv).rgb;\r\n float self_lit = clamp(max(em.r, max(em.g, em.b)) * 1.6, 0.0, 1.0);\r\n color *= max(light, vec3(self_lit));\r\n }\r\n\r\n // \u{2500}\u{2500} Exposure, then light \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n color *= u_exposure;\r\n vec3 bloom = texture(u_bloom, uv).rgb;\r\n\r\n // Indirect light. The bloom pyramid is, among other things, a blurred\r\n // map of where the light in the frame is. Multiplying the scene by it\r\n // lights the matter standing near a lit thing in that thing\'s colour,\r\n // by that matter\'s own albedo, which is what bounce light does and what\r\n // adding bloom on top never does: bloom brightens the air, this\r\n // brightens the surfaces.\r\n color += color * bloom * u_indirect;\r\n\r\n color += bloom * u_bloom_intensity;\r\n\r\n // Halation: the warm bleed real film gets around a bright edge.\r\n color += vec3(bloom.r, bloom.r * 0.42, bloom.r * 0.22) * u_halation;\r\n\r\n // \u{2500}\u{2500} Light shafts \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // A radial blur of the light toward its source. Twenty-four samples\r\n // stepping from this pixel toward the origin, each weighted a little\r\n // less than the last, so the light streams out from the source and\r\n // fades along its length. Sampled from the bloom, which is half the\r\n // resolution and already thresholded, so only the lit things cast.\r\n if (u_light_shafts > 0.0 && u_shaft.z > 0.0) {\r\n const int STEPS = 24;\r\n vec2 to_src = (u_shaft.xy - uv) / float(STEPS) * 0.85;\r\n float decay = 0.94;\r\n float weight = 1.0;\r\n vec3 shaft = vec3(0.0);\r\n vec2 p = uv;\r\n for (int i = 0; i < STEPS; ++i) {\r\n p += to_src;\r\n shaft += texture(u_bloom, p).rgb * weight;\r\n weight *= decay;\r\n }\r\n shaft /= float(STEPS) * 0.55;\r\n // Fade with distance from the source, so the far side of the frame\r\n // is not lit by a brazier on the near side.\r\n float reach = 1.0 - smoothstep(0.0, 1.1, length((uv - u_shaft.xy) * vec2(1.0, u_screen.y / u_screen.x)));\r\n color += shaft * u_shaft_tint * u_light_shafts * u_shaft.z * reach;\r\n }\r\n\r\n // \u{2500}\u{2500} Lens flare \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n //\r\n // Ghosts are the bloom mirrored through the centre of the lens, at a few\r\n // spacings, each with a slight spectral tint; the halo is a ring at a\r\n // fixed radius from the centre. Both are weighted toward the centre so\r\n // a light at the edge of frame puts its ghosts across the middle, the\r\n // way real glass does, and nothing flares out of a dark frame because\r\n // the bloom is already thresholded.\r\n if (u_lens_flare > 0.0) {\r\n vec2 ghost_vec = (0.5 - uv) * 0.44;\r\n vec3 flare = vec3(0.0);\r\n for (int i = 1; i <= 3; ++i) {\r\n vec2 g = uv + ghost_vec * float(i);\r\n float w = 1.0 - smoothstep(0.0, 0.75, length(g - 0.5));\r\n w = w * w;\r\n vec3 tint = (i == 1) ? vec3(1.0, 0.85, 0.7)\r\n : (i == 2) ? vec3(0.7, 0.9, 1.0)\r\n : vec3(0.9, 0.7, 1.0);\r\n flare += texture(u_bloom, g).rgb * w * tint * (0.5 / float(i));\r\n }\r\n vec2 halo_dir = normalize(ghost_vec + vec2(1e-5));\r\n vec2 halo_uv = uv + halo_dir * 0.38;\r\n float halo_w = 1.0 - smoothstep(0.0, 0.6, length(halo_uv - 0.5));\r\n flare += texture(u_bloom, halo_uv).rgb * halo_w * halo_w * vec3(0.8, 0.9, 1.0) * 0.25;\r\n color += flare * u_lens_flare;\r\n }\r\n\r\n // Shockwave brightening and any flash, before the roll-off so a strong\r\n // one goes to white through the highlights rather than by clipping.\r\n color += vec3(shock_light);\r\n color += u_flash;\r\n\r\n // Tonemap, blended rather than switched so a caller can dial it back\r\n // without a hard change of look at some threshold.\r\n color = mix(clamp(color, 0.0, 1.0), aces(color), u_tonemap);\r\n\r\n // Grade. Lift and gain move shadows and highlights independently, which is\r\n // what a colour grade actually is.\r\n color = color * u_gain + u_lift * (1.0 - color);\r\n color *= u_tint;\r\n color += u_brightness;\r\n color = (color - 0.5) * u_contrast + 0.5;\r\n\r\n float lum = dot(color, vec3(0.2126, 0.7152, 0.0722));\r\n color = mix(vec3(lum), color, u_saturation);\r\n\r\n // Vignette, smoothstepped from an adjustable edge rather than a raw\r\n // quadratic, so it frames the picture rather than dimming the middle.\r\n float d = length(centred) * 1.41421356;\r\n color *= 1.0 - u_vignette * smoothstep(u_vignette_softness, 1.0, d);\r\n\r\n // Grain, weighted toward the shadows where film grain actually lives.\r\n float grain = rand(uv + vec2(u_grain_seed)) * 2.0 - 1.0;\r\n float shadow_weight = 1.0 - smoothstep(0.0, 0.7, lum);\r\n color += grain * u_grain_intensity * (0.35 + shadow_weight);\r\n\r\n // A 4x4 ordered dither, under one quantisation step, against banding in\r\n // the dark gradients this game is made of.\r\n if (u_dither > 0.0) {\r\n const float bayer[16] = float[](\r\n 0.0, 8.0, 2.0, 10.0,\r\n 12.0, 4.0, 14.0, 6.0,\r\n 3.0, 11.0, 1.0, 9.0,\r\n 15.0, 7.0, 13.0, 5.0\r\n );\r\n ivec2 px = ivec2(gl_FragCoord.xy) % 4;\r\n color += ((bayer[px.y * 4 + px.x] + 0.5) / 16.0 - 0.5) * u_dither / 255.0;\r\n }\r\n\r\n if (u_scanlines_enabled) {\r\n float scanline = sin(uv.y * u_screen.y * 3.14159) * 0.5 + 0.5;\r\n color *= 1.0 - u_scanline_intensity * (1.0 - scanline);\r\n }\r\n\r\n o_color = vec4(clamp(color, 0.0, 1.0), 1.0);\r\n}\r\n";