/* * MIT License * Author: Mark Allyn * * sweep.frag — phosphor accumulation update shader. * * The FBO is GL_RG32F (two independent energy channels): * R — signal energy: target echoes + sweep-background glow. * Multiplied by u_gain in the display pass so operators can * adjust received-signal brightness without touching rings. * G — range-ring energy: written at u_ringBrightness; NOT scaled * by gain. Rings are a precision timing reference, not a * received echo. Both channels decay at the same P7 rate. * * The sweep background (u_sweepBg) goes into the G channel so the * rotating beam is always visible regardless of the gain setting. * * PPI convention: north = +y, east = +x; bearing = atan2(x, y) * in degrees, clockwise from north. */ #version 330 core in vec2 vTexCoord; layout(location = 0) out vec4 fragOut; // .r = signal; .g = ring+sweep; .ba unused uniform sampler2D u_prevPhosphor; // previous frame's energy texture (GL_RG32F) uniform float u_decayFactor; // exp(-decay_rate * dt) uniform float u_beamAngle; // current beam angle, degrees CW from north uniform float u_beamAnglePrev; // beam angle at previous frame uniform float u_sweepBg; // ambient sweep-line energy (gain-independent) uniform float u_halfBeamDeg; // half-beamwidth for target blobs (display widening) // Targets: .x = range_norm (0-1), .y = bearing_deg, .z = brightness, .w = size_norm uniform vec4 u_targets[32]; uniform int u_targetCount; // Range rings: up to 4 normalised radii uniform float u_ringRadii[4]; uniform int u_ringCount; uniform float u_ringWidth; // half-width in normalised range units uniform float u_ringBrightness; // ---------------------------------------------------------------- // Smallest unsigned angular distance between two bearings [0,360) float angleDiff(float a, float b) { float d = mod(abs(a - b), 360.0); return (d > 180.0) ? (360.0 - d) : d; } // True if bearing b is inside the arc [prev, curr] swept this frame. // Handles the 0/360 wraparound when the sweep crosses north. bool inSweep(float b, float prev, float curr) { if (curr >= prev) { return (b >= prev && b <= curr); } // Wraparound: arc crosses 360→0 return (b >= prev || b <= curr); } // ---------------------------------------------------------------- void main() { vec2 pos = vTexCoord * 2.0 - 1.0; // PPI coords: (-1,-1) SW … (+1,+1) NE float rng = length(pos); if (rng > 1.0) { fragOut = vec4(0.0); return; } // Bearing: clockwise from north — atan2(east, north) = atan2(x, y) float brg = degrees(atan(pos.x, pos.y)); if (brg < 0.0) brg += 360.0; vec2 prev = texture(u_prevPhosphor, vTexCoord).rg; float signal = prev.r * u_decayFactor; float ring = prev.g * u_decayFactor; if (inSweep(brg, u_beamAnglePrev, u_beamAngle)) { // ---- Range rings → G channel (gain-independent) ---- float ringContrib = u_sweepBg; // sweep-background glow also in G channel for (int i = 0; i < u_ringCount; i++) { float d = abs(rng - u_ringRadii[i]); if (d < u_ringWidth) { float w = 1.0 - d / u_ringWidth; ringContrib = max(ringContrib, u_ringBrightness * w * w); } } ring = max(ring, ringContrib); // ---- Target echoes → R channel (gain-scaled in display pass) ---- float sigContrib = 0.0; for (int i = 0; i < u_targetCount; i++) { float tRng = u_targets[i].x; float tBrg = u_targets[i].y; float tBrt = u_targets[i].z; float tSize = u_targets[i].w; if (tRng <= 0.0 || tBrt <= 0.0) continue; float dBrg = angleDiff(brg, tBrg); if (dBrg >= u_halfBeamDeg) continue; float dRng = abs(rng - tRng); if (dRng >= tSize) continue; float bw = 1.0 - dBrg / u_halfBeamDeg; float rw = 1.0 - dRng / tSize; sigContrib = max(sigContrib, tBrt * bw * rw); } signal = max(signal, sigContrib); } fragOut = vec4(clamp(signal, 0.0, 1.0), clamp(ring, 0.0, 1.0), 0.0, 1.0); }