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
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.
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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::translateto afterglm::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 = ttofloat angle = t * 3.0ffor 3× speed, ort * 0.2ffor slow.
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
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.
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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.
┌────────────────┐ ┌──────────────────────┐ ┌───────────────────┐
│ 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)
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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 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.
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
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──|
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.
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
)// ═══════════════════════════════════════════════════════════════════════════
// 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;
}
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
- 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_SandWRAP_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.pngwith any PNG. Change the filename instbi_load(). Rebuild — any image wraps correctly around all 6 faces.
Koenig Original AI-Courseware · Day 2 Complete