/* * MIT License * Author: Mark Allyn * * phosphor.frag — maps the two-channel phosphor energy texture to the * P7 colour sequence (blue → green → yellow-green → dark) and applies * a simple inline bloom (box-filter glow) to bright pixels. * * The phosphor FBO is GL_RG32F: * R channel — signal energy (target echoes, sweep background) * multiplied by u_gain before display * G channel — range ring energy, gain-independent; mixed with signal * after gain is applied so rings never dim with gain * * Coordinate system: gl_FragCoord.xy in GL viewport pixels (origin * bottom-left). Scope centre is passed as u_scopeCenter in the same * coordinate system. */ #version 330 core out vec4 fragColor; uniform sampler2D u_phosphor; // GL_RG32F phosphor energy FBO uniform vec2 u_scopeCenter; // scope centre in GL viewport pixels (bottom-left origin) uniform float u_scopeRadius; // scope radius in pixels uniform float u_gain; // receiver gain [0,1] — scales signal (R) channel only uniform float u_bloomStep; // UV step for bloom sample (≈ 2.5 / FBO_SIZE) uniform float u_bloomStrength; // additive blend weight for bloom // P7 energy thresholds — MUST match settings.h P7_THRESH_* constants. // T_YGREE is intentionally low (0.05) to keep most of the decay in the // GREEN zone; see the comment in settings.h for the full rationale. const float T_BLUE = 0.82; const float T_GREEN = 0.55; const float T_YGREE = 0.05; const float T_DARK = 0.03; // P7 colour anchors const vec3 C_BLUE = vec3(0.30, 0.70, 1.00); const vec3 C_GREEN = vec3(0.05, 1.00, 0.30); const vec3 C_YGREE = vec3(0.50, 1.00, 0.05); const vec3 C_YELLW = vec3(0.70, 0.70, 0.00); const vec3 C_BLACK = vec3(0.00, 0.00, 0.00); // P7 colour ramp: hue selected by energy level, then scaled by energy so // brightness decreases monotonically from fresh strike (peak) to dark. // This prevents intermediate decay colours (yellow-green) from appearing // brighter than the initial blue flash. vec3 p7Color(float e) { if (e < T_DARK) return C_BLACK; vec3 hue; if (e >= T_BLUE) hue = C_BLUE; else if (e >= T_GREEN) hue = mix(C_GREEN, C_BLUE, (e - T_GREEN) / (T_BLUE - T_GREEN)); else if (e >= T_YGREE) hue = mix(C_YGREE, C_GREEN, (e - T_YGREE) / (T_GREEN - T_YGREE)); else hue = mix(C_YELLW, C_YGREE, (e - T_DARK) / (T_YGREE - T_DARK)); return hue * e; } void main() { // Fragment position relative to scope centre vec2 delta = (gl_FragCoord.xy - u_scopeCenter) / u_scopeRadius; float dist = length(delta); if (dist > 1.0) { fragColor = vec4(0.0); // outside scope circle — transparent black return; } // Map from PPI delta [-1,+1] to phosphor texture UV [0,1] vec2 uv = delta * 0.5 + 0.5; vec2 rg = texture(u_phosphor, uv).rg; // Signal (R): gain-scaled received echoes. // Ring (G): gain-independent timing reference; always at full brightness. float energy = max(rg.r * u_gain, rg.g); // Inline bloom: weighted box-filter over a 5×5 neighbourhood float bloom = 0.0; float wsum = 0.0; for (int dx = -2; dx <= 2; dx++) { for (int dy = -2; dy <= 2; dy++) { float w = exp(-float(dx*dx + dy*dy) * 0.45); vec2 srg = texture(u_phosphor, uv + vec2(dx, dy) * u_bloomStep).rg; float e = max(srg.r * u_gain, srg.g); bloom += e * w; wsum += w; } } bloom = (bloom / wsum) * u_bloomStrength; float finalE = clamp(energy + bloom, 0.0, 1.0); vec3 col = p7Color(finalE); // Soft-edge vignette at the scope boundary float edge = smoothstep(1.0, 0.97, dist); fragColor = vec4(col * edge, 1.0); }