964 lines
36 KiB
C++
964 lines
36 KiB
C++
// 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 <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 <algorithm>
|
||
#include <cstring>
|
||
|
||
// ─── 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<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;
|
||
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<uint8_t> 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<float>& 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<float> 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<float> 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<float> 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<float> 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<float> 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<float>& 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<void*>(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<float> 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<float>& 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<void*>(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<float> 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<float> 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<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);
|
||
|
||
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);
|
||
|
||
// 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);
|
||
|
||
// 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<float>(glfwGetTime());
|
||
|
||
glEnable(GL_BLEND);
|
||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||
|
||
while (!glfwWindowShouldClose(win)) {
|
||
const float now = static_cast<float>(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;
|
||
}
|