Day 1 Capstone · 5 Labs · Build a Real System
Tactical Contact Display
You are building a 2D naval operations display — the kind used to track contacts (ships, aircraft, submarines) on a tactical screen. Each lab builds on the previous one. By Lab 5, you have a fully animated, real-time contact display with threat-level colour coding and a pulsing hostile indicator. Every lab is one complete main.cpp — copy the whole file, build, run.
VBO VAO EBO GLSL Shaders Uniforms Real-time Animation
📁 Project Setup — Do This Once Before Lab 1

All 5 labs share one project folder: C:\Labs\TacticalDisplay\. The CMakeLists.txt is identical for all labs — you write it once and never change it. Each lab replaces only src\main.cpp. Build once after Lab 1 setup, then just replace main.cpp + rebuild for each subsequent lab.

Create This Folder Structure — Before Lab 1
C:\Labs\TacticalDisplay\
├── CMakeLists.txt     ← write once, same for all 5 labs
└── src\
    └── main.cpp           ← replace with each lab's complete file
CMakeLists.txt — same for all 5 labs
CMake
cmake_minimum_required(VERSION 3.20)
project(TacticalDisplay)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenGL REQUIRED)
set(GLFW_DIR $ENV{GLFW_DIR})
set(GLEW_DIR $ENV{GLEW_DIR})
set(GLM_DIR  $ENV{GLM_DIR})
include_directories(
    ${GLFW_DIR}/include
    ${GLEW_DIR}/include
    ${GLM_DIR}
)
add_executable(TacticalDisplay src/main.cpp)
target_link_libraries(TacticalDisplay
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\TacticalDisplay
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\TacticalDisplay.exe
✓ Build Procedure — Same Every Lab

Write your lab's main.cpp (or copy the complete file above) → open Developer Command Prompt for VS 2022 → run the 4 build commands above. The CMakeLists.txt and the folder path never change. Only src\main.cpp changes between labs.

Lab 1 of 5 ⬛ Beginner · ~25 min
Display Window Setup
Create the operational display window — the blank screen your tactical system will run on. Near-black background, correct viewport, working render loop, ESC to quit. GPU info printed to terminal to confirm the environment is live. This is identical to Demo 2 (First Window) but with tactical display settings.
📋 Concepts: GLFW · GLEW · Context · Render Loop ⚡ Do Demo 2 first (First Window)
➕ What You Build in This Lab
  • 1024×768 window titled “Tactical Contact Display — Naval Ops”
  • Near-black background (R=0.02, G=0.03, B=0.07) — ops-room dark display colour
  • GPU name + OpenGL version + ready message printed to terminal
  • ESC closes cleanly, resize maintains correct viewport
🎯 What You Will See When This Runs
Screen: Dark near-black window 1024×768 with title “Tactical Contact Display — Naval Ops”
Terminal: GPU name, OpenGL version, and “Display ready. Press ESC to shut down.”
ESC: Window closes with no crash, no hang
Objectives — Check Off As You Complete
  • CMakeLists.txt written (use the shared one above)
  • GLFW initialised with correct OpenGL 3.3 core hints
  • Window created 1024×768 with correct title
  • GLEW initialised after making context current
  • GPU info printed to terminal on startup
  • Near-black tactical background colour set
  • ESC closes the window cleanly
  • Window resize keeps viewport correct
✍️ Write It First — Then Verify

Try writing main.cpp from memory using what you learned in Demo 2. The 5-step GLFW sequence is always the same: Init → Hints → CreateWindow → MakeContextCurrent → GLEW. Only refer to the complete file below if you get stuck after trying.

Complete src/main.cpp — Copy Entire File
src/main.cpp — Lab 1 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · LAB 1 of 5
//  Tactical Contact Display — Display Window Setup
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  TacticalDisplay  (use this in CMakeLists.txt)
//  FOLDER:        C:\Labs\TacticalDisplay\
//
//  WHAT THIS LAB DOES:
//  Creates the tactical operations display window — dark background,
//  correct viewport, working render loop, ESC to quit.
//  GPU info printed to terminal to confirm environment is live.
//
//  WHAT YOU WILL SEE:
//  - Window titled "Tactical Contact Display — Naval Ops" at 1024x768
//  - Near-black background (ops-room display colour)
//  - Terminal: GPU name + OpenGL version + "Display ready" message
//  - ESC closes cleanly
// ═══════════════════════════════════════════════════════════════════════════

// GLEW must always be included BEFORE GLFW — no exceptions
// GLEW_STATIC = static link, no .dll needed at runtime
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// Called automatically by GLFW whenever the window is resized.
// Keeps the OpenGL drawing area matching the new window dimensions.
// ─────────────────────────────────────────────────────────────────────────────
void onResize(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

// ─────────────────────────────────────────────────────────────────────────────
// Checked every frame inside the render loop.
// If ESC is pressed, signals the window to close on next iteration.
// ─────────────────────────────────────────────────────────────────────────────
void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// ─────────────────────────────────────────────────────────────────────────────
// MAIN
// ─────────────────────────────────────────────────────────────────────────────
int main() {

    // ── STEP 1: Initialise GLFW ───────────────────────────────────────────────
    // Must be the very first GLFW call — sets up its internal state.
    if (!glfwInit()) {
        std::cerr << "ERROR: GLFW init failed\n";
        return -1;
    }

    // ── STEP 2: Context hints — what kind of OpenGL we want ───────────────────
    // MUST be called before glfwCreateWindow — they are like a spec form.
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);   // OpenGL 3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // ── STEP 3: Create the OS window AND the OpenGL context ───────────────────
    // 1024x768 = common tactical display resolution
    GLFWwindow* window = glfwCreateWindow(
        1024, 768,
        "Tactical Contact Display — Naval Ops",
        NULL,   // NULL = windowed (not fullscreen)
        NULL    // NULL = no shared context
    );
    if (!window) {
        std::cerr << "ERROR: Window creation failed\n";
        glfwTerminate();
        return -1;
    }

    // ── STEP 4: Make the context current on this thread ───────────────────────
    // All gl* calls from this point forward go to this context.
    // CRITICAL: glewInit() will fail silently without a current context.
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);

    // ── STEP 5: Initialise GLEW ───────────────────────────────────────────────
    // Loads all OpenGL function pointers from the GPU driver.
    // Without this, calling glClear() = crash.
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "ERROR: GLEW init failed\n";
        return -1;
    }

    // Confirm the environment is working — print GPU info to terminal
    std::cout << "\n[ Tactical Contact Display — System Check ]\n";
    std::cout << "GPU:            " << glGetString(GL_RENDERER) << "\n";
    std::cout << "OpenGL Version: " << glGetString(GL_VERSION)  << "\n";
    std::cout << "GLSL Version:   " << glGetString(GL_SHADING_LANGUAGE_VERSION) << "\n";
    std::cout << "Display ready.  Press ESC to shut down.\n\n";

    // ── STEP 6: RENDER LOOP ───────────────────────────────────────────────────
    // Runs 60 times per second until window is closed.
    // Each iteration = one frame on screen.
    while (!glfwWindowShouldClose(window)) {

        // ① Check for ESC key
        processInput(window);

        // ② Clear to tactical display dark background
        // RGB(5, 8, 18) / 255 = (0.02, 0.03, 0.07) — near-black with blue tint
        // Matches real operational display colours (reduces eye strain in dark ops rooms)
        glClearColor(0.02f, 0.03f, 0.07f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // ③ Draw contact markers HERE — added from Lab 2 onward
        // (Nothing drawn yet — proving the window and loop work first)

        // ④ Show completed frame
        glfwSwapBuffers(window);

        // ⑤ Process OS events — keyboard, mouse, resize
        glfwPollEvents();
    }

    // ── CLEANUP ───────────────────────────────────────────────────────────────
    glfwTerminate();
    std::cout << "Display shutdown cleanly.\n";
    return 0;
}
Expected Terminal Output
[ Tactical Contact Display — System Check ]
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2
OpenGL Version: 3.3.0 NVIDIA 546.01
GLSL Version: 3.30 NVIDIA via Cg compiler
Display ready. Press ESC to shut down.
🔬 3 Experiments Before Moving to Lab 2
  • Change background: Try glClearColor(0.0f, 0.1f, 0.05f, 1.0f) → dark green (submarine sonar display colour). Rebuild.
  • Comment out glfwPollEvents(): Rebuild → window freezes immediately. Ctrl+C to kill. Proves why polling is mandatory.
  • Comment out glfwSwapBuffers(): Rebuild → window stays black even with glClearColor set. Proves double buffering.
✓ Lab 1 Deliverable
A working tactical display window
  • Window opens at 1024×768 with correct title
  • Near-black background visible (not pure black — slight blue tint)
  • Terminal shows GPU name + version + ready message
  • ESC closes without crash
Lab 2 of 5 ⬛⬛ Intermediate · ~35 min
Contact Marker Geometry
Add geometry to the display. Three small triangle markers appear on screen, each representing a naval contact at a fixed position. You write your first GLSL shaders, upload vertex data with VBO + VAO, and draw three contacts as dim white triangles. This is Demo 3 (VBO+VAO+Triangle) applied to the tactical scenario.
📋 Concepts: VBO · VAO · Vertex Shader · Fragment Shader ⚡ Do Demo 3 first
➕ What This Lab Adds Over Lab 1
  • Vertex shader + fragment shader written in GLSL and compiled by the GPU driver
  • 3 contact triangles uploaded to GPU VRAM with VBO (108 bytes = 27 floats)
  • VAO records the vertex layout (3 floats per vertex, stride=12, offset=0)
  • All 3 contacts drawn in one glDrawArrays(GL_TRIANGLES, 0, 9) call
  • Terminal prints 4 confirmation steps: shader ID, bytes uploaded, attribute info
Contact Positions on the Tactical Display (NDC Space — range -1.0 to +1.0)
          Y=+1.0 (top)
              |
  ▲ Friendly  |     ▲ Unknown
  (0.0, 0.6)  |     (0.5, 0.3)
              |
  X=-1 ------+------- X=+1
              |
  ▲ Hostile   |
  (-0.4,-0.4) |
              |
          Y=-1.0 (bottom)

  Each contact = small upward-pointing triangle
  Triangle size: tip at Y+0.06, base at Y-0.06 from centre
  All the same dim-white colour in Lab 2 (colours added in Lab 3)
💡 Why We Draw All 3 Contacts in ONE Draw Call

All 27 float values (9 vertices × 3 floats each) for all 3 contacts go into a single float array and one VBO. glDrawArrays(GL_TRIANGLES, 0, 9) draws them all: the GPU reads vertices 0,1,2 as triangle 1, vertices 3,4,5 as triangle 2, vertices 6,7,8 as triangle 3. One draw call = three contacts. We split into separate draw calls in Lab 5 when we need different uniform values per contact.

Objectives
  • Vertex shader written: aPos at location=0, outputs gl_Position
  • Fragment shader written: outputs fixed dim-white colour
  • compileShader helper written with error checking
  • Contact geometry defined: 9 vertices × 3 floats = 27 floats
  • VAO bound FIRST, then VBO created and data uploaded
  • glVertexAttribPointer: 3 floats, stride=12, offset=0
  • Render loop draws all 9 vertices in one call
Complete src/main.cpp — Replace Entire File with This
src/main.cpp — Lab 2 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · LAB 2 of 5
//  Tactical Contact Display — Contact Marker Geometry
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  TacticalDisplay
//  FOLDER:        C:\Labs\TacticalDisplay\
//
//  WHAT THIS LAB ADDS OVER LAB 1:
//  - Vertex + Fragment shaders (GLSL) compiled and linked at runtime
//  - 3 contact triangle markers uploaded to GPU with VBO + VAO
//  - All 3 contacts drawn in the render loop as dim white triangles
//  - Terminal prints setup confirmation steps
//
//  WHAT YOU WILL SEE:
//  - Same dark window as Lab 1
//  - 3 small white triangles: top-centre, lower-left, right-centre
//  - Terminal: shader compiled, VBO/VAO ready messages
//
//  CONTACT POSITIONS (NDC — range -1.0 to +1.0):
//   Contact 1 (Friendly): centre (0.0,  0.6) — top-centre
//   Contact 2 (Hostile):  centre (-0.4,-0.4) — lower-left
//   Contact 3 (Unknown):  centre (0.5,  0.3) — right-centre
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER
// Runs once per vertex on the GPU.
// Takes aPos (X,Y,Z) from the VBO and places it on screen.
// layout(location=0) MUST match glVertexAttribPointer(0, ...) below.
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    void main() {
        gl_Position = vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER
// Runs once per pixel the triangle covers.
// Returns dim white for all contacts — we add threat colours in Lab 3.
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;
    void main() {
        FragColor = vec4(0.85, 0.85, 0.85, 1.0);  // dim white
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// HELPER: compileShader
// Compiles one shader (vertex or fragment) and returns its GPU-side ID.
// Always checks for errors — GLSL errors only appear at runtime.
// ─────────────────────────────────────────────────────────────────────────────
unsigned int compileShader(unsigned int type, const char* source) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &source, NULL);
    glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        glGetShaderInfoLog(id, 512, NULL, log);
        std::cerr << "Shader compile error:\n" << log << "\n";
    }
    return id;
}

// ─────────────────────────────────────────────────────────────────────────────
// HELPER: createShaderProgram
// Links vertex + fragment into one shader program. Returns program ID.
// ─────────────────────────────────────────────────────────────────────────────
unsigned int createShaderProgram(const char* vSrc, const char* fSrc) {
    unsigned int vs = compileShader(GL_VERTEX_SHADER,   vSrc);
    unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fSrc);
    unsigned int prog = glCreateProgram();
    glAttachShader(prog, vs);
    glAttachShader(prog, fs);
    glLinkProgram(prog);
    glDeleteShader(vs);
    glDeleteShader(fs);
    return prog;
}

void onResize(GLFWwindow* w, int W, int H) { glViewport(0, 0, W, H); }
void processInput(GLFWwindow* w) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(w, true);
}

int main() {

    // Window + context setup (same as Lab 1)
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(1024, 768, "Tactical Contact Display — Naval Ops", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE;
    glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    // ── Compile shader program ────────────────────────────────────────────────
    unsigned int shaderProg = createShaderProgram(vertSrc, fragSrc);
    std::cout << "[1] Shader program compiled + linked. ID = " << shaderProg << "\n";

    // ── Contact geometry ──────────────────────────────────────────────────────
    // 3 contacts x 3 vertices x 3 floats (x,y,z) = 27 floats = 108 bytes
    // Each contact = a small upward-pointing triangle (tactical icon)
    // All vertices use position ONLY — colour added in Lab 3
    float contacts[] = {
    //    X        Y       Z
        // Contact 1 — FRIENDLY vessel (top-centre of display)
         0.00f,  0.66f,  0.0f,   // tip (top)
        -0.05f,  0.54f,  0.0f,   // bottom-left
         0.05f,  0.54f,  0.0f,   // bottom-right

        // Contact 2 — HOSTILE vessel (lower-left quadrant)
        -0.40f, -0.34f,  0.0f,   // tip
        -0.45f, -0.46f,  0.0f,   // bottom-left
        -0.35f, -0.46f,  0.0f,   // bottom-right

        // Contact 3 — UNKNOWN contact (right-centre area)
         0.50f,  0.36f,  0.0f,   // tip
         0.45f,  0.24f,  0.0f,   // bottom-left
         0.55f,  0.24f,  0.0f,   // bottom-right
    };
    // 9 vertices × 3 floats = 27 floats × 4 bytes = 108 bytes

    // ── VAO + VBO setup ───────────────────────────────────────────────────────
    // VAO bind FIRST → then VBO → then glVertexAttribPointer
    // (VAO must be recording before you describe the layout)
    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);              // ← RECORDING STARTS

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(contacts), contacts, GL_STATIC_DRAW);
    std::cout << "[2] " << sizeof(contacts) << " bytes uploaded to GPU VRAM (VBO ID=" << VBO << ")\n";

    // Attribute 0: position — 3 floats, stride=12 (3x4), offset=0
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    std::cout << "[3] VAO recorded. Attribute 0: 3 floats, stride=12, offset=0\n";

    glBindVertexArray(0);               // ← RECORDING STOPS
    std::cout << "[4] Setup complete. Render loop starting...\n";
    std::cout << "    ESC = quit\n\n";

    // ── RENDER LOOP ───────────────────────────────────────────────────────────
    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        glClearColor(0.02f, 0.03f, 0.07f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Activate shader program
        glUseProgram(shaderProg);

        // Bind VAO — restores VBO + attribute layout in one call
        glBindVertexArray(VAO);

        // Draw all 3 contacts: 9 vertices = 3 triangles
        // GL_TRIANGLES: every 3 consecutive vertices = 1 filled triangle
        glDrawArrays(GL_TRIANGLES, 0, 9);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // ── CLEANUP ───────────────────────────────────────────────────────────────
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProg);
    glfwTerminate();
    return 0;
}
Expected Terminal Output
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2

[1] Shader program compiled + linked. ID = 1
[2] 108 bytes uploaded to GPU VRAM (VBO ID=2)
[3] VAO recorded. Attribute 0: 3 floats, stride=12, offset=0
[4] Setup complete. Render loop starting...
ESC = quit

Screen: 3 small white triangles on dark background
🔬 Break-to-Learn Experiments
  • Comment out glUseProgram(): Black screen. GPU has no shader to run. Restore and verify it works again.
  • Comment out glBindVertexArray(VAO): Black screen. GPU has no data to read. Restore.
  • Move a contact: Change the X,Y values of Contact 1's vertices (first 9 floats) to move it to a different position on screen.
  • Add a 4th contact: Add 3 more vertices after the Unknown contact in the float array and change the draw count from 9 to 12.
✓ Lab 2 Deliverable
Three white contact marker triangles on the dark display
  • Three small triangles visible: top-centre, lower-left, right-centre
  • No shader compile errors in terminal
  • Terminal shows the 4 setup confirmation messages
  • ESC closes cleanly
Lab 3 of 5 ⬛⬛ Intermediate · ~30 min
Threat Classification Colours
Add per-vertex colour data to classify each contact by threat level. Friendly = blue, Hostile = red, Unknown = yellow. Each vertex now carries 6 floats (X Y Z R G B) in an interleaved VBO. The VAO records two attributes. The GPU interpolates colour automatically across each triangle. This is Demo 4 (per-vertex colour) applied to the tactical scenario.
📋 Concepts: Interleaved VBO · Two Attributes · Stride/Offset ⚡ Do Demo 4 first
➕ What This Lab Adds Over Lab 2
  • Each vertex now has 6 floats: X Y Z R G B (was 3 floats: X Y Z)
  • Vertex shader receives aColour at location=1 and passes it to fragment shader
  • Fragment shader uses vColour from vertex shader instead of hardcoded white
  • VAO records TWO glVertexAttribPointer calls: pos (stride=24, offset=0) and colour (stride=24, offset=12)
  • Render loop is IDENTICAL to Lab 2 — only the data description changed
Interleaved VBO Memory Layout — 6 Floats = 24 Bytes per Vertex
  Byte:  0    4    8   12   16   20  |  24   28   32   36   40   44  | ...
         [X ] [Y ] [Z ] [R ] [G ] [B ]  [X ] [Y ] [Z ] [R ] [G ] [B ]  ...
         |───position───|──colour──|  |───position───|──colour──|
                 Vertex 0 (24 bytes)              Vertex 1 (24 bytes)

  Stride = 24 bytes (6 floats × 4 bytes) — SAME for both attributes
  Position offset =  0 bytes (starts at byte 0 of each vertex)
  Colour   offset = 12 bytes (starts after 3 position floats: 3 × 4 = 12)
Objectives
  • Vertex shader: aColour at location=1, out vec3 vColour passes colour to fragment shader
  • Fragment shader: in vec3 vColour (must match vertex shader name), FragColor = vec4(vColour, 1.0)
  • Contact data updated: 9 vertices × 6 floats = 54 floats × 4 bytes = 216 bytes
  • Friendly contact (first 3 vertices): all blue (0.20, 0.50, 1.00)
  • Hostile contact (next 3 vertices): all red (1.00, 0.20, 0.20)
  • Unknown contact (last 3 vertices): all yellow (1.00, 0.85, 0.10)
  • Attribute 0 (pos): stride=24, offset=0 — Attribute 1 (colour): stride=24, offset=12
✓ Why Render Loop Is Identical to Lab 2

The draw call glDrawArrays(GL_TRIANGLES, 0, 9) is unchanged. The VAO recorded the new 2-attribute layout during setup, so it restores everything with one bind in the loop. This is exactly the VAO power you saw in Demo 4: describe the data once, the draw call never changes.

Complete src/main.cpp — Replace Entire File with This
src/main.cpp — Lab 3 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · LAB 3 of 5
//  Tactical Contact Display — Threat Classification Colours
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  TacticalDisplay
//  FOLDER:        C:\Labs\TacticalDisplay\
//
//  WHAT THIS LAB ADDS OVER LAB 2:
//  - Each vertex now carries 6 floats: X Y Z R G B (interleaved VBO)
//  - VAO records TWO attributes (position + colour)
//  - Vertex shader receives and passes colour to fragment shader
//  - GPU auto-interpolates colour between vertices
//  - Friendly = BLUE, Hostile = RED, Unknown = YELLOW
//
//  KEY CONCEPT — Interleaved stride/offset:
//  Stride = 24 bytes (6 floats × 4 bytes per float)
//  Position offset =  0 bytes (starts at byte 0)
//  Colour   offset = 12 bytes (after the 3 position floats)
//
//  WHAT YOU WILL SEE:
//  - Top-centre triangle: BLUE (friendly)
//  - Lower-left triangle: RED (hostile)
//  - Right-centre triangle: YELLOW (unknown)
//  - Render loop is IDENTICAL to Lab 2 — only data description changed
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — receives TWO attributes now
// location=0: aPos    (x,y,z) — position
// location=1: aColour (r,g,b) — threat colour
// "out vec3 vColour" passes colour to fragment shader (GPU interpolates between vertices)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;

    out vec3 vColour;   // GPU will interpolate this between the 3 vertices

    void main() {
        gl_Position = vec4(aPos, 1.0);
        vColour = aColour;   // pass threat colour through unchanged
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — receives the interpolated colour from vertex shader
// "in vec3 vColour" MUST match "out vec3 vColour" name in vertex shader
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;
    void main() {
        FragColor = vec4(vColour, 1.0);  // RGB from vertex shader, Alpha=1 (fully opaque)
    }
)GLSL";

unsigned int compileShader(unsigned int type, const char* source) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &source, NULL);
    glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) { glGetShaderInfoLog(id, 512, NULL, log); std::cerr << "Shader error:\n" << log << "\n"; }
    return id;
}
unsigned int createShaderProgram(const char* vSrc, const char* fSrc) {
    unsigned int vs = compileShader(GL_VERTEX_SHADER, vSrc);
    unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fSrc);
    unsigned int prog = glCreateProgram();
    glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog);
    glDeleteShader(vs); glDeleteShader(fs);
    return prog;
}
void onResize(GLFWwindow* w, int W, int H) { glViewport(0, 0, W, H); }
void processInput(GLFWwindow* w) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(w, true);
}

int main() {

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(1024, 768, "Tactical Contact Display — Naval Ops", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    unsigned int shaderProg = createShaderProgram(vertSrc, fragSrc);
    std::cout << "[1] Shader compiled. ID = " << shaderProg << "\n";

    // ── Contact geometry: X Y Z R G B per vertex (6 floats = 24 bytes) ───────
    //
    // Memory layout per vertex (24 bytes):
    //  Byte: 0    4    8   12   16   20
    //        [X ] [Y ] [Z ] [R ] [G ] [B ]
    //        |position  |  |colour    |
    //
    // Stride = 24 (6 × 4), Position offset = 0, Colour offset = 12
    float contacts[] = {
    //    X        Y       Z      R      G      B
        // Contact 1 — FRIENDLY (BLUE)
         0.00f,  0.66f,  0.0f,  0.20f, 0.50f, 1.00f,   // tip
        -0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,   // bottom-left
         0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,   // bottom-right

        // Contact 2 — HOSTILE (RED)
        -0.40f, -0.34f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.45f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.35f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,

        // Contact 3 — UNKNOWN (YELLOW)
         0.50f,  0.36f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.45f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.55f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
    };
    // 9 vertices × 6 floats = 54 floats × 4 bytes = 216 bytes

    // ── VAO + VBO with TWO attributes ────────────────────────────────────────
    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);                // ← RECORDING STARTS

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(contacts), contacts, GL_STATIC_DRAW);
    std::cout << "[2] " << sizeof(contacts) << " bytes on GPU (6 floats/vertex)\n";

    // Attribute 0: POSITION — 3 floats, stride=24, offset=0
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    std::cout << "[3] Attr 0 (position): stride=24, offset=0\n";

    // Attribute 1: COLOUR — 3 floats, stride=24, offset=12
    // offset=12 because position (3 floats × 4 bytes) takes the first 12 bytes
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    std::cout << "[4] Attr 1 (colour):   stride=24, offset=12\n";

    glBindVertexArray(0);                  // ← RECORDING STOPS
    std::cout << "[5] Setup complete. Render loop starting...\n";
    std::cout << "    ESC = quit\n\n";

    // ── RENDER LOOP ───────────────────────────────────────────────────────────
    // IDENTICAL to Lab 2 render loop.
    // Same glDrawArrays(GL_TRIANGLES, 0, 9) call.
    // Different output because VAO now has 2 attributes.
    // This proves VAO power: describe once, change data not draw calls.
    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        glClearColor(0.02f, 0.03f, 0.07f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProg);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 9);   // 9 vertices = 3 contacts

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProg);
    glfwTerminate();
    return 0;
}
✓ Lab 3 Deliverable
Three colour-coded threat contacts on the display
  • Top-centre contact = BLUE (friendly vessel)
  • Lower-left contact = RED (hostile vessel)
  • Right-centre contact = YELLOW (unknown contact)
  • Each triangle is a solid uniform colour (no gradient per vertex — all 3 vertices same colour)
  • Terminal shows 5 setup messages with correct stride/offset values
Lab 4 of 5 ⬛⬛⬛ Intermediate+ · ~35 min
Display Boundary and Grid Lines with EBO
Add the tactical display boundary rectangle and crosshair grid lines. The boundary uses an EBO (Element Buffer Object) with 4 corner vertices and 4 indices, drawn with GL_LINE_LOOP. The crosshair is drawn with GL_LINES. A second VAO + VBO + EBO is created alongside the contact VAO from Lab 3. Press W to toggle wireframe. This is Demo 5 (EBO) applied to the tactical scenario.
📋 Concepts: EBO · glDrawElements · GL_LINE_LOOP · GL_LINES · Two VAOs ⚡ Do Demo 5 first
➕ What This Lab Adds Over Lab 3
  • Second VAO + VBO + EBO for the display boundary + crosshair grid
  • 8 grid vertices total: 4 boundary corners (vertices 0-3) + 4 crosshair points (vertices 4-7)
  • EBO index list {0,1,2,3} + GL_LINE_LOOP draws boundary as a closed rectangle outline
  • GL_LINES draws vertices 4-7 as 2 crosshair line segments (horizontal + vertical)
  • Render loop now has 3 draw calls: contacts + boundary + crosshair
  • W key toggles wireframe mode on contact triangles
Grid VAO Vertex Layout — 8 Vertices Total
  Vertices 0-3: boundary rectangle corners
  0(-0.9, 0.9) ──────────────────── 1(0.9, 0.9)
        |                                  |
        |         tactical display         |
        |                                  |
  3(-0.9,-0.9) ──────────────────── 2(0.9,-0.9)

  EBO index list {0, 1, 2, 3} + GL_LINE_LOOP = closed rectangle outline
  (draws: 0→1, 1→2, 2→3, 3→0)

  Vertices 4-5: horizontal crosshair
  4(-0.9, 0.0) ─── vertical centre ─── 5(0.9, 0.0)

  Vertices 6-7: vertical crosshair
                6(0.0, 0.9)
                     |
                     |
                7(0.0,-0.9)

  GL_LINES with start=4, count=4 draws: 4→5 (horizontal) and 6→7 (vertical)
Objectives
  • Contact VAO + VBO from Lab 3 unchanged and still working
  • 8 grid vertices defined (X Y Z R G B format, dim grey colour)
  • EBO index list {0,1,2,3} created for boundary rectangle
  • Grid VAO + VBO + EBO created and configured
  • Boundary drawn with glDrawElements(GL_LINE_LOOP, 4, ...)
  • Crosshair drawn with glDrawArrays(GL_LINES, 4, 4)
  • W key toggles wireframe on/off
Complete src/main.cpp — Replace Entire File with This
src/main.cpp — Lab 4 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · LAB 4 of 5
//  Tactical Contact Display — Boundary + Grid Lines with EBO
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  TacticalDisplay
//  FOLDER:        C:\Labs\TacticalDisplay\
//
//  WHAT THIS LAB ADDS OVER LAB 3:
//  - A second VAO + VBO + EBO for the display boundary + grid lines
//  - EBO (Element Buffer Object) stores index list for boundary rectangle
//  - GL_LINE_LOOP draws the 4 corners as a closed rectangle outline
//  - GL_LINES draws 2 crosshair lines dividing display into quadrants
//  - Two separate draw calls per frame: one for contacts, one for grid
//  - Press W to toggle wireframe on contacts (to see triangle edges)
//
//  WHAT YOU WILL SEE:
//  - Blue/red/yellow contact triangles (same as Lab 3)
//  - Dim grey rectangle outline around the display boundary
//  - Dim horizontal + vertical crosshair lines (quadrant dividers)
//  - W key: toggle wireframe
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — unchanged from Lab 3 (position + colour, 2 attributes)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;
    out vec3 vColour;
    void main() {
        gl_Position = vec4(aPos, 1.0);
        vColour = aColour;
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — unchanged from Lab 3
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;
    void main() {
        FragColor = vec4(vColour, 1.0);
    }
)GLSL";

unsigned int compileShader(unsigned int type, const char* source) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &source, NULL);
    glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) { glGetShaderInfoLog(id, 512, NULL, log); std::cerr << "Shader error:\n" << log; }
    return id;
}
unsigned int createShaderProgram(const char* vSrc, const char* fSrc) {
    unsigned int vs = compileShader(GL_VERTEX_SHADER, vSrc);
    unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fSrc);
    unsigned int prog = glCreateProgram();
    glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog);
    glDeleteShader(vs); glDeleteShader(fs);
    return prog;
}
void onResize(GLFWwindow* w, int W, int H) { glViewport(0, 0, W, H); }
void processInput(GLFWwindow* w) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(w, true);
}

int main() {

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(1024, 768, "Tactical Contact Display — Naval Ops", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    unsigned int shaderProg = createShaderProgram(vertSrc, fragSrc);
    std::cout << "[1] Shader compiled. ID = " << shaderProg << "\n";

    // ── SHAPE 1: Contact markers (from Lab 3, unchanged) ─────────────────────
    // Format: X Y Z R G B (6 floats = 24 bytes per vertex)
    float contacts[] = {
    //    X        Y       Z      R      G      B
        // Contact 1 — FRIENDLY (BLUE)
         0.00f,  0.66f,  0.0f,  0.20f, 0.50f, 1.00f,
        -0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,
         0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,
        // Contact 2 — HOSTILE (RED)
        -0.40f, -0.34f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.45f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.35f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,
        // Contact 3 — UNKNOWN (YELLOW)
         0.50f,  0.36f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.45f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.55f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
    };

    // Contact VAO + VBO (same as Lab 3)
    unsigned int contactVAO, contactVBO;
    glGenVertexArrays(1, &contactVAO);
    glBindVertexArray(contactVAO);
    glGenBuffers(1, &contactVBO);
    glBindBuffer(GL_ARRAY_BUFFER, contactVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(contacts), contacts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
    std::cout << "[2] Contact VAO+VBO ready\n";

    // ── SHAPE 2: Display boundary + crosshair grid ────────────────────────────
    //
    // 8 vertices total — same format: X Y Z R G B (6 floats, 24 bytes each)
    // All grid geometry = dim grey (R=0.22, G=0.28, B=0.36)
    //
    // Vertices 0-3: 4 corners of boundary rectangle
    // Vertex 4-5:   horizontal crosshair line (left-mid to right-mid)
    // Vertex 6-7:   vertical crosshair line (top-mid to bottom-mid)
    //
    // EBO index list {0,1,2,3} used with GL_LINE_LOOP to draw boundary outline
    // Vertices 4-7 used with GL_LINES to draw crosshair (2 line segments)
    float gridVerts[] = {
    //    X        Y       Z       R      G      B
        // Corners 0-3: boundary rectangle
        -0.90f,  0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 0: top-left
         0.90f,  0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 1: top-right
         0.90f, -0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 2: bottom-right
        -0.90f, -0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 3: bottom-left
        // Vertices 4-5: horizontal crosshair
        -0.90f,  0.00f,  0.0f,  0.18f, 0.23f, 0.30f,   // 4: left-mid
         0.90f,  0.00f,  0.0f,  0.18f, 0.23f, 0.30f,   // 5: right-mid
        // Vertices 6-7: vertical crosshair
         0.00f,  0.90f,  0.0f,  0.18f, 0.23f, 0.30f,   // 6: top-mid
         0.00f, -0.90f,  0.0f,  0.18f, 0.23f, 0.30f,   // 7: bottom-mid
    };

    // EBO index list for the boundary rectangle
    // GL_LINE_LOOP will draw edges: 0→1, 1→2, 2→3, 3→0 (automatically closes)
    unsigned int boundaryIdx[] = { 0, 1, 2, 3 };

    // Grid VAO + VBO + EBO
    unsigned int gridVAO, gridVBO, gridEBO;
    glGenVertexArrays(1, &gridVAO);
    glBindVertexArray(gridVAO);            // ← RECORDING STARTS
    glGenBuffers(1, &gridVBO);
    glBindBuffer(GL_ARRAY_BUFFER, gridVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(gridVerts), gridVerts, GL_STATIC_DRAW);
    glGenBuffers(1, &gridEBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gridEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(boundaryIdx), boundaryIdx, GL_STATIC_DRAW);
    // Same two attributes as contacts (stride=24, pos at 0, colour at 12)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);                  // ← RECORDING STOPS
    std::cout << "[3] Grid VAO+VBO+EBO ready (4 boundary corners + 4 crosshair vertices)\n";
    std::cout << "[4] Render loop starting. ESC=quit | W=wireframe toggle\n\n";

    // ── RENDER LOOP ───────────────────────────────────────────────────────────
    bool wireframe = false;
    bool wPrev = false;

    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        // W key: toggle wireframe (edge-detect — one toggle per press)
        bool wNow = (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS);
        if (wNow && !wPrev) {
            wireframe = !wireframe;
            glPolygonMode(GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
            std::cout << (wireframe ? "Wireframe ON\n" : "Wireframe OFF\n");
        }
        wPrev = wNow;

        glClearColor(0.02f, 0.03f, 0.07f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProg);

        // ── Draw 1: Contact markers (triangles) ───────────────────────────────
        glBindVertexArray(contactVAO);
        glDrawArrays(GL_TRIANGLES, 0, 9);   // 9 vertices = 3 contacts

        // ── Draw 2: Boundary rectangle outline ───────────────────────────────
        // GL_LINE_LOOP: connects indexed vertices as a closed line polygon
        // 4 indices → 4 edges drawn (0→1, 1→2, 2→3, 3→0)
        glBindVertexArray(gridVAO);
        glDrawElements(GL_LINE_LOOP, 4, GL_UNSIGNED_INT, 0);

        // ── Draw 3: Crosshair lines ───────────────────────────────────────────
        // GL_LINES: every pair of vertices = one line segment
        // Start at vertex 4 (left-mid), draw 4 vertices = 2 line segments
        // Vertices 4,5 = horizontal line | Vertices 6,7 = vertical line
        glDrawArrays(GL_LINES, 4, 4);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // ── CLEANUP ───────────────────────────────────────────────────────────────
    glDeleteVertexArrays(1, &contactVAO);
    glDeleteVertexArrays(1, &gridVAO);
    glDeleteBuffers(1, &contactVBO);
    glDeleteBuffers(1, &gridVBO);
    glDeleteBuffers(1, &gridEBO);
    glDeleteProgram(shaderProg);
    glfwTerminate();
    return 0;
}
Expected Terminal Output
[1] Shader compiled. ID = 1
[2] Contact VAO+VBO ready
[3] Grid VAO+VBO+EBO ready (4 boundary corners + 4 crosshair vertices)
[4] Render loop starting. ESC=quit | W=wireframe toggle

Screen: Blue/red/yellow contacts + dim boundary rectangle + crosshair
Press W → Wireframe ON — see triangle edges on contacts
✓ Lab 4 Deliverable
Contact markers + display boundary + quadrant grid
  • Dim grey rectangle outline around the display area
  • Horizontal + vertical crosshair dividing display into 4 quadrants
  • All 3 colour-coded contact triangles still visible and correct
  • Grid lines noticeably dimmer than contact markers
  • W key toggles wireframe on contacts
Lab 5 of 5 ⬛⬛⬛⬛ Challenge · ~40 min
Live Threat Pulse Animation
Make the display live. The hostile contact pulses between dark-red and bright-red in a continuous warning pattern. Two new uniforms are added: uTime (current time sent every frame) and uIsHostile (1.0 for hostile, 0.0 for others). The 3 contacts are drawn in separate draw calls so each can have different uniform values. This combines Demo 6 (uniforms) with everything built in Labs 1–4.
📋 Concepts: Uniforms · glfwGetTime · sin() animation · Per-draw-call uniforms ⚡ Do Demo 6 first
➕ What This Lab Adds Over Lab 4
  • Fragment shader: uniform float uTime receives seconds since start each frame
  • Fragment shader: uniform float uIsHostile activates pulse logic when 1.0
  • Pulse logic: sin(uTime × 4.0) oscillates colour between dark-red and bright-red
  • 3 separate draw calls for contacts (so each can get different uIsHostile value)
  • glGetUniformLocation called once BEFORE loop, cached integer used inside loop
  • Time value printed to terminal every 60 frames to confirm uniform updates
💡 Why Three Separate glDrawArrays Calls in This Lab

To give each contact a different uniform value (uIsHostile), we set the uniform then draw, set it again then draw, etc. glDrawArrays(GL_TRIANGLES, 0, 3) draws vertices 0-2 (Contact 1). glDrawArrays(GL_TRIANGLES, 3, 3) draws vertices 3-5 (Contact 2). glDrawArrays(GL_TRIANGLES, 6, 3) draws vertices 6-8 (Contact 3). The VAO is bound once — the start offset tells OpenGL which vertices to read. No data changes. Same VAO, different draw-call offsets.

Objectives
  • Fragment shader has both uTime and uIsHostile uniforms declared
  • Fragment shader pulse logic: sin(uTime×4.0) mapped to 0.0–1.0, applied only when uIsHostile > 0.5
  • mix(darkRed, brightRed, pulse) correctly oscillates hostile contact colour
  • uTimeLoc and uHostileLoc obtained with glGetUniformLocation BEFORE render loop
  • glfwGetTime() cast to float and sent via glUniform1f each frame
  • Contact 1: glUniform1f(uHostileLoc, 0.0f) then glDrawArrays(... 0, 3)
  • Contact 2: glUniform1f(uHostileLoc, 1.0f) then glDrawArrays(... 3, 3)
  • Contact 3: glUniform1f(uHostileLoc, 0.0f) then glDrawArrays(... 6, 3)
  • Hostile contact visibly pulsing — friendly and unknown stay steady
Complete src/main.cpp — Replace Entire File with This
src/main.cpp — Lab 5 Complete File (Day 1 Capstone)
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · LAB 5 of 5
//  Tactical Contact Display — Live Threat Pulse Animation
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  TacticalDisplay
//  FOLDER:        C:\Labs\TacticalDisplay\
//
//  WHAT THIS LAB ADDS OVER LAB 4:
//  - uTime uniform: current time (seconds) sent to GPU every frame
//  - uIsHostile uniform: 1.0 for hostile contact, 0.0 for others
//  - Fragment shader: hostile contact pulses between dark-red and bright-red
//  - Contacts drawn in 3 separate draw calls so each can have different uniform values
//  - W key: wireframe toggle (from Lab 4)
//
//  HOW THE PULSING WORKS:
//  sin(uTime × 4.0) oscillates between -1 and +1 (4× normal speed)
//  (sin() + 1.0) / 2.0 maps to 0.0 – 1.0
//  mix(darkRed, brightRed, 0.0–1.0) = colour oscillates between dark and bright
//  Only applied when uIsHostile == 1.0
//
//  WHAT YOU WILL SEE:
//  - Friendly (blue triangle): steady — no pulse
//  - HOSTILE (red triangle): visibly pulsing brighter/darker — warning pattern
//  - Unknown (yellow triangle): steady — no pulse
//  - Boundary + crosshair: steady dim grey (same as Lab 4)
//  - Terminal: time printed every 60 frames to confirm uniform is updating
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — unchanged from Lab 4
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;
    out vec3 vColour;
    void main() {
        gl_Position = vec4(aPos, 1.0);
        vColour = aColour;
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — NEW: uses uTime and uIsHostile uniforms
//
// uniform float uTime      → seconds since program start, sent every frame
// uniform float uIsHostile → 1.0 = this is hostile, 0.0 = not hostile
//
// if uIsHostile == 1.0: override colour with pulsing red effect
// else:                 use vColour from vertex shader unchanged
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;

    uniform float uTime;       // sent from C++ every frame via glUniform1f
    uniform float uIsHostile;  // 1.0 = hostile contact, 0.0 = not hostile

    void main() {
        vec3 col = vColour;   // start with the colour from vertex shader

        if (uIsHostile > 0.5) {
            // pulse: sin oscillates -1 to +1 → map to 0.0–1.0 for brightness
            // multiply by 4.0 for faster warning pulse rate
            float pulse = (sin(uTime * 4.0) + 1.0) / 2.0;
            // mix(a, b, t) = a*(1-t) + b*t
            // at pulse=0.0: dark red (0.30, 0.04, 0.04)
            // at pulse=1.0: bright red (1.00, 0.18, 0.18)
            col = mix(vec3(0.30, 0.04, 0.04), vec3(1.00, 0.18, 0.18), pulse);
        }

        FragColor = vec4(col, 1.0);
    }
)GLSL";

unsigned int compileShader(unsigned int type, const char* source) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &source, NULL);
    glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) { glGetShaderInfoLog(id, 512, NULL, log); std::cerr << "Shader error:\n" << log; }
    return id;
}
unsigned int createShaderProgram(const char* vSrc, const char* fSrc) {
    unsigned int vs = compileShader(GL_VERTEX_SHADER, vSrc);
    unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fSrc);
    unsigned int prog = glCreateProgram();
    glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog);
    glDeleteShader(vs); glDeleteShader(fs);
    return prog;
}
void onResize(GLFWwindow* w, int W, int H) { glViewport(0, 0, W, H); }
void processInput(GLFWwindow* w) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(w, true);
}

int main() {

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(1024, 768, "Tactical Contact Display — Naval Ops", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    unsigned int shaderProg = createShaderProgram(vertSrc, fragSrc);
    std::cout << "[1] Shader compiled. ID = " << shaderProg << "\n";

    // ── Contact markers (format: X Y Z R G B — same as Labs 3 and 4) ─────────
    float contacts[] = {
    //    X        Y       Z      R      G      B
        // Contact 1 — FRIENDLY (BLUE)
         0.00f,  0.66f,  0.0f,  0.20f, 0.50f, 1.00f,
        -0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,
         0.05f,  0.54f,  0.0f,  0.20f, 0.50f, 1.00f,
        // Contact 2 — HOSTILE (RED — but fragment shader overrides with pulse)
        -0.40f, -0.34f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.45f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,
        -0.35f, -0.46f,  0.0f,  1.00f, 0.20f, 0.20f,
        // Contact 3 — UNKNOWN (YELLOW)
         0.50f,  0.36f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.45f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
         0.55f,  0.24f,  0.0f,  1.00f, 0.85f, 0.10f,
    };

    unsigned int contactVAO, contactVBO;
    glGenVertexArrays(1, &contactVAO);
    glBindVertexArray(contactVAO);
    glGenBuffers(1, &contactVBO);
    glBindBuffer(GL_ARRAY_BUFFER, contactVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(contacts), contacts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
    std::cout << "[2] Contact VAO+VBO ready\n";

    // ── Boundary + crosshair grid (same as Lab 4) ─────────────────────────────
    float gridVerts[] = {
    //    X        Y       Z       R      G      B
        -0.90f,  0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 0: boundary top-left
         0.90f,  0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 1: boundary top-right
         0.90f, -0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 2: boundary bottom-right
        -0.90f, -0.90f,  0.0f,  0.22f, 0.28f, 0.36f,   // 3: boundary bottom-left
        -0.90f,  0.00f,  0.0f,  0.18f, 0.23f, 0.30f,   // 4: crosshair left-mid
         0.90f,  0.00f,  0.0f,  0.18f, 0.23f, 0.30f,   // 5: crosshair right-mid
         0.00f,  0.90f,  0.0f,  0.18f, 0.23f, 0.30f,   // 6: crosshair top-mid
         0.00f, -0.90f,  0.0f,  0.18f, 0.23f, 0.30f,   // 7: crosshair bottom-mid
    };
    unsigned int boundaryIdx[] = { 0, 1, 2, 3 };

    unsigned int gridVAO, gridVBO, gridEBO;
    glGenVertexArrays(1, &gridVAO);
    glBindVertexArray(gridVAO);
    glGenBuffers(1, &gridVBO);
    glBindBuffer(GL_ARRAY_BUFFER, gridVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(gridVerts), gridVerts, GL_STATIC_DRAW);
    glGenBuffers(1, &gridEBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gridEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(boundaryIdx), boundaryIdx, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
    std::cout << "[3] Grid VAO+VBO+EBO ready\n";

    // ── Get uniform locations BEFORE the loop (call once, cache the ID) ───────
    // glGetUniformLocation is slow — never call inside the render loop
    int uTimeLoc     = glGetUniformLocation(shaderProg, "uTime");
    int uHostileLoc  = glGetUniformLocation(shaderProg, "uIsHostile");
    std::cout << "[4] uTime location = " << uTimeLoc
              << " | uIsHostile location = " << uHostileLoc << "\n";
    std::cout << "[5] Render loop starting. ESC=quit | W=wireframe\n\n";

    // ── RENDER LOOP ───────────────────────────────────────────────────────────
    bool wireframe = false;
    bool wPrev = false;
    int frameCount = 0;

    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        // W key wireframe toggle
        bool wNow = (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS);
        if (wNow && !wPrev) {
            wireframe = !wireframe;
            glPolygonMode(GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
            std::cout << (wireframe ? "Wireframe ON\n" : "Wireframe OFF\n");
        }
        wPrev = wNow;

        glClearColor(0.02f, 0.03f, 0.07f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProg);

        // Send current time to shader every frame
        float t = (float)glfwGetTime();
        glUniform1f(uTimeLoc, t);

        // Print time to terminal every 60 frames — confirm uniform updates
        frameCount++;
        if (frameCount % 60 == 0) {
            std::cout << "Frame " << frameCount << ": uTime = " << t << "s\n";
        }

        glBindVertexArray(contactVAO);

        // ── Draw Contact 1: FRIENDLY — uIsHostile=0.0, no pulse ──────────────
        glUniform1f(uHostileLoc, 0.0f);
        glDrawArrays(GL_TRIANGLES, 0, 3);   // vertices 0,1,2

        // ── Draw Contact 2: HOSTILE — uIsHostile=1.0, pulse ACTIVE ───────────
        glUniform1f(uHostileLoc, 1.0f);
        glDrawArrays(GL_TRIANGLES, 3, 3);   // vertices 3,4,5

        // ── Draw Contact 3: UNKNOWN — uIsHostile=0.0, no pulse ───────────────
        glUniform1f(uHostileLoc, 0.0f);
        glDrawArrays(GL_TRIANGLES, 6, 3);   // vertices 6,7,8

        // ── Draw boundary + crosshair (not hostile, no pulse) ─────────────────
        glUniform1f(uHostileLoc, 0.0f);
        glBindVertexArray(gridVAO);
        glDrawElements(GL_LINE_LOOP, 4, GL_UNSIGNED_INT, 0);   // boundary outline
        glDrawArrays(GL_LINES, 4, 4);                           // crosshair lines

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // ── CLEANUP ───────────────────────────────────────────────────────────────
    glDeleteVertexArrays(1, &contactVAO);
    glDeleteVertexArrays(1, &gridVAO);
    glDeleteBuffers(1, &contactVBO);
    glDeleteBuffers(1, &gridVBO);
    glDeleteBuffers(1, &gridEBO);
    glDeleteProgram(shaderProg);
    glfwTerminate();
    std::cout << "\nTactical display shutdown. Day 1 complete.\n";
    return 0;
}
Expected Terminal Output
[1] Shader compiled. ID = 1
[2] Contact VAO+VBO ready
[3] Grid VAO+VBO+EBO ready
[4] uTime location = 0 | uIsHostile location = 1
[5] Render loop starting. ESC=quit | W=wireframe

Frame 60: uTime = 1.002s
Frame 120: uTime = 2.003s

Screen: Friendly (steady blue) + HOSTILE (pulsing red) + Unknown (steady yellow)
Dim boundary rectangle + crosshair grid visible
🔬 Bonus Challenges — If You Finish Early
  • Add a 4th contact (missile): Add 3 more vertices to contacts[] (green colour, any position). Add a 4th draw call with uIsHostile=1.0 but faster pulse (change 4.0 to 8.0 in the shader). You'll need to pass uPulseSpeed as a second uniform or hardcode differently.
  • Animate contact position: Add uniform float uOffset to vertex shader. Apply to X position: gl_Position = vec4(aPos.x + uOffset, aPos.y, aPos.z, 1.0). Send sin(t) * 0.3f each frame — hostile contact drifts left and right.
  • Make unknown flash: Set uIsHostile=0.5 for the unknown contact. In the fragment shader, check uIsHostile > 0.3 && uIsHostile < 0.7 and apply a slower yellow pulse.
✓ Lab 5 Deliverable — Day 1 Capstone Complete
Fully animated tactical contact display
Show your instructor the complete running system:
  • FRIENDLY contact (blue triangle) — steady, does not pulse
  • HOSTILE contact (red triangle) — visibly pulsing dark-red to bright-red continuously
  • UNKNOWN contact (yellow triangle) — steady, does not pulse
  • Display boundary rectangle (dim grey, drawn as a line outline)
  • Crosshair grid lines dividing display into quadrants
  • Animation runs smoothly at 60fps — no flicker
  • Terminal prints time value every 60 frames
  • Can explain: what a VBO is, what a VAO records, what a uniform does
← All Labs
By Raushan Ranjan (MCT | Educator)
Koenig Original AI-Courseware · Day 1 Complete
Day 2 Labs — Coming Soon →