Demo 7 · Module 4Live Code — First Moving Geometry
The Model Matrix — Making Geometry Move
The Day 1 triangle was pinned to the screen in fixed NDC coordinates. It could never move. The Model matrix changes that. It is a 4×4 matrix sent to the vertex shader every frame that encodes translation (where), rotation (which way), and scale (how big). The vertex shader multiplies every vertex by it before placing it on screen.
💻 Project: M07_ModelMatrix ⏱ ~25 min New: GLM · glm::mat4 · glUniformMatrix4fv
BUILDS ON: Demo 3 triangle (VBO+VAO+shaders) + glm::mat4 model matrix + glUniformMatrix4fv = Continuously rotating triangle ✓
🎯 What You Will See
Screen: Orange triangle spinning continuously around the Z axis (out of screen)
Terminal: Angle in radians + degrees printed every 60 frames
T key: Toggles translation (moves triangle to top-right while spinning)
S key: Toggles scale (shrinks triangle to 50%)
Model Matrix — What Each GLM Call Does
  glm::mat4 model = glm::mat4(1.0f);   // Identity: no transformation
       ↓
  model = glm::translate(model, glm::vec3(0.3, 0.3, 0.0));
  // Moves every vertex +0.3 in X and +0.3 in Y (to top-right)
       ↓
  model = glm::rotate(model, angle, glm::vec3(0.0, 0.0, 1.0));
  // Rotates around Z axis (pointing out of screen toward you)
  // angle = glfwGetTime() → 1 radian/sec = continuous spin
       ↓
  model = glm::scale(model, glm::vec3(0.5, 0.5, 0.5));
  // Scales all vertices to 50% of original size
       ↓
  glUniformMatrix4fv(uModelLoc, 1, GL_FALSE, glm::value_ptr(model));
  // Sends the 16-float matrix to the vertex shader as uniform uModel
       ↓
  // In vertex shader: gl_Position = uModel * vec4(aPos, 1.0);
  // Matrix × vertex = transformed position on screen
💡 Why w=1.0 for Positions and w=0.0 for Directions

vec4(aPos, 1.0) promotes your vec3 position to a 4D vector with w=1.0. This is homogeneous coordinates. With w=1.0, the translation part of the matrix does affect the vertex — it moves. If you used w=0.0 (for a direction vector like a normal), translation has no effect on it — directions don't have a position, so moving them makes no sense. This distinction matters from Demo 9 onward when we pass normals.

⚠ Transform Order Matters — Right to Left

In GLSL: uModel * vec4(aPos, 1.0) — the rightmost transform applies first. In C++ GLM code, transforms chain left to right on the model matrix, but each new call applies to the result of all previous ones. Scale first, rotate second, translate last is the standard order. Reversing it gives a completely different result.

CMakeLists.txt
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M07_ModelMatrix)
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(M07_ModelMatrix src/main.cpp)
target_link_libraries(M07_ModelMatrix
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · DEMO 7
//  The Model Matrix — First Moving Geometry
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: M07_ModelMatrix
//  FOLDER:       C:\Labs\M07_ModelMatrix\
//
//  WHAT THIS DEMO TEACHES:
//  - The Model matrix: translate, rotate, scale your geometry in 3D space
//  - GLM (OpenGL Mathematics): glm::mat4, glm::rotate, glm::translate, glm::scale
//  - Sending a mat4 to the vertex shader with glUniformMatrix4fv
//  - How gl_Position = model * vec4(aPos, 1.0) replaces the bare passthrough
//  - Why w=1.0 for positions and w=0.0 for direction vectors
//
//  WHAT YOU WILL SEE:
//  - The Day 1 orange triangle continuously rotating around the Z axis
//  - Terminal: model matrix uniform location + angle printed every 60 frames
//  - T key: toggle translation (moves triangle to top-right while rotating)
//  - S key: toggle scale (triangle shrinks to 50%)
//
//  BREAK-TO-LEARN:
//  - Comment out glm::rotate line -> triangle frozen (identity matrix = no transform)
//  - Swap rotation axis to glm::vec3(1,0,0) -> rotates around X (tumbles over)
//  - Add glm::translate before rotate vs after rotate -> different pivot point
//
//  BUILDS ON: Demo 3 (VBO + VAO + Shaders + Triangle)
//  NEW ADDITIONS: GLM model matrix + glUniformMatrix4fv
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER
//
// KEY CHANGE from Day 1:
//   Day 1: gl_Position = vec4(aPos, 1.0)   -- passthrough, fixed on screen
//   Day 2: gl_Position = uModel * vec4(aPos, 1.0)  -- transformed by matrix
//
// uModel is a mat4 uniform sent from C++ every frame.
// mat4 * vec4 = matrix multiplication: applies the model transform to each vertex.
//
// Why vec4(aPos, 1.0)?
//   w = 1.0 for POSITIONS  -- translation affects them (you want to move them)
//   w = 0.0 for DIRECTIONS -- translation doesn't affect directions (normals, etc.)
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;

    uniform mat4 uModel;    // Model matrix: sent from CPU each frame

    void main() {
        // Transform vertex from object space into world space (then clip space)
        // uModel encodes: rotation + translation + scale combined
        gl_Position = uModel * vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — unchanged from Demo 3 (solid orange)
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;
    void main() {
        FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // orange
    }
)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 << "Shader error:\n" << log; }
    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);
}

int main() {

    std::cout << "\n=== RR Graphics Lab - Demo 7: Model Matrix ===\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 7 - Model Matrix", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glewExperimental = GL_TRUE; glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

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

    // Triangle vertices (same as Demo 3 — position only, 3 floats each)
    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 ONCE before the loop ─────────────────────────────
    // uModel is a mat4 — 16 floats. We find it by name once, cache the integer.
    int uModelLoc = glGetUniformLocation(shader, "uModel");
    std::cout << "[3] uModel uniform location = " << uModelLoc << "\n";
    std::cout << "[4] Render loop starting.\n";
    std::cout << "    ESC=quit | T=toggle translate | S=toggle scale\n\n";

    bool translate = false, scale50 = false;
    bool tPrev = false, sPrev = false;
    int frameCount = 0;

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

        // T key: toggle translation
        bool tNow = (glfwGetKey(window, GLFW_KEY_T) == GLFW_PRESS);
        if (tNow && !tPrev) { translate = !translate; std::cout << (translate ? "Translate ON\n" : "Translate OFF\n"); }
        tPrev = tNow;

        // S key: toggle scale
        bool sNow = (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS);
        if (sNow && !sPrev) { scale50 = !scale50; std::cout << (scale50 ? "Scale 50% ON\n" : "Scale 100% ON\n"); }
        sPrev = sNow;

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

        // ── BUILD MODEL MATRIX each frame ─────────────────────────────────────
        //
        // glm::mat4(1.0f) = identity matrix = no transformation
        // Transformations are applied RIGHT TO LEFT when multiplied:
        //   model = T * R * S  means: Scale first, then Rotate, then Translate
        //   (reading code left to right = reading transforms right to left)
        //
        // glm::radians converts degrees to radians (OpenGL/GLM use radians)
        // glfwGetTime() = seconds since start → angle increases = continuous spin
        //
        float t = (float)glfwGetTime();
        float angle = t;   // 1 radian per second = ~57 degrees/sec rotation

        glm::mat4 model = glm::mat4(1.0f);   // start with identity

        if (translate)
            model = glm::translate(model, glm::vec3(0.3f, 0.3f, 0.0f));

        // Rotate around Z axis (pointing out of screen toward you)
        // glm::vec3(0,0,1) = Z axis
        model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));

        if (scale50)
            model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));

        // ── SEND MODEL MATRIX to vertex shader ────────────────────────────────
        // glUniformMatrix4fv(location, count, transpose, pointer_to_floats)
        //   count     = 1 (one matrix)
        //   transpose = GL_FALSE (GLM is already column-major, same as OpenGL)
        //   glm::value_ptr(model) = raw pointer to the 16 floats of the matrix
        glUniformMatrix4fv(uModelLoc, 1, GL_FALSE, glm::value_ptr(model));

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

        // Print angle every 60 frames to confirm matrix is updating
        frameCount++;
        if (frameCount % 60 == 0)
            std::cout << "Frame " << frameCount << ": angle = " << t << " rad ("
                      << (int)(t * 57.3f) % 360 << " deg)\n";

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shader);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\M07_ModelMatrix
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M07_ModelMatrix.exe
Expected Terminal Output
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2
[1] Shader compiled. ID = 1
[2] VAO + VBO ready
[3] uModel uniform location = 0
[4] Render loop starting.
ESC=quit | T=toggle translate | S=toggle scale

Frame 60: angle = 1.002 rad (57 deg)
Frame 120: angle = 2.003 rad (114 deg)
Screen: Orange triangle spinning around Z axis
🔬 Break-to-Learn Experiments
  • Comment out the rotate line: Triangle freezes. The identity matrix means no transformation. Proves the matrix is driving the animation.
  • Change rotation axis to (1,0,0): Triangle tumbles forward. X axis rotates the geometry in the vertical plane — looks like it's flipping over.
  • Swap translate and rotate order: Move glm::translate to after glm::rotate. Triangle now orbits the centre rather than spinning in place. Same two operations, different result purely from order change.
  • Make it spin at different speeds: Change float angle = t to float angle = t * 3.0f for 3× speed, or t * 0.2f for slow.
Demo 8 · Module 4Live Code — Full 3D Pipeline + Interactive Camera
Full MVP Matrix + Fly Camera — WASD + Mouse Look
This is the moment NDC coordinates disappear forever. Demo 7 used the Model matrix alone — geometry still lives in NDC space. Demo 8 adds the View matrix (camera position and direction) and the Projection matrix (perspective). Together they are the complete MVP pipeline: Model → View → Projection. Also introduces delta time, depth testing, and a full fly camera with mouse look.
💻 Project: M08_MVPCamera ⏱ ~40 min New: glm::lookAt · glm::perspective · GL_DEPTH_TEST · Mouse camera
BUILDS ON: Demo 7 model matrix + View: glm::lookAt + Projection: glm::perspective + glEnable(GL_DEPTH_TEST) = 3D coloured cube + fly camera ✓
🎯 What You Will See + Controls
Screen: A rotating 3D cube with 6 different-coloured faces in correct perspective
WASD: Fly through 3D space — W forward, S back, A strafe left, D strafe right
Mouse: Look around — click window first to capture cursor. Yaw (left/right) + Pitch (up/down)
Scroll wheel: Zoom in/out by changing field of view (45° default)
ESC: Release mouse and quit
The Complete MVP Pipeline — Every Vertex Goes Through This
  Object Space      World Space       Camera Space      Clip Space     Screen
  (VBO data)        (in the scene)    (from camera)     (perspective)  (pixels)
  ┌────────┐   M    ┌────────┐   V    ┌────────┐   P    ┌──────┐ divide ┌───┐
  │-0.5,-0.5,0│ → │in world│ → │from cam│ → │NDC   │ → by W → │pixel│
  └────────┘        └────────┘        └────────┘        └──────┘        └───┘

  glm::mat4 model = glm::rotate(glm::mat4(1.0f), time, glm::vec3(0.5,1,0));
  glm::mat4 view  = glm::lookAt(camPos, camPos+camFront, camUp);
  glm::mat4 proj  = glm::perspective(glm::radians(45.0f), 800.0f/600.0f, 0.1f, 100.0f);

  // In vertex shader:
  gl_Position = proj * view * model * vec4(aPos, 1.0);
  // Right to left: apply model first, then view, then projection
💡 Delta Time — Frame-Rate-Independent Movement

Without delta time: camPos += speed * camFront — on a 144Hz machine you move 144 units/sec. On a 30Hz machine you move 30 units/sec. Same code, different speed. Delta time fixes this: camPos += speed * deltaTime * camFront. Delta time = seconds since last frame. On 144Hz that's 0.007 sec. On 30Hz that's 0.033 sec. Speed × time = same distance per second regardless of frame rate.

💡 glEnable(GL_DEPTH_TEST) — Why It's Critical

Without depth testing, the last triangle drawn always wins. As the cube rotates, back faces would draw over front faces — the cube would look inside-out or flickering. Depth testing tells the GPU: keep a depth buffer (one float per pixel), and only let a new fragment win if it is closer to the camera than the current winner. Also: glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) resets the depth buffer every frame to maximum distance so the new frame starts fresh.

CMakeLists.txt
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M08_MVPCamera)
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(M08_MVPCamera src/main.cpp)
target_link_libraries(M08_MVPCamera
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · DEMO 8
//  Full MVP Matrix + Fly Camera (WASD + Mouse Look)
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: M08_MVPCamera
//  FOLDER:       C:\Labs\M08_MVPCamera\
//
//  WHAT THIS DEMO TEACHES:
//  - The complete MVP pipeline: Model × View × Projection → gl_Position
//  - View matrix: glm::lookAt(cameraPos, cameraPos+front, up)
//  - Projection matrix: glm::perspective(fov, aspect, near, far)
//  - Fly camera: WASD moves along camera direction, mouse rotates view
//  - Depth testing: glEnable(GL_DEPTH_TEST) + clearing depth buffer each frame
//  - Euler angles: yaw (left/right) and pitch (up/down) → front vector
//
//  WHAT YOU WILL SEE:
//  - A 3D rotating coloured cube in a perspective 3D space
//  - WASD: fly forward/backward/left/right through 3D space
//  - Mouse: look around (click window first to capture mouse)
//  - Scroll: zoom in/out (changes field of view)
//  - ESC: quit
//  - The cube recedes realistically as you fly away from it
//
//  CONTROLS:
//  W/S          — fly forward / backward
//  A/D          — strafe left / right
//  Mouse move   — look around (yaw + pitch)
//  Scroll wheel — zoom (FOV change)
//  ESC          — quit
//
//  BUILDS ON: Demo 4 (per-vertex colour VAO) + Demo 7 (model matrix)
//  NEW ADDITIONS: View matrix, Projection matrix, Depth buffer, Fly camera
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <cmath>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER
//
// Full MVP transform: projection * view * model * local_position
// Each matrix handles one coordinate space transition:
//   model      → object space to world space
//   view       → world space to camera space
//   projection → camera space to clip space (introduces perspective)
//
// vColour passed through to fragment shader for per-vertex colour gradient
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColour;

    out vec3 vColour;

    uniform mat4 uModel;       // object → world
    uniform mat4 uView;        // world  → camera
    uniform mat4 uProjection;  // camera → clip (perspective)

    void main() {
        // Apply all three transforms in sequence (right to left in GLSL):
        // 1. model moves/rotates/scales the object in world space
        // 2. view transforms world to camera-relative space
        // 3. projection warps camera space into NDC (introduces perspective)
        gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
        vColour = aColour;
    }
)GLSL";

// Fragment shader: per-vertex colour from attribute (same as Demo 4)
const char* fragSrc = R"GLSL(
    #version 330 core
    in vec3 vColour;
    out vec4 FragColor;
    void main() {
        FragColor = vec4(vColour, 1.0);
    }
)GLSL";

unsigned int compileShader(unsigned int type, const char* 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; }
    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;
}

// ─────────────────────────────────────────────────────────────────────────────
// CAMERA STATE (global so callbacks can access)
// ─────────────────────────────────────────────────────────────────────────────
glm::vec3 camPos   = glm::vec3(0.0f, 0.0f,  3.0f);  // camera starts 3 units back
glm::vec3 camFront = glm::vec3(0.0f, 0.0f, -1.0f);  // looking toward -Z
glm::vec3 camUp    = glm::vec3(0.0f, 1.0f,  0.0f);  // Y is up

float yaw   = -90.0f;  // degrees, start looking toward -Z
float pitch =   0.0f;  // degrees, level horizon

float lastX = 400.0f, lastY = 300.0f;  // last mouse position
bool  firstMouse = true;               // prevent jump on first move

float fov       = 45.0f;  // field of view in degrees
float moveSpeed = 2.5f;   // units per second

// Mouse callback — called by GLFW whenever mouse moves
void mouseCallback(GLFWwindow* w, double xpos, double ypos) {
    if (firstMouse) { lastX = (float)xpos; lastY = (float)ypos; firstMouse = false; }

    float dx = ((float)xpos - lastX) * 0.1f;   // sensitivity = 0.1
    float dy = (lastY - (float)ypos) * 0.1f;   // Y flipped: screen Y grows down
    lastX = (float)xpos; lastY = (float)ypos;

    yaw   += dx;
    pitch += dy;
    // Clamp pitch to prevent gimbal flip at exactly ±90°
    if (pitch >  89.0f) pitch =  89.0f;
    if (pitch < -89.0f) pitch = -89.0f;

    // Euler angles → front direction vector
    // This is the classic spherical-to-Cartesian conversion:
    //   X = cos(yaw) * cos(pitch)
    //   Y = sin(pitch)
    //   Z = sin(yaw) * cos(pitch)
    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    camFront = glm::normalize(front);
}

// Scroll callback — adjusts field of view (zoom effect)
void scrollCallback(GLFWwindow* w, double xoff, double yoff) {
    fov -= (float)yoff;
    if (fov <  1.0f) fov =  1.0f;
    if (fov > 90.0f) fov = 90.0f;
}

void onResize(GLFWwindow* w, int W, int H) { glViewport(0, 0, W, H); }

// WASD movement — called every frame with delta time for frame-rate-independent speed
void processInput(GLFWwindow* w, float deltaTime) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(w, true);

    float speed = moveSpeed * deltaTime;

    // Move forward/backward along the look direction
    if (glfwGetKey(w, GLFW_KEY_W) == GLFW_PRESS) camPos += speed * camFront;
    if (glfwGetKey(w, GLFW_KEY_S) == GLFW_PRESS) camPos -= speed * camFront;

    // Strafe left/right: cross product of front and up gives right vector
    // normalize() keeps speed constant regardless of front/up angle
    if (glfwGetKey(w, GLFW_KEY_A) == GLFW_PRESS)
        camPos -= glm::normalize(glm::cross(camFront, camUp)) * speed;
    if (glfwGetKey(w, GLFW_KEY_D) == GLFW_PRESS)
        camPos += glm::normalize(glm::cross(camFront, camUp)) * speed;
}

int main() {

    std::cout << "\n=== RR Graphics Lab - Demo 8: Full MVP + Fly Camera ===\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 8 - MVP + Fly Camera", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glfwSetCursorPosCallback(window, mouseCallback);
    glfwSetScrollCallback(window, scrollCallback);

    // Capture mouse cursor — hides it and locks it to window centre
    // Click the window once to activate. ESC to release.
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    glewExperimental = GL_TRUE; glewInit();
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    // ── DEPTH TESTING — CRITICAL for 3D ───────────────────────────────────────
    // Without this: back faces of the cube draw over front faces
    // GL_DEPTH_TEST: GPU keeps track of the closest fragment per pixel
    // Also add GL_DEPTH_BUFFER_BIT to glClear each frame (resets depth values)
    glEnable(GL_DEPTH_TEST);
    std::cout << "[1] Depth testing enabled\n";

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

    // ── CUBE GEOMETRY — 36 vertices (6 faces × 2 triangles × 3 vertices) ─────
    // Format: X Y Z R G B (6 floats = 24 bytes per vertex)
    // Each face gets a different colour for easy identification of orientation
    // No EBO used here — we accept the duplication for clarity
    float cubeVerts[] = {
        // Front face (Z+) — orange
        -0.5f,-0.5f, 0.5f,  1.0f,0.5f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,0.5f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.5f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.5f,0.0f,
        -0.5f, 0.5f, 0.5f,  1.0f,0.5f,0.0f,
        -0.5f,-0.5f, 0.5f,  1.0f,0.5f,0.0f,
        // Back face (Z-) — blue
        -0.5f,-0.5f,-0.5f,  0.2f,0.5f,1.0f,
         0.5f,-0.5f,-0.5f,  0.2f,0.5f,1.0f,
         0.5f, 0.5f,-0.5f,  0.2f,0.5f,1.0f,
         0.5f, 0.5f,-0.5f,  0.2f,0.5f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.2f,0.5f,1.0f,
        -0.5f,-0.5f,-0.5f,  0.2f,0.5f,1.0f,
        // Left face (X-) — green
        -0.5f, 0.5f, 0.5f,  0.2f,0.9f,0.4f,
        -0.5f, 0.5f,-0.5f,  0.2f,0.9f,0.4f,
        -0.5f,-0.5f,-0.5f,  0.2f,0.9f,0.4f,
        -0.5f,-0.5f,-0.5f,  0.2f,0.9f,0.4f,
        -0.5f,-0.5f, 0.5f,  0.2f,0.9f,0.4f,
        -0.5f, 0.5f, 0.5f,  0.2f,0.9f,0.4f,
        // Right face (X+) — red
         0.5f, 0.5f, 0.5f,  1.0f,0.2f,0.2f,
         0.5f, 0.5f,-0.5f,  1.0f,0.2f,0.2f,
         0.5f,-0.5f,-0.5f,  1.0f,0.2f,0.2f,
         0.5f,-0.5f,-0.5f,  1.0f,0.2f,0.2f,
         0.5f,-0.5f, 0.5f,  1.0f,0.2f,0.2f,
         0.5f, 0.5f, 0.5f,  1.0f,0.2f,0.2f,
        // Top face (Y+) — yellow
        -0.5f, 0.5f,-0.5f,  1.0f,0.9f,0.2f,
         0.5f, 0.5f,-0.5f,  1.0f,0.9f,0.2f,
         0.5f, 0.5f, 0.5f,  1.0f,0.9f,0.2f,
         0.5f, 0.5f, 0.5f,  1.0f,0.9f,0.2f,
        -0.5f, 0.5f, 0.5f,  1.0f,0.9f,0.2f,
        -0.5f, 0.5f,-0.5f,  1.0f,0.9f,0.2f,
        // Bottom face (Y-) — purple
        -0.5f,-0.5f,-0.5f,  0.7f,0.3f,1.0f,
         0.5f,-0.5f,-0.5f,  0.7f,0.3f,1.0f,
         0.5f,-0.5f, 0.5f,  0.7f,0.3f,1.0f,
         0.5f,-0.5f, 0.5f,  0.7f,0.3f,1.0f,
        -0.5f,-0.5f, 0.5f,  0.7f,0.3f,1.0f,
        -0.5f,-0.5f,-0.5f,  0.7f,0.3f,1.0f,
    };
    // 36 vertices × 6 floats × 4 bytes = 864 bytes

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW);
    // 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);
    glBindVertexArray(0);
    std::cout << "[3] Cube VAO+VBO ready (36 vertices, " << sizeof(cubeVerts) << " bytes)\n";

    // Cache all three matrix uniform locations
    int uModelLoc = glGetUniformLocation(shader, "uModel");
    int uViewLoc  = glGetUniformLocation(shader, "uView");
    int uProjLoc  = glGetUniformLocation(shader, "uProjection");
    std::cout << "[4] Matrix uniforms: model=" << uModelLoc
              << " view=" << uViewLoc << " proj=" << uProjLoc << "\n";
    std::cout << "[5] Render loop starting. WASD=fly | Mouse=look | Scroll=zoom | ESC=quit\n\n";

    float lastFrame = 0.0f;   // for delta time calculation

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

        // Delta time: seconds since last frame
        // Multiplying movement by deltaTime makes speed frame-rate-independent
        // Without deltaTime: fast machines move faster than slow machines
        float currentFrame = (float)glfwGetTime();
        float deltaTime    = currentFrame - lastFrame;
        lastFrame          = currentFrame;

        processInput(window, deltaTime);

        glClearColor(0.08f, 0.10f, 0.16f, 1.0f);
        // Clear BOTH colour AND depth buffer every frame:
        // GL_COLOR_BUFFER_BIT  = reset pixel colours
        // GL_DEPTH_BUFFER_BIT  = reset depth values to maximum (far plane)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(shader);

        // ── MODEL MATRIX: rotate cube slowly around Y and X axes ─────────────
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::rotate(model, currentFrame * 0.5f, glm::vec3(0.5f, 1.0f, 0.0f));

        // ── VIEW MATRIX: the camera ───────────────────────────────────────────
        // glm::lookAt(eye, centre, up)
        //   eye    = where the camera is in world space
        //   centre = what point the camera is looking at
        //   up     = which direction is "up" for the camera
        // Internally: builds 3 axes (right, up, forward) from these vectors
        // and constructs a matrix that transforms world coords into camera coords
        glm::mat4 view = glm::lookAt(camPos, camPos + camFront, camUp);

        // ── PROJECTION MATRIX: perspective ───────────────────────────────────
        // glm::perspective(fov, aspectRatio, nearPlane, farPlane)
        //   fov         = vertical field of view in radians (45° = natural view)
        //   aspectRatio = window width / height (prevents squishing)
        //   nearPlane   = closest visible distance (don't set to 0 — Z-fighting)
        //   farPlane    = furthest visible distance
        // Objects outside near/far are clipped (not drawn)
        // Objects between near/far get perspective: distant = smaller
        glm::mat4 projection = glm::perspective(
            glm::radians(fov),   // FOV: decreases with scroll (zoom in)
            800.0f / 600.0f,     // aspect ratio (update on resize in production)
            0.1f,                // near plane
            100.0f               // far plane
        );

        // Send all three matrices to the vertex shader
        glUniformMatrix4fv(uModelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(uViewLoc,  1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(uProjLoc,  1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);   // 36 vertices = 12 triangles = 6 faces

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shader);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\M08_MVPCamera
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M08_MVPCamera.exe
Expected Terminal Output
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2
[1] Depth testing enabled
[2] Shader compiled. ID = 1
[3] Cube VAO+VBO ready (36 vertices, 864 bytes)
[4] Matrix uniforms: model=0 view=1 proj=2
[5] Render loop starting. WASD=fly | Mouse=look | Scroll=zoom | ESC=quit

Screen: 3D coloured rotating cube. Fly through with WASD.
🔬 Break-to-Learn Experiments
  • Comment out glEnable(GL_DEPTH_TEST): Back faces start drawing over front faces as cube rotates. Looks broken and inside-out. Re-enable to see the fix.
  • Change near plane from 0.1f to 2.0f: Objects closer than 2 units get clipped (disappear). Fly into the cube — you can see inside through the clipped faces.
  • Change FOV from 45.0f to 90.0f: Very wide angle — extreme fisheye. Change to 20.0f for telephoto (zoom in). Shows how projection controls the perceived 3D world.
  • Remove view matrix: Comment out glUniformMatrix4fv(uViewLoc, ...). Camera is stuck at origin, WASD does nothing. Proves view matrix is what makes camera movement work.
Demo 9 · Module 5Live Code — Phong Lighting on a 3D Cube
Phong Lighting — Ambient + Diffuse + Specular
Without lighting, a 3D cube looks like a flat hexagon — every face the same brightness. Phong lighting changes this by computing how much light each pixel receives based on the angle between the surface normal and the light direction. The result: different face brightnesses, a visible highlight, and a convincing sense of depth and solidity.
💻 Project: M09_PhongLighting ⏱ ~40 min New: Normals attribute · Normal matrix · dot() · reflect()
BUILDS ON: Demo 8 cube + camera + Normals as 3rd attribute + Phong in fragment shader = Lit grey cube + orbiting light ✓
🎯 What You Will See + Controls
Screen: Grey cube with realistic lighting: bright face toward light, dark faces away
Small white cube: Shows the light position orbiting around the grey cube
1/2/3 keys: Switch modes — ambient only / + diffuse / full Phong (see each component)
Arrow Up/Down: Increase / decrease ambient strength (floor brightness)
Arrow Left/Right: Decrease / increase shininess (specular highlight size)
WASD + Mouse: Fly camera (same as Demo 8)
Phong Lighting — Three Components Decoded
  ┌────────────────┐   ┌──────────────────────┐   ┌───────────────────┐
  │   AMBIENT      │   │      DIFFUSE           │   │     SPECULAR         │
  │                │   │                          │   │                     │
  │ Constant fill  │   │  dot(normal, lightDir)  │   │ pow(dot(view,       │
  │ light. Never   │   │  = cosine of angle.     │   │ reflect), shininess)│
  │ fully black.   │   │  1.0=facing, 0.0=side,  │   │ High shininess=     │
  │ strength×colour│   │  negative=back (clamp)  │   │ tight bright spot   │
  └────────────────┘   └──────────────────────┘   └───────────────────┘
        +                         +                         =
                     result = (ambient + diffuse + specular) * objectColour

  VBO layout per vertex: X Y Z  NX NY NZ  (6 floats, stride=24)
  Normal = unit vector perpendicular to the face surface (which way it faces)
💡 The Normal Matrix — Why Not Just Use the Model Matrix for Normals

Normals must stay perpendicular to the surface after transformation. If you applied a non-uniform scale (say, stretch X by 3) directly to a normal, it would no longer point the right direction and lighting would break. The correct transform is mat3(transpose(inverse(uModel))). This is computed in the vertex shader. mat3() strips the translation row (normals are directions, not positions). inverse() corrects for scale. transpose() converts the result to the column-major format OpenGL expects.

CMakeLists.txt
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M09_PhongLighting)
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(M09_PhongLighting src/main.cpp)
target_link_libraries(M09_PhongLighting
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · DEMO 9
//  Phong Lighting — Ambient + Diffuse + Specular on a 3D Cube
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: M09_PhongLighting
//  FOLDER:       C:\Labs\M09_PhongLighting\
//
//  WHAT THIS DEMO TEACHES:
//  - Surface normals as a third vertex attribute (X Y Z  NX NY NZ)
//  - Normal matrix: transposed inverse of model matrix (keeps normals correct
//    after non-uniform scaling or rotation)
//  - Phong lighting model: ambient + diffuse + specular
//  - Per-fragment (per-pixel) lighting: calculation done in fragment shader
//  - Light position as a vec3 uniform, orbiting the cube via sin/cos
//  - The dot product: cosine of angle between normal and light direction
//
//  WHAT YOU WILL SEE:
//  - A grey lit cube — one face bright (facing light), others progressively darker
//  - A small white cube showing where the light is (rendered separately)
//  - The light orbits the grey cube automatically
//  - Key controls:
//      A/D keys: change ambient strength (floor brightness)
//      W/S keys: change specular shininess (128 = tight, 4 = wide highlight)
//      1/2/3 keys: ambient-only / diffuse-only / full Phong
//
//  BUILDS ON: Demo 8 (MVP + fly camera structure)
//  NEW ADDITIONS: Normals attribute, Normal matrix, Phong lighting in frag shader
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <cmath>

// ─────────────────────────────────────────────────────────────────────────────
// OBJECT VERTEX SHADER
// Passes position AND normal to the fragment shader.
// The normal must be transformed by the Normal Matrix (not the Model matrix)
// to remain perpendicular to the surface after any model transform.
//
// Normal Matrix = transpose(inverse(mat3(model)))
// mat3() strips the translation column (we don't want normals to translate).
// inverse() corrects for non-uniform scale.
// transpose() converts row-major to column-major for correct dot products.
// ─────────────────────────────────────────────────────────────────────────────
const char* objVertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aNormal;

    out vec3 vFragPos;    // world-space position of this fragment
    out vec3 vNormal;     // transformed normal in world space

    uniform mat4 uModel;
    uniform mat4 uView;
    uniform mat4 uProjection;

    void main() {
        // World-space position used in fragment shader for lighting math
        vFragPos = vec3(uModel * vec4(aPos, 1.0));

        // Normal matrix: transpose(inverse(mat3(model)))
        // Keeps normals perpendicular to surface after model transform
        vNormal = mat3(transpose(inverse(uModel))) * aNormal;

        gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// OBJECT FRAGMENT SHADER — Phong Lighting
//
// Three components:
//   Ambient:  constant background light, prevents pure black shadows
//   Diffuse:  angle-dependent — bright when facing light, dark when away
//   Specular: viewer-dependent highlight — creates glossy appearance
//
// All in world space so lighting is physically consistent.
// ─────────────────────────────────────────────────────────────────────────────
const char* objFragSrc = R"GLSL(
    #version 330 core
    in vec3 vFragPos;
    in vec3 vNormal;
    out vec4 FragColor;

    uniform vec3 uLightPos;      // where the light is in world space
    uniform vec3 uViewPos;       // where the camera is in world space
    uniform vec3 uLightColour;   // light colour (usually white: 1,1,1)
    uniform vec3 uObjectColour;  // base surface colour
    uniform float uAmbientStr;   // ambient strength (0.0 = no ambient, 1.0 = full)
    uniform float uShininess;    // specular shininess (4=wide, 32=medium, 128=tight)
    uniform int   uMode;         // 1=ambient only, 2=+diffuse, 3=full Phong

    void main() {
        // ── AMBIENT ──────────────────────────────────────────────────────────
        // Constant background illumination — simulates indirect light bouncing
        vec3 ambient = uAmbientStr * uLightColour;

        // ── DIFFUSE ──────────────────────────────────────────────────────────
        // Surface brightness depends on angle to light
        vec3 norm     = normalize(vNormal);        // ensure unit length
        vec3 lightDir = normalize(uLightPos - vFragPos);  // direction: frag → light

        // dot(normal, lightDir) = cos(angle between them)
        // 1.0 = facing directly at light (maximum brightness)
        // 0.0 = perpendicular to light (no direct illumination)
        // negative = back-facing (clamped to 0 with max())
        float diff    = max(dot(norm, lightDir), 0.0);
        vec3 diffuse  = diff * uLightColour;

        // ── SPECULAR ─────────────────────────────────────────────────────────
        // Glossy highlight: depends on viewer direction and reflected light
        vec3 viewDir    = normalize(uViewPos - vFragPos);   // frag → camera
        vec3 reflectDir = reflect(-lightDir, norm);         // mirrored light ray

        // dot(viewDir, reflectDir) = how well viewer aligns with reflection
        // pow(x, shininess) = sharpness: high shininess = tight bright spot
        float spec      = pow(max(dot(viewDir, reflectDir), 0.0), uShininess);
        float specStr   = 0.6;
        vec3 specular   = specStr * spec * uLightColour;

        // ── COMBINE ──────────────────────────────────────────────────────────
        vec3 result;
        if      (uMode == 1) result = ambient * uObjectColour;
        else if (uMode == 2) result = (ambient + diffuse) * uObjectColour;
        else                 result = (ambient + diffuse + specular) * uObjectColour;

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

// Light cube: simple solid white — just shows where the light source is
const char* lightVertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    uniform mat4 uModel;
    uniform mat4 uView;
    uniform mat4 uProjection;
    void main() {
        gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
    }
)GLSL";
const char* lightFragSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;
    void main() { FragColor = vec4(1.0); }  // pure white
)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; }
    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;
}

glm::vec3 camPos   = glm::vec3(0.0f, 1.0f, 4.0f);
glm::vec3 camFront = glm::vec3(0.0f, -0.2f, -1.0f);
glm::vec3 camUp    = glm::vec3(0.0f, 1.0f, 0.0f);
float yaw = -90.0f, pitch = 0.0f, lastX = 400, lastY = 300;
bool firstMouse = true;
float fov = 45.0f;

void mouseCallback(GLFWwindow* w, double xpos, double ypos) {
    if (firstMouse) { lastX=(float)xpos; lastY=(float)ypos; firstMouse=false; }
    float dx=((float)xpos-lastX)*0.1f, dy=(lastY-(float)ypos)*0.1f;
    lastX=(float)xpos; lastY=(float)ypos;
    yaw+=dx; pitch+=dy;
    if(pitch>89) pitch=89; if(pitch<-89) pitch=-89;
    glm::vec3 f;
    f.x=cos(glm::radians(yaw))*cos(glm::radians(pitch));
    f.y=sin(glm::radians(pitch));
    f.z=sin(glm::radians(yaw))*cos(glm::radians(pitch));
    camFront=glm::normalize(f);
}
void scrollCallback(GLFWwindow* w, double xo, double yo) {
    fov-=(float)yo; if(fov<1)fov=1; if(fov>90)fov=90;
}
void onResize(GLFWwindow* w, int W, int H) { glViewport(0,0,W,H); }

float ambientStr = 0.15f;
float shininess  = 32.0f;
int   lightMode  = 3;    // 3 = full Phong
bool key1Prev=false, key2Prev=false, key3Prev=false;

void processInput(GLFWwindow* w, float dt) {
    if (glfwGetKey(w, GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(w,true);
    float spd = 2.5f * dt;
    if (glfwGetKey(w, GLFW_KEY_W)==GLFW_PRESS) camPos += spd * camFront;
    if (glfwGetKey(w, GLFW_KEY_S)==GLFW_PRESS) camPos -= spd * camFront;
    if (glfwGetKey(w, GLFW_KEY_A)==GLFW_PRESS) camPos -= glm::normalize(glm::cross(camFront,camUp))*spd;
    if (glfwGetKey(w, GLFW_KEY_D)==GLFW_PRESS) camPos += glm::normalize(glm::cross(camFront,camUp))*spd;

    // Ambient strength: arrow keys
    if (glfwGetKey(w, GLFW_KEY_UP)==GLFW_PRESS)   { ambientStr += 0.01f; if(ambientStr>1.0f) ambientStr=1.0f; }
    if (glfwGetKey(w, GLFW_KEY_DOWN)==GLFW_PRESS) { ambientStr -= 0.01f; if(ambientStr<0.0f) ambientStr=0.0f; }
    // Shininess: left/right keys
    if (glfwGetKey(w, GLFW_KEY_RIGHT)==GLFW_PRESS) { shininess *= 1.02f; if(shininess>256) shininess=256; }
    if (glfwGetKey(w, GLFW_KEY_LEFT)==GLFW_PRESS)  { shininess /= 1.02f; if(shininess<1)   shininess=1;   }

    // Mode switching
    bool k1=(glfwGetKey(w,GLFW_KEY_1)==GLFW_PRESS);
    bool k2=(glfwGetKey(w,GLFW_KEY_2)==GLFW_PRESS);
    bool k3=(glfwGetKey(w,GLFW_KEY_3)==GLFW_PRESS);
    if(k1&&!key1Prev){lightMode=1;std::cout<<"Mode: Ambient only\n";}
    if(k2&&!key2Prev){lightMode=2;std::cout<<"Mode: Ambient + Diffuse\n";}
    if(k3&&!key3Prev){lightMode=3;std::cout<<"Mode: Full Phong\n";}
    key1Prev=k1; key2Prev=k2; key3Prev=k3;
}

int main() {

    std::cout << "\n=== RR Graphics Lab - Demo 9: Phong Lighting ===\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 9 - Phong Lighting", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, onResize);
    glfwSetCursorPosCallback(window, mouseCallback);
    glfwSetScrollCallback(window, scrollCallback);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    glewExperimental = GL_TRUE; glewInit();
    glEnable(GL_DEPTH_TEST);
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    unsigned int objShader   = createShaderProgram(objVertSrc,   objFragSrc);
    unsigned int lightShader = createShaderProgram(lightVertSrc, lightFragSrc);
    std::cout << "[1] Object shader=" << objShader << " | Light shader=" << lightShader << "\n";

    // ── CUBE VERTICES: position (XYZ) + normal (NX NY NZ) — 6 floats per vertex
    // Each face has the same outward-pointing normal for all its vertices.
    // Normals must be unit vectors (length = 1.0) — the normalize() in shader
    // handles any drift from floating-point imprecision.
    float cubeVerts[] = {
        // Front face  normal (0, 0, +1)
        -0.5f,-0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
         0.5f,-0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
        -0.5f, 0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
        -0.5f,-0.5f, 0.5f,  0.0f, 0.0f, 1.0f,
        // Back face   normal (0, 0, -1)
        -0.5f,-0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
         0.5f,-0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
        -0.5f,-0.5f,-0.5f,  0.0f, 0.0f,-1.0f,
        // Left face   normal (-1, 0, 0)
        -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f,-0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f,-0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f,-0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
        // Right face  normal (+1, 0, 0)
         0.5f, 0.5f, 0.5f,  1.0f, 0.0f, 0.0f,
         0.5f, 0.5f,-0.5f,  1.0f, 0.0f, 0.0f,
         0.5f,-0.5f,-0.5f,  1.0f, 0.0f, 0.0f,
         0.5f,-0.5f,-0.5f,  1.0f, 0.0f, 0.0f,
         0.5f,-0.5f, 0.5f,  1.0f, 0.0f, 0.0f,
         0.5f, 0.5f, 0.5f,  1.0f, 0.0f, 0.0f,
        // Top face    normal (0, +1, 0)
        -0.5f, 0.5f,-0.5f,  0.0f, 1.0f, 0.0f,
         0.5f, 0.5f,-0.5f,  0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,  0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,  0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f, 0.5f,  0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,  0.0f, 1.0f, 0.0f,
        // Bottom face normal (0, -1, 0)
        -0.5f,-0.5f,-0.5f,  0.0f,-1.0f, 0.0f,
         0.5f,-0.5f,-0.5f,  0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,  0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,  0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f, 0.5f,  0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f,-0.5f,  0.0f,-1.0f, 0.0f,
    };

    // Object VAO — attribute 0: position, attribute 1: normal (both stride=24)
    unsigned int objVAO, objVBO;
    glGenVertexArrays(1, &objVAO); glBindVertexArray(objVAO);
    glGenBuffers(1, &objVBO); glBindBuffer(GL_ARRAY_BUFFER, objVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);

    // Light cube VAO — same geometry but only position attribute used (no normals needed)
    unsigned int lightVAO;
    glGenVertexArrays(1, &lightVAO); glBindVertexArray(lightVAO);
    glBindBuffer(GL_ARRAY_BUFFER, objVBO);   // reuse same VBO — same cube geometry
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);            // only attribute 0 (position), skip normals
    glBindVertexArray(0);

    std::cout << "[2] Object VAO (pos+normal) and Light VAO (pos only) ready\n";
    std::cout << "[3] Render loop starting.\n";
    std::cout << "    WASD=fly | Mouse=look | Arrow keys=ambient | L/R=shininess\n";
    std::cout << "    1=ambient only | 2=+diffuse | 3=full Phong | ESC=quit\n\n";

    float lastFrame = 0.0f;

    while (!glfwWindowShouldClose(window)) {
        float now = (float)glfwGetTime();
        float dt  = now - lastFrame; lastFrame = now;

        processInput(window, dt);

        glClearColor(0.06f, 0.08f, 0.12f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Light position orbits around the cube
        glm::vec3 lightPos(
            cos(now * 0.8f) * 2.5f,   // X: oscillates
            1.2f,                       // Y: slightly above cube
            sin(now * 0.8f) * 2.5f    // Z: oscillates
        );

        glm::mat4 view = glm::lookAt(camPos, camPos + camFront, camUp);
        glm::mat4 proj = glm::perspective(glm::radians(fov), 800.0f/600.0f, 0.1f, 100.0f);

        // ── Draw the lit object cube ──────────────────────────────────────────
        glUseProgram(objShader);
        glm::mat4 model = glm::mat4(1.0f);
        glUniformMatrix4fv(glGetUniformLocation(objShader,"uModel"),      1,GL_FALSE,glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(objShader,"uView"),       1,GL_FALSE,glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(objShader,"uProjection"), 1,GL_FALSE,glm::value_ptr(proj));
        glUniform3fv(glGetUniformLocation(objShader,"uLightPos"),    1, glm::value_ptr(lightPos));
        glUniform3fv(glGetUniformLocation(objShader,"uViewPos"),     1, glm::value_ptr(camPos));
        glUniform3f(glGetUniformLocation(objShader,"uLightColour"),  1.0f, 1.0f, 1.0f);
        glUniform3f(glGetUniformLocation(objShader,"uObjectColour"), 0.6f, 0.65f, 0.7f);
        glUniform1f(glGetUniformLocation(objShader,"uAmbientStr"),   ambientStr);
        glUniform1f(glGetUniformLocation(objShader,"uShininess"),    shininess);
        glUniform1i(glGetUniformLocation(objShader,"uMode"),         lightMode);
        glBindVertexArray(objVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        // ── Draw small white cube at light position ───────────────────────────
        glUseProgram(lightShader);
        glm::mat4 lightModel = glm::translate(glm::mat4(1.0f), lightPos);
        lightModel = glm::scale(lightModel, glm::vec3(0.15f));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uModel"),      1,GL_FALSE,glm::value_ptr(lightModel));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uView"),       1,GL_FALSE,glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uProjection"), 1,GL_FALSE,glm::value_ptr(proj));
        glBindVertexArray(lightVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &objVAO);
    glDeleteVertexArrays(1, &lightVAO);
    glDeleteBuffers(1, &objVBO);
    glDeleteProgram(objShader);
    glDeleteProgram(lightShader);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\M09_PhongLighting
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M09_PhongLighting.exe
Expected Terminal Output
GPU: NVIDIA GeForce RTX 3080 Laptop GPU/PCIe/SSE2
[1] Object shader=1 | Light shader=2
[2] Object VAO (pos+normal) and Light VAO (pos only) ready
[3] Render loop starting.
WASD=fly | Mouse=look | Arrow keys=ambient | L/R=shininess
1=ambient only | 2=+diffuse | 3=full Phong | ESC=quit

Press 1 → cube is flat uniform grey (ambient only)
Press 2 → cube has bright/dark faces (+ diffuse)
Press 3 → cube has bright spot highlight (full Phong)
🔬 Break-to-Learn Experiments
  • Press 1, then 2, then 3 in sequence: Watch the cube transform from flat grey to shaded to fully lit. This isolates each component so you see exactly what ambient, diffuse, and specular each contribute.
  • Arrow Right (increase shininess to 256): Specular highlight becomes a tiny, very bright point. Arrow Left to 4: highlight spreads across most of the face. Shows how shininess controls material “glossiness.”
  • Arrow Up (increase ambient to 1.0): All faces same brightness — lighting disappears. The cube looks flat like in Mode 1 even in Mode 3. Shows ambient is the floor below which no shadow goes.
  • Fly the camera around the cube: The specular highlight moves as you move — it depends on both the light direction AND the viewer direction. Diffuse does not move. This proves specular is viewer-dependent.
Demo 10 · Module 6Live Code — UV Coordinates, stb_image, Textured Lit Cube
Textures — UV Coordinates, stb_image, Textured + Lit Cube
Textures replace hard-coded colours with images sampled per pixel. A UV coordinate (2 floats per vertex, ranging 0.0–1.0) maps each vertex to a point on a 2D image. The GPU interpolates UV across each triangle so every pixel looks up the right texel. Combined with Phong lighting, the result is a surface that looks genuinely detailed and solid.
💻 Project: M10_Textures ⏱ ~40 min New: UV coords · glTexImage2D · stb_image · sampler2D
BUILDS ON: Demo 9 lit cube + UV as 4th attribute (8 floats/vertex) + stb_image + glTexImage2D + sampler2D in fragment shader = Textured lit cube ✓
🎯 What You Will See + Controls
Screen: Textured cube (crate pattern or checkerboard) with Phong lighting on all faces
T key: Toggle texture ON/OFF — see difference between textured and plain grey surface
L key: Toggle lighting ON/OFF — see raw texture vs lit texture side by side
WASD + Mouse: Fly camera (same as Demo 8 and 9)
⚠ Setup Required Before Building

Demo 10 needs two things that the other demos do not:

1. stb_image.h — copy this file into C:\Labs\M10_Textures\src\. Download from https://github.com/nothings/stb (single file, no install). Or copy from C:\libs\stb\stb_image.h if it was pre-installed.

2. Texture image — create folder C:\Labs\M10_Texturesssets\ and put any PNG or JPG named crate.png inside. Any 256×256 image works. If no file is found, the program generates a procedural 4×4 checkerboard automatically — so it will still run either way.

Project Structure for Demo 10
C:\Labs\M10_Textures├── CMakeLists.txt
├── assets│   └── crate.png          ← any PNG/JPG texture (optional, has fallback)
└── src    ├── main.cpp
    └── stb_image.h        ← REQUIRED: copy from github.com/nothings/stb
Texture Pipeline — From Image File to Pixel Colour
  Disk (crate.png)
       ↓ stbi_load("assets/crate.png", &w, &h, &channels, 0)
  CPU RAM (unsigned char* pixels)
       ↓ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels)
  GPU VRAM (texture object, ID=1)
       ↓ glGenerateMipmap(GL_TEXTURE_2D)   ← creates 1/2, 1/4, 1/8... sizes
  GPU VRAM (texture + mipmap chain)

  Per vertex: U, V coordinates (0.0 to 1.0) stored in VBO as 4th attribute
       ↓ GPU interpolates UV per fragment
  In fragment shader: texture(uTexture, vTexCoord) → returns vec4 colour at that UV

  VBO layout with all 4 attributes (8 floats = 32 bytes per vertex):
  Byte: 0    4    8   12   16   20   24   28
        [X ] [Y ] [Z ] [NX] [NY] [NZ] [U ] [V ]
        |─position─|───normal───|──uv──|
💡 Why stbi_set_flip_vertically_on_load(true)

Image files (PNG, JPG) store pixel rows with Y=0 at the top. OpenGL textures store Y=0 at the bottom. Without the flip, every texture appears upside-down. stbi_set_flip_vertically_on_load(true) mirrors the image vertically while loading so the UV coordinate (0,0) at the bottom-left of your geometry correctly maps to the bottom-left of the texture image.

CMakeLists.txt — includes src/ for stb_image.h
CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.20)
project(M10_Textures)
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}
    src/    # stb_image.h lives here
)
add_executable(M10_Textures src/main.cpp)
target_link_libraries(M10_Textures
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32s.lib
)
src/main.cpp — Complete Program
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · DEMO 10
//  Textures — UV Coordinates, stb_image, Lit Textured Cube
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: M10_Textures
//  FOLDER:       C:\Labs\M10_Textures\
//
//  BEFORE BUILDING:
//  1. Copy stb_image.h into C:\Labs\M10_Textures\src\
//     (from C:\libs\stb\ or download: https://github.com/nothings/stb)
//  2. Create a PNG/JPG texture file: C:\Labs\M10_Textures\assets\crate.png
//     (any 256x256 or 512x512 image — use any crate/wood/metal texture)
//     The program generates a checkerboard procedurally if the file is missing.
//
//  WHAT THIS DEMO TEACHES:
//  - UV (texture) coordinates as a 4th vertex attribute: X Y Z NX NY NZ U V
//  - glGenTextures / glBindTexture / glTexImage2D / glGenerateMipmap pipeline
//  - stb_image single-header library: stbi_load + stbi_image_free
//  - Why stbi_set_flip_vertically_on_load(true) is needed
//  - sampler2D in GLSL: texture(uTexture, TexCoords) for colour lookup
//  - Combining texture colour with Phong lighting in one fragment shader
//  - Texture wrapping modes (GL_REPEAT) and filtering (GL_LINEAR_MIPMAP_LINEAR)
//
//  WHAT YOU WILL SEE:
//  - A textured cube (crate pattern or checkerboard if no file found)
//  - Phong lighting applied on top of the texture: bright faces, dark faces
//  - The light orbits the cube automatically
//  - WASD + mouse camera from Demo 8/9
//  - T key: toggle texture ON/OFF (see difference texture makes)
//  - L key: toggle lighting ON/OFF (see raw texture vs lit texture)
//
//  BUILDS ON: Demo 9 (Phong lighting on cube with normals)
//  NEW ADDITIONS: UV attribute, texture load pipeline, sampler2D in shader
// ═══════════════════════════════════════════════════════════════════════════

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// stb_image: single-header image loading library
// STB_IMAGE_IMPLEMENTATION must be defined in exactly ONE .cpp file
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include <iostream>
#include <cmath>

// ─────────────────────────────────────────────────────────────────────────────
// VERTEX SHADER — now has 4 attributes:
//   location 0: aPos    (vec3) — position
//   location 1: aNormal (vec3) — surface normal
//   location 2: aTexCoord (vec2) — UV texture coordinate
//
// UV coordinates are passed through to fragment shader where texture() uses them.
// ─────────────────────────────────────────────────────────────────────────────
const char* vertSrc = R"GLSL(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aNormal;
    layout(location = 2) in vec2 aTexCoord;

    out vec3 vFragPos;
    out vec3 vNormal;
    out vec2 vTexCoord;    // passed to fragment shader for texture lookup

    uniform mat4 uModel;
    uniform mat4 uView;
    uniform mat4 uProjection;

    void main() {
        vFragPos  = vec3(uModel * vec4(aPos, 1.0));
        vNormal   = mat3(transpose(inverse(uModel))) * aNormal;
        vTexCoord = aTexCoord;   // GPU interpolates UV per fragment automatically
        gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
    }
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// FRAGMENT SHADER — Phong lighting × texture colour
//
// texture(uTexture, vTexCoord) looks up the texel at the interpolated UV.
// The Phong lighting result multiplies the texture colour — bright areas of
// the texture stay bright, dark areas of the texture stay dark.
//
// uUseTexture and uUseLighting allow toggling with T/L keys for comparison.
// ─────────────────────────────────────────────────────────────────────────────
const char* fragSrc = R"GLSL(
    #version 330 core
    in vec3 vFragPos;
    in vec3 vNormal;
    in vec2 vTexCoord;
    out vec4 FragColor;

    uniform sampler2D uTexture;    // texture unit 0 — the bound texture
    uniform vec3  uLightPos;
    uniform vec3  uViewPos;
    uniform vec3  uLightColour;
    uniform bool  uUseTexture;     // T key toggle
    uniform bool  uUseLighting;    // L key toggle

    void main() {
        // Base surface colour: from texture or plain grey
        vec3 surfaceColour = uUseTexture
            ? vec3(texture(uTexture, vTexCoord))
            : vec3(0.6, 0.65, 0.7);

        if (!uUseLighting) {
            FragColor = vec4(surfaceColour, 1.0);
            return;
        }

        // Phong lighting (same as Demo 9)
        vec3 norm     = normalize(vNormal);
        vec3 lightDir = normalize(uLightPos - vFragPos);
        vec3 viewDir  = normalize(uViewPos  - vFragPos);

        vec3 ambient  = 0.15 * uLightColour;
        float diff    = max(dot(norm, lightDir), 0.0);
        vec3 diffuse  = diff * uLightColour;
        vec3 reflDir  = reflect(-lightDir, norm);
        float spec    = pow(max(dot(viewDir, reflDir), 0.0), 32.0);
        vec3 specular = 0.5 * spec * uLightColour;

        vec3 result = (ambient + diffuse + specular) * surfaceColour;
        FragColor = vec4(result, 1.0);
    }
)GLSL";

// Light indicator cube shaders (same simple pass-through as Demo 9)
const char* lightVertSrc = R"GLSL(
    #version 330 core
    layout(location=0) in vec3 aPos;
    uniform mat4 uModel, uView, uProjection;
    void main(){ gl_Position = uProjection*uView*uModel*vec4(aPos,1.0); }
)GLSL";
const char* lightFragSrc = R"GLSL(
    #version 330 core
    out vec4 FragColor;
    void main(){ FragColor = vec4(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;}
    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;
}

glm::vec3 camPos(0,1,4), camFront(0,-0.2f,-1), camUp(0,1,0);
float yaw=-90,pitch=0,lastX=400,lastY=300,fov=45;
bool firstMouse=true;
bool useTexture=true, useLighting=true;
bool tPrev=false, lPrev=false;

void mouseCallback(GLFWwindow*w,double x,double y){
    if(firstMouse){lastX=(float)x;lastY=(float)y;firstMouse=false;}
    float dx=((float)x-lastX)*0.1f,dy=(lastY-(float)y)*0.1f;
    lastX=(float)x;lastY=(float)y;
    yaw+=dx;pitch+=dy;
    if(pitch>89)pitch=89;if(pitch<-89)pitch=-89;
    glm::vec3 f;
    f.x=cos(glm::radians(yaw))*cos(glm::radians(pitch));
    f.y=sin(glm::radians(pitch));
    f.z=sin(glm::radians(yaw))*cos(glm::radians(pitch));
    camFront=glm::normalize(f);
}
void scrollCallback(GLFWwindow*w,double xo,double yo){fov-=(float)yo;if(fov<1)fov=1;if(fov>90)fov=90;}
void onResize(GLFWwindow*w,int W,int H){glViewport(0,0,W,H);}

void processInput(GLFWwindow* w, float dt) {
    if(glfwGetKey(w,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(w,true);
    float s=2.5f*dt;
    if(glfwGetKey(w,GLFW_KEY_W)==GLFW_PRESS) camPos+=s*camFront;
    if(glfwGetKey(w,GLFW_KEY_S)==GLFW_PRESS) camPos-=s*camFront;
    if(glfwGetKey(w,GLFW_KEY_A)==GLFW_PRESS) camPos-=glm::normalize(glm::cross(camFront,camUp))*s;
    if(glfwGetKey(w,GLFW_KEY_D)==GLFW_PRESS) camPos+=glm::normalize(glm::cross(camFront,camUp))*s;

    bool tNow=(glfwGetKey(w,GLFW_KEY_T)==GLFW_PRESS);
    if(tNow&&!tPrev){useTexture=!useTexture;std::cout<<(useTexture?"Texture ON\n":"Texture OFF\n");}
    tPrev=tNow;
    bool lNow=(glfwGetKey(w,GLFW_KEY_L)==GLFW_PRESS);
    if(lNow&&!lPrev){useLighting=!useLighting;std::cout<<(useLighting?"Lighting ON\n":"Lighting OFF\n");}
    lPrev=lNow;
}

int main() {

    std::cout << "\n=== RR Graphics Lab - Demo 10: Textures ===\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 10 - Textures",NULL,NULL);
    if(!window){glfwTerminate();return -1;}
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window,onResize);
    glfwSetCursorPosCallback(window,mouseCallback);
    glfwSetScrollCallback(window,scrollCallback);
    glfwSetInputMode(window,GLFW_CURSOR,GLFW_CURSOR_DISABLED);
    glewExperimental=GL_TRUE; glewInit();
    glEnable(GL_DEPTH_TEST);
    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n\n";

    unsigned int objShader   = createShaderProgram(vertSrc, fragSrc);
    unsigned int lightShader = createShaderProgram(lightVertSrc, lightFragSrc);
    std::cout << "[1] Shaders compiled\n";

    // ── CUBE VERTICES: X Y Z  NX NY NZ  U V (8 floats = 32 bytes per vertex) ─
    // UV coords: (0,0)=bottom-left, (1,0)=bottom-right, (1,1)=top-right, (0,1)=top-left
    // Same normal as Demo 9 per face, plus UV coords per vertex
    float cubeVerts[] = {
        // Front face (Z+, normal 0,0,1)      U    V
        -0.5f,-0.5f, 0.5f,  0,0,1,  0.0f,0.0f,
         0.5f,-0.5f, 0.5f,  0,0,1,  1.0f,0.0f,
         0.5f, 0.5f, 0.5f,  0,0,1,  1.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0,0,1,  1.0f,1.0f,
        -0.5f, 0.5f, 0.5f,  0,0,1,  0.0f,1.0f,
        -0.5f,-0.5f, 0.5f,  0,0,1,  0.0f,0.0f,
        // Back face (Z-, normal 0,0,-1)
        -0.5f,-0.5f,-0.5f,  0,0,-1, 1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  0,0,-1, 0.0f,0.0f,
         0.5f, 0.5f,-0.5f,  0,0,-1, 0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  0,0,-1, 0.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0,0,-1, 1.0f,1.0f,
        -0.5f,-0.5f,-0.5f,  0,0,-1, 1.0f,0.0f,
        // Left face (X-, normal -1,0,0)
        -0.5f, 0.5f, 0.5f, -1,0,0,  1.0f,1.0f,
        -0.5f, 0.5f,-0.5f, -1,0,0,  0.0f,1.0f,
        -0.5f,-0.5f,-0.5f, -1,0,0,  0.0f,0.0f,
        -0.5f,-0.5f,-0.5f, -1,0,0,  0.0f,0.0f,
        -0.5f,-0.5f, 0.5f, -1,0,0,  1.0f,0.0f,
        -0.5f, 0.5f, 0.5f, -1,0,0,  1.0f,1.0f,
        // Right face (X+, normal 1,0,0)
         0.5f, 0.5f, 0.5f,  1,0,0,  0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  1,0,0,  1.0f,1.0f,
         0.5f,-0.5f,-0.5f,  1,0,0,  1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  1,0,0,  1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1,0,0,  0.0f,0.0f,
         0.5f, 0.5f, 0.5f,  1,0,0,  0.0f,1.0f,
        // Top face (Y+, normal 0,1,0)
        -0.5f, 0.5f,-0.5f,  0,1,0,  0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  0,1,0,  1.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0,1,0,  1.0f,0.0f,
         0.5f, 0.5f, 0.5f,  0,1,0,  1.0f,0.0f,
        -0.5f, 0.5f, 0.5f,  0,1,0,  0.0f,0.0f,
        -0.5f, 0.5f,-0.5f,  0,1,0,  0.0f,1.0f,
        // Bottom face (Y-, normal 0,-1,0)
        -0.5f,-0.5f,-0.5f,  0,-1,0, 0.0f,0.0f,
         0.5f,-0.5f,-0.5f,  0,-1,0, 1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  0,-1,0, 1.0f,1.0f,
         0.5f,-0.5f, 0.5f,  0,-1,0, 1.0f,1.0f,
        -0.5f,-0.5f, 0.5f,  0,-1,0, 0.0f,1.0f,
        -0.5f,-0.5f,-0.5f,  0,-1,0, 0.0f,0.0f,
    };

    // Object VAO with 3 attributes: pos(0), normal(1), texcoord(2)
    // Stride = 8 floats × 4 bytes = 32 bytes
    unsigned int objVAO, objVBO;
    glGenVertexArrays(1,&objVAO); glBindVertexArray(objVAO);
    glGenBuffers(1,&objVBO); glBindBuffer(GL_ARRAY_BUFFER,objVBO);
    glBufferData(GL_ARRAY_BUFFER,sizeof(cubeVerts),cubeVerts,GL_STATIC_DRAW);
    // Attr 0: position  (3 floats, stride=32, offset=0)
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,8*sizeof(float),(void*)0);
    glEnableVertexAttribArray(0);
    // Attr 1: normal    (3 floats, stride=32, offset=12)
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,8*sizeof(float),(void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    // Attr 2: texcoord  (2 floats, stride=32, offset=24)
    glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,8*sizeof(float),(void*)(6*sizeof(float)));
    glEnableVertexAttribArray(2);
    glBindVertexArray(0);

    // Light cube VAO — same VBO, position only (stride=32, just read first 3 floats)
    unsigned int lightVAO;
    glGenVertexArrays(1,&lightVAO); glBindVertexArray(lightVAO);
    glBindBuffer(GL_ARRAY_BUFFER,objVBO);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,8*sizeof(float),(void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    std::cout << "[2] VAOs ready (8 floats/vertex: pos+normal+UV, stride=32)\n";

    // ── TEXTURE LOADING ───────────────────────────────────────────────────────
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    // Wrapping mode: what happens when UV goes outside 0.0-1.0 range
    // GL_REPEAT: tiles the texture (common for surfaces like floors)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    // Filtering: how texture looks when magnified or minified
    // GL_LINEAR_MIPMAP_LINEAR: best quality with mipmapping (trilinear filtering)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // OpenGL has Y=0 at bottom. Image files have Y=0 at top.
    // stbi_set_flip_vertically_on_load fixes this mismatch.
    stbi_set_flip_vertically_on_load(true);

    int imgW, imgH, imgChannels;
    unsigned char* data = stbi_load("assets/crate.png", &imgW, &imgH, &imgChannels, 0);

    if (data) {
        GLenum fmt = (imgChannels == 4) ? GL_RGBA : GL_RGB;
        glTexImage2D(GL_TEXTURE_2D, 0, fmt, imgW, imgH, 0, fmt, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);  // auto-generates all mipmap levels
        stbi_image_free(data);
        std::cout << "[3] Texture loaded: " << imgW << "x" << imgH
                  << " (" << imgChannels << " channels)\n";
    } else {
        // Generate a procedural 4x4 checkerboard if no image file found
        std::cout << "[3] No texture file found at assets/crate.png\n";
        std::cout << "    Generating procedural 4x4 checkerboard fallback\n";
        unsigned char checker[4*4*3];
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                bool white = (x + y) % 2 == 0;
                int i = (y * 4 + x) * 3;
                checker[i+0] = white ? 220 : 40;
                checker[i+1] = white ? 220 : 40;
                checker[i+2] = white ? 220 : 40;
            }
        }
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 4, 4, 0, GL_RGB, GL_UNSIGNED_BYTE, checker);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }

    // Tell the shader which texture unit to sample from
    // We use unit 0 (the default). glUniform1i sets sampler2D to unit 0.
    glUseProgram(objShader);
    glUniform1i(glGetUniformLocation(objShader, "uTexture"), 0);

    std::cout << "[4] Render loop starting.\n";
    std::cout << "    WASD=fly | Mouse=look | T=texture toggle | L=lighting toggle | ESC=quit\n\n";

    float lastFrame = 0.0f;

    while (!glfwWindowShouldClose(window)) {
        float now = (float)glfwGetTime();
        float dt  = now - lastFrame; lastFrame = now;
        processInput(window, dt);

        glClearColor(0.06f, 0.08f, 0.12f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glm::vec3 lightPos(cos(now*0.8f)*2.5f, 1.2f, sin(now*0.8f)*2.5f);
        glm::mat4 view = glm::lookAt(camPos, camPos+camFront, camUp);
        glm::mat4 proj = glm::perspective(glm::radians(fov), 800.0f/600.0f, 0.1f, 100.0f);
        glm::mat4 model = glm::mat4(1.0f);

        // ── Draw textured + lit cube ──────────────────────────────────────────
        glUseProgram(objShader);
        glActiveTexture(GL_TEXTURE0);      // activate texture unit 0
        glBindTexture(GL_TEXTURE_2D, texture);   // bind our texture to unit 0

        glUniformMatrix4fv(glGetUniformLocation(objShader,"uModel"),      1,GL_FALSE,glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(objShader,"uView"),       1,GL_FALSE,glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(objShader,"uProjection"), 1,GL_FALSE,glm::value_ptr(proj));
        glUniform3fv(glGetUniformLocation(objShader,"uLightPos"),  1, glm::value_ptr(lightPos));
        glUniform3fv(glGetUniformLocation(objShader,"uViewPos"),   1, glm::value_ptr(camPos));
        glUniform3f(glGetUniformLocation(objShader,"uLightColour"),1.0f,1.0f,1.0f);
        glUniform1i(glGetUniformLocation(objShader,"uUseTexture"),  useTexture  ? 1 : 0);
        glUniform1i(glGetUniformLocation(objShader,"uUseLighting"), useLighting ? 1 : 0);

        glBindVertexArray(objVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        // ── Draw light indicator ──────────────────────────────────────────────
        glUseProgram(lightShader);
        glm::mat4 lm = glm::scale(glm::translate(glm::mat4(1.0f),lightPos),glm::vec3(0.15f));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uModel"),      1,GL_FALSE,glm::value_ptr(lm));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uView"),       1,GL_FALSE,glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightShader,"uProjection"), 1,GL_FALSE,glm::value_ptr(proj));
        glBindVertexArray(lightVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        glfwSwapBuffers(window); glfwPollEvents();
    }

    glDeleteVertexArrays(1,&objVAO);
    glDeleteVertexArrays(1,&lightVAO);
    glDeleteBuffers(1,&objVBO);
    glDeleteTextures(1,&texture);
    glDeleteProgram(objShader);
    glDeleteProgram(lightShader);
    glfwTerminate();
    return 0;
}
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\M10_Textures
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\M10_Textures.exe
Expected Terminal Output (with texture file)
[1] Shaders compiled
[2] VAOs ready (8 floats/vertex: pos+normal+UV, stride=32)
[3] Texture loaded: 512x512 (3 channels)
[4] Render loop starting.
WASD=fly | Mouse=look | T=texture toggle | L=lighting toggle | ESC=quit

Without texture file: [3] No texture file found at assets/crate.png
Generating procedural 4x4 checkerboard fallback
🔬 Break-to-Learn Experiments
  • Press T then L in sequence: Four combinations: texture+lighting / texture only / lighting only / nothing. See exactly what each adds. This is how you debug rendering pipelines in production.
  • Change GL_REPEAT to GL_CLAMP_TO_EDGE: Any UV outside 0–1 range is stretched to the edge colour instead of tiling. Change both WRAP_S and WRAP_T. Rebuild to see.
  • Change GL_LINEAR to GL_NEAREST for MAG_FILTER: Texture looks pixelated/blocky at close range. GL_LINEAR blends between texels. GL_NEAREST picks the nearest one. GL_NEAREST is correct for pixel art styles.
  • Load a different image: Replace assets/crate.png with any PNG. Change the filename in stbi_load(). Rebuild — any image wraps correctly around all 6 faces.
← Day 1 Demos
By Raushan Ranjan (MCT | Educator)
Koenig Original AI-Courseware · Day 2 Complete
Day 2 Lab Challenges →