Tactical Radar
OpenGL → Vulkan
Build a real-time 3D tactical radar display — first as a working OpenGL prototype, then rebuild it in Vulkan. Every concept from the week crystallises into one coherent application.
What We're Building
A Tactical Radar Display — the kind you'd see in a naval operations room. A circular scope, a rotating sweep line leaving a green phosphor trail, and blip contacts that fade in and out. Real-time, animated, hardware-accelerated.
What you will have running by the end of Phase 1
Features You Will Build
How to Think About This Project
The most important skill in graphics programming is not knowing the API — it is knowing how to decompose a visual into geometry, attributes, and draw calls. Read this section before writing a single line.
The Decomposition Rule
Every graphics programme starts with one question: what shapes do I need, and what data describes each shape? The radar display is complex visually but decomposes simply:
Four separate "things" to draw: (1) The ring geometry — a circle approximated by 360 line segments, repeated 4 times at different radii. (2) The sweep line — a single line from center to edge, rotated by the current angle. (3) The blip contacts — points or small circles at known positions. (4) The HUD text — a textured quad per character OR a simple vertex-coloured overlay. Each "thing" is its own VBO, its own draw call, possibly its own shader.
The Right Order to Build
Always build in this order: get something visible first, then add detail. A programmer who tries to build the whole radar at once writes 400 lines and sees a black screen. A programmer who follows this order sees progress every 20 minutes:
Every time you complete a stage and it works — stop. Look at the screen. Understand why it works before moving to the next stage. This reflection is what converts working code into transferable understanding. A checklist cannot force this — but your professional instinct should.
NDC to Radar Coordinates
The GPU thinks in NDC: X and Y from −1 to +1, center at (0,0). The radar scope is a circle of radius ~0.9 (leaving a small margin). A ring at range R (where R is 0.0 to 1.0) is a circle of radius R * 0.9 in NDC. A blip at polar position (angle θ, range r) has NDC coordinates:
// Blip at bearing theta (radians, 0=North=up) and range r (0..1) // Radar radius in NDC = 0.9 float radar_r = r * 0.9f; float ndc_x = radar_r * sin(theta); // East = +X in NDC float ndc_y = radar_r * cos(theta); // North = +Y in NDC (OpenGL convention) // In Vulkan: ndc_y = -radar_r * cos(theta) (Y axis is flipped)
Draw this on paper first. Mark (0,0) at center. Mark NDC (+1,0) = East, (0,+1) = North. Mark where a blip at bearing 045° range 50% would land on the circle. Now you know your coordinate system and no code will surprise you.
Folder & Build Setup
Create the project skeleton in under 5 minutes. Every file has a defined purpose. You will never wonder where a file goes.
Create the Folder Structure — Exact Commands
Open a Developer Command Prompt for VS 2022 (not regular cmd). Run:
mkdir C:\Labs\Capstone\RadarGL
cd C:\Labs\Capstone\RadarGL
mkdir src shaders
# Create empty files you will fill in the following stages
type nul > CMakeLists.txt
type nul > src\main.cpp
type nul > shaders\radar.vert
type nul > shaders\radar.frag
Your folder now looks like this. Every file below is the complete content — no missing pieces:
├── CMakeLists.txt ← build config (fill next)
├── src\
│ └── main.cpp ← all C++ code lives here
└── shaders\
├── radar.vert ← vertex shader (GLSL)
└── radar.frag ← fragment shader (GLSL)
CMakeLists.txt — Complete, Copy Exactly
cmake_minimum_required(VERSION 3.20) project(RadarGL) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # GLEW — OpenGL function loader set(GLEW_DIR $ENV{GLEW_DIR}) include_directories(${GLEW_DIR}/include) # GLFW — window and input set(GLFW_DIR $ENV{GLFW_DIR}) include_directories(${GLFW_DIR}/include) # GLM — math (header only, no linking needed) set(GLM_DIR $ENV{GLM_DIR}) include_directories(${GLM_DIR}) add_executable(RadarGL src/main.cpp) target_link_libraries(RadarGL ${GLEW_DIR}/lib/Release/x64/glew32.lib ${GLFW_DIR}/lib-vc2022/glfw3.lib opengl32.lib ) # Copy GLEW dll next to the exe automatically add_custom_command(TARGET RadarGL POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${GLEW_DIR}/bin/Release/x64/glew32.dll" "$<TARGET_FILE_DIR:RadarGL>" )
The CMakeLists.txt reads GLEW_DIR, GLFW_DIR, and GLM_DIR from your system environment. From Day 1–3 you already have GLFW_DIR set. If GLEW_DIR or GLM_DIR are missing: System Properties → Environment Variables → New. Then restart your command prompt. GLEW_DIR points to your GLEW root, GLM_DIR points to the glm/ header folder.
Configure CMake — Run This Now
cd C:\Labs\Capstone\RadarGL # Generate Visual Studio project files — run ONCE, here in Stage 1 cmake -B build -G "Visual Studio 17 2022" -A x64 # You should see: "Build files have been written to: ...build" # Do NOT build yet — src\main.cpp is still empty. # Build + Run happens in Stage 2 after you write the C++ code.
Stage 1 Checkpoints
dir C:\Labs\Capstone\RadarGLThe Radar Rings
Get something visible immediately. A ring is just a polygon with enough sides that it looks round. 360 vertices = a perfect circle. Write the shaders first, then the C++.
A ring is a set of N points on a circle of radius R, connected as a line loop. The vertex shader receives each point's (x,y) position in NDC and passes it through to gl_Position. The fragment shader outputs a solid green colour. That's all the shaders do at this stage — no uniforms, no interpolation, no tricks. Keep it minimal until it works.
The Shaders — Create These First
#version 330 core layout(location = 0) in vec2 inPos; // NDC position of each vertex uniform float uAngle; // sweep angle in radians — used by sweep line draw uniform float uAlpha; // per-object opacity for trail fade void main() { gl_Position = vec4(inPos, 0.0, 1.0); // No transform matrix needed — everything is already in NDC. // The radar circle fills the viewport, centered at (0,0). }
#version 330 core uniform float uAlpha; // 0.0 = fully transparent, 1.0 = fully opaque uniform vec3 uColour; // RGB colour to draw this geometry out vec4 fragColour; void main() { fragColour = vec4(uColour, uAlpha); // Alpha is controlled by C++ per draw call. // The trail effect comes from drawing with low alpha over a dark background. }
main.cpp — Stage 2: Rings Only
Write src/main.cpp with this complete content. Read every comment — they explain why, not just what.
// ═══════════════════════════════════════════════════════════════════ // RadarGL — Capstone Project · RR Skillverse // Stage 2: Four concentric range rings // ═══════════════════════════════════════════════════════════════════ #include <GL/glew.h> #include <GLFW/glfw3.h> #include <glm/glm.hpp> #include <glm/gtc/type_ptr.hpp> #include <iostream> #include <vector> #include <fstream> #include <sstream> #include <cmath> const int WIN_W = 900, WIN_H = 900; const float PI = 3.14159265358979f; const float RADAR_R = 0.88f; // radar circle radius in NDC const int SEGMENTS = 360; // vertices per ring — more = smoother circle // ── Load shader source from file ────────────────────────────────────── std::string loadFile(const std::string& path) { std::ifstream f(path); if (!f.is_open()) { std::cerr << "ERROR: Cannot open " << path << "\n"; exit(1); } std::ostringstream ss; ss << f.rdbuf(); return ss.str(); } // ── Compile + link a vertex/fragment shader pair ────────────────────── GLuint makeProgram(const std::string& vsPath, const std::string& fsPath) { auto vsSrc = loadFile(vsPath), fsSrc = loadFile(fsPath); const char* vsC = vsSrc.c_str(), *fsC = fsSrc.c_str(); GLuint vs = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vs, 1, &vsC, nullptr); glCompileShader(vs); GLint ok; glGetShaderiv(vs, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(vs, 512, nullptr, log); std::cerr << "VERT ERROR:\n" << log; exit(1); } GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fs, 1, &fsC, nullptr); glCompileShader(fs); glGetShaderiv(fs, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(fs, 512, nullptr, log); std::cerr << "FRAG ERROR:\n" << log; exit(1); } GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog); glDeleteShader(vs); glDeleteShader(fs); return prog; } // ── Generate circle vertices (line loop) ───────────────────────────── // Returns a vector of 2D NDC points forming a circle of given radius std::vector<glm::vec2> makeCircle(float radius, int segs) { std::vector<glm::vec2> pts; for (int i = 0; i <= segs; i++) { float a = (float)i / segs * 2.0f * PI; pts.push_back({ radius * cos(a), radius * sin(a) }); } return pts; } // ── Upload geometry to a VAO/VBO pair, return VAO id ───────────────── GLuint makeVAO(const std::vector<glm::vec2>& pts) { GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao); glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, pts.size() * sizeof(glm::vec2), pts.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec2), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); return vao; } // ── Helper: set shader uniforms ─────────────────────────────────────── void setColour(GLuint prog, glm::vec3 col, float alpha) { glUniform3fv(glGetUniformLocation(prog, "uColour"), 1, glm::value_ptr(col)); glUniform1f(glGetUniformLocation(prog, "uAlpha"), alpha); } int main() { // ── Init GLFW ───────────────────────────────────────────────────── glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); GLFWwindow* win = glfwCreateWindow(WIN_W, WIN_H, "Tactical Radar — RR Skillverse", nullptr, nullptr); glfwMakeContextCurrent(win); glewExperimental = GL_TRUE; glewInit(); // ── Shader programme ────────────────────────────────────────────── GLuint prog = makeProgram("shaders/radar.vert", "shaders/radar.frag"); // ── Build four range rings at 25%, 50%, 75%, 100% radar radius ─── struct Ring { GLuint vao; int count; }; std::vector<Ring> rings; float radii[] = { 0.22f, 0.44f, 0.66f, RADAR_R }; for (float r : radii) { auto pts = makeCircle(r, SEGMENTS); rings.push_back({ makeVAO(pts), (int)pts.size() }); } // ── Background colour: near-black green-tinted ──────────────────── glClearColor(0.02f, 0.06f, 0.02f, 1.0f); // ── Enable blending for alpha (needed for trail effect later) ───── glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // ── Render loop ─────────────────────────────────────────────────── while (!glfwWindowShouldClose(win)) { glfwPollEvents(); if (glfwGetKey(win, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(win, true); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(prog); // Draw range rings: outer ring brighter, inner rings dimmer glm::vec3 greenDim = { 0.0f, 0.5f, 0.15f }; glm::vec3 greenBold = { 0.0f, 0.9f, 0.3f }; for (int i = 0; i < (int)rings.size(); i++) { float alpha = (i == (int)rings.size()-1) ? 0.8f : 0.3f; glm::vec3 col = (i == (int)rings.size()-1) ? greenBold : greenDim; setColour(prog, col, alpha); glBindVertexArray(rings[i].vao); glDrawArrays(GL_LINE_STRIP, 0, rings[i].count); } glfwSwapBuffers(win); } glfwTerminate(); return 0; }
Build and Run
cd C:\Labs\Capstone\RadarGL cmake --build build --config Release build\Release\RadarGL.exe
Stage 2 Checkpoints
The Rotating Sweep Line
Add real-time animation. The sweep line is a single line from (0,0) to the edge of the radar circle. Its angle changes every frame via a uniform. This is your first animated draw call.
The sweep line is TWO vertices: the center (0, 0) and the tip at angle θ. The tip's NDC position is (RADAR_R × sin(θ), RADAR_R × cos(θ)). Every frame, θ increases by a small delta. Instead of updating the VBO each frame (expensive), pass θ as a uniform and compute the tip position inside the vertex shader for the second vertex. The center vertex always stays at (0,0).
Update the Vertex Shader
Replace the content of shaders/radar.vert with this version that handles the sweep line rotation:
#version 330 core layout(location = 0) in vec2 inPos; uniform float uAngle; // current sweep angle in radians uniform float uAlpha; uniform int uIsSweep; // 1 = this is a sweep line draw, 0 = normal uniform float uRadarR; // radar radius in NDC (passed from C++) void main() { vec2 pos = inPos; if (uIsSweep == 1) { // Vertex 0 = center, stays at (0,0) // Vertex 1 = tip, computed from angle // We pass vertex index via inPos.x: 0.0 = center, 1.0 = tip if (inPos.x > 0.5) { // tip vertex pos = vec2(uRadarR * sin(uAngle), uRadarR * cos(uAngle)); } else { // center vertex pos = vec2(0.0, 0.0); } } gl_Position = vec4(pos, 0.0, 1.0); }
Add Sweep Line to main.cpp
Add these additions to your existing main.cpp. Find the section comment and insert the new code there:
// ── Sweep line: two vertices — 0.0 (center) and 1.0 (tip) ─────────── // The vertex shader uses this "index" to compute actual NDC positions. // This VBO never changes — the angle comes in via uAngle uniform. GLuint makeSweepLine() { float pts[] = { 0.0f, 0.0f, // vertex 0: center (inPos.x = 0.0 → stays at origin) 1.0f, 0.0f // vertex 1: tip (inPos.x = 1.0 → rotated by shader) }; GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao); glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); return vao; }
// Sweep line VAO GLuint sweepVAO = makeSweepLine(); float sweepAngle = 0.0f; // current angle in radians const float SWEEP_SPEED = 1.2f; // radians per second float lastTime = (float)glfwGetTime();
// ── Update sweep angle ──────────────────────────────────────────── float now = (float)glfwGetTime(); float delta = now - lastTime; lastTime = now; sweepAngle += SWEEP_SPEED * delta; if (sweepAngle > 2.0f * PI) sweepAngle -= 2.0f * PI; // ── Draw sweep line ─────────────────────────────────────────────── glUniform1f(glGetUniformLocation(prog, "uAngle"), sweepAngle); glUniform1f(glGetUniformLocation(prog, "uRadarR"), RADAR_R); glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 1); setColour(prog, { 0.0f, 1.0f, 0.4f }, 0.95f); // bright green glLineWidth(2.0f); glBindVertexArray(sweepVAO); glDrawArrays(GL_LINES, 0, 2); glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0); // reset glLineWidth(1.0f);
Stage 3 Checkpoints
Blip Contacts
Add radar contacts — the small bright dots that represent tracked objects. Each blip has a polar position (bearing, range). You convert these to NDC and render them as GL_POINTS with a custom size.
Define a Blip struct with bearing (0–360°), range (0–1), and age (seconds since sweep last passed over it — controls brightness). Upload all blip NDC positions to one VBO. Redraw with GL_POINTS. The brightness fade is just the alpha uniform: age drives alpha. A newly-hit blip = alpha 1.0, old blip = alpha 0.1.
// ── Blip contact definition ─────────────────────────────────────────── struct Blip { float bearing; // degrees, 0=North, clockwise float range; // 0.0 (center) to 1.0 (outer ring) float age; // seconds since last sweep — drives fade }; // Hardcoded contact list — in a real system this comes from sensor data std::vector<Blip> contacts = { { 045.0f, 0.60f, 0.1f }, // NE, medium range { 135.0f, 0.35f, 1.8f }, // SE, short range, older { 260.0f, 0.75f, 0.4f }, // W, long range, fresh { 340.0f, 0.50f, 2.5f }, // NNW, medium range, fading { 090.0f, 0.85f, 0.8f }, // E, far range }; // ── Convert polar blip to NDC vec2 ──────────────────────────────────── glm::vec2 blipNDC(const Blip& b) { float rad = b.bearing * PI / 180.0f; float r = b.range * RADAR_R; return { r * sin(rad), r * cos(rad) }; } // ── Build blip VBO from contact list ───────────────────────────────── GLuint makeBlipVAO(const std::vector<Blip>& blips) { std::vector<glm::vec2> pts; for (auto& b : blips) pts.push_back(blipNDC(b)); return makeVAO(pts); // reuse existing makeVAO helper } // ── Inside render loop: draw blips ─────────────────────────────────── // (add after sweep line draw, before SwapBuffers) glPointSize(7.0f); // pixel size of GL_POINTS glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0); // not a sweep draw for (int i = 0; i < (int)contacts.size(); i++) { float age = contacts[i].age; float alpha = glm::clamp(1.0f - age / 3.0f, 0.1f, 1.0f); // age 0 → alpha 1.0 (just hit by sweep), age 3 → alpha 0.1 (faded) setColour(prog, { 0.0f, 1.0f, 0.45f }, alpha); glBindVertexArray(blipVAO); glDrawArrays(GL_POINTS, i, 1); // draw one point at offset i // Tick the age forward each frame (simulates elapsed time) contacts[i].age += delta; if (contacts[i].age > 3.5f) contacts[i].age = 0.05f; // respawn } glPointSize(1.0f);
Declare GLuint blipVAO = makeBlipVAO(contacts); in main() after the sweep line setup, before the render loop. The VAO is built once — the per-frame fade is controlled only by the alpha uniform, not by re-uploading data.
Stage 4 Checkpoints
Phosphor Trail Effect
The glowing green trail behind the sweep line is what makes a radar display look authentic. It emerges from a clever use of alpha blending — no special effects needed.
Do not clear the screen to full black every frame. Instead, draw a near-opaque dark rectangle over the entire screen first — covering about 97% of the previous frame. Old bright pixels get 97% darker. New pixels (the sweep line) are drawn at full brightness. After many frames: the sweep tip is bright, the recent trail is medium green, the old trail has faded. This is exactly how a real phosphor CRT works.
// ── Full-screen fade quad: covers entire NDC space ──────────────────── // Every frame we draw this with alpha ~0.06, darkening everything slightly. // Old bright pixels from previous frames fade slowly — creating the trail. float quadVerts[] = { -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f }; GLuint fadeVAO, fadeVBO; glGenVertexArrays(1, &fadeVAO); glBindVertexArray(fadeVAO); glGenBuffers(1, &fadeVBO); glBindBuffer(GL_ARRAY_BUFFER, fadeVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0);
// ═══ RENDER ORDER — This order is critical for the trail effect ═══ // 1. Draw fade quad — darkens everything from last frame // 2. Draw range rings — always at same brightness (re-drawn fresh) // 3. Draw sweep line — full brightness at current angle // 4. Draw blips — brightness based on age // NOTE: NO glClear() — we never fully clear, the fade quad does it // ── 1. Fade quad: draw a semi-transparent black over everything ─── glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0); setColour(prog, { 0.01f, 0.04f, 0.01f }, 0.18f); // Alpha 0.18 means: each frame retains 82% of previous brightness // Lower alpha = longer, brighter trail. 0.18 is a good starting point. glBindVertexArray(fadeVAO); glDrawArrays(GL_TRIANGLES, 0, 6); // ── 2. Range rings (drawn fresh each frame over the faded bg) ──── /* ... your existing ring draw code here ... */ // ── 3. Sweep line ───────────────────────────────────────────────── /* ... your existing sweep draw code here ... */ // ── 4. Blips ────────────────────────────────────────────────────── /* ... your existing blip draw code here ... */
Stage 5 Checkpoints
HUD Overlay — GLFW Window Title
For this project, the HUD is added using GLFW's window title and on-screen geometry rather than a full text rendering system (which requires a font atlas and is a project in itself). We add angular tick marks and a center dot to complete the tactical look.
// ── Build heading tick marks (every 30°, short lines at edge) ──────── std::vector<glm::vec2> makeTickMarks() { std::vector<glm::vec2> pts; for (int deg = 0; deg < 360; deg += 30) { float a = deg * PI / 180.0f; float r1 = RADAR_R * 0.93f; // inner end of tick float r2 = RADAR_R; // outer end = on the ring pts.push_back({ r1 * sin(a), r1 * cos(a) }); pts.push_back({ r2 * sin(a), r2 * cos(a) }); } return pts; } // ── Center dot: small circle to mark own-ship position ─────────────── auto tickPts = makeTickMarks(); GLuint tickVAO = makeVAO(tickPts); auto centerPts = makeCircle(0.015f, 16); // tiny circle GLuint centerVAO = makeVAO(centerPts); // ── Inside render loop: draw ticks and center ───────────────────────── setColour(prog, { 0.0f, 0.6f, 0.2f }, 0.55f); glBindVertexArray(tickVAO); glDrawArrays(GL_LINES, 0, (int)tickPts.size()); setColour(prog, { 0.0f, 1.0f, 0.5f }, 1.0f); glBindVertexArray(centerVAO); glDrawArrays(GL_LINE_LOOP, 0, (int)centerPts.size()); // ── Update window title as HUD (cheap but effective) ───────────────── char titleBuf[128]; int contacts_count = (int)contacts.size(); float headingDisplay = fmod(sweepAngle * 180.0f / PI, 360.0f); snprintf(titleBuf, sizeof(titleBuf), "RADAR DISPLAY | SWEEP: %03.0f° | CONTACTS: %d | RR Skillverse", headingDisplay, contacts_count); glfwSetWindowTitle(win, titleBuf);
Phase 1 Final Checkpoints
New Project — Vulkan Structure
You are rebuilding the same radar display in Vulkan. You already know what it should look like — so every Vulkan object you create has a clear visual purpose you can test immediately.
Porting OpenGL to Vulkan line-by-line teaches you nothing. Rebuilding with the same goal forces you to think: "what OpenGL did automatically here, what do I now explicitly control?" Every Vulkan object you create maps to something OpenGL was hiding from you. This is the entire point of Phase 2.
mkdir C:\Labs\Capstone\RadarVK cd C:\Labs\Capstone\RadarVK mkdir src shaders
├── CMakeLists.txt
├── src\
│ └── main.cpp
└── shaders\
├── radar.vert ← GLSL 4.50 vertex shader
├── radar.frag ← GLSL 4.50 fragment shader
├── radar.vert.spv ← compiled by glslc (you generate this)
└── radar.frag.spv ← compiled by glslc
cmake_minimum_required(VERSION 3.20) project(RadarVK) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Vulkan REQUIRED) set(GLFW_DIR $ENV{GLFW_DIR}) include_directories(${GLFW_DIR}/include) set(GLM_DIR $ENV{GLM_DIR}) include_directories(${GLM_DIR}) add_executable(RadarVK src/main.cpp) target_link_libraries(RadarVK Vulkan::Vulkan ${GLFW_DIR}/lib-vc2022/glfw3.lib )
Compile the Shaders First
#version 450 layout(location = 0) in vec2 inPos; layout(location = 0) out vec4 outColour; // pass colour to frag // Push constant: small per-draw data (angle, alpha, colour, flags) // Max 128 bytes guaranteed — we use 32 bytes layout(push_constant) uniform Push { float angle; // sweep angle (radians) float alpha; // opacity float radarR; // radar radius in NDC int isSweep; // 1 = compute sweep tip from angle vec3 colour; // RGB float _pad; // alignment padding to 32 bytes } push; void main() { vec2 pos = inPos; if (push.isSweep == 1 && inPos.x > 0.5) { pos = vec2(push.radarR * sin(push.angle), push.radarR * cos(push.angle)); } // Vulkan: negate Y so North=up matches our OpenGL prototype gl_Position = vec4(pos.x, -pos.y, 0.0, 1.0); outColour = vec4(push.colour, push.alpha); }
#version 450 layout(location = 0) in vec4 inColour; layout(location = 0) out vec4 outColour; void main() { outColour = inColour; }
cd C:\Labs\Capstone\RadarVK
glslc shaders/radar.vert -o shaders/radar.vert.spv
glslc shaders/radar.frag -o shaders/radar.frag.spv
# Both commands must succeed with no output = success
Stage 1 Checkpoints
dir shaders shows radar.vert.spv and radar.frag.spvVulkan Boilerplate — The Foundation Layer
Create the Vulkan object chain: Instance → Surface → PhysDevice → Device → Swapchain. Nothing visible yet — but when this runs without validation errors, the entire GPU pipeline is ready.
VkInstance = glewInit(). VkSurface = GLFW context. VkPhysicalDevice = the GPU you actually render on. VkDevice = the GL context that owns resources. VkSwapchain = glfwSwapBuffers() made explicit. This mapping is why you built the OpenGL version first — every Vulkan object now has a meaning.
// ═══════════════════════════════════════════════════════════════════ // RadarVK — Capstone Project · RR Skillverse // Phase 2: Vulkan Radar Display // Build: cmake -B build -G "Visual Studio 17 2022" -A x64 // cmake --build build --config Release // (run from project root — shaders/ must be present) // ═══════════════════════════════════════════════════════════════════ #define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h> #include <vulkan/vulkan.h> #include <glm/glm.hpp> #include <iostream> #include <vector> #include <fstream> #include <cmath> #include <stdexcept> #include <cstring> // ── Constants ───────────────────────────────────────────────────────── const int WIN_W = 900, WIN_H = 900; const float PI = 3.14159265358979f; const float RADAR_R = 0.88f; const int SEGMENTS = 360; const char* VALIDATION = "VK_LAYER_KHRONOS_validation"; // ── Push constant layout (must match shaders/radar.vert EXACTLY) ────── struct PushData { float angle, alpha, radarR; int isSweep; float r, g, b, _pad; // 32 bytes total }; // ── Helper: load SPIR-V file ─────────────────────────────────────────── std::vector<char> readSpv(const std::string& p) { std::ifstream f(p, std::ios::ate | std::ios::binary); if (!f.is_open()) throw std::runtime_error("Cannot open: " + p); size_t sz = f.tellg(); std::vector<char> buf(sz); f.seekg(0); f.read(buf.data(), sz); return buf; } // ── Helper: create shader module from .spv bytes ────────────────────── VkShaderModule makeShaderMod(VkDevice dev, const std::vector<char>& code) { VkShaderModuleCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; ci.codeSize = code.size(); ci.pCode = reinterpret_cast<const uint32_t*>(code.data()); VkShaderModule m; vkCreateShaderModule(dev, &ci, nullptr, &m); return m; } // ── Generate circle vertices ─────────────────────────────────────────── std::vector<glm::vec2> makeCircle(float r, int segs) { std::vector<glm::vec2> pts; for (int i = 0; i <= segs; i++) { float a = (float)i / segs * 2.0f * PI; pts.push_back({ r * cos(a), r * sin(a) }); } return pts; } // ═══════════════════════════════════════════════════════════════════════ // RadarApp class — all Vulkan state in one place // ═══════════════════════════════════════════════════════════════════════ class RadarApp { public: GLFWwindow* window = nullptr; VkInstance instance = VK_NULL_HANDLE; VkSurfaceKHR surface = VK_NULL_HANDLE; VkPhysicalDevice physDev = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE; VkQueue gfxQueue = VK_NULL_HANDLE; uint32_t gfxFamily = 0; VkSwapchainKHR swapchain = VK_NULL_HANDLE; VkFormat swapFmt = VK_FORMAT_UNDEFINED; VkExtent2D swapExt = {}; std::vector<VkImage> swapImgs; std::vector<VkImageView> swapViews; std::vector<VkFramebuffer> framebuffers; VkRenderPass renderPass = VK_NULL_HANDLE; VkPipelineLayout pipeLayout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; VkCommandPool cmdPool = VK_NULL_HANDLE; VkCommandBuffer cmdBuf = VK_NULL_HANDLE; VkSemaphore imgAvail = VK_NULL_HANDLE; VkSemaphore renderDone = VK_NULL_HANDLE; VkFence inFlight = VK_NULL_HANDLE; // Geometry buffers (one VkBuffer+VkDeviceMemory per ring + sweep + blips) struct GeomBuf { VkBuffer buf; VkDeviceMemory mem; int count; }; std::vector<GeomBuf> rings; GeomBuf sweepBuf, blipBuf, fadeBuf; float sweepAngle = 0.0f; // ── Init ────────────────────────────────────────────────────────── void init() { initWindow(); createInstance(); createSurface(); pickPhysDevice(); createDevice(); createSwapchain(); createImageViews(); createRenderPass(); createPipeline(); createFramebuffers(); createCommandPool(); createGeometry(); createSyncObjects(); allocCommandBuffer(); } // ── Run ─────────────────────────────────────────────────────────── void run() { float last = (float)glfwGetTime(); while (!glfwWindowShouldClose(window)) { glfwPollEvents(); if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); float now = (float)glfwGetTime(); sweepAngle += 1.2f * (now - last); last = now; if (sweepAngle > 2.0f * PI) sweepAngle -= 2.0f * PI; drawFrame(); } vkDeviceWaitIdle(device); } // ── Cleanup ─────────────────────────────────────────────────────── void cleanup() { vkDestroySemaphore(device, renderDone, nullptr); vkDestroySemaphore(device, imgAvail, nullptr); vkDestroyFence(device, inFlight, nullptr); vkDestroyCommandPool(device, cmdPool, nullptr); for (auto& fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr); vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, pipeLayout, nullptr); vkDestroyRenderPass(device, renderPass, nullptr); for (auto& v : swapViews) vkDestroyImageView(device, v, nullptr); vkDestroySwapchainKHR(device, swapchain, nullptr); vkDestroyDevice(device, nullptr); vkDestroySurfaceKHR(instance, surface, nullptr); vkDestroyInstance(instance, nullptr); glfwDestroyWindow(window); glfwTerminate(); } private: void initWindow() { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); window = glfwCreateWindow(WIN_W, WIN_H, "Tactical Radar — Vulkan — RR Skillverse", nullptr, nullptr); } void createInstance() { uint32_t ec = 0; auto glfwExts = glfwGetRequiredInstanceExtensions(&ec); VkApplicationInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; ai.apiVersion = VK_API_VERSION_1_3; VkInstanceCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; ci.pApplicationInfo = &ai; ci.enabledExtensionCount = ec; ci.ppEnabledExtensionNames = glfwExts; ci.enabledLayerCount = 1; ci.ppEnabledLayerNames = &VALIDATION; if (vkCreateInstance(&ci, nullptr, &instance) != VK_SUCCESS) throw std::runtime_error("vkCreateInstance failed"); std::cout << "[1] Instance OK\n"; } void createSurface() { glfwCreateWindowSurface(instance, window, nullptr, &surface); std::cout << "[2] Surface OK\n"; } void pickPhysDevice() { uint32_t cnt = 0; vkEnumeratePhysicalDevices(instance, &cnt, nullptr); std::vector<VkPhysicalDevice> devs(cnt); vkEnumeratePhysicalDevices(instance, &cnt, devs.data()); for (auto& d : devs) { VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(d, &p); if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { physDev = d; break; } if (!physDev) physDev = d; } VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(physDev, &p); std::cout << "[3] GPU: " << p.deviceName << "\n"; } void createDevice() { // Find graphics+present queue family uint32_t qc; vkGetPhysicalDeviceQueueFamilyProperties(physDev, &qc, nullptr); std::vector<VkQueueFamilyProperties> qfs(qc); vkGetPhysicalDeviceQueueFamilyProperties(physDev, &qc, qfs.data()); for (uint32_t i = 0; i < qc; i++) { VkBool32 pres; vkGetPhysicalDeviceSurfaceSupportKHR(physDev, i, surface, &pres); if ((qfs[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && pres) { gfxFamily = i; break; } } float pri = 1.0f; VkDeviceQueueCreateInfo qci{}; qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; qci.queueFamilyIndex = gfxFamily; qci.queueCount = 1; qci.pQueuePriorities = &pri; const char* ext = VK_KHR_SWAPCHAIN_EXTENSION_NAME; VkPhysicalDeviceFeatures feat{}; VkDeviceCreateInfo dci{}; dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &qci; dci.enabledExtensionCount = 1; dci.ppEnabledExtensionNames = &ext; dci.pEnabledFeatures = &feat; if (vkCreateDevice(physDev, &dci, nullptr, &device) != VK_SUCCESS) throw std::runtime_error("vkCreateDevice failed"); vkGetDeviceQueue(device, gfxFamily, 0, &gfxQueue); std::cout << "[4] Device + Queue OK\n"; } void createSwapchain() { VkSurfaceCapabilitiesKHR caps; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDev, surface, &caps); swapExt = caps.currentExtent; uint32_t fmtCount; vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtCount, nullptr); std::vector<VkSurfaceFormatKHR> fmts(fmtCount); vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtCount, fmts.data()); swapFmt = fmts[0].format; for (auto& f : fmts) if (f.format == VK_FORMAT_B8G8R8A8_SRGB && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) swapFmt = f.format; VkSwapchainCreateInfoKHR sci{}; sci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; sci.surface = surface; sci.minImageCount = caps.minImageCount + 1; sci.imageFormat = swapFmt; sci.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; sci.imageExtent = swapExt; sci.imageArrayLayers = 1; sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE; if (vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain) != VK_SUCCESS) throw std::runtime_error("vkCreateSwapchainKHR failed"); uint32_t ic; vkGetSwapchainImagesKHR(device, swapchain, &ic, nullptr); swapImgs.resize(ic); vkGetSwapchainImagesKHR(device, swapchain, &ic, swapImgs.data()); std::cout << "[5] Swapchain OK (" << ic << " images)\n"; } void createImageViews() { swapViews.resize(swapImgs.size()); for (size_t i = 0; i < swapImgs.size(); i++) { VkImageViewCreateInfo vi{}; vi.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; vi.image = swapImgs[i]; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = swapFmt; vi.components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; vi.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; vkCreateImageView(device, &vi, nullptr, &swapViews[i]); } } void createRenderPass() { VkAttachmentDescription att{}; att.format = swapFmt; att.samples = VK_SAMPLE_COUNT_1_BIT; att.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; // LOAD (not CLEAR): we keep the previous frame for the phosphor trail effect. // The fade pass draws a semi-transparent quad to darken the old frame. // This is the exact Vulkan equivalent of what we did in OpenGL. att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; att.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkSubpassDescription sub{}; sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; sub.colorAttachmentCount = 1; sub.pColorAttachments = &ref; VkSubpassDependency dep{}; dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; VkRenderPassCreateInfo rpi{}; rpi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; rpi.attachmentCount = 1; rpi.pAttachments = &att; rpi.subpassCount = 1; rpi.pSubpasses = ⊂ rpi.dependencyCount = 1; rpi.pDependencies = &dep; vkCreateRenderPass(device, &rpi, nullptr, &renderPass); std::cout << "[6] RenderPass OK\n"; } void createPipeline() { auto vs = makeShaderMod(device, readSpv("shaders/radar.vert.spv")); auto fs = makeShaderMod(device, readSpv("shaders/radar.frag.spv")); VkPipelineShaderStageCreateInfo stages[2]{}; stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vs; stages[0].pName = "main"; stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fs; stages[1].pName = "main"; VkVertexInputBindingDescription bind{0, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_VERTEX}; VkVertexInputAttributeDescription attr{0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; VkPipelineVertexInputStateCreateInfo vi{}; vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &bind; vi.vertexAttributeDescriptionCount = 1; vi.pVertexAttributeDescriptions = &attr; VkPipelineInputAssemblyStateCreateInfo ia{}; ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; ia.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; // LINE_STRIP: works for rings (line loop) and lines. // We'll switch topology per draw using dynamic state. VkDynamicState dynStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_LINE_WIDTH, VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY }; VkPipelineDynamicStateCreateInfo dynCI{}; dynCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynCI.dynamicStateCount = 4; dynCI.pDynamicStates = dynStates; VkPipelineViewportStateCreateInfo vpS{}; vpS.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; vpS.viewportCount = 1; vpS.scissorCount = 1; VkPipelineRasterizationStateCreateInfo rast{}; rast.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rast.polygonMode = VK_POLYGON_MODE_FILL; rast.cullMode = VK_CULL_MODE_NONE; rast.frontFace = VK_FRONT_FACE_CLOCKWISE; rast.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo ms{}; ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineColorBlendAttachmentState blAtt{}; blAtt.colorWriteMask = VK_COLOR_COMPONENT_R_BIT|VK_COLOR_COMPONENT_G_BIT| VK_COLOR_COMPONENT_B_BIT|VK_COLOR_COMPONENT_A_BIT; blAtt.blendEnable = VK_TRUE; blAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blAtt.colorBlendOp = VK_BLEND_OP_ADD; blAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; blAtt.alphaBlendOp = VK_BLEND_OP_ADD; VkPipelineColorBlendStateCreateInfo bl{}; bl.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; bl.attachmentCount = 1; bl.pAttachments = &blAtt; VkPushConstantRange pcRange{}; pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pcRange.offset = 0; pcRange.size = sizeof(PushData); // 32 bytes VkPipelineLayoutCreateInfo plCI{}; plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; plCI.pushConstantRangeCount = 1; plCI.pPushConstantRanges = &pcRange; vkCreatePipelineLayout(device, &plCI, nullptr, &pipeLayout); VkGraphicsPipelineCreateInfo gpCI{}; gpCI.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; gpCI.stageCount = 2; gpCI.pStages = stages; gpCI.pVertexInputState = &vi; gpCI.pInputAssemblyState = &ia; gpCI.pViewportState = &vpS; gpCI.pRasterizationState = &rast; gpCI.pMultisampleState = &ms; gpCI.pColorBlendState = &bl; gpCI.pDynamicState = &dynCI; gpCI.layout = pipeLayout; gpCI.renderPass = renderPass; gpCI.subpass = 0; vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpCI, nullptr, &pipeline); vkDestroyShaderModule(device, vs, nullptr); vkDestroyShaderModule(device, fs, nullptr); std::cout << "[7] Pipeline OK\n"; } void createFramebuffers() { framebuffers.resize(swapViews.size()); for (size_t i = 0; i < swapViews.size(); i++) { VkFramebufferCreateInfo fi{}; fi.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; fi.renderPass = renderPass; fi.attachmentCount = 1; fi.pAttachments = &swapViews[i]; fi.width = swapExt.width; fi.height = swapExt.height; fi.layers = 1; vkCreateFramebuffer(device, &fi, nullptr, &framebuffers[i]); } } void createCommandPool() { VkCommandPoolCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; ci.queueFamilyIndex = gfxFamily; ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; vkCreateCommandPool(device, &ci, nullptr, &cmdPool); } // ── Allocate a VkBuffer + bind VkDeviceMemory ───────────────────── GeomBuf uploadGeom(const std::vector<glm::vec2>& pts, VkBufferUsageFlags usage) { VkDeviceSize sz = pts.size() * sizeof(glm::vec2); VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.size = sz; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; GeomBuf gb; gb.count = (int)pts.size(); vkCreateBuffer(device, &bci, nullptr, &gb.buf); VkMemoryRequirements req; vkGetBufferMemoryRequirements(device, gb.buf, &req); VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(physDev, &mp); uint32_t mIdx = UINT32_MAX; auto need = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; for (uint32_t i = 0; i < mp.memoryTypeCount; i++) if ((req.memoryTypeBits &(1u<<i)) && ((mp.memoryTypes[i].propertyFlags&need)==need)) { mIdx=i; break; } VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.allocationSize = req.size; mai.memoryTypeIndex = mIdx; vkAllocateMemory(device, &mai, nullptr, &gb.mem); vkBindBufferMemory(device, gb.buf, gb.mem, 0); void* data; vkMapMemory(device, gb.mem, 0, sz, 0, &data); memcpy(data, pts.data(), sz); vkUnmapMemory(device, gb.mem); return gb; } void createGeometry() { float radii[] = { 0.22f, 0.44f, 0.66f, RADAR_R }; for (float r : radii) rings.push_back(uploadGeom(makeCircle(r, SEGMENTS), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)); float swPts[] = { 0.0f, 0.0f, 1.0f, 0.0f }; std::vector<glm::vec2> swVec = {{0.0f,0.0f},{1.0f,0.0f}}; sweepBuf = uploadGeom(swVec, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::vector<glm::vec2> blips = { { RADAR_R * 0.6f * sin(45.0f*PI/180), RADAR_R * 0.6f * cos(45.0f*PI/180) }, { RADAR_R * 0.35f* sin(135.0f*PI/180), RADAR_R * 0.35f* cos(135.0f*PI/180) }, { RADAR_R * 0.75f* sin(260.0f*PI/180), RADAR_R * 0.75f* cos(260.0f*PI/180) }, }; blipBuf = uploadGeom(blips, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::vector<glm::vec2> quad = { {-1,-1},{1,-1},{1,1},{-1,-1},{1,1},{-1,1} }; fadeBuf = uploadGeom(quad, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::cout << "[8] Geometry uploaded\n"; } void allocCommandBuffer() { VkCommandBufferAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; ai.commandPool = cmdPool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; vkAllocateCommandBuffers(device, &ai, &cmdBuf); } void createSyncObjects() { VkSemaphoreCreateInfo si{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; VkFenceCreateInfo fi{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; // start signaled so first frame doesn't hang vkCreateSemaphore(device, &si, nullptr, &imgAvail); vkCreateSemaphore(device, &si, nullptr, &renderDone); vkCreateFence(device, &fi, nullptr, &inFlight); std::cout << "[9] Sync objects OK\n"; } // ── Push helper ─────────────────────────────────────────────────── void push(float angle, float alpha, float r, float g, float b, int isSweep = 0) { PushData pd{ angle, alpha, RADAR_R, isSweep, r, g, b, 0.0f }; vkCmdPushConstants(cmdBuf, pipeLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushData), &pd); } void drawFrame() { // 1. Wait for previous frame to finish vkWaitForFences(device, 1, &inFlight, VK_TRUE, UINT64_MAX); vkResetFences(device, 1, &inFlight); // 2. Acquire next swapchain image uint32_t imgIdx; vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imgAvail, VK_NULL_HANDLE, &imgIdx); // 3. Record commands vkResetCommandBuffer(cmdBuf, 0); VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; vkBeginCommandBuffer(cmdBuf, &bi); VkClearValue clear{}; clear.color = {{ 0.0f, 0.0f, 0.0f, 1.0f }}; VkRenderPassBeginInfo rpBI{}; rpBI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rpBI.renderPass = renderPass; rpBI.framebuffer = framebuffers[imgIdx]; rpBI.renderArea = {{0,0}, swapExt}; rpBI.clearValueCount = 1; rpBI.pClearValues = &clear; vkCmdBeginRenderPass(cmdBuf, &rpBI, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); VkViewport vp{ 0, 0, (float)swapExt.width, (float)swapExt.height, 0, 1 }; VkRect2D sc{ {0,0}, swapExt }; vkCmdSetViewport(cmdBuf, 0, 1, &vp); vkCmdSetScissor(cmdBuf, 0, 1, &sc); vkCmdSetLineWidth(cmdBuf, 1.0f); // ═══ Draw order (mirrors the OpenGL version exactly) ═══════════ // DRAW 1 — Fade quad: darkens previous frame for phosphor trail vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmdBuf, 0, 1, &fadeBuf.buf, &off); push(0, 0.18f, 0.01f, 0.04f, 0.01f); // near-black, low alpha vkCmdDraw(cmdBuf, 6, 1, 0, 0); // DRAW 2 — Range rings vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP); for (int i = 0; i < (int)rings.size(); i++) { float a = (i == (int)rings.size()-1) ? 0.75f : 0.25f; push(0, a, 0.0f, i==(int)rings.size()-1 ? 0.9f : 0.45f, 0.15f); vkCmdBindVertexBuffers(cmdBuf, 0, 1, &rings[i].buf, &off); vkCmdDraw(cmdBuf, rings[i].count, 1, 0, 0); } // DRAW 3 — Sweep line vkCmdSetLineWidth(cmdBuf, 2.0f); push(sweepAngle, 0.95f, 0.0f, 1.0f, 0.4f, 1); // isSweep=1 vkCmdBindVertexBuffers(cmdBuf, 0, 1, &sweepBuf.buf, &off); vkCmdDraw(cmdBuf, 2, 1, 0, 0); vkCmdSetLineWidth(cmdBuf, 1.0f); // DRAW 4 — Blip contacts as points vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_POINT_LIST); push(0, 0.9f, 0.0f, 1.0f, 0.5f); vkCmdBindVertexBuffers(cmdBuf, 0, 1, &blipBuf.buf, &off); vkCmdDraw(cmdBuf, blipBuf.count, 1, 0, 0); vkCmdEndRenderPass(cmdBuf); vkEndCommandBuffer(cmdBuf); // 4. Submit to queue VkPipelineStageFlags ws = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.waitSemaphoreCount = 1; si.pWaitSemaphores = &imgAvail; si.pWaitDstStageMask = &ws; si.commandBufferCount = 1; si.pCommandBuffers = &cmdBuf; si.signalSemaphoreCount = 1; si.pSignalSemaphores = &renderDone; vkQueueSubmit(gfxQueue, 1, &si, inFlight); // 5. Present VkPresentInfoKHR pi{}; pi.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &renderDone; pi.swapchainCount = 1; pi.pSwapchains = &swapchain; pi.pImageIndices = &imgIdx; vkQueuePresentKHR(gfxQueue, &pi); } }; int main() { std::cout << "\n=== Radar VK — Capstone Project · RR Skillverse ===\n\n"; RadarApp app; try { app.init(); std::cout << "\n[✓] All Vulkan objects created. Rendering...\n"; std::cout << " Press ESC to exit.\n\n"; app.run(); app.cleanup(); } catch (const std::exception& e) { std::cerr << "ERROR: " << e.what() << "\n"; return 1; } std::cout << "Done.\n"; return 0; }
Phase 2 Build + Run
cd C:\Labs\Capstone\RadarVK # Compile shaders (always first) glslc shaders/radar.vert -o shaders/radar.vert.spv glslc shaders/radar.frag -o shaders/radar.frag.spv # Build cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release # Run from project root build\Release\RadarVK.exe
[1] Instance OK → [2] Surface OK → [3] GPU: NVIDIA GeForce RTX... → [4] Device + Queue OK → [5] Swapchain OK (3 images) → [6] RenderPass OK → [7] Pipeline OK → [8] Geometry uploaded → [9] Sync objects OK → [✓] All Vulkan objects created. Rendering.... If any line is missing, check the validation layer output — it will name the exact failed call.
Phase 2 Checkpoints
Final Polish & Known Extensions
Your radar display is working. Here are optional improvements to explore after completing the core project — each one is a natural next step using concepts from the handbook.
Optional Extensions (pick any one)
glUniform2fv array of blip positions updated each frame. Contacts that appear as the sweep passes over them — refresh the positions randomly when swept.uThreatLevel uniform (0=green, 1=amber, 2=red). Draw hostile contacts in red using a second colour in the fragment shader.vkAllocateMemory calls with the Vulkan Memory Allocator library (VMA). This is how production engines manage GPU memory. One include, three functions.If you do not have time for extensions, that is correct. The core project is complete. Extensions exist so students who finish early have meaningful work to do — not as requirements. A working, clean core project is worth more than a buggy feature-heavy one.
Final Checkpoints
What You Just Built — and What It Means
A moment to understand what this week actually achieved.
One Week Ago vs Right Now
You did not just learn two APIs. You learned how to think about rendering problems: decompose visual into geometry, understand the pipeline, control exactly what the GPU does. OpenGL showed you the results. Vulkan showed you the mechanism. The radar display was the shared goal that made both lessons concrete. That is the pedagogy of "build the same thing twice" — you can compare them, and the comparison is the learning.
RR Skillverse — Capstone Project: Tactical Radar Display
By Raushan Ranjan · MCT | RR Skillverse, Noida
"Sweat in the right direction brings Peace, Money, and Respect."