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.
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.
C:\Labs\M02_Window\
├── CMakeLists.txt ← copy from below
└── src\
└── main.cpp ← copy from below
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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.
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 │
└─────────────────────────────┘ └────────────────────────────────────────┘
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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)tovec4(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).
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)
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
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.
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
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}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
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.
C:\Labs\Capstone_Day1\
├── CMakeLists.txt ← same pattern as all other demos
└── src\
└── main.cpp ← copy entire file below
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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 uOffsetto vertex shader. Apply to X. Sendsin(t)*0.3feach frame. Triangle moves left-right. - GL_POINTS: Change
GL_TRIANGLEStoGL_POINTSand addglPointSize(12.0f)before the draw call. See the 3 vertex positions as large dots.
Koenig Original AI-Courseware · Day 1 Complete