// Radar Simulation — Feature Test: 1, 2 & 3 // Feature 1: Initialize display, draw scope boundaries (PPI circle, A scope box) // Feature 2: PPI bearing ring with tick marks and degree labels // Feature 3: Replaceable A scope graticule — cycles through 2/5/10/15 mi ranges, // 5 s hold per range, 0.5 s slide animation between ranges. // Press ESC to exit. #include #include #include #include FT_FREETYPE_H #include #include #include #include #include #include #include #include // ─── Constants ─────────────────────────────────────────────────────────────── static constexpr float PI = 3.14159265358979323846f; static constexpr int CIRCLE_SEGS = 360; // Incandescent (warm lamp) — bearing graticule and A scope graticule static constexpr float INCAN_R = 1.00f; static constexpr float INCAN_G = 0.78f; static constexpr float INCAN_B = 0.35f; // P1 phosphor (green) — A scope boundary static constexpr float P1_R = 0.00f; static constexpr float P1_G = 0.90f; static constexpr float P1_B = 0.20f; // Digits rendered into the font atlas: '0'–'9' static constexpr int GLYPH_FIRST = '0'; static constexpr int GLYPH_COUNT = 10; // Feature 3 timing static constexpr float HOLD_SEC = 5.0f; static constexpr float SLIDE_SEC = 0.5f; // P7 phosphor — active (blueish white) and persistence (greenish yellow) static constexpr float P7A_R = 0.85f, P7A_G = 0.92f, P7A_B = 1.00f; static constexpr float P7P_R = 0.35f, P7P_G = 0.88f, P7P_B = 0.18f; // Sweep / persistence static constexpr float SWEEP_DEG_PS = 20.0f * 6.0f; // 20 RPM → 120 °/s static constexpr float TRAIL_DEG = 50.0f; // lit arc behind sweep (°) static constexpr float TARG_PERSIST = 5.0f; // target glow lifetime (s) static constexpr int RING_SEGS = 300; static constexpr int TRAIL_SEGS = 50; // ─── NDC helpers ───────────────────────────────────────────────────────────── static inline float ndcX(float px, float W) { return px / W * 2.0f - 1.0f; } static inline float ndcY(float py, float H) { return 1.0f - py / H * 2.0f; } // ─── Shader utilities ──────────────────────────────────────────────────────── static std::string readFile(const std::string& path) { std::ifstream f(path); if (!f) { std::cerr << "Cannot read shader: " << path << "\n"; return ""; } return { std::istreambuf_iterator(f), std::istreambuf_iterator() }; } static GLuint compileShader(GLenum type, const std::string& src) { GLuint sh = glCreateShader(type); const char* s = src.c_str(); glShaderSource(sh, 1, &s, nullptr); glCompileShader(sh); GLint ok; glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(sh, 512, nullptr, log); std::cerr << "Shader compile: " << log << "\n"; } return sh; } static GLuint makeProgram(const std::string& vp, const std::string& fp) { GLuint vs = compileShader(GL_VERTEX_SHADER, readFile(vp)); GLuint fs = compileShader(GL_FRAGMENT_SHADER, readFile(fp)); GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog); GLint ok; glGetProgramiv(prog, GL_LINK_STATUS, &ok); if (!ok) { char log[512]; glGetProgramInfoLog(prog, 512, nullptr, log); std::cerr << "Program link: " << log << "\n"; } glDeleteShader(vs); glDeleteShader(fs); return prog; } // ─── VAO / VBO helpers ─────────────────────────────────────────────────────── // 2-float-per-vertex (NDC positions only) static void makeLineVAO(GLuint& vao, GLuint& vbo, const std::vector& v) { glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(float), v.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); glEnableVertexAttribArray(0); glBindVertexArray(0); } // 4-float-per-vertex (NDC x,y + UV u,v) static void makeTextVAO(GLuint& vao, GLuint& vbo, const std::vector& v) { glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(float), v.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); } // ─── FreeType font atlas ───────────────────────────────────────────────────── struct GlyphInfo { int atlasX; int bitmapW; int bitmapH; int bearingX; int bearingY; int advance; }; struct FontAtlas { GLuint texture = 0; int atlasW = 0; int atlasH = 0; GlyphInfo glyphs[GLYPH_COUNT]{}; }; static bool buildFontAtlas(FontAtlas& fa, const std::string& fontPath, int sizePx) { FT_Library ft; if (FT_Init_FreeType(&ft)) { std::cerr << "FreeType init failed\n"; return false; } FT_Face face; if (FT_New_Face(ft, fontPath.c_str(), 0, &face)) { std::cerr << "FT_New_Face failed: " << fontPath << "\n"; FT_Done_FreeType(ft); return false; } FT_Set_Pixel_Sizes(face, 0, sizePx); int totalW = 0, maxH = 0; for (int i = 0; i < GLYPH_COUNT; ++i) { if (FT_Load_Char(face, GLYPH_FIRST + i, FT_LOAD_RENDER)) continue; FT_GlyphSlot g = face->glyph; totalW += (int)g->bitmap.width + 2; maxH = std::max(maxH, (int)g->bitmap.rows); } fa.atlasW = totalW; fa.atlasH = maxH; std::vector atlas(fa.atlasW * fa.atlasH, 0); int xOff = 0; for (int i = 0; i < GLYPH_COUNT; ++i) { if (FT_Load_Char(face, GLYPH_FIRST + i, FT_LOAD_RENDER)) continue; FT_GlyphSlot g = face->glyph; GlyphInfo& gi = fa.glyphs[i]; gi.atlasX = xOff; gi.bitmapW = (int)g->bitmap.width; gi.bitmapH = (int)g->bitmap.rows; gi.bearingX = g->bitmap_left; gi.bearingY = g->bitmap_top; gi.advance = (int)(g->advance.x >> 6); for (int row = 0; row < gi.bitmapH; ++row) { const uint8_t* src = g->bitmap.buffer + row * std::abs(g->bitmap.pitch); uint8_t* dst = atlas.data() + row * fa.atlasW + xOff; std::memcpy(dst, src, gi.bitmapW); } xOff += gi.bitmapW + 2; } glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glGenTextures(1, &fa.texture); glBindTexture(GL_TEXTURE_2D, fa.texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, fa.atlasW, fa.atlasH, 0, GL_RED, GL_UNSIGNED_BYTE, atlas.data()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); FT_Done_Face(face); FT_Done_FreeType(ft); return true; } // Append NDC quad vertices for string s, visually centered at screen pixel (cx, cy). // Only digits '0'–'9' are supported. static void appendTextQuads(std::vector& verts, const FontAtlas& fa, const std::string& s, float cx, float cy, float W, float H) { if (s.empty()) return; float totalAdv = 0.0f; int maxBY = 0; int minBY = 0; for (char c : s) { int i = (int)c - GLYPH_FIRST; if (i < 0 || i >= GLYPH_COUNT) continue; const GlyphInfo& g = fa.glyphs[i]; totalAdv += g.advance; maxBY = std::max(maxBY, g.bearingY); minBY = std::min(minBY, g.bearingY - g.bitmapH); } float visualCenterAboveBaseline = (maxBY + minBY) * 0.5f; float baselineY = cy + visualCenterAboveBaseline; float cursorX = cx - totalAdv * 0.5f; for (char c : s) { int i = (int)c - GLYPH_FIRST; if (i < 0 || i >= GLYPH_COUNT) continue; const GlyphInfo& g = fa.glyphs[i]; float gx0 = cursorX + g.bearingX; float gy0 = baselineY - g.bearingY; float gx1 = gx0 + g.bitmapW; float gy1 = gy0 + g.bitmapH; float u0 = (float) g.atlasX / fa.atlasW; float u1 = (float)(g.atlasX + g.bitmapW)/ fa.atlasW; float v0 = 0.0f; float v1 = (float) g.bitmapH / fa.atlasH; verts.push_back(ndcX(gx0,W)); verts.push_back(ndcY(gy0,H)); verts.push_back(u0); verts.push_back(v0); verts.push_back(ndcX(gx1,W)); verts.push_back(ndcY(gy0,H)); verts.push_back(u1); verts.push_back(v0); verts.push_back(ndcX(gx1,W)); verts.push_back(ndcY(gy1,H)); verts.push_back(u1); verts.push_back(v1); verts.push_back(ndcX(gx0,W)); verts.push_back(ndcY(gy0,H)); verts.push_back(u0); verts.push_back(v0); verts.push_back(ndcX(gx1,W)); verts.push_back(ndcY(gy1,H)); verts.push_back(u1); verts.push_back(v1); verts.push_back(ndcX(gx0,W)); verts.push_back(ndcY(gy1,H)); verts.push_back(u0); verts.push_back(v1); cursorX += g.advance; } } // ─── Layout ────────────────────────────────────────────────────────────────── struct Layout { float ppiCX, ppiCY, ppiR; float asLeft, asTop, asRight, asBot; }; static Layout computeLayout(float W, float H, float marginPx) { Layout L{}; float availW = (W - marginPx) - W * 0.5f; float availH = (H - marginPx) - marginPx; L.ppiR = std::min(availW, availH) * 0.5f; L.ppiCX = W * 0.5f + availW * 0.5f; L.ppiCY = marginPx + availH * 0.5f; float asW = (W * 0.5f - 2.0f * marginPx) * 0.65f; float asH = H * 0.22f; L.asLeft = marginPx; L.asRight = marginPx + asW; L.asTop = (H - asH) * 0.5f; L.asBot = L.asTop + asH; return L; } // ─── Feature 1: Scope boundaries ───────────────────────────────────────────── struct ScopeBounds { GLuint prog = 0, vao = 0, vbo = 0; int ppiStart = 0, ppiCount = 0; int asStart = 0, asCount = 0; }; static ScopeBounds buildScopeBounds(const Layout& L, float W, float H) { ScopeBounds sb{}; std::vector v; sb.ppiStart = 0; sb.ppiCount = CIRCLE_SEGS + 1; for (int i = 0; i <= CIRCLE_SEGS; ++i) { float a = 2.0f * PI * i / CIRCLE_SEGS; v.push_back(ndcX(L.ppiCX + L.ppiR * std::cos(a), W)); v.push_back(ndcY(L.ppiCY + L.ppiR * std::sin(a), H)); } sb.asStart = sb.ppiStart + sb.ppiCount; sb.asCount = 8; auto ln = [&](float x1, float y1, float x2, float y2) { v.push_back(ndcX(x1,W)); v.push_back(ndcY(y1,H)); v.push_back(ndcX(x2,W)); v.push_back(ndcY(y2,H)); }; ln(L.asLeft, L.asTop, L.asRight, L.asTop); ln(L.asRight, L.asTop, L.asRight, L.asBot); ln(L.asRight, L.asBot, L.asLeft, L.asBot); ln(L.asLeft, L.asBot, L.asLeft, L.asTop); makeLineVAO(sb.vao, sb.vbo, v); sb.prog = makeProgram("shaders/scope_bounds.vert", "shaders/scope_bounds.frag"); return sb; } // ─── Feature 2: PPI bearing graticule ──────────────────────────────────────── struct BearingGraticule { GLuint lineProg = 0, textProg = 0; GLuint lineVAO = 0, lineVBO = 0; GLuint textVAO = 0, textVBO = 0; int ringStart = 0, ringCount = 0; int tickStart = 0, tickCount = 0; int textVerts = 0; GLuint fontTex = 0; }; static BearingGraticule buildBearingGraticule(const Layout& L, const FontAtlas& fa, float W, float H) { BearingGraticule bg{}; const float cx = L.ppiCX, cy = L.ppiCY, R = L.ppiR; std::vector lineV; bg.ringStart = 0; bg.ringCount = CIRCLE_SEGS + 1; for (int i = 0; i <= CIRCLE_SEGS; ++i) { float a = 2.0f * PI * i / CIRCLE_SEGS; lineV.push_back(ndcX(cx + R * std::cos(a), W)); lineV.push_back(ndcY(cy + R * std::sin(a), H)); } const float majorLen = 0.055f * R; const float minorLen = 0.025f * R; bg.tickStart = bg.ringStart + bg.ringCount; bg.tickCount = 360 * 2; for (int b = 0; b < 360; ++b) { float brad = b * PI / 180.0f; float sb = std::sin(brad); float cb = std::cos(brad); float len = (b % 10 == 0) ? majorLen : minorLen; lineV.push_back(ndcX(cx + R * sb, W)); lineV.push_back(ndcY(cy - R * cb, H)); lineV.push_back(ndcX(cx + (R-len) * sb, W)); lineV.push_back(ndcY(cy - (R-len) * cb, H)); } makeLineVAO(bg.lineVAO, bg.lineVBO, lineV); std::vector textV; const float textR = R * 1.07f; for (int b = 0; b < 360; b += 10) { float brad = b * PI / 180.0f; float tx = cx + textR * std::sin(brad); float ty = cy - textR * std::cos(brad); appendTextQuads(textV, fa, std::to_string(b), tx, ty, W, H); } bg.textVerts = (int)textV.size() / 4; bg.fontTex = fa.texture; makeTextVAO(bg.textVAO, bg.textVBO, textV); bg.lineProg = makeProgram("shaders/ppi_bearing.vert", "shaders/ppi_bearing.frag"); bg.textProg = makeProgram("shaders/text.vert", "shaders/text.frag"); return bg; } // ─── Feature 3: A scope replaceable graticule ──────────────────────────────── // One entry per selectable range. The graticule has numMajor labeled ticks // (full-height vertical lines) and numMinorPerMajor minor ticks between each // pair of major ticks. Label values are computed as (m * maxMiles / numMajor). struct RangeConfig { float maxMiles; int numMajor; int numMinorPerMajor; }; static const RangeConfig RANGE_CONFIGS[4] = { { 2.0f, 2, 4 }, // labels: 1, 2 mi { 5.0f, 5, 1 }, // labels: 1,2,3,4,5 mi { 10.0f, 5, 1 }, // labels: 2,4,6,8,10 mi { 15.0f, 5, 2 }, // labels: 3,6,9,12,15 mi }; static constexpr int RANGE_COUNT = 4; struct AScopeGraticule { GLuint lineVAO = 0, lineVBO = 0; GLuint textVAO = 0, textVBO = 0; int lineCount = 0; int textVerts = 0; }; // Shared shader programs for all A scope graticules (uYOffset drives animation) struct AScopeGratProg { GLuint line = 0; GLuint text = 0; }; static AScopeGratProg buildAScopeGratPrograms() { return { makeProgram("shaders/ascope_graticule.vert", "shaders/ascope_graticule.frag"), makeProgram("shaders/ascope_graticule_text.vert", "shaders/ascope_graticule_text.frag") }; } static AScopeGraticule buildAScopeGraticule( const Layout& L, const FontAtlas& fa, const RangeConfig& rc, float W, float H) { AScopeGraticule ag{}; const float asH = L.asBot - L.asTop; const float pad = 4.0f; const float gx0 = L.asLeft + pad; // signal area left const float gx1 = L.asRight - pad; // signal area right const float gy0 = L.asTop + pad; // top of signal area const float gy1 = L.asTop + asH * 0.80f; // baseline (signal y = 0) const float sigW = gx1 - gx0; const float sigH = gy1 - gy0; std::vector lineV; auto ln = [&](float x1, float y1, float x2, float y2) { lineV.push_back(ndcX(x1,W)); lineV.push_back(ndcY(y1,H)); lineV.push_back(ndcX(x2,W)); lineV.push_back(ndcY(y2,H)); }; // Outer frame (the physical glass plate border) ln(L.asLeft, L.asTop, L.asRight, L.asTop); ln(L.asRight, L.asTop, L.asRight, L.asBot); ln(L.asRight, L.asBot, L.asLeft, L.asBot); ln(L.asLeft, L.asBot, L.asLeft, L.asTop); // Baseline and top amplitude reference ln(gx0, gy1, gx1, gy1); ln(gx0, gy0, gx1, gy0); // Horizontal amplitude guide lines at 25 %, 50 %, 75 % for (int i = 1; i <= 3; ++i) ln(gx0, gy0 + sigH * i * 0.25f, gx1, gy0 + sigH * i * 0.25f); // Vertical tick marks const float majorSpan = sigW / rc.numMajor; const float minorSpan = majorSpan / (rc.numMinorPerMajor + 1); const float minorTickH = sigH * 0.35f; for (int m = 0; m < rc.numMajor; ++m) { // Major tick (full signal height) at right edge of each interval float xMaj = gx0 + (m + 1) * majorSpan; ln(xMaj, gy1, xMaj, gy0); // Minor ticks between this major and the next float xBase = gx0 + m * majorSpan; for (int n = 1; n <= rc.numMinorPerMajor; ++n) { float xMin = xBase + n * minorSpan; ln(xMin, gy1, xMin, gy1 - minorTickH); } } ag.lineCount = (int)lineV.size() / 2; makeLineVAO(ag.lineVAO, ag.lineVBO, lineV); // Range labels at each major tick (whole-number miles, centred in label area) std::vector textV; const float labelY = L.asTop + asH * 0.90f; const float milesPerMajor = rc.maxMiles / rc.numMajor; for (int m = 1; m <= rc.numMajor; ++m) { float x = gx0 + m * majorSpan; int labelMi = (int)std::round(m * milesPerMajor); appendTextQuads(textV, fa, std::to_string(labelMi), x, labelY, W, H); } ag.textVerts = (int)textV.size() / 4; makeTextVAO(ag.textVAO, ag.textVBO, textV); return ag; } // Draw one graticule with a vertical NDC offset (positive = up on screen). // glScissor clips to the A scope box so the slide animation looks like the // graticule is being pulled out / pushed in through a slot at the top. static void drawAScopeGraticule( const AScopeGratProg& prog, const AScopeGraticule& ag, float yOffNDC, const FontAtlas& fa, float W, float H, const Layout& L) { glEnable(GL_SCISSOR_TEST); // OpenGL scissor uses window coords (y=0 at bottom of window) glScissor((GLint) L.asLeft, (GLint)(H - L.asBot), (GLint)(L.asRight - L.asLeft), (GLint)(L.asBot - L.asTop)); glUseProgram(prog.line); glUniform3f(glGetUniformLocation(prog.line, "uColor"), INCAN_R, INCAN_G, INCAN_B); glUniform1f(glGetUniformLocation(prog.line, "uYOffset"), yOffNDC); glBindVertexArray(ag.lineVAO); glDrawArrays(GL_LINES, 0, ag.lineCount); if (ag.textVerts > 0) { glUseProgram(prog.text); glUniform3f(glGetUniformLocation(prog.text, "uColor"), INCAN_R, INCAN_G, INCAN_B); glUniform1f(glGetUniformLocation(prog.text, "uYOffset"), yOffNDC); glUniform1i(glGetUniformLocation(prog.text, "uTexture"), 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, fa.texture); glBindVertexArray(ag.textVAO); glDrawArrays(GL_TRIANGLES, 0, ag.textVerts); glBindTexture(GL_TEXTURE_2D, 0); } glBindVertexArray(0); glDisable(GL_SCISSOR_TEST); } // ─── Polar coordinate helpers (bearing ° CW from N, range fraction) ───────── static inline float polarPx(const Layout& L, float bearDeg, float frac) { return L.ppiCX + frac * L.ppiR * std::sin(bearDeg * (PI/180.0f)); } static inline float polarPy(const Layout& L, float bearDeg, float frac) { return L.ppiCY - frac * L.ppiR * std::cos(bearDeg * (PI/180.0f)); } // ─── Feature 4: PPI range rings ────────────────────────────────────────────── // Build vertex data (x,y,r,g,b = 5 floats/vertex): // 1. Full persistence rings (dim P7 green-yellow) // 2. Active sweep-trail arc per ring (active → persistence colour gradient) // 3. Sweep line from centre to edge static void buildRingVerts(std::vector& v, const Layout& L, int rangeIdx, float sweepAngle, float W, float H) { const RangeConfig& rc = RANGE_CONFIGS[rangeIdx]; const int nr = rc.numMajor; auto push = [&](float px, float py, float r, float g, float b) { v.push_back(ndcX(px,W)); v.push_back(ndcY(py,H)); v.push_back(r); v.push_back(g); v.push_back(b); }; // Full persistence rings (dim) for (int ri = 1; ri <= nr; ++ri) { float frac = (float)ri / nr; for (int i = 0; i <= RING_SEGS; ++i) { float a = 2.0f * PI * i / RING_SEGS; push(L.ppiCX + frac*L.ppiR*std::cos(a), L.ppiCY + frac*L.ppiR*std::sin(a), P7P_R*0.45f, P7P_G*0.45f, P7P_B*0.45f); } } // Sweep trail arcs (active at head → persistence at tail) for (int ri = 1; ri <= nr; ++ri) { float frac = (float)ri / nr; for (int i = 0; i <= TRAIL_SEGS; ++i) { float t = 1.0f - (float)i / TRAIL_SEGS; float ang = sweepAngle - (float)i * TRAIL_DEG / TRAIL_SEGS; float r = P7A_R * t + P7P_R * 0.45f * (1.0f - t); float g2 = P7A_G * t + P7P_G * 0.45f * (1.0f - t); float b = P7A_B * t + P7P_B * 0.45f * (1.0f - t); push(polarPx(L,ang,frac), polarPy(L,ang,frac), r, g2, b); } } // Sweep line (centre → edge) push(L.ppiCX, L.ppiCY, P7A_R, P7A_G, P7A_B); push(polarPx(L,sweepAngle,1.0f), polarPy(L,sweepAngle,1.0f), P7A_R, P7A_G, P7A_B); } struct RingLayer { GLuint prog = 0, vao = 0, vbo = 0; }; static RingLayer buildRingLayer() { RingLayer rl{}; glGenVertexArrays(1, &rl.vao); glGenBuffers(1, &rl.vbo); glBindVertexArray(rl.vao); glBindBuffer(GL_ARRAY_BUFFER, rl.vbo); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5*sizeof(float), nullptr); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5*sizeof(float), reinterpret_cast(2*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); rl.prog = makeProgram("shaders/ppi_range_rings.vert", "shaders/ppi_range_rings.frag"); return rl; } static void renderRingLayer(RingLayer& rl, const Layout& L, int rangeIdx, float sweepAngle, float W, float H) { std::vector v; buildRingVerts(v, L, rangeIdx, sweepAngle, W, H); glBindBuffer(GL_ARRAY_BUFFER, rl.vbo); glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(v.size()*sizeof(float)), v.data(), GL_DYNAMIC_DRAW); glUseProgram(rl.prog); glBindVertexArray(rl.vao); const int nr = RANGE_CONFIGS[rangeIdx].numMajor; int off = 0; for (int ri = 0; ri < nr; ++ri) { glDrawArrays(GL_LINE_STRIP, off, RING_SEGS+1); off += RING_SEGS+1; } for (int ri = 0; ri < nr; ++ri) { glDrawArrays(GL_LINE_STRIP, off, TRAIL_SEGS+1); off += TRAIL_SEGS+1; } glDrawArrays(GL_LINES, off, 2); glBindVertexArray(0); } // ─── Feature 5: Fake targets ────────────────────────────────────────────────── struct FakeTarget { float bearingDeg; float rangeMiles; float coreRadPx; // core blob radius (pixels) float bloomRadPx; // bloom glow radius (pixels); 0 = no bloom mutable float lastActT; // glfwGetTime() when sweep last lit this target }; static float angDiff(float a, float b) // unsigned angular separation (°) { float d = std::fmod(std::fabs(a - b), 360.0f); return d > 180.0f ? 360.0f - d : d; } // Build one textured quad (x,y,u,v = 4 floats/vertex) centred at screen pixel (px,py). static void genBlob(std::vector& v, float px, float py, float radiusPx, float W, float H) { float x0=ndcX(px-radiusPx,W), y0=ndcY(py-radiusPx,H); float x1=ndcX(px+radiusPx,W), y1=ndcY(py+radiusPx,H); v.push_back(x0);v.push_back(y0);v.push_back(-1.f);v.push_back(-1.f); v.push_back(x1);v.push_back(y0);v.push_back( 1.f);v.push_back(-1.f); v.push_back(x1);v.push_back(y1);v.push_back( 1.f);v.push_back( 1.f); v.push_back(x0);v.push_back(y0);v.push_back(-1.f);v.push_back(-1.f); v.push_back(x1);v.push_back(y1);v.push_back( 1.f);v.push_back( 1.f); v.push_back(x0);v.push_back(y1);v.push_back(-1.f);v.push_back( 1.f); } struct TargetLayer { GLuint prog = 0, vao = 0, vbo = 0; }; static TargetLayer buildTargetLayer() { TargetLayer tl{}; glGenVertexArrays(1, &tl.vao); glGenBuffers(1, &tl.vbo); glBindVertexArray(tl.vao); glBindBuffer(GL_ARRAY_BUFFER, tl.vbo); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), nullptr); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), reinterpret_cast(2*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); tl.prog = makeProgram("shaders/ppi_targets.vert","shaders/ppi_targets.frag"); return tl; } static void renderTargets(TargetLayer& tl, FakeTarget* tgts, int nTgts, const Layout& L, float maxRangeMi, float sweepAngle, float curTime, float W, float H) { static const float THRESH = 3.5f; // sweep activation threshold (°) glUseProgram(tl.prog); glBindVertexArray(tl.vao); const GLint locCol = glGetUniformLocation(tl.prog, "uColor"); const GLint locFall = glGetUniformLocation(tl.prog, "uFalloff"); for (int i = 0; i < nTgts; ++i) { FakeTarget& t = tgts[i]; if (angDiff(sweepAngle, t.bearingDeg) < THRESH) t.lastActT = curTime; float rangeFrac = t.rangeMiles / maxRangeMi; if (rangeFrac > 1.0f) continue; float px = polarPx(L, t.bearingDeg, rangeFrac); float py = polarPy(L, t.bearingDeg, rangeFrac); float dt = curTime - t.lastActT; float fade = (dt < 0.0f) ? 0.0f : std::max(0.0f, 1.0f - dt/TARG_PERSIST); bool active = angDiff(sweepAngle, t.bearingDeg) < THRESH; float bright = active ? 1.0f : fade; if (bright < 0.01f) continue; float cr, cg, cb; if (active) { cr=P7A_R; cg=P7A_G; cb=P7A_B; } else { cr=P7P_R; cg=P7P_G; cb=P7P_B; } // Core blob { std::vector bv; genBlob(bv, px, py, t.coreRadPx, W, H); glBindBuffer(GL_ARRAY_BUFFER, tl.vbo); glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(bv.size()*sizeof(float)), bv.data(), GL_DYNAMIC_DRAW); glUniform3f(locCol, cr*bright, cg*bright, cb*bright); glUniform1f(locFall, 4.5f); glDrawArrays(GL_TRIANGLES, 0, 6); } // Bloom glow (large targets only) if (t.bloomRadPx > 0.0f && bright > 0.05f) { std::vector bv; genBlob(bv, px, py, t.bloomRadPx, W, H); glBindBuffer(GL_ARRAY_BUFFER, tl.vbo); glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(bv.size()*sizeof(float)), bv.data(), GL_DYNAMIC_DRAW); glUniform3f(locCol, cr*bright*0.35f, cg*bright*0.35f, cb*bright*0.35f); glUniform1f(locFall, 0.8f); glDrawArrays(GL_TRIANGLES, 0, 6); } } glBindVertexArray(0); } // ─── Key callback ──────────────────────────────────────────────────────────── static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/) { if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(win, GLFW_TRUE); } // ─── main ──────────────────────────────────────────────────────────────────── int main() { if (!glfwInit()) { std::cerr << "GLFW init failed\n"; return 1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWmonitor* mon = glfwGetPrimaryMonitor(); const GLFWvidmode* mode = glfwGetVideoMode(mon); GLFWwindow* win = glfwCreateWindow( mode->width, mode->height, "Radar Test — Features 1–5", nullptr, nullptr); if (!win) { std::cerr << "Window create failed\n"; glfwTerminate(); return 1; } glfwMakeContextCurrent(win); glfwSetKeyCallback(win, onKey); if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) { std::cerr << "GLAD init failed\n"; return 1; } int fbW, fbH; glfwGetFramebufferSize(win, &fbW, &fbH); glViewport(0, 0, fbW, fbH); const float W = static_cast(fbW); const float H = static_cast(fbH); int mmW, mmH; glfwGetMonitorPhysicalSize(mon, &mmW, &mmH); const float dpiX = static_cast(mode->width) / (static_cast(mmW) / 25.4f); const float margin = 0.5f * dpiX; const Layout layout = computeLayout(W, H, margin); // Font atlas (digits only, ~1.8 % of screen height) FontAtlas fa{}; const std::string fontPath = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"; const int fontSizePx = std::max(12, static_cast(H * 0.018f)); if (!buildFontAtlas(fa, fontPath, fontSizePx)) { glfwTerminate(); return 1; } // Feature 1 — scope boundaries ScopeBounds sb = buildScopeBounds(layout, W, H); // Feature 2 — PPI bearing graticule BearingGraticule bg = buildBearingGraticule(layout, fa, W, H); // Feature 3 — A scope replaceable graticules AScopeGratProg agProg = buildAScopeGratPrograms(); AScopeGraticule graticules[RANGE_COUNT]; for (int i = 0; i < RANGE_COUNT; ++i) graticules[i] = buildAScopeGraticule(layout, fa, RANGE_CONFIGS[i], W, H); // Feature 4 — PPI range rings RingLayer rl = buildRingLayer(); // Feature 5 — Fake targets (one per quadrant: NE small, SE large, SW/NW very large) const float pR = layout.ppiR; FakeTarget targets[4] = { { 55.0f, 1.3f, pR*0.010f, 0.0f, -999.0f }, // NE – small (kayak) { 135.0f, 3.8f, pR*0.022f, pR*0.048f, -999.0f }, // SE – large, blooming { 215.0f, 2.2f, pR*0.032f, pR*0.075f, -999.0f }, // SW – very large { 310.0f, 9.0f, pR*0.032f, pR*0.075f, -999.0f }, // NW – very large }; TargetLayer tl = buildTargetLayer(); float sweepAngle = 0.0f; // degrees, 0 = north, clockwise // NDC height of the A scope box — used to compute slide distance const float scopeNDCH = (layout.asBot - layout.asTop) * 2.0f / H; // Animation state int curRange = 0; int nextRange = 1; bool sliding = false; float holdTimer = 0.0f; float slideTimer = 0.0f; float prevTime = static_cast(glfwGetTime()); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); while (!glfwWindowShouldClose(win)) { const float now = static_cast(glfwGetTime()); const float dt = now - prevTime; prevTime = now; // ── Advance sweep (Features 4 & 5) ─────────────────────────────────── sweepAngle = std::fmod(sweepAngle + SWEEP_DEG_PS * dt, 360.0f); // ── Advance Feature 3 animation ─────────────────────────────────────── if (!sliding) { holdTimer += dt; if (holdTimer >= HOLD_SEC) { holdTimer = 0.0f; slideTimer = 0.0f; nextRange = (curRange + 1) % RANGE_COUNT; sliding = true; } } else { slideTimer += dt; if (slideTimer >= SLIDE_SEC) { curRange = nextRange; sliding = false; } } glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // ── Feature 1: scope outlines ───────────────────────────────────────── glUseProgram(sb.prog); const GLint sbCol = glGetUniformLocation(sb.prog, "uColor"); glBindVertexArray(sb.vao); glUniform3f(sbCol, 0.25f, 0.35f, 0.55f); glDrawArrays(GL_LINE_STRIP, sb.ppiStart, sb.ppiCount); glUniform3f(sbCol, P1_R, P1_G, P1_B); glDrawArrays(GL_LINES, sb.asStart, sb.asCount); glBindVertexArray(0); // ── Feature 2: PPI bearing ring + ticks ────────────────────────────── glUseProgram(bg.lineProg); glUniform3f(glGetUniformLocation(bg.lineProg, "uColor"), INCAN_R, INCAN_G, INCAN_B); glBindVertexArray(bg.lineVAO); glDrawArrays(GL_LINE_STRIP, bg.ringStart, bg.ringCount); glDrawArrays(GL_LINES, bg.tickStart, bg.tickCount); glBindVertexArray(0); glUseProgram(bg.textProg); glUniform3f(glGetUniformLocation(bg.textProg, "uColor"), INCAN_R, INCAN_G, INCAN_B); glUniform1i(glGetUniformLocation(bg.textProg, "uTexture"), 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, bg.fontTex); glBindVertexArray(bg.textVAO); glDrawArrays(GL_TRIANGLES, 0, bg.textVerts); glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); // ── Feature 3: A scope graticule (with slide animation) ─────────────── if (!sliding) { drawAScopeGraticule(agProg, graticules[curRange], 0.0f, fa, W, H, layout); } else { float t = std::min(slideTimer / SLIDE_SEC, 1.0f); // Old graticule slides UP (+NDC Y = up) and out through the top slot drawAScopeGraticule(agProg, graticules[curRange], t * scopeNDCH, fa, W, H, layout); // New graticule descends from above into position drawAScopeGraticule(agProg, graticules[nextRange], (1.0f - t) * scopeNDCH, fa, W, H, layout); } // ── Feature 4: PPI range rings ──────────────────────────────────────── renderRingLayer(rl, layout, curRange, sweepAngle, W, H); // ── Feature 5: Active targets + persistence ─────────────────────────── renderTargets(tl, targets, 4, layout, RANGE_CONFIGS[curRange].maxMiles, sweepAngle, now, W, H); glfwSwapBuffers(win); glfwPollEvents(); } // ── Cleanup ─────────────────────────────────────────────────────────────── glDeleteVertexArrays(1, &sb.vao); glDeleteBuffers(1, &sb.vbo); glDeleteVertexArrays(1, &bg.lineVAO); glDeleteBuffers(1, &bg.lineVBO); glDeleteVertexArrays(1, &bg.textVAO); glDeleteBuffers(1, &bg.textVBO); glDeleteTextures(1, &fa.texture); glDeleteProgram(sb.prog); glDeleteProgram(bg.lineProg); glDeleteProgram(bg.textProg); for (int i = 0; i < RANGE_COUNT; ++i) { glDeleteVertexArrays(1, &graticules[i].lineVAO); glDeleteBuffers(1, &graticules[i].lineVBO); glDeleteVertexArrays(1, &graticules[i].textVAO); glDeleteBuffers(1, &graticules[i].textVBO); } glDeleteProgram(agProg.line); glDeleteProgram(agProg.text); glDeleteVertexArrays(1, &rl.vao); glDeleteBuffers(1, &rl.vbo); glDeleteProgram(rl.prog); glDeleteVertexArrays(1, &tl.vao); glDeleteBuffers(1, &tl.vbo); glDeleteProgram(tl.prog); glfwDestroyWindow(win); glfwTerminate(); return 0; }