6eb28ffcac
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
173 lines
7.7 KiB
GLSL
173 lines
7.7 KiB
GLSL
#include <flutter/runtime_effect.glsl>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Base Variant (Branchless, No Bloom)
|
|
// ----------------------------------------------------------------------------
|
|
// This shader is the bloom-disabled base variant of the Wolf CRT post-process
|
|
// path.
|
|
//
|
|
// Why keep a separate base file?
|
|
// - Dart/CPU side can select this program when bloom is off.
|
|
// - That avoids bloom texture taps entirely in the hot path.
|
|
// - This is a direct performance optimization, not just stylistic separation.
|
|
//
|
|
// Why branchless math throughout?
|
|
// - Fragment programs run many pixels in lockstep.
|
|
// - Divergent control flow can reduce throughput.
|
|
// - Mask-based selection using mix/step/smoothstep keeps execution uniform.
|
|
//
|
|
// For Dart developers:
|
|
// - Think dataflow over fields of values, not object-level control logic.
|
|
// - Most operations transform scalar/vector fields per pixel.
|
|
// - Alternative outcomes are blended by masks instead of branching.
|
|
|
|
// Output surface size in pixels for the current draw call.
|
|
uniform vec2 uResolution;
|
|
// One source texel step in UV space: (1/width, 1/height).
|
|
// This keeps neighborhood kernels stable across resolutions.
|
|
uniform vec2 uTexel;
|
|
// 1.0 enables CRT warp/scanline stack, 0.0 keeps only base AA stack.
|
|
// Even though this is conceptually boolean, it is expressed as float so the
|
|
// shader can blend outcomes natively with mask math.
|
|
uniform float uEffectsEnabled;
|
|
// Engine time in seconds used to animate scanline travel.
|
|
uniform float uTime;
|
|
// Source frame produced by the software renderer.
|
|
uniform sampler2D uTexture;
|
|
|
|
out vec4 fragColor;
|
|
|
|
// Perceptual brightness approximation for edge detection.
|
|
// This uses Rec.601-style luma weighting.
|
|
float luma(vec3 color) {
|
|
return dot(color, vec3(0.299, 0.587, 0.114));
|
|
}
|
|
|
|
// Returns 1.0 when uv is inside [0,1] on both axes, else 0.0.
|
|
// Implemented branchlessly as a product of step() tests.
|
|
float uvInsideMask(vec2 uv) {
|
|
vec2 lower = step(vec2(0.0), uv);
|
|
vec2 upper = step(uv, vec2(1.0));
|
|
return lower.x * lower.y * upper.x * upper.y;
|
|
}
|
|
|
|
void main() {
|
|
// Normalize destination pixel coordinate into UV space.
|
|
vec2 uv = FlutterFragCoord().xy / uResolution;
|
|
|
|
// --------------------------------------------------------------------------
|
|
// 1) CRT warp selection (branchless)
|
|
// --------------------------------------------------------------------------
|
|
// The centered radius term drives a mild barrel distortion.
|
|
vec2 centered = uv * 2.0 - 1.0;
|
|
float radius2 = dot(centered, centered);
|
|
vec2 warpedUv = centered * (1.0 + radius2 * 0.045) * 0.5 + 0.5;
|
|
|
|
// Mix between linear and warped UV with a float mask.
|
|
// In Dart, this is conceptually: useWarped ? warpedUv : uv.
|
|
vec2 effectiveUv = mix(uv, warpedUv, uEffectsEnabled);
|
|
|
|
// Clamp once to keep all downstream texture fetches in bounds.
|
|
vec2 sampleUv = clamp(effectiveUv, 0.0, 1.0);
|
|
|
|
// 1.0 for screen interior, 0.0 outside.
|
|
float insideMask = uvInsideMask(effectiveUv);
|
|
// Bezel contributes only where warped coordinates leave the source screen.
|
|
float bezelMask = (1.0 - insideMask) * uEffectsEnabled;
|
|
|
|
// --------------------------------------------------------------------------
|
|
// 2) Lightweight edge-aware AA
|
|
// --------------------------------------------------------------------------
|
|
// Sample center + cardinal neighbors.
|
|
vec4 centerSample = texture(uTexture, sampleUv);
|
|
vec3 sampleN = texture(uTexture, clamp(sampleUv + vec2(0.0, -uTexel.y), 0.0, 1.0)).rgb;
|
|
vec3 sampleS = texture(uTexture, clamp(sampleUv + vec2(0.0, uTexel.y), 0.0, 1.0)).rgb;
|
|
vec3 sampleE = texture(uTexture, clamp(sampleUv + vec2(uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
|
vec3 sampleW = texture(uTexture, clamp(sampleUv + vec2(-uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
|
|
|
// Luma span estimates local contrast; high span implies stronger edge.
|
|
float lumaCenter = luma(centerSample.rgb);
|
|
float lumaMin = min(
|
|
lumaCenter,
|
|
min(min(luma(sampleN), luma(sampleS)), min(luma(sampleE), luma(sampleW)))
|
|
);
|
|
float lumaMax = max(
|
|
lumaCenter,
|
|
max(max(luma(sampleN), luma(sampleS)), max(luma(sampleE), luma(sampleW)))
|
|
);
|
|
float edgeAmount = smoothstep(0.03, 0.18, max(lumaMax - lumaMin, 0.0001));
|
|
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
|
|
|
// Blend toward neighborhood average near likely edges.
|
|
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
|
|
|
// --------------------------------------------------------------------------
|
|
// 3) CRT scanline/sweep/vignette stack
|
|
// --------------------------------------------------------------------------
|
|
float scanlineBand = 0.88 + 0.12 * sin(sampleUv.y * uResolution.y * 3.14159265);
|
|
float sweepPos = fract(uTime * 0.08);
|
|
float sweepBand = 1.0 + 0.16 * exp(-pow((sampleUv.y - sweepPos) * 120.0, 2.0));
|
|
vec2 centeredUv = sampleUv * 2.0 - 1.0;
|
|
float vignette = smoothstep(1.15, 0.25, length(centeredUv));
|
|
float centerLift = 1.0 + 0.08 * (1.0 - length(centeredUv));
|
|
|
|
vec3 crtColor = aaColor;
|
|
crtColor *= scanlineBand * sweepBand * centerLift;
|
|
crtColor *= mix(0.62, 1.0, vignette);
|
|
|
|
// Effects mask decides whether CRT modulation is applied.
|
|
vec3 screenColor = mix(aaColor, crtColor, uEffectsEnabled);
|
|
|
|
// --------------------------------------------------------------------------
|
|
// 4) Bezel shading path (branchless selection)
|
|
// --------------------------------------------------------------------------
|
|
// This base variant intentionally does not include bloom calculations.
|
|
|
|
// edgeDelta is non-zero when warp pushes UV outside source bounds.
|
|
vec2 edgeDelta = effectiveUv - sampleUv;
|
|
float overflow = max(abs(edgeDelta.x), abs(edgeDelta.y));
|
|
|
|
// Sample inward from clamped border to pull scene tint into bezel.
|
|
vec2 inwardDir = normalize(-edgeDelta + vec2(1e-6));
|
|
vec2 bleedStep = vec2(uTexel.x * 1.6, uTexel.y * 1.6);
|
|
vec2 bleedUv1 = clamp(sampleUv + inwardDir * bleedStep, 0.0, 1.0);
|
|
vec2 bleedUv2 = clamp(sampleUv + inwardDir * bleedStep * 2.6, 0.0, 1.0);
|
|
vec2 bleedUv3 = clamp(sampleUv + inwardDir * bleedStep * 4.2, 0.0, 1.0);
|
|
vec3 edgeBleedColor =
|
|
texture(uTexture, sampleUv).rgb * 0.52 +
|
|
texture(uTexture, bleedUv1).rgb * 0.28 +
|
|
texture(uTexture, bleedUv2).rgb * 0.14 +
|
|
texture(uTexture, bleedUv3).rgb * 0.06;
|
|
|
|
// Aspect-corrected radial metrics for bezel falloff shaping.
|
|
vec2 aspectScale = vec2(uResolution.x / max(uResolution.y, 1.0), 1.0);
|
|
float bezelDistance = length(edgeDelta * aspectScale);
|
|
vec2 clampedCentered = sampleUv * 2.0 - 1.0;
|
|
float cornerFactor = smoothstep(0.60, 1.15, length(clampedCentered));
|
|
|
|
// Shading layers:
|
|
// - verticalShade: slight top/bottom tonal variance
|
|
// - depthShade: darkens with overflow depth
|
|
// - grain: subtle analog texture
|
|
// - moldedHighlight: narrow inner edge highlight
|
|
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
|
|
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
|
|
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
|
|
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
|
|
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
|
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
|
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
|
|
|
|
vec3 bezelColor =
|
|
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
|
edgeBleedColor * bezelGlow * bleedStrength * 1.12 +
|
|
edgeBleedColor * innerLip * 0.36 +
|
|
vec3(moldedHighlight) +
|
|
vec3(grain);
|
|
|
|
// Final branchless selection between emissive screen and bezel.
|
|
vec3 outColor = mix(screenColor, bezelColor, bezelMask);
|
|
float outAlpha = mix(centerSample.a, 1.0, bezelMask);
|
|
fragColor = vec4(outColor, outAlpha);
|
|
}
|