Demo 1 · Module 1Concept Visual — No Code
The Rendering Pipeline — What the GPU Does
Before writing one line of code, understand where your code fits inside the GPU assembly line. This demo is purely conceptual. Understand the pipeline first and every function call you write later makes immediate sense.
🎓 Concept only — 15 minNo code file needed
🎯 What You Will Understand
Why a GPU exists and how it differs from a CPU
The 7 pipeline stages — which two you write, which five are automatic
What a vertex shader does vs what a fragment shader does
Why OpenGL exists as a layer between your C++ and the GPU driver
💡 The Assembly Line Analogy

Think of a car factory. Steel enters one end, a finished car exits the other. Each station has exactly one job. You only write code for two stations: the Vertex Shader (positions each vertex on screen) and the Fragment Shader (decides the colour of each pixel). Everything else is automatic GPU hardware.

The 7-Stage Rendering Pipeline — Your code lives in the ✏ stages only
CPU DATA float vertices[] VBO / VAO VERTEX SHADER ✏ YOU WRITE 3D pos → NDC screen camera + projection PRIMITIVE ASSEMBLY connects vertices RASTER- IZATION triangles → fragments FRAGMENT SHADER ✏ YOU WRITE pixel colour lighting, texture, fx DEPTH / STENCIL discard hidden FRAME- BUFFER → Screen pixels! You write GLSL for these stages Fixed-function — automatic GPU hardware THE 7-STAGE RENDERING PIPELINE Raw numbers enter left. Coloured pixels exit right.
💡 Key Insight — Why Two Separate Shader Stages?

Vertex Shader — runs once per vertex. Triangle = 3 vertices = runs 3 times. Job: where does this point go on screen? (position math)

Fragment Shader — runs once per pixel the triangle covers. A triangle covering 5,000 pixels runs 5,000 times simultaneously on GPU cores. Job: what colour is this pixel? (colour, texture, lighting)

This is why GPUs are fast — the fragment shader runs massively in parallel across thousands of shader cores at the exact same time.

Demo 2 · Module 2Live Code — Visible Output in Window + Terminal
First OpenGL Window — Context, Loop, Proof
Create the window and render loop from scratch. Every single line is explained with inline comments. This demo produces two visible outputs: a dark window on screen AND printed hardware info in the terminal. This confirms your entire environment is working before any geometry is drawn.
💻 Project: M02_Window⏱ ~20 min📁 C:\Labs\M02_Window\
TEACHES: glfwInitglfwCreateWindowOpenGL ContextglewInitRender LoopDouble Buffering
🎯 What You Will See When This Runs
Screen: Dark navy window 800×600 titled “RR Graphics Lab - Demo 2”
Terminal: 5 numbered steps printed, then GPU name + OpenGL version — proof context is live
ESC key: Window closes cleanly — proves input handling works
Create This Folder Structure First
C:\Labs\M02_Window\
├── CMakeLists.txt     ← copy from below
└── src\
    └── main.cpp           ← copy from below
💡 What CMake Actually Does

CMake is not a compiler. It is a project file generator. You write one CMakeLists.txt, CMake reads it, and generates a Visual Studio .sln + .vcxproj for you. The advantage: same file works on Windows, Linux, Mac. No manual project settings.

CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M02_Window)
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(M02_Window src/main.cpp)
target_link_libraries(M02_Window
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
main.cpp — Complete Program, Every Line Commented
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · DEMO 2
//  First OpenGL Window
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. Use the CMakeLists.txt from the website (project name: M02_Window)
//  3. Build and run — you will see a dark window + GPU info in terminal
//
//  WHAT THIS DEMO TEACHES:
//  - GLFW  → creates the OS window and OpenGL context
//  - GLEW  → loads all OpenGL function pointers from the GPU driver
//  - The render loop → the infinite loop that runs every frame
//  - Double buffering → why glfwSwapBuffers exists
// ═══════════════════════════════════════════════════════════════════════════

// RULE: GLEW must be included BEFORE GLFW — always, no exceptions
// GLEW_STATIC means we link the .lib file directly — no .dll needed at runtime
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// CALLBACK FUNCTION: onResize
// Called automatically by GLFW whenever the user resizes the window.
// We register it below with glfwSetFramebufferSizeCallback.
// glViewport tells OpenGL: "your drawing area is now this many pixels wide/tall"
// ─────────────────────────────────────────────────────────────────────────────
void onResize(GLFWwindow* window, int width, int height) {
    // (0, 0) = start from bottom-left corner of the window
    // width, height = new size in pixels after resize
    glViewport(0, 0, width, height);
}

// ─────────────────────────────────────────────────────────────────────────────
// FUNCTION: processInput
// Called once per frame inside the render loop.
// Checks if ESC is pressed and signals the window to close.
// ─────────────────────────────────────────────────────────────────────────────
void processInput(GLFWwindow* window) {
    // glfwGetKey returns GLFW_PRESS if the key is currently held down
    // glfwSetWindowShouldClose sets the internal "please quit" flag to true
    // The render loop reads this flag via glfwWindowShouldClose()
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// ─────────────────────────────────────────────────────────────────────────────
// MAIN FUNCTION
// Everything happens here: init → window → context → loop → cleanup
// ─────────────────────────────────────────────────────────────────────────────
int main() {

    std::cout << "\n=== RR Graphics Lab - Demo 2: First Window ===\n\n";

    // ── STEP 1: Initialise GLFW ───────────────────────────────────────────────
    // Must be the very first GLFW call.
    // Sets up GLFW's internal state and detects the operating system.
    if (!glfwInit()) {
        std::cerr << "ERROR: GLFW init failed\n";
        return -1;
    }
    std::cout << "[1/5] GLFW initialised OK\n";

    // ── STEP 2: Tell GLFW what kind of OpenGL context we want ─────────────────
    // These MUST be set before glfwCreateWindow — they are like a specification form.
    // We want OpenGL 3.3, Core Profile (modern only, no legacy functions).
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // major version = 3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);  // minor version = 3 → so 3.3
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // CORE_PROFILE = modern OpenGL only (recommended)
    // Alternative: GLFW_OPENGL_COMPAT_PROFILE = includes legacy functions (not needed)

    // ── STEP 3: Create the window AND the OpenGL context ─────────────────────
    // glfwCreateWindow does TWO things at once:
    //   1. Creates a native OS window (same as any other window on your desktop)
    //   2. Creates an OpenGL context tied to that window
    // Parameters: width, height, title, monitor (NULL = windowed), context to share (NULL = none)
    GLFWwindow* window = glfwCreateWindow(
        800, 600,
        "RR Graphics Lab - Demo 2",
        NULL,
        NULL
    );
    if (!window) {
        std::cerr << "ERROR: Window creation failed\n";
        glfwTerminate();
        return -1;
    }
    std::cout << "[2/5] Window + OpenGL context created (800x600)\n";

    // ── STEP 4: Make the context current on this thread ───────────────────────
    // "Current" = all gl* function calls on this thread now operate on THIS context.
    // CRITICAL: glewInit() WILL FAIL silently if no context is current.
    // So this call must happen BEFORE glewInit().
    glfwMakeContextCurrent(window);

    // Register our resize callback function.
    // GLFW will call onResize(window, newWidth, newHeight) automatically on resize.
    glfwSetFramebufferSizeCallback(window, onResize);
    std::cout << "[3/5] Context made current + resize callback registered\n";

    // ── STEP 5: Initialise GLEW ───────────────────────────────────────────────
    // OpenGL functions like glClear, glGenBuffers etc. are NOT in a standard library.
    // They live inside the GPU driver installed on your machine.
    // GLEW finds each function pointer inside the driver and gives your code access.
    // Without GLEW: calling glClear() = crash (unresolved function pointer).
    glewExperimental = GL_TRUE; // needed for full extension loading support
    if (glewInit() != GLEW_OK) {
        std::cerr << "ERROR: GLEW init failed\n";
        return -1;
    }
    std::cout << "[4/5] GLEW initialised — all OpenGL functions now accessible\n";

    // Print hardware information — proof the context is alive
    std::cout << "\n--- Hardware Info ---\n";
    std::cout << "GPU     : " << glGetString(GL_RENDERER) << "\n";
    std::cout << "OpenGL  : " << glGetString(GL_VERSION)  << "\n";
    std::cout << "GLSL    : " << glGetString(GL_SHADING_LANGUAGE_VERSION) << "\n";
    std::cout << "Vendor  : " << glGetString(GL_VENDOR)   << "\n";
    std::cout << "---------------------\n\n";

    std::cout << "[5/5] Starting render loop. Press ESC to quit.\n\n";

    // ── STEP 6: THE RENDER LOOP ───────────────────────────────────────────────
    // This loop runs continuously — once per frame, ~60 times per second.
    // glfwWindowShouldClose returns true when:
    //   - User clicks the X button on the window, OR
    //   - We call glfwSetWindowShouldClose(window, true) — e.g. on ESC
    while (!glfwWindowShouldClose(window)) {

        // ① Check input — is ESC pressed?
        processInput(window);

        // ② Set the colour used to clear the screen
        // RGBA values: 0.0 to 1.0  (tip: divide any RGB 0-255 value by 255)
        // This is a dark navy blue: R=0.05, G=0.08, B=0.15
        glClearColor(0.05f, 0.08f, 0.15f, 1.0f);

        // ③ Clear the BACK buffer to the colour we just set
        // GL_COLOR_BUFFER_BIT = clear the colour/image buffer
        // (We will also clear GL_DEPTH_BUFFER_BIT once we add 3D in Module 4)
        glClear(GL_COLOR_BUFFER_BIT);

        // ④ DRAW CALLS GO HERE — added from Demo 3 onward
        // (This demo has no geometry to draw — just proving the window works)

        // ⑤ SWAP BUFFERS — show the completed frame on screen
        // OpenGL uses DOUBLE BUFFERING:
        //   Back buffer  = where you draw (invisible)
        //   Front buffer = what is displayed on screen
        // SwapBuffers makes back→front in one atomic operation (no flicker)
        // Without this call: screen stays black even after glClear
        glfwSwapBuffers(window);

        // ⑥ POLL EVENTS — process keyboard, mouse, resize events
        // Without this: window freezes completely (cannot move, resize, or close)
        glfwPollEvents();
    }

    // ── CLEANUP ───────────────────────────────────────────────────────────────
    // Free all GLFW resources: windows, contexts, monitors, callbacks
    glfwTerminate();
    std::cout << "Window closed cleanly.\n";
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\M02_Window
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M02_Window.exe
Expected Terminal Output
[1/5] GLFW initialised OK
[2/5] Window + OpenGL context created (800x600)
[3/5] Context made current + resize callback registered
[4/5] GLEW initialised — all OpenGL functions now accessible

--- Hardware Info ---
GPU : NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2
OpenGL : 3.3.0 NVIDIA 546.01
GLSL : 3.30 NVIDIA via Cg compiler
Vendor : NVIDIA Corporation

[5/5] Starting render loop. Press ESC to quit.
🔬 3 Experiments — Do These One at a Time
  • Change background colour: glClearColor(1.0f, 0.0f, 0.0f, 1.0f) → bright red. RGB values 0.0–1.0. Also try green (0,1,0,1) and black (0,0,0,1).
  • Comment out glfwPollEvents(): Rebuild and run. Window freezes — cannot move, resize, or close. Proves polling is mandatory every single frame.
  • Comment out glfwSwapBuffers(): Rebuild. Screen stays black even after glClearColor sets navy blue. Proves double buffering — you drew to back buffer but never showed it.
Demo 3 · Module 3Live Code — VBO + VAO + Shaders All Together
VBO → VAO → Shaders → Triangle — The Complete Journey
The most important demo of Day 1. VBO, VAO, and shaders only make sense together — showing them separately produces no visible output and creates confusion. This demo builds all four as one flowing system. Remove any one piece and nothing appears on screen. The terminal prints each step so you see exactly what is happening before pixels show up.
💻 Project: M03_Triangle⏱ ~35 min🔑 Most important demo of Day 1
BUILDS ON: Demo 2 window+ VBO: upload data+ VAO: describe layout+ Vertex Shader+ Fragment Shader= Orange Triangle ✓
🎯 What You Will See
Screen: Solid orange triangle on dark background — your first rendered geometry ever
Terminal: 6 numbered steps confirming shader compiled, VAO bound, VBO uploaded, layout described
Break test: Comment out glUseProgram → black screen. Comment out glBindVertexArray → black screen. Each piece is essential.
How VBO, VAO and Shaders Connect Together
  CPU (your C++ code)                        GPU
  ┌─────────────────────────────┐           ┌────────────────────────────────────────┐
  │ float verts[] = {           │ PCIe  │ VBO (GPU VRAM buffer)               │
  │  -0.5,-0.5, 0.5,-0.5, 0,0.5 │─────▲│ [-0.5,-0.5,0, 0.5,-0.5,0, 0,0.5,0]│
  │ };                           │       │                                    │
  │ glGenBuffers → VBO ID        │       │ VAO (records the layout):          │
  │ glBindBuffer → select it     │       │ "attr0 = 3 floats, stride=12,off=0"│
  │ glBufferData → copy data     │       │                                    │
  │                             │       │ Vertex Shader (runs 3x for triangle)│
  │ glGenVertexArrays → VAO ID  │       │   in vec3 aPos → gl_Position       │
  │ glBindVertexArray → RECORD  │       │                                    │
  │ glVertexAttribPointer        │       │ Fragment Shader (per pixel):       │
  │ glEnableVertexAttribArray    │       │   FragColor = orange vec4           │
  └─────────────────────────────┘           └────────────────────────────────────────┘
⚠ Critical Order Rule

Bind VAO first → then bind VBO → then call glVertexAttribPointer. The VAO records everything that happens while it is bound. Think of it as pressing Record before describing the layout. Bind VAO after VBO and it will not capture the layout.

CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M03_Triangle)
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(M03_Triangle src/main.cpp)
target_link_libraries(M03_Triangle
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · DEMO 3
//  VBO + VAO + Shaders + Triangle — All Together, One Complete File
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. Use the CMakeLists.txt from the website (project name: M03_Triangle)
//  3. Build and run — orange triangle appears + terminal shows all steps
//
//  WHAT THIS DEMO TEACHES:
//  - VBO  → uploads your float[] array from CPU RAM to GPU VRAM
//  - VAO  → records HOW to read the VBO bytes (stride, offset, type)
//  - Vertex Shader  → tiny GPU program: positions each vertex on screen
//  - Fragment Shader → tiny GPU program: decides the colour of each pixel
//  All four work TOGETHER — removing any one makes nothing appear.
//
//  BREAK-TO-LEARN EXPERIMENTS (after it runs):
//  - Comment out glUseProgram(shader)   → black screen, no shader active
//  - Comment out glBindVertexArray(VAO) → black screen, GPU has no data
//  - Change vec4(1.0, 0.5, 0.2, 1.0) in fragment shader → change colour
//  - Move vertex positions in the verts[] array → triangle moves
// ═══════════════════════════════════════════════════════════════════════════

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

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER SOURCE (GLSL)
//
// This string is sent to the GPU driver at runtime — the driver compiles it.
// It is NOT compiled by your C++ compiler or CMake.
//
// layout(location = 0) in vec3 aPos
//   "location=0" → attribute slot 0 — MUST match glVertexAttribPointer(0, ...)
//   "in"         → data comes IN from the VBO per vertex
//   "vec3"       → 3 floats (x, y, z)
//   "aPos"       → your chosen name — can be anything (myPos, pos, vertex etc.)
//
// gl_Position
//   → BUILT-IN output variable. Required. GPU uses it for rasterisation.
//   → Must be vec4. We promote vec3 to vec4 by adding w=1.0 (standard for positions)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertexShaderSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;

    void main() {
        gl_Position = vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER SOURCE (GLSL)
//
// Runs once per pixel (fragment) that the triangle covers.
// A triangle covering 5000 pixels → this runs 5000 times simultaneously.
//
// out vec4 FragColor
//   "out"       → output from this shader = the final pixel colour
//   "vec4"      → 4 floats: Red, Green, Blue, Alpha
//   "FragColor" → your chosen name — can be anything (outColour, pixelColour etc.)
//
// vec4(1.0, 0.5, 0.2, 1.0) → orange colour
//   R=1.0 (full red), G=0.5 (half green), B=0.2 (low blue), A=1.0 (fully opaque)
// ─────────────────────────────────────────────────────────────────────────────
const char* fragmentShaderSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;

    void main() {
        FragColor = vec4(1.0, 0.5, 0.2, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// HELPER: compileShader
// Compiles one shader (vertex OR fragment) and returns its GPU-side ID.
// The GPU driver does the compilation — not the C++ compiler.
// Always check for errors — GLSL syntax errors show up here at runtime.
// ─────────────────────────────────────────────────────────────────────────────
unsigned int compileShader(unsigned int type, const char* source) {
    // glCreateShader allocates a shader object on the GPU, returns its integer ID
    // type = GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
    unsigned int shaderID = glCreateShader(type);

    // Give the GLSL source string to the GPU driver
    // Parameters: (shaderID, number_of_strings, pointer_to_strings, string_lengths)
    // NULL for lengths = driver reads until null terminator
    glShaderSource(shaderID, 1, &source, NULL);

    // Tell the GPU driver to compile the GLSL source
    glCompileShader(shaderID);

    // Check for compilation errors — IMPORTANT, always do this
    int success;
    char infoLog[512];
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(shaderID, 512, NULL, infoLog);
        std::cerr << "ERROR: Shader compile failed:\n" << infoLog << "\n";
    }

    return shaderID;
}

// ─────────────────────────────────────────────────────────────────────────────
// HELPER: createShaderProgram
// Links a compiled vertex shader + fragment shader into one shader program.
// The program is what you activate with glUseProgram() before drawing.
// ─────────────────────────────────────────────────────────────────────────────
unsigned int createShaderProgram(const char* vertSrc, const char* fragSrc) {
    unsigned int vs   = compileShader(GL_VERTEX_SHADER,   vertSrc);
    unsigned int fs   = compileShader(GL_FRAGMENT_SHADER, fragSrc);
    unsigned int prog = glCreateProgram();

    glAttachShader(prog, vs);   // attach vertex shader
    glAttachShader(prog, fs);   // attach fragment shader
    glLinkProgram(prog);        // link them together into one executable

    // After linking, individual shaders are no longer needed
    // The program object contains everything
    glDeleteShader(vs);
    glDeleteShader(fs);

    return prog;
}

// ─────────────────────────────────────────────────────────────────────────────
// CALLBACKS (same as Demo 2)
// ─────────────────────────────────────────────────────────────────────────────
void onResize(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

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

    std::cout << "\n=== RR Graphics Lab - Demo 3: First Triangle ===\n\n";

    // ── WINDOW SETUP (same as Demo 2 — you already know this) ────────────────
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Demo 3 - First Triangle", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }

    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);

    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) { return -1; }

    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 1: COMPILE + LINK SHADER PROGRAM
    // Create the GPU-side program from our GLSL source strings above.
    // ─────────────────────────────────────────────────────────────────────────
    unsigned int shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);
    std::cout << "[1] Shader program compiled + linked. GPU program ID = " << shaderProgram << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 2: DEFINE VERTEX DATA ON THE CPU
    //
    // A triangle has 3 corners (vertices). Each vertex = 3 floats (X, Y, Z).
    // These coordinates are in NDC (Normalized Device Coordinates):
    //   X: -1.0 = left edge,  0.0 = centre,  +1.0 = right edge
    //   Y: -1.0 = bottom,     0.0 = centre,  +1.0 = top
    //   Z:  0.0 = on the screen plane (no depth for now)
    //
    // RIGHT NOW this data lives in CPU RAM — the GPU cannot read it yet.
    // ─────────────────────────────────────────────────────────────────────────
    float verts[] = {
    //    X       Y       Z
        -0.5f,  -0.5f,   0.0f,   // vertex 0: bottom-left corner
         0.5f,  -0.5f,   0.0f,   // vertex 1: bottom-right corner
         0.0f,   0.5f,   0.0f    // vertex 2: top-centre
    };
    // Total: 3 vertices x 3 floats = 9 floats x 4 bytes = 36 bytes

    std::cout << "[2] Vertex data defined in CPU RAM: "
              << sizeof(verts) << " bytes ("
              << sizeof(verts)/sizeof(float) << " floats)\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 3: CREATE VAO — Vertex Array Object
    //
    // The VAO is a recorder. While it is bound (active), it remembers:
    //   - Which VBO is bound
    //   - How to interpret the VBO bytes (stride, offset, type, count)
    //   - Which attribute locations are enabled
    //
    // WHY: In the render loop, you need to restore all this state every frame.
    // Without VAO: 5+ calls per draw. With VAO: 1 call (glBindVertexArray).
    //
    // IMPORTANT ORDER: Bind VAO FIRST, then create VBO, then glVertexAttribPointer.
    // The VAO must be recording BEFORE you describe the layout.
    // ─────────────────────────────────────────────────────────────────────────
    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);  // allocate 1 VAO on GPU, store ID in VAO
    glBindVertexArray(VAO);      // ← RECORDING STARTS HERE

    std::cout << "[3] VAO created and bound. VAO ID = " << VAO << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 4: CREATE VBO — Vertex Buffer Object
    // Upload our triangle vertices from CPU RAM to GPU VRAM.
    //
    // Three sub-steps always happen in this order:
    //   glGenBuffers   → allocate buffer on GPU (get ID)
    //   glBindBuffer   → select this buffer as active (state machine pattern)
    //   glBufferData   → copy data from CPU to GPU
    // ─────────────────────────────────────────────────────────────────────────
    glGenBuffers(1, &VBO);
    // glGenBuffers(count, id_array)
    //   Allocates 1 buffer object on the GPU. Returns its integer ID.
    //   The ID is just a number — think of it as a "handle" or "label".

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // glBindBuffer(target, id)
    //   GL_ARRAY_BUFFER = the slot for vertex attribute data
    //   VBO             = use our buffer in this slot
    //   OpenGL is a state machine: you SELECT a buffer, then OPERATE on it.

    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    // glBufferData(target, size_in_bytes, data_pointer, usage_hint)
    //   GL_ARRAY_BUFFER = operates on the currently bound buffer (our VBO)
    //   sizeof(verts)   = 36 bytes to copy
    //   verts           = pointer to the start of our CPU float array
    //   GL_STATIC_DRAW  = hint: this data will not change → GPU can optimise placement

    std::cout << "[4] VBO created. " << sizeof(verts)
              << " bytes copied from CPU RAM to GPU VRAM. VBO ID = " << VBO << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 5: DESCRIBE THE VERTEX LAYOUT — glVertexAttribPointer
    //
    // The GPU now has 36 raw bytes. It has NO IDEA what they mean.
    // Is byte 0 the start of a position? A colour? A UV coordinate?
    // glVertexAttribPointer answers this: "here is how to read each vertex."
    //
    // Our layout (position only — 3 floats per vertex):
    //
    //  Byte: 0    4    8   12   16   20   24   28   32
    //        [X0] [Y0] [Z0] [X1] [Y1] [Z1] [X2] [Y2] [Z2]
    //         ─────────────  ─────────────  ─────────────
    //           Vertex 0       Vertex 1       Vertex 2
    //           12 bytes       12 bytes       12 bytes
    //
    //  Stride = 12 bytes (from X of one vertex to X of next vertex)
    //  Offset = 0 bytes (position starts at byte 0 of each vertex)
    // ─────────────────────────────────────────────────────────────────────────
    glVertexAttribPointer(
        0,                      // Attribute index 0 → matches layout(location=0) in vertex shader
        3,                      // 3 components per vertex (x, y, z)
        GL_FLOAT,               // each component is a 32-bit float (4 bytes)
        GL_FALSE,               // do NOT normalise — use raw float values as-is
        3 * sizeof(float),      // stride = 12 bytes (3 floats × 4 bytes)
        (void*)0                // offset = 0 bytes from the start of each vertex
    );

    glEnableVertexAttribArray(0);
    // Enable attribute slot 0 so the GPU actually reads it.
    // By default all attributes are DISABLED. You must enable each one.

    glBindVertexArray(0);  // ← RECORDING STOPS — unbind to prevent accidental changes

    std::cout << "[5] Vertex layout described to GPU.\n";
    std::cout << "    Attribute 0: 3 floats, stride=12, offset=0\n";
    std::cout << "    VAO recording complete.\n\n";
    std::cout << "[6] All setup done. Starting render loop...\n";
    std::cout << "    ESC = quit\n\n";

    // ─────────────────────────────────────────────────────────────────────────
    // STEP 6: RENDER LOOP
    // Every frame, in order:
    //   1. Check input
    //   2. Clear the screen
    //   3. Activate the shader program
    //   4. Bind the VAO (restores all state in one call)
    //   5. Draw
    //   6. Swap buffers
    //   7. Poll events
    // ─────────────────────────────────────────────────────────────────────────
    while (!glfwWindowShouldClose(window)) {

        // 1. Input
        processInput(window);

        // 2. Clear
        glClearColor(0.08f, 0.10f, 0.16f, 1.0f);  // dark blue-grey background
        glClear(GL_COLOR_BUFFER_BIT);

        // 3. Activate shader program
        // glUseProgram tells the GPU: "use this vertex + fragment shader for drawing"
        // Without this: nothing is drawn (GPU has no program to execute)
        glUseProgram(shaderProgram);

        // 4. Bind VAO
        // This single call restores: VBO binding + vertex layout + enabled attributes
        // That is why VAO exists — one call replaces 4-5 state-restore calls
        glBindVertexArray(VAO);

        // 5. Draw
        // glDrawArrays(primitive, start_vertex, vertex_count)
        //   GL_TRIANGLES = every 3 consecutive vertices form one filled triangle
        //   0            = start reading from vertex 0 in the VBO
        //   3            = read 3 vertices = draw 1 triangle
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 6. Swap buffers — show completed frame
        glfwSwapBuffers(window);

        // 7. Poll events — keyboard, mouse, resize
        glfwPollEvents();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CLEANUP: Always delete GPU resources before exit
    // ─────────────────────────────────────────────────────────────────────────
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);
    glfwTerminate();

    std::cout << "Closed cleanly.\n";
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\M03_Triangle
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M03_Triangle.exe
Expected Terminal Output
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2

[1] Shader program compiled + linked. GPU program ID = 1
[2] Vertex data defined in CPU RAM: 36 bytes (9 floats)
[3] VAO created and bound. VAO ID = 1
[4] VBO created. 36 bytes copied CPU RAM to GPU VRAM. VBO ID = 2
[5] Vertex layout described to GPU.
    Attribute 0: 3 floats, stride=12, offset=0
    VAO recording complete.
[6] All setup done. Starting render loop...

🍐 Screen: Orange triangle on dark background
🔬 Break-to-Learn Experiments
  • Comment out glUseProgram(shaderProgram): Black screen. No shader = GPU has no programme to execute. Proves shaders are mandatory.
  • Comment out glBindVertexArray(VAO): Black screen. GPU has no data layout. Proves VAO is essential every frame.
  • Change colour: In fragmentShaderSrc change vec4(1.0, 0.5, 0.2, 1.0) to vec4(0.2, 0.8, 0.3, 1.0) for green. Rebuild to see it.
  • Move the triangle: Edit X/Y values in the verts[] array. Move it to top-right: try (0.2, 0.2), (0.8, 0.2), (0.5, 0.9).
Demo 4 · Module 3Live Code — Two Attributes, GPU Colour Interpolation
VAO with Two Attributes — Per-Vertex Colour Gradient
Extend Demo 3 by adding RGB colour data per vertex. Each vertex now has 6 floats (X Y Z R G B) in one interleaved VBO. The VAO records TWO attribute descriptions. The GPU automatically interpolates colour between vertices — creating a smooth gradient with zero extra code.
💻 Project: M04_Colours⏱ ~25 min
EXTENDS: Demo 3+ 6 floats per vertex (XYZ+RGB)+ Two glVertexAttribPointer calls= RGB Gradient Triangle ✓
🎯 What You Will See
Screen: Triangle with red bottom-left, green bottom-right, blue top — GPU auto-interpolates the gradient
Terminal: Attribute 0 (position): stride=24 offset=0 — Attribute 1 (colour): stride=24 offset=12
Key lesson: Render loop draw call is IDENTICAL to Demo 3. Only the data description changed. VAO power proven.
Interleaved VBO — 6 Floats = 24 Bytes per Vertex
  Byte:  0    4    8   12   16   20  |  24   28   32   36   40   44  | 48...
         [X0] [Y0] [Z0] [R0] [G0] [B0]  [X1] [Y1] [Z1] [R1] [G1] [B1]  ...
         |───position───|──colour──|  |───position───|──colour──|
                 Vertex 0 (24 bytes)              Vertex 1 (24 bytes)

  Stride = 24 bytes (6 floats x 4 bytes) — SAME for both attributes
  Position offset =  0 bytes (starts at byte 0 of each vertex)
  Colour   offset = 12 bytes (starts after the 3 position floats: 3x4=12)
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M04_Colours)
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(M04_Colours src/main.cpp)
target_link_libraries(M04_Colours
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · DEMO 4
//  VAO with Two Attributes — Per-Vertex Colour (Gradient Triangle)
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. Use the same CMakeLists.txt — change project name to M04_Colours
//  3. Build and run — gradient RGB triangle appears
//
//  WHAT THIS ADDS OVER DEMO 3:
//  - Each vertex now has 6 floats: X Y Z R G B   (was 3 floats: X Y Z)
//  - VBO contains interleaved position + colour data
//  - VAO records TWO attribute descriptions (position AND colour)
//  - Vertex shader passes colour through to fragment shader
//  - GPU automatically interpolates colour between vertices = gradient!
//
//  THE KEY LESSON:
//  The render loop draw call is IDENTICAL to Demo 3:
//      glBindVertexArray(VAO);
//      glDrawArrays(GL_TRIANGLES, 0, 3);
//  But the output is completely different — gradient instead of solid colour.
//  This proves VAO power: change the data description, not the draw call.
// ═══════════════════════════════════════════════════════════════════════════

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

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — updated from Demo 3
//
// NEW: receives TWO attributes now:
//   location=0 → aPos    (same as before)
//   location=1 → aColour (NEW — receives the RGB colour from VBO)
//
// NEW: "out vec3 vColour" passes colour to the fragment shader.
// The GPU automatically INTERPOLATES vColour across the triangle surface.
// That interpolation is what creates the smooth colour gradient.
// ─────────────────────────────────────────────────────────────────────────────
const char* vertexShaderSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;

    out vec3 vColour;   // send colour to fragment shader (GPU interpolates between vertices)

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

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — updated from Demo 3
//
// "in vec3 vColour" MUST match the "out vec3 vColour" name in vertex shader exactly.
// This is how GLSL connects vertex shader outputs to fragment shader inputs.
// ─────────────────────────────────────────────────────────────────────────────
const char* fragmentShaderSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;

    void main() {
        FragColor = vec4(vColour, 1.0);   // RGB from vertex shader, Alpha=1 (opaque)
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// HELPERS (same as Demo 3 — unchanged)
// ─────────────────────────────────────────────────────────────────────────────
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* vs, const char* fs) {
    unsigned int v = compileShader(GL_VERTEX_SHADER,   vs);
    unsigned int f = compileShader(GL_FRAGMENT_SHADER, fs);
    unsigned int p = glCreateProgram();
    glAttachShader(p, v); glAttachShader(p, f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f);
    return p;
}

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);
}

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

    std::cout << "\n=== RR Graphics Lab - Demo 4: Per-Vertex Colour ===\n\n";

    // Window setup (same as Demo 2 and 3 — you already know this)
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(800, 600, "Demo 4 - Colour Triangle", 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 (uses our UPDATED shaders above)
    unsigned int shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);
    std::cout << "[1] Shader program ready. ID = " << shaderProgram << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // INTERLEAVED VERTEX DATA: Position + Colour per vertex
    //
    // Each vertex now has 6 floats: X Y Z R G B
    //
    // Memory layout (24 bytes per vertex):
    //
    //  Byte: 0    4    8   12   16   20   |  24   28   32   36   40   44  | 48 ...
    //        [X0] [Y0] [Z0] [R0] [G0] [B0]  [X1] [Y1] [Z1] [R1] [G1] [B1]  ...
    //        |----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  (position starts at byte 0 of each vertex)
    //  Colour   offset = 12 bytes (colour starts after 3 position floats: 3 × 4 = 12)
    // ─────────────────────────────────────────────────────────────────────────
    float verts[] = {
    //    X       Y       Z       R      G      B
        -0.5f,  -0.5f,   0.0f,   1.0f,  0.2f,  0.2f,   // vertex 0: bottom-left  = RED
         0.5f,  -0.5f,   0.0f,   0.2f,  1.0f,  0.3f,   // vertex 1: bottom-right = GREEN
         0.0f,   0.5f,   0.0f,   0.3f,  0.4f,  1.0f    // vertex 2: top-centre   = BLUE
    };
    // 3 vertices × 6 floats = 18 floats × 4 bytes = 72 bytes total

    std::cout << "[2] Vertex data: " << sizeof(verts)
              << " bytes, 6 floats per vertex (XYZ + RGB)\n";

    // ─────────────────────────────────────────────────────────────────────────
    // VAO + VBO SETUP
    // Same pattern as Demo 3, but now we call glVertexAttribPointer TWICE
    // — once for position (attr 0) and once for colour (attr 1).
    // ─────────────────────────────────────────────────────────────────────────
    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);              // RECORDING STARTS

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    std::cout << "[3] VBO: " << sizeof(verts) << " bytes on GPU. ID = " << VBO << "\n";

    // ── Attribute 0: POSITION ─────────────────────────────────────────────────
    // index  = 0                      → slot 0 in vertex shader (layout(location=0))
    // count  = 3                      → 3 floats per position (x, y, z)
    // type   = GL_FLOAT               → each is 32-bit float
    // norm   = GL_FALSE               → use raw values, no normalisation
    // stride = 6 * sizeof(float) = 24 → 24 bytes from one vertex's X to next vertex's X
    // offset = (void*)0               → position starts at byte 0 of each vertex
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    std::cout << "[4] Attribute 0 (position): stride=24, offset=0\n";

    // ── Attribute 1: COLOUR ───────────────────────────────────────────────────
    // index  = 1                      → slot 1 in vertex shader (layout(location=1))
    // count  = 3                      → 3 floats per colour (r, g, b)
    // type   = GL_FLOAT
    // norm   = GL_FALSE
    // stride = 6 * sizeof(float) = 24 → same stride (24 bytes between consecutive colours)
    // offset = (void*)(3 * sizeof(float)) = (void*)12
    //        → colour starts 12 bytes into each vertex (after the 3 position floats)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    std::cout << "[5] Attribute 1 (colour):   stride=24, offset=12\n";

    glBindVertexArray(0);               // RECORDING STOPS
    std::cout << "[6] VAO recording done. Both attributes stored.\n\n";
    std::cout << "Render loop starting. ESC = quit.\n\n";

    // ─────────────────────────────────────────────────────────────────────────
    // RENDER LOOP
    // IDENTICAL to Demo 3 — same 3 lines inside the loop.
    // Different output because the VAO now has 2 attributes instead of 1.
    // ─────────────────────────────────────────────────────────────────────────
    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        glClearColor(0.08f, 0.10f, 0.16f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);    // activate shaders
        glBindVertexArray(VAO);         // restore VBO + both attribute layouts
        glDrawArrays(GL_TRIANGLES, 0, 3);  // draw 3 vertices = 1 triangle

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // Cleanup
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\M04_Colours
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M04_Colours.exe
✓ This Is What VAO Really Proves

The render loop is identical to Demo 3: glBindVertexArray(VAO) then glDrawArrays(GL_TRIANGLES, 0, 3). Nothing changed in the loop. Yet the output is completely different (RGB gradient vs solid orange). The VAO captured the 2-attribute layout during setup. Describe once, restore instantly every frame.

Demo 5 · Module 3Live Code — Index Buffer, Memory-Efficient Geometry
EBO — Rectangle from 4 Vertices, Not 6
Draw a rectangle without duplicating vertices. The EBO (Element Buffer Object) stores an index list. The GPU looks up which vertices to use for each triangle. 4 unique corners + 6 indices = same rectangle as 6 vertices, but no wasted memory. Press W to toggle wireframe and see the two triangles inside.
💻 Project: M05_EBO⏱ ~25 minW key = wireframe toggle
EXTENDS: Demo 4+ EBO for indices+ glDrawElements= Gradient Rectangle + Wireframe ✓
🎯 What You Will See
Screen: Colour-gradient rectangle (red, yellow, green, blue corners)
Terminal: “4 vertices + 6 indices. Without EBO would need 6 vertices.”
W key: Wireframe — see the two triangles that make up the rectangle
EBO — Index List Strategy
  4 unique vertices:       Index list:           Two triangles formed:
  0 ─── 1               { 0, 1, 3,             Triangle 1: top-left, top-right, bottom-left
  |   \ |                  1, 2, 3 }             Triangle 2: top-right, bottom-right, bottom-left
  3 ─── 2

  Without EBO: 6 vertices, 2 duplicated = 6 x 24 bytes = 144 bytes
  With    EBO: 4 vertices (96b) + 6 indices (24b) = 120 bytes  SAVES memory!
  For a model with 100,000 vertices: EBO saves ~30-40% VRAM
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M05_EBO)
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(M05_EBO src/main.cpp)
target_link_libraries(M05_EBO
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · DEMO 5
//  EBO — Rectangle from 4 Vertices (not 6)
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. CMakeLists.txt — change project name to M05_EBO
//  3. Build and run — colour rectangle appears
//  4. Press W to toggle wireframe — SEE the two triangles inside
//
//  WHAT THIS ADDS OVER DEMO 4:
//  - EBO (Element Buffer Object) stores an index list
//  - 4 unique corner vertices instead of 6 (no duplication)
//  - glDrawElements instead of glDrawArrays
//  - W key toggles wireframe to see how two triangles form a rectangle
// ═══════════════════════════════════════════════════════════════════════════

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

const char* vertexShaderSrc = 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";

const char* fragmentShaderSrc = 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* src) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &src, NULL); glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) { glGetShaderInfoLog(id, 512, NULL, log); std::cerr << log << "\n"; }
    return id;
}
unsigned int createShaderProgram(const char* vs, const char* fs) {
    unsigned int v=compileShader(GL_VERTEX_SHADER,vs), f=compileShader(GL_FRAGMENT_SHADER,fs);
    unsigned int p=glCreateProgram();
    glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f); return p;
}
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() {

    std::cout << "\n=== RR Graphics Lab - Demo 5: EBO Rectangle ===\n\n";

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(800, 600, "Demo 5 - EBO Rectangle", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();

    unsigned int shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);
    std::cout << "[1] Shader compiled. ID = " << shaderProgram << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // 4 UNIQUE CORNER VERTICES (X Y Z R G B)
    //
    // A rectangle needs 2 triangles = 6 vertices.
    // Without EBO: we would list 6 vertices and duplicate 2 of them.
    // With EBO:    we list 4 unique vertices + an index list of 6 numbers.
    //
    //  Vertex 0: top-left
    //  Vertex 1: top-right
    //  Vertex 2: bottom-right
    //  Vertex 3: bottom-left
    //
    //  0 ────── 1
    //  │  \     │
    //  │   \    │   ← rectangle made of 2 triangles
    //  │    \   │
    //  3 ────── 2
    // ─────────────────────────────────────────────────────────────────────────
    float verts[] = {
    //    X       Y       Z       R      G      B
        -0.5f,   0.5f,   0.0f,   1.0f,  0.3f,  0.3f,   // vertex 0: top-left     RED
         0.5f,   0.5f,   0.0f,   1.0f,  0.9f,  0.2f,   // vertex 1: top-right    YELLOW
         0.5f,  -0.5f,   0.0f,   0.2f,  0.9f,  0.3f,   // vertex 2: bottom-right GREEN
        -0.5f,  -0.5f,   0.0f,   0.3f,  0.4f,  1.0f    // vertex 3: bottom-left  BLUE
    };
    // 4 vertices × 6 floats = 24 floats × 4 bytes = 96 bytes

    // ─────────────────────────────────────────────────────────────────────────
    // INDEX LIST — tells GPU which vertices to use for each triangle
    //
    // Triangle 1: vertices 0, 1, 3  (top-left, top-right, bottom-left)
    // Triangle 2: vertices 1, 2, 3  (top-right, bottom-right, bottom-left)
    //
    // Compare:
    //   WITHOUT EBO: need to list   {0-vert, 1-vert, 3-vert, 1-vert, 2-vert, 3-vert} = 6 × 24 bytes = 144 bytes
    //   WITH    EBO: 4 verts (96b) + 6 indices (24b) = 120 bytes  SAVES memory!
    //   For a model with 100k vertices → EBO saves ~30-40% VRAM
    // ─────────────────────────────────────────────────────────────────────────
    unsigned int indices[] = {
        0, 1, 3,    // triangle 1: top-left, top-right, bottom-left
        1, 2, 3     // triangle 2: top-right, bottom-right, bottom-left
    };

    std::cout << "[2] Geometry: 4 vertices + 6 indices\n";
    std::cout << "    Without EBO would need 6 vertices (2 duplicated)\n";

    // ─────────────────────────────────────────────────────────────────────────
    // VAO + VBO + EBO SETUP
    // Same pattern as Demo 4, but we ADD an EBO for the indices.
    // The EBO is bound to GL_ELEMENT_ARRAY_BUFFER (different target from VBO).
    // When the VAO is bound, binding the EBO here makes VAO remember it too.
    // ─────────────────────────────────────────────────────────────────────────
    unsigned int VAO, VBO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);                       // RECORDING STARTS

    // Upload vertex data to VBO (same as Demo 4)
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    std::cout << "[3] VBO: " << sizeof(verts) << " bytes on GPU. ID = " << VBO << "\n";

    // Upload index data to EBO — different target: GL_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    std::cout << "[4] EBO: " << sizeof(indices) << " bytes on GPU. ID = " << EBO << "\n";

    // Attribute 0: position (stride=24, offset=0)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // Attribute 1: colour (stride=24, offset=12)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    std::cout << "[5] VAO: 2 attributes + EBO recorded.\n\n";

    glBindVertexArray(0);                         // RECORDING STOPS

    std::cout << "Render loop starting.\n";
    std::cout << "Controls: ESC = quit  |  W = toggle wireframe\n\n";

    bool wireframe = false;
    bool wKeyWasPressedLastFrame = false;

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

        // W key: toggle wireframe — edge-detect the press so it toggles once per press
        bool wKeyNow = (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS);
        if (wKeyNow && !wKeyWasPressedLastFrame) {
            wireframe = !wireframe;
            glPolygonMode(GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
            std::cout << (wireframe ? "Wireframe ON  (see the two triangles)\n"
                                    : "Wireframe OFF (solid fill)\n");
        }
        wKeyWasPressedLastFrame = wKeyNow;

        glClearColor(0.08f, 0.10f, 0.16f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);

        // KEY DIFFERENCE from Demo 3 and 4:
        // glDrawElements uses the EBO index list instead of sequential vertices
        // Parameters: (primitive, index_count, index_type, offset_in_EBO)
        //   GL_TRIANGLES    = each group of 3 indices = 1 triangle
        //   6               = 6 indices total (2 triangles × 3 indices each)
        //   GL_UNSIGNED_INT = our index array type is unsigned int
        //   0               = start from the beginning of the EBO
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\M05_EBO
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M05_EBO.exe
Expected Terminal Output
[1] Shader compiled. ID = 1
[2] Geometry: 4 vertices + 6 indices
    Without EBO would need 6 vertices (2 duplicated)
[3] VBO: 96 bytes on GPU. ID = 2
[4] EBO: 24 bytes on GPU. ID = 3
[5] VAO: 2 attributes + EBO recorded.
Render loop starting. Controls: ESC = quit | W = toggle wireframe

Press W → Wireframe ON — see the two triangles
Press W → Wireframe OFF — solid fill
Demo 6 · Module 3Live Code — CPU→GPU Live Data Every Frame
Uniforms — Sending Live Data from CPU to GPU Every Frame
A uniform is a value your C++ code sends to the GPU shader every single frame. Unlike vertex attributes (uploaded once at setup), uniforms change each frame. Pass current time → shader uses sin(time) to cycle colours. This is how all real-time rendering and animation works.
💻 Project: M06_Uniforms⏱ ~20 min
EXTENDS: Demo 3 triangle+ uniform float uTime in shader+ glUniform1f each frame= Pulsing Animated Triangle ✓
🎯 What You Will See
Screen: Triangle smoothly cycling through all colours continuously — no input required
Terminal: Current time value printed every 60 frames confirming uniform updates each frame
💡 Uniform vs Vertex Attribute

Vertex attribute: Different value per vertex. Uploaded to VBO once before drawing. Position and colour are attributes.

Uniform: Same value for ALL vertices AND all pixels in one draw call. You set it from C++ each frame via glUniform*(). Examples: time, light position, transformation matrix.

CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M06_Uniforms)
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(M06_Uniforms src/main.cpp)
target_link_libraries(M06_Uniforms
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · DEMO 6
//  Uniforms — Sending Live Data from CPU to GPU Every Frame
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. CMakeLists.txt — change project name to M06_Uniforms
//  3. Build and run — triangle pulsing through colours
//  4. Watch terminal — time value printed every 60 frames
//
//  WHAT THIS ADDS OVER DEMO 3:
//  - "uniform float uTime" in fragment shader receives time from CPU
//  - sin(uTime) maps time to a 0.0-1.0 colour cycle
//  - glGetUniformLocation finds "uTime" in compiled shader by name
//  - glUniform1f sends a float value to that uniform every frame
// ═══════════════════════════════════════════════════════════════════════════

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

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — same as Demo 3 (position only, no colour attribute)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertexShaderSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    void main() {
        gl_Position = vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — new: receives uTime uniform
//
// "uniform float uTime" → value sent from C++ every frame via glUniform1f
// sin() oscillates between -1.0 and +1.0
// (sin(x) + 1.0) / 2.0 maps that to 0.0 - 1.0 range for colour
// Each channel uses a different speed/offset → cycling colour effect
// ─────────────────────────────────────────────────────────────────────────────
const char* fragmentShaderSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;

    uniform float uTime;    // sent from C++ every frame

    void main() {
        float r = (sin(uTime * 1.0)       + 1.0) / 2.0;
        float g = (sin(uTime * 0.7 + 1.0) + 1.0) / 2.0;
        float b = (sin(uTime * 0.4 + 2.0) + 1.0) / 2.0;
        FragColor = vec4(r, g, b, 1.0);
    }
)GLSL";

unsigned int compileShader(unsigned int type, const char* src) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &src, NULL); glCompileShader(id);
    int ok; char log[512];
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if (!ok) { glGetShaderInfoLog(id,512,NULL,log); std::cerr << log << "\n"; }
    return id;
}
unsigned int createShaderProgram(const char* vs, const char* fs) {
    unsigned int v=compileShader(GL_VERTEX_SHADER,vs), f=compileShader(GL_FRAGMENT_SHADER,fs);
    unsigned int p=glCreateProgram();
    glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f); return p;
}
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() {

    std::cout << "\n=== RR Graphics Lab - Demo 6: Uniforms + Animation ===\n\n";

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(800, 600, "Demo 6 - Uniforms", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();

    unsigned int shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);
    std::cout << "[1] Shader compiled. ID = " << shaderProgram << "\n";

    // Triangle vertex data — position only (3 floats per vertex, no colour)
    float verts[] = {
    //    X       Y       Z
        -0.5f,  -0.5f,   0.0f,
         0.5f,  -0.5f,   0.0f,
         0.0f,   0.5f,   0.0f
    };

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

    // ─────────────────────────────────────────────────────────────────────────
    // GET UNIFORM LOCATION — do this ONCE before the loop
    //
    // glGetUniformLocation searches the compiled shader for a uniform named "uTime"
    // Returns: an integer location ID (0, 1, 2...) or -1 if not found
    // -1 means the name is wrong OR the uniform was optimised away (unused)
    // IMPORTANT: do NOT call this every frame — it is slow. Cache the result.
    // ─────────────────────────────────────────────────────────────────────────
    int uTimeLoc = glGetUniformLocation(shaderProgram, "uTime");
    std::cout << "[3] uTime uniform location = " << uTimeLoc
              << "  (if -1: name mismatch or unused)\n\n";
    std::cout << "Render loop starting. ESC = quit.\n\n";

    int frameCount = 0;

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

        glClearColor(0.08f, 0.10f, 0.16f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);

        // ─────────────────────────────────────────────────────────────────────
        // SEND UNIFORM EVERY FRAME
        //
        // glfwGetTime() returns seconds elapsed since glfwInit() as a double.
        // We cast to float because our uniform is "uniform float uTime".
        //
        // glUniform1f(location, value)
        //   1f = one float value
        //   Other variants: glUniform2f, glUniform3f, glUniform4f, glUniformMatrix4fv
        //   The shader program must be active (glUseProgram) before calling glUniform*
        // ─────────────────────────────────────────────────────────────────────
        float currentTime = (float)glfwGetTime();
        glUniform1f(uTimeLoc, currentTime);

        // Print time to terminal every 60 frames to confirm it is updating
        frameCount++;
        if (frameCount % 60 == 0) {
            std::cout << "Frame " << frameCount
                      << ": uTime = " << currentTime << "s  (cycling colour)\n";
        }

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\M06_Uniforms
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M06_Uniforms.exe
Expected Terminal Output
[1] Shader compiled. ID = 1
[2] VAO + VBO ready.
[3] uTime uniform location = 0 (if -1: name mismatch or unused)
Render loop starting. ESC = quit.

Frame 60: uTime = 1.002s (cycling colour)
Frame 120: uTime = 2.003s (cycling colour)
Frame 180: uTime = 3.005s (cycling colour)

🎱 Screen: Triangle cycles smoothly through all colours
✓ Performance — Cache Uniform Location

glGetUniformLocation() searches the compiled shader by name and is slow. Always call it ONCE before the render loop and store the integer result. Never call it every frame. This is a real production pattern in all professional OpenGL code.

★ Capstone Demo · All Day 1 ConceptsComplete One-File Program
Day 1 Complete — Triangle + Rectangle + Pulse Animation
One complete program using every Day 1 concept simultaneously. VBO, VAO, EBO, vertex shader, fragment shader, two vertex attributes (position + colour), a uniform, and real-time animation. Run this at the end of Day 1 to see everything working together. Walk through the code top to bottom — each section maps to one demo.
💻 Project: Capstone_Day1⏱ ~30 minW key = wireframe
USES ALL: D2: WindowD3: VBO+VAO+Shaders D4: 2 AttributesD5: EBOD6: Uniform = Complete Scene ✓
🎯 What You Will See
Left: RGB gradient triangle — VBO + VAO + 2 attributes (position + colour)
Right: Gradient rectangle — VBO + VAO + EBO (4 vertices, 6 indices)
Both: Brightness pulses in sync via sin(time) uniform animation
W key: Wireframe toggle — see 2 triangles inside the rectangle
Terminal: 7 numbered setup steps with byte sizes and GPU IDs for every object
Project Structure
C:\Labs\Capstone_Day1\
├── CMakeLists.txt     ← same pattern as all other demos
└── src\
    └── main.cpp           ← copy entire file below
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(Capstone_Day1)
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(Capstone_Day1 src/main.cpp)
target_link_libraries(Capstone_Day1
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Capstone (copy entire file)
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 1 · CAPSTONE
//  All Day 1 Concepts in One Complete Program
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  HOW TO USE THIS FILE:
//  1. Copy THIS ENTIRE FILE into src/main.cpp
//  2. CMakeLists.txt — change project name to Capstone_Day1
//  3. Build and run
//
//  WHAT YOU WILL SEE:
//  - Left:  Gradient triangle  (VBO + VAO + 2 attributes: position + colour)
//  - Right: Gradient rectangle (VBO + VAO + EBO + 2 attributes)
//  - Both:  Brightness pulses in sync  (uniform + sin(time))
//  - W key: Toggle wireframe — see 2 triangles inside rectangle
//  - Terminal: All 6 setup steps printed with sizes and IDs
//
//  CONCEPTS ACTIVE SIMULTANEOUSLY:
//  Demo 2: Window, context, render loop
//  Demo 3: VBO + VAO + shaders → triangle
//  Demo 4: Two attributes (position + colour) → gradient colour
//  Demo 5: EBO + glDrawElements → efficient rectangle
//  Demo 6: Uniform + sin(time) → pulsing animation
//
//  HOW TO TEACH WITH THIS:
//  Run the complete program first so students see the goal.
//  Then walk through the code top to bottom — each section maps to one demo.
//  Point to the terminal output to confirm each step happened.
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>    // for std::sin()

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER
// Receives: position (attr 0) + colour (attr 1)
// Uniform:  uPulse → brightness multiplier from CPU (0.4 to 1.0)
// Output:   vColour passed to fragment shader (GPU interpolates between vertices)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertexShaderSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;

    out vec3 vColour;

    uniform float uPulse;   // 0.4 to 1.0 — set each frame from CPU via glUniform1f

    void main() {
        gl_Position = vec4(aPos, 1.0);
        vColour = aColour * uPulse;   // multiply each colour channel by brightness
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER
// Simple pass-through — colour already computed in vertex shader
// ─────────────────────────────────────────────────────────────────────────────
const char* fragmentShaderSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;

    void main() {
        FragColor = vec4(vColour, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
unsigned int compileShader(unsigned int type, const char* src) {
    unsigned int id = glCreateShader(type);
    glShaderSource(id, 1, &src, 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;
}

unsigned int createShaderProgram(const char* vs, const char* fs) {
    unsigned int v = compileShader(GL_VERTEX_SHADER,   vs);
    unsigned int f = compileShader(GL_FRAGMENT_SHADER, fs);
    unsigned int p = glCreateProgram();
    glAttachShader(p, v); glAttachShader(p, f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f);
    return p;
}

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);
}

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

    std::cout << "\n=== Day 1 Capstone: All Concepts Together ===\n\n";

    // ── Window + context (from Demo 2) ────────────────────────────────────────
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(900, 600, "Day 1 Capstone", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE;
    glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    // ── Step 1: Compile shader program (from Demo 3) ──────────────────────────
    // One shader program is used for BOTH shapes — same shaders handle both
    unsigned int shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);
    std::cout << "[1] Shader program compiled. ID = " << shaderProgram << "\n";

    // ─────────────────────────────────────────────────────────────────────────
    // SHAPE 1: TRIANGLE — positioned on the LEFT side of the screen
    // Vertex format: X Y Z R G B (6 floats = 24 bytes per vertex)
    // ─────────────────────────────────────────────────────────────────────────
    float triangleVerts[] = {
    //    X       Y       Z       R      G      B
        -0.55f, -0.5f,   0.0f,   1.0f,  0.25f, 0.25f,  // bottom-left  RED
        -0.1f,  -0.5f,   0.0f,   0.2f,  0.95f, 0.3f,   // bottom-right GREEN
        -0.3f,   0.5f,   0.0f,   0.3f,  0.4f,  1.0f    // top-centre   BLUE
    };
    // 3 vertices × 6 floats = 18 floats × 4 bytes = 72 bytes

    // Create VAO for triangle
    unsigned int triVAO, triVBO;
    glGenVertexArrays(1, &triVAO);
    glBindVertexArray(triVAO);              // ← RECORDING STARTS

    glGenBuffers(1, &triVBO);
    glBindBuffer(GL_ARRAY_BUFFER, triVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerts), triangleVerts, GL_STATIC_DRAW);
    std::cout << "[2] Triangle VBO: " << sizeof(triangleVerts) << " bytes on GPU. ID = " << triVBO << "\n";

    // Attribute 0: position (3 floats, stride=24, offset=0)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // Attribute 1: colour (3 floats, stride=24, offset=12)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    std::cout << "[3] Triangle VAO: 2 attributes recorded. ID = " << triVAO << "\n";

    glBindVertexArray(0);                   // ← RECORDING STOPS

    // ─────────────────────────────────────────────────────────────────────────
    // SHAPE 2: RECTANGLE — positioned on the RIGHT side of the screen
    // Uses EBO so we only need 4 unique corner vertices, not 6
    // ─────────────────────────────────────────────────────────────────────────
    float rectangleVerts[] = {
    //    X      Y       Z       R      G      B
         0.1f,  0.5f,   0.0f,   1.0f,  0.85f, 0.2f,   // 0: top-left     YELLOW
         0.9f,  0.5f,   0.0f,   1.0f,  0.4f,  0.9f,   // 1: top-right    MAGENTA
         0.9f, -0.5f,   0.0f,   0.2f,  0.9f,  0.9f,   // 2: bottom-right CYAN
         0.1f, -0.5f,   0.0f,   0.5f,  0.5f,  0.5f    // 3: bottom-left  GREY
    };
    // 4 vertices × 6 floats = 24 floats × 4 bytes = 96 bytes

    unsigned int rectangleIndices[] = {
        0, 1, 3,    // triangle 1: top-left, top-right, bottom-left
        1, 2, 3     // triangle 2: top-right, bottom-right, bottom-left
    };
    // 6 indices × 4 bytes = 24 bytes

    // Create VAO + VBO + EBO for rectangle
    unsigned int rectVAO, rectVBO, rectEBO;
    glGenVertexArrays(1, &rectVAO);
    glBindVertexArray(rectVAO);             // ← RECORDING STARTS

    glGenBuffers(1, &rectVBO);
    glBindBuffer(GL_ARRAY_BUFFER, rectVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(rectangleVerts), rectangleVerts, GL_STATIC_DRAW);
    std::cout << "[4] Rectangle VBO: " << sizeof(rectangleVerts) << " bytes on GPU. ID = " << rectVBO << "\n";

    glGenBuffers(1, &rectEBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, rectEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(rectangleIndices), rectangleIndices, GL_STATIC_DRAW);
    std::cout << "[5] Rectangle EBO: " << sizeof(rectangleIndices) << " bytes (6 indices). ID = " << rectEBO << "\n";

    // Same two attributes as triangle
    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);
    std::cout << "[6] Rectangle VAO: 2 attributes + EBO recorded. ID = " << rectVAO << "\n";

    glBindVertexArray(0);                   // ← RECORDING STOPS

    // ── Get uniform location BEFORE the loop ──────────────────────────────────
    // Call once, cache the result — calling every frame is slow
    int uPulseLoc = glGetUniformLocation(shaderProgram, "uPulse");
    std::cout << "[7] uPulse uniform location = " << uPulseLoc << "\n\n";

    std::cout << "=== Setup complete. Render loop starting. ===\n";
    std::cout << "Controls: ESC = quit   |   W = toggle wireframe\n\n";

    // ─────────────────────────────────────────────────────────────────────────
    // RENDER LOOP
    // Each frame:
    //   1. Compute pulse value from sin(time)
    //   2. Send to GPU via uniform
    //   3. Draw triangle with glDrawArrays
    //   4. Draw rectangle with glDrawElements
    // ─────────────────────────────────────────────────────────────────────────
    bool wireframe = false;
    bool wPrevFrame = false;

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

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

        glClearColor(0.06f, 0.08f, 0.14f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);

        // Compute pulse: sin() gives -1 to +1 → remap to 0.4 to 1.0
        // 0.4 = minimum brightness (never fully black)
        // 1.0 = full brightness
        float t     = (float)glfwGetTime();
        float pulse = 0.4f + ((std::sin(t * 2.0f) + 1.0f) / 2.0f) * 0.6f;

        // Send pulse value to vertex shader — it scales colour brightness
        glUniform1f(uPulseLoc, pulse);

        // ── Draw triangle: glDrawArrays ───────────────────────────────────────
        glBindVertexArray(triVAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);   // 3 vertices → 1 triangle

        // ── Draw rectangle: glDrawElements (uses EBO) ─────────────────────────
        glBindVertexArray(rectVAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);  // 6 indices → 2 triangles

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CLEANUP — always delete GPU resources before exit
    // ─────────────────────────────────────────────────────────────────────────
    glDeleteVertexArrays(1, &triVAO);
    glDeleteVertexArrays(1, &rectVAO);
    glDeleteBuffers(1, &triVBO);
    glDeleteBuffers(1, &rectVBO);
    glDeleteBuffers(1, &rectEBO);
    glDeleteProgram(shaderProgram);
    glfwTerminate();

    std::cout << "\nClosed cleanly. Day 1 Capstone complete.\n";
    return 0;
}
Build & Run — Developer Command Prompt
cd C:\Labs\Capstone_Day1
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\Capstone_Day1.exe
Expected Terminal Output
=== Day 1 Capstone: All Concepts Together ===
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2

[1] Shader program compiled. ID = 1
[2] Triangle VBO: 72 bytes on GPU. ID = 2
[3] Triangle VAO: 2 attributes recorded. ID = 1
[4] Rectangle VBO: 96 bytes on GPU. ID = 3
[5] Rectangle EBO: 24 bytes (6 indices). ID = 4
[6] Rectangle VAO: 2 attributes + EBO recorded. ID = 2
[7] uPulse uniform location = 0

=== Setup complete. Render loop starting. ===
Controls: ESC = quit | W = toggle wireframe

🎨 Left = RGB gradient triangle — Right = gradient rectangle
✨ Both pulse in sync via sin(time) uniform
🔬 Further Experiments
  • Add a third shape: Create another VAO + VBO with a second triangle at a different position — same pattern as triVAO/triVBO.
  • Different pulse speeds: Send two uniforms: uPulse1 for triangle (fast: t*4) and uPulse2 for rectangle (slow: t*0.5). Draw each with its own value.
  • Animate position: Add uniform float uOffset to vertex shader. Apply to X. Send sin(t)*0.3f each frame. Triangle moves left-right.
  • GL_POINTS: Change GL_TRIANGLES to GL_POINTS and add glPointSize(12.0f) before the draw call. See the 3 vertex positions as large dots.
← All Demos
By Raushan Ranjan (MCT | Educator)
Koenig Original AI-Courseware · Day 1 Complete
Day 1 Lab Challenges →