first code attempt
This commit is contained in:
527
src/main.cpp
Normal file
527
src/main.cpp
Normal file
@@ -0,0 +1,527 @@
|
||||
// Radar Simulation — Feature Test: 1 & 2
|
||||
// Feature 1: Initialize display, draw scope boundaries (PPI circle, A scope box)
|
||||
// Feature 2: PPI bearing ring with tick marks and degree labels
|
||||
// Runs for 10 seconds then exits. Press ESC to exit early.
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
static constexpr float PI = 3.14159265358979323846f;
|
||||
static constexpr int CIRCLE_SEGS = 360;
|
||||
|
||||
// Incandescent (warm lamp) — bearing 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
|
||||
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;
|
||||
|
||||
// ─── 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<char>(f), std::istreambuf_iterator<char>() };
|
||||
}
|
||||
|
||||
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<float>& 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<float>& 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<void*>(2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// ─── FreeType font atlas ─────────────────────────────────────────────────────
|
||||
|
||||
struct GlyphInfo {
|
||||
int atlasX; // pixel x offset in atlas
|
||||
int bitmapW; // rendered width
|
||||
int bitmapH; // rendered height
|
||||
int bearingX; // horizontal offset from cursor to left of bitmap
|
||||
int bearingY; // vertical offset from baseline to top of bitmap
|
||||
int advance; // horizontal advance in pixels
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// First pass — measure atlas dimensions
|
||||
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; // 2-pixel padding between glyphs
|
||||
maxH = std::max(maxH, (int)g->bitmap.rows);
|
||||
}
|
||||
|
||||
fa.atlasW = totalW;
|
||||
fa.atlasH = maxH;
|
||||
std::vector<uint8_t> atlas(fa.atlasW * fa.atlasH, 0);
|
||||
|
||||
// Second pass — render each glyph into the atlas
|
||||
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;
|
||||
}
|
||||
|
||||
// Upload atlas as single-channel (RED) texture
|
||||
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); // restore default
|
||||
|
||||
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 (sufficient for 0..350 degree labels).
|
||||
static void appendTextQuads(std::vector<float>& verts, const FontAtlas& fa,
|
||||
const std::string& s, float cx, float cy,
|
||||
float W, float H)
|
||||
{
|
||||
if (s.empty()) return;
|
||||
|
||||
// Measure total advance and vertical extent
|
||||
float totalAdv = 0.0f;
|
||||
int maxBY = 0; // highest bearingY (top above baseline)
|
||||
int minBY = 0; // lowest (bearingY - bitmapH), typically <= 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);
|
||||
}
|
||||
|
||||
// Position baseline so the visual center of the glyphs sits at (cx, cy)
|
||||
float visualCenterAboveBaseline = (maxBY + minBY) * 0.5f;
|
||||
float baselineY = cy + visualCenterAboveBaseline; // screen y (y-down)
|
||||
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];
|
||||
|
||||
// Glyph bounding box in screen pixel space (y increases downward)
|
||||
float gx0 = cursorX + g.bearingX;
|
||||
float gy0 = baselineY - g.bearingY; // top edge
|
||||
float gx1 = gx0 + g.bitmapW;
|
||||
float gy1 = gy0 + g.bitmapH; // bottom edge
|
||||
|
||||
// Atlas UV:
|
||||
// GL uploads atlas row 0 as texture bottom (t = 0).
|
||||
// Atlas row 0 is the TOP of the glyph bitmap.
|
||||
// Screen top vertex (gy0) → t = 0 → samples glyph top ✓
|
||||
// Screen bottom vertex (gy1) → t = v1 → samples glyph bottom ✓
|
||||
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;
|
||||
|
||||
// Triangle 1: TL, TR, BR
|
||||
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);
|
||||
// Triangle 2: TL, BR, BL
|
||||
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; // PPI scope center and radius (pixels)
|
||||
float asLeft, asTop, asRight, asBot; // A scope rectangle (pixels)
|
||||
};
|
||||
|
||||
static Layout computeLayout(float W, float H, float marginPx)
|
||||
{
|
||||
Layout L{};
|
||||
|
||||
// PPI: right half of screen; margins top/bottom/right = marginPx, left = screen centre
|
||||
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;
|
||||
|
||||
// A scope: left half, smaller, centred vertically, near left margin
|
||||
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<float> v;
|
||||
|
||||
// PPI circle (LINE_STRIP, CIRCLE_SEGS+1 verts — last = first, closes the loop)
|
||||
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));
|
||||
}
|
||||
|
||||
// A scope box (4 × GL_LINES = 8 verts)
|
||||
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); // top edge
|
||||
ln(L.asRight, L.asTop, L.asRight, L.asBot); // right edge
|
||||
ln(L.asRight, L.asBot, L.asLeft, L.asBot); // bottom edge
|
||||
ln(L.asLeft, L.asBot, L.asLeft, L.asTop); // left edge
|
||||
|
||||
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;
|
||||
|
||||
// ── Line geometry (ring + ticks) ──────────────────────────────────────────
|
||||
std::vector<float> lineV;
|
||||
|
||||
// Bearing ring circle (LINE_STRIP, closed)
|
||||
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));
|
||||
}
|
||||
|
||||
// Tick marks: every 1 degree, longer every 10 degrees
|
||||
// Radar convention: 0° = top (north), increasing clockwise
|
||||
// screen_x = cx + R * sin(bearing_rad)
|
||||
// screen_y = cy - R * cos(bearing_rad)
|
||||
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;
|
||||
// Outer point (on the ring)
|
||||
lineV.push_back(ndcX(cx + R * sb, W));
|
||||
lineV.push_back(ndcY(cy - R * cb, H));
|
||||
// Inner point
|
||||
lineV.push_back(ndcX(cx + (R-len) * sb, W));
|
||||
lineV.push_back(ndcY(cy - (R-len) * cb, H));
|
||||
}
|
||||
|
||||
makeLineVAO(bg.lineVAO, bg.lineVBO, lineV);
|
||||
|
||||
// ── Text geometry (degree labels every 10°) ────────────────────────────────
|
||||
std::vector<float> textV;
|
||||
const float textR = R * 1.07f; // label-centre radius (just outside the ring)
|
||||
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;
|
||||
}
|
||||
|
||||
// ─── 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 & 2",
|
||||
mon, nullptr);
|
||||
if (!win) { std::cerr << "Window create failed\n"; glfwTerminate(); return 1; }
|
||||
|
||||
glfwMakeContextCurrent(win);
|
||||
glfwSetKeyCallback(win, onKey);
|
||||
|
||||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(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<float>(fbW);
|
||||
const float H = static_cast<float>(fbH);
|
||||
|
||||
// Convert 0.5-inch margin to pixels using monitor physical size
|
||||
int mmW, mmH;
|
||||
glfwGetMonitorPhysicalSize(mon, &mmW, &mmH);
|
||||
const float dpiX = static_cast<float>(mode->width) / (static_cast<float>(mmW) / 25.4f);
|
||||
const float margin = 0.5f * dpiX;
|
||||
|
||||
const Layout layout = computeLayout(W, H, margin);
|
||||
|
||||
// Build FreeType 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<int>(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);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
|
||||
while (!glfwWindowShouldClose(win)) {
|
||||
const float elapsed = std::chrono::duration<float>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
if (elapsed >= 10.0f) break;
|
||||
|
||||
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);
|
||||
|
||||
// PPI scope boundary — dim blue-white ring
|
||||
glUniform3f(sbCol, 0.25f, 0.35f, 0.55f);
|
||||
glDrawArrays(GL_LINE_STRIP, sb.ppiStart, sb.ppiCount);
|
||||
|
||||
// A scope boundary — P1 green
|
||||
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);
|
||||
|
||||
// ── Feature 2: degree labels ──────────────────────────────────────────
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
glfwDestroyWindow(win);
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user