Day 2 Capstone · 3 Labs · Build a Real 3D System
3D Vessel Briefing System
You are building a 3D briefing display for a naval operations centre — used to show a vessel model with correct lighting and surface textures for threat classification and visual recognition. Each lab builds on the previous one. By Lab 3, you have a fully lit, textured, rotating vessel with all Day 2 concepts active simultaneously. Every lab is one complete main.cpp — copy the whole file, build, run.
MVP Matrix Depth Testing Phong Lighting Texture Mapping Normals stb_image
📁 Project Setup — Do This Once Before Lab 1

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

Create This Folder Structure — Before Lab 1
C:\Labs\VesselBriefing\
├── CMakeLists.txt     ← write once, same for all 3 labs
├── metal_plate.jpg    ← texture for Lab 3 (from C:\Resources\textures\)
└── src\
    └── main.cpp          ← replace with each lab's complete file
CMakeLists.txt — same for all 3 labs
CMake
cmake_minimum_required(VERSION 3.20)
project(VesselBriefing)
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(VesselBriefing src/main.cpp)
target_link_libraries(VesselBriefing
    OpenGL::GL
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    ${GLEW_DIR}/lib/Release/x64/glew32.lib
)
Build & Run — Developer Command Prompt for VS 2022
cd C:\Labs\VesselBriefing
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\Release\VesselBriefing.exe
✓ Build Procedure — Same Every Lab

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

Lab 1 of 3 ⬛ Intermediate · ~35 min
Place a 3D Vessel Shape in World Space
Build a recognisable 3D shape using a cube mesh. Apply the full MVP pipeline so it sits in a 3D world with a proper fixed camera and perspective projection. Enable depth testing. This geometry foundation carries through Labs 2 and 3 — lighting and texture are applied on top of it.
📋 Concepts: Model Matrix · View Matrix · Projection · Depth Test ⚡ Do Demo 7 (Model Matrix) and Demo 8 (Full MVP) first
➕ What You Build in This Lab
  • 36-vertex cube mesh (6 faces × 2 triangles) with position + normal per vertex
  • Model matrix: slow Y-axis rotation (12°/sec) for 360° visual identification
  • Fixed briefing camera: above-right angle at vec3(2, 1.5, 3) looking at origin
  • Perspective projection 45° FOV — realistic depth appearance
  • Depth test enabled — near faces correctly cover far faces
🎯 What You Will See When This Runs
Screen: Solid grey/white cube rotating slowly on its Y axis, camera from above-right
Depth: Near faces cover far faces — no bleed-through or flickering
Perspective: Far edges appear smaller — realistic 3D depth
💡 Why 36 Vertices for a Cube?

OpenGL draws triangles, not quads. Each face = 2 triangles × 3 vertices = 6. Six faces = 36 total. We cannot share vertices across faces because each face has a different outward normal — the same corner vertex needs a different normal depending on which face it belongs to. So we duplicate all 4 corners per face.

MVP Transform Chain — Applied Right to Left Inside the GPU
  Local coords         World coords         Camera/View          Clip coords
  (vertex data)   ×M   (after model)   ×V   (lookAt space)  ×P   (gl_Position)

  aPos  ────────→  FragPos  ──────────→  view space  ──────→  gl_Position
                   uModel               uView                 uProj

  gl_Position = uProj * uView * uModel * vec4(aPos, 1.0)
  uModel: translate / rotate / scale the vessel
  uView:  glm::lookAt(eye, centre, up) — camera position
  uProj:  glm::perspective(45deg, aspect, 0.1, 100.0) — frustum
Objectives — Check Off As You Complete
  • 36-vertex cube data declared (position + normal, 6 floats per vertex)
  • VAO + VBO uploaded with stride 6, attribute 0 = pos, attribute 1 = normal
  • Vertex shader: MVP applied, FragPos and Normal passed out for Lab 2
  • Fragment shader: solid grey colour (all lighting deferred to Lab 2)
  • glEnable(GL_DEPTH_TEST) called, glClear includes GL_DEPTH_BUFFER_BIT
  • Model matrix rotates 12 degrees per second around Y
  • Camera at (2, 1.5, 3) looking at origin, Y-up
  • Perspective 45°, near 0.1, far 100
✍ Write It First — Then Verify

Try writing main.cpp from memory using what you learned in Demos 7 and 8. The vertex shader MVP line and the glm::lookAt call are the key pieces. Only look at the complete file below if you are stuck after a genuine attempt.

Complete src/main.cpp — Lab 1
src/main.cpp — Lab 1 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · LAB 1 of 3
//  3D Vessel Briefing System — Place a 3D Shape in World Space
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME:  VesselBriefing
//  FOLDER:        C:\Labs\VesselBriefing\
//
//  WHAT THIS LAB DOES:
//  Renders a rotating 3D cube (vessel silhouette) using the full MVP matrix
//  pipeline. Depth testing ensures correct face ordering. The vertex shader
//  already outputs FragPos and Normal so Lab 2 can add lighting without
//  changing the vertex shader.
//
//  WHAT YOU WILL SEE:
//  - Grey cube rotating slowly on Y axis, camera from above-right
//  - Near faces correctly cover far faces (depth test working)
//  - Perspective gives realistic 3D depth appearance
// ═══════════════════════════════════════════════════════════════════════════

#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>

const int W = 900, H = 600;

// 36 vertices: 6 faces x 2 triangles x 3 vertices
// Format per vertex: x, y, z (position)  nx, ny, nz (outward normal)
float cubeVerts[] = {
    // FRONT (+Z)
    -0.5f,-0.5f, 0.5f,  0,0,1,   0.5f,-0.5f, 0.5f,  0,0,1,   0.5f, 0.5f, 0.5f,  0,0,1,
     0.5f, 0.5f, 0.5f,  0,0,1,  -0.5f, 0.5f, 0.5f,  0,0,1,  -0.5f,-0.5f, 0.5f,  0,0,1,
    // BACK (-Z)
     0.5f,-0.5f,-0.5f,  0,0,-1, -0.5f,-0.5f,-0.5f,  0,0,-1, -0.5f, 0.5f,-0.5f,  0,0,-1,
    -0.5f, 0.5f,-0.5f,  0,0,-1,  0.5f, 0.5f,-0.5f,  0,0,-1,  0.5f,-0.5f,-0.5f,  0,0,-1,
    // LEFT (-X)
    -0.5f,-0.5f,-0.5f, -1,0,0,  -0.5f,-0.5f, 0.5f, -1,0,0,  -0.5f, 0.5f, 0.5f, -1,0,0,
    -0.5f, 0.5f, 0.5f, -1,0,0,  -0.5f, 0.5f,-0.5f, -1,0,0,  -0.5f,-0.5f,-0.5f, -1,0,0,
    // RIGHT (+X)
     0.5f,-0.5f, 0.5f,  1,0,0,   0.5f,-0.5f,-0.5f,  1,0,0,   0.5f, 0.5f,-0.5f,  1,0,0,
     0.5f, 0.5f,-0.5f,  1,0,0,   0.5f, 0.5f, 0.5f,  1,0,0,   0.5f,-0.5f, 0.5f,  1,0,0,
    // TOP (+Y)
    -0.5f, 0.5f, 0.5f,  0,1,0,   0.5f, 0.5f, 0.5f,  0,1,0,   0.5f, 0.5f,-0.5f,  0,1,0,
     0.5f, 0.5f,-0.5f,  0,1,0,  -0.5f, 0.5f,-0.5f,  0,1,0,  -0.5f, 0.5f, 0.5f,  0,1,0,
    // BOTTOM (-Y)
    -0.5f,-0.5f,-0.5f,  0,-1,0,  0.5f,-0.5f,-0.5f,  0,-1,0,  0.5f,-0.5f, 0.5f,  0,-1,0,
     0.5f,-0.5f, 0.5f,  0,-1,0, -0.5f,-0.5f, 0.5f,  0,-1,0, -0.5f,-0.5f,-0.5f,  0,-1,0,
};

// Vertex shader: MVP pipeline + pass FragPos and Normal for Lab 2 lighting
const char* vertSrc = R"(
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 uModel, uView, uProj;
void main() {
    FragPos     = vec3(uModel * vec4(aPos, 1.0));
    Normal      = mat3(transpose(inverse(uModel))) * aNormal;
    gl_Position = uProj * uView * vec4(FragPos, 1.0);
}
)";

// Fragment shader: solid grey — lighting added in Lab 2 without changing vert shader
const char* fragSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
out vec4 FragColor;
void main() {
    // Simple flat grey — Phong lighting replaces this in Lab 2
    FragColor = vec4(0.5, 0.55, 0.6, 1.0);  // navy grey
}
)";

static unsigned int makeShader(const char* vs, const char* fs) {
    auto compile = [](GLenum t, const char* src) {
        unsigned int s = glCreateShader(t);
        glShaderSource(s, 1, &src, nullptr);
        glCompileShader(s);
        int ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
        if (!ok) { char buf[512]; glGetShaderInfoLog(s,512,nullptr,buf); std::cerr << buf << "\n"; }
        return s;
    };
    unsigned int v = compile(GL_VERTEX_SHADER, vs), f = compile(GL_FRAGMENT_SHADER, fs);
    unsigned int p = glCreateProgram();
    glAttachShader(p, v); glAttachShader(p, f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f);
    return p;
}

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* win = glfwCreateWindow(W, H, "Vessel Briefing System — Lab 1", nullptr, nullptr);
    glfwMakeContextCurrent(win);
    glewExperimental = GL_TRUE; glewInit();

    glViewport(0, 0, W, H);
    glfwSetFramebufferSizeCallback(win, [](GLFWwindow*, int w, int h){ glViewport(0,0,w,h); });

    std::cout << "GPU: " << glGetString(GL_RENDERER) << "\n";
    std::cout << "OpenGL: " << glGetString(GL_VERSION) << "\n";
    std::cout << "Lab 1 ready. ESC=quit.\n";

    // Upload cube to GPU
    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW);
    // Attribute 0: position  (3 floats, stride 6, offset 0)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // Attribute 1: normal    (3 floats, stride 6, offset 3)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);

    unsigned int shader = makeShader(vertSrc, fragSrc);
    glEnable(GL_DEPTH_TEST);

    // Fixed projection and view — set once before loop
    glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)W/H, 0.1f, 100.0f);
    glm::mat4 view = glm::lookAt(
        glm::vec3(2.0f, 1.5f, 3.0f),  // camera: above-right briefing angle
        glm::vec3(0.0f, 0.0f, 0.0f),  // looking at vessel centre
        glm::vec3(0.0f, 1.0f, 0.0f)   // Y is up
    );
    glUseProgram(shader);
    glUniformMatrix4fv(glGetUniformLocation(shader, "uProj"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniformMatrix4fv(glGetUniformLocation(shader, "uView"), 1, GL_FALSE, glm::value_ptr(view));

    while (!glfwWindowShouldClose(win)) {
        if (glfwGetKey(win, GLFW_KEY_ESCAPE) == GLFW_PRESS)
            glfwSetWindowShouldClose(win, true);

        // Clear BOTH colour and depth buffers each frame
        glClearColor(0.04f, 0.06f, 0.10f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Model matrix: rotate 12 degrees per second on Y axis
        float t = (float)glfwGetTime();
        glm::mat4 model = glm::rotate(glm::mat4(1.0f),
            glm::radians(t * 12.0f), glm::vec3(0, 1, 0));
        glUniformMatrix4fv(glGetUniformLocation(shader, "uModel"), 1, GL_FALSE, glm::value_ptr(model));

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

        glfwSwapBuffers(win); glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO);
    glDeleteProgram(shader);
    glfwTerminate();
    return 0;
}
🔬 Break-to-Learn Experiments
  • Comment out glEnable(GL_DEPTH_TEST): The cube faces flicker as it rotates — far faces bleed through near faces. This is why depth testing is mandatory for any 3D scene.
  • Change the rotation speed: Replace t * 12.0f with t * 60.0f for fast spin, or t * 3.0f for slow presentation rotation. The 12 deg/sec rate is chosen for slow identification viewing.
  • Move the camera: Change glm::vec3(2.0f, 1.5f, 3.0f) to glm::vec3(0, 5, 0) for top-down view. Try vec3(5, 0, 0) for a pure side view. Notice how the perspective changes what you see.
✓ Lab 1 Deliverable — Show Your Instructor
3D Vessel Shape in World Space
A solid grey cube rotating slowly on its Y axis, viewed from a fixed above-right camera with perspective projection and correct depth ordering.
  • Cube rotates continuously at ~12 degrees per second
  • Depth test: far faces hidden behind near faces — no bleed-through
  • Perspective: far edges visibly smaller than near edges
  • ESC closes cleanly
Lab 2 of 3 ⬛ Intermediate · ~35 min · Builds on Lab 1
Add Phong Lighting to the Vessel
Take the rotating cube from Lab 1 and add full Phong lighting: ambient (base visibility), diffuse (surface-angle brightness), and specular (metallic sheen). The vertex shader is unchanged — only the fragment shader and a few uniform calls are added. The vessel must read clearly as a 3D solid shape with depth.
📋 Concepts: Ambient · Diffuse · Specular · Blinn-Phong · Normal matrix ⚡ Do Demo 9 (Phong Lighting) first
➕ What You Add Over Lab 1
  • Light position fixed at (3, 3, 3) — upper-front-right corner
  • Ambient strength 0.1 — dark surfaces dim but not black
  • Diffuse: dot(norm, lightDir) — surface angle determines brightness
  • Specular: Blinn-Phong half-vector, shininess 64 — moderate metallic sheen
  • Object colour navy grey vec3(0.5, 0.55, 0.6)
  • Vertex shader: unchanged from Lab 1 (FragPos and Normal already in output)
🎯 What You Will See When This Runs
Lit face: Bright when facing the light — clearly the front of the vessel
Dark face: Dim but not black — ambient keeps it readable
Specular: Small bright highlight on the most lit face — metallic surface
Rotation: Different faces catch the light as the vessel turns
💡 Why the Normal Matrix is not just uModel

If you scale the vessel non-uniformly (e.g. stretch it), the model matrix distorts the normals — a normal that was perpendicular to the surface would no longer be perpendicular after the same transform. The normal matrix (transpose(inverse(uModel))) corrects this distortion. For uniform scaling or rotation-only models it produces the same result, but it is correct practice for all cases.

Objectives — Check Off As You Complete
  • Lab 1 folder copied to VesselBriefing_L2 (or replace main.cpp in same folder)
  • Fragment shader: ambient + diffuse + specular computation
  • Uniforms: uLightPos, uViewPos, uObjectColor, uLightColor, uAmbientStr, uShininess
  • uViewPos matches the eye position used in glm::lookAt (2, 1.5, 3)
  • Lit face bright, dark face dim (not black), specular highlight visible
  • Experiment: change uShininess to 8 (wide glow) and 256 (tiny dot)
Complete src/main.cpp — Lab 2
src/main.cpp — Lab 2 Complete File
C++
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 2 · LAB 2 of 3
//  3D Vessel Briefing System — Phong Lighting on the Hull
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  WHAT THIS LAB ADDS OVER LAB 1:
//  Full Phong lighting in the fragment shader. Vertex shader UNCHANGED.
//  Light at (3,3,3). Ambient 0.1. Shininess 64. Navy grey vessel colour.
// ═══════════════════════════════════════════════════════════════════════════

#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>

const int W = 900, H = 600;

// Same cube vertex data as Lab 1 (position + normal, 6 floats per vertex)
float cubeVerts[] = {
    -0.5f,-0.5f, 0.5f,  0,0,1,   0.5f,-0.5f, 0.5f,  0,0,1,   0.5f, 0.5f, 0.5f,  0,0,1,
     0.5f, 0.5f, 0.5f,  0,0,1,  -0.5f, 0.5f, 0.5f,  0,0,1,  -0.5f,-0.5f, 0.5f,  0,0,1,
     0.5f,-0.5f,-0.5f,  0,0,-1, -0.5f,-0.5f,-0.5f,  0,0,-1, -0.5f, 0.5f,-0.5f,  0,0,-1,
    -0.5f, 0.5f,-0.5f,  0,0,-1,  0.5f, 0.5f,-0.5f,  0,0,-1,  0.5f,-0.5f,-0.5f,  0,0,-1,
    -0.5f,-0.5f,-0.5f, -1,0,0,  -0.5f,-0.5f, 0.5f, -1,0,0,  -0.5f, 0.5f, 0.5f, -1,0,0,
    -0.5f, 0.5f, 0.5f, -1,0,0,  -0.5f, 0.5f,-0.5f, -1,0,0,  -0.5f,-0.5f,-0.5f, -1,0,0,
     0.5f,-0.5f, 0.5f,  1,0,0,   0.5f,-0.5f,-0.5f,  1,0,0,   0.5f, 0.5f,-0.5f,  1,0,0,
     0.5f, 0.5f,-0.5f,  1,0,0,   0.5f, 0.5f, 0.5f,  1,0,0,   0.5f,-0.5f, 0.5f,  1,0,0,
    -0.5f, 0.5f, 0.5f,  0,1,0,   0.5f, 0.5f, 0.5f,  0,1,0,   0.5f, 0.5f,-0.5f,  0,1,0,
     0.5f, 0.5f,-0.5f,  0,1,0,  -0.5f, 0.5f,-0.5f,  0,1,0,  -0.5f, 0.5f, 0.5f,  0,1,0,
    -0.5f,-0.5f,-0.5f,  0,-1,0,  0.5f,-0.5f,-0.5f,  0,-1,0,  0.5f,-0.5f, 0.5f,  0,-1,0,
     0.5f,-0.5f, 0.5f,  0,-1,0, -0.5f,-0.5f, 0.5f,  0,-1,0, -0.5f,-0.5f,-0.5f,  0,-1,0,
};

// Vertex shader: UNCHANGED from Lab 1
const char* vertSrc = R"(
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 uModel, uView, uProj;
void main() {
    FragPos     = vec3(uModel * vec4(aPos, 1.0));
    Normal      = mat3(transpose(inverse(uModel))) * aNormal;
    gl_Position = uProj * uView * vec4(FragPos, 1.0);
}
)";

// Fragment shader: full Phong lighting — ONLY change from Lab 1
const char* fragSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
out vec4 FragColor;

uniform vec3  uLightPos;     // (3,3,3) — upper front right
uniform vec3  uViewPos;      // camera eye — must match lookAt eye
uniform vec3  uObjectColor;  // navy grey (0.5, 0.55, 0.6)
uniform vec3  uLightColor;   // white (1, 1, 1)
uniform float uAmbientStr;   // 0.1
uniform float uShininess;    // 64.0

void main() {
    // AMBIENT — base visibility on dark side
    vec3 ambient = uAmbientStr * uLightColor;

    // DIFFUSE — surface angle to light
    vec3 norm     = normalize(Normal);
    vec3 lightDir = normalize(uLightPos - FragPos);
    float diff    = max(dot(norm, lightDir), 0.0);
    vec3 diffuse  = diff * uLightColor;

    // SPECULAR — Blinn-Phong half vector
    vec3 viewDir  = normalize(uViewPos - FragPos);
    vec3 halfDir  = normalize(lightDir + viewDir);
    float spec    = pow(max(dot(norm, halfDir), 0.0), uShininess);
    vec3 specular = 0.5 * spec * uLightColor;

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

static unsigned int makeShader(const char* vs, const char* fs) {
    auto compile = [](GLenum t, const char* src) {
        unsigned int s = glCreateShader(t);
        glShaderSource(s, 1, &src, nullptr);
        glCompileShader(s);
        int ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
        if (!ok) { char buf[512]; glGetShaderInfoLog(s,512,nullptr,buf); std::cerr << buf << "\n"; }
        return s;
    };
    unsigned int v = compile(GL_VERTEX_SHADER, vs), f = compile(GL_FRAGMENT_SHADER, fs);
    unsigned int p = glCreateProgram();
    glAttachShader(p, v); glAttachShader(p, f); glLinkProgram(p);
    glDeleteShader(v); glDeleteShader(f);
    return p;
}

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* win = glfwCreateWindow(W, H, "Vessel Briefing System — Lab 2: Phong Lighting", nullptr, nullptr);
    glfwMakeContextCurrent(win);
    glewExperimental = GL_TRUE; glewInit();
    glViewport(0, 0, W, H);
    glfwSetFramebufferSizeCallback(win, [](GLFWwindow*, int w, int h){ glViewport(0,0,w,h); });

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    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);

    unsigned int shader = makeShader(vertSrc, fragSrc);
    glEnable(GL_DEPTH_TEST);

    glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)W/H, 0.1f, 100.0f);
    glm::vec3 camPos(2.0f, 1.5f, 3.0f);
    glm::mat4 view = glm::lookAt(camPos, glm::vec3(0.0f), glm::vec3(0,1,0));

    glUseProgram(shader);
    glUniformMatrix4fv(glGetUniformLocation(shader, "uProj"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniformMatrix4fv(glGetUniformLocation(shader, "uView"), 1, GL_FALSE, glm::value_ptr(view));

    // Lighting uniforms — set once (constant for this scene)
    glUniform3f(glGetUniformLocation(shader, "uLightPos"),    3.0f, 3.0f, 3.0f);
    glUniform3f(glGetUniformLocation(shader, "uViewPos"),     camPos.x, camPos.y, camPos.z);
    glUniform3f(glGetUniformLocation(shader, "uObjectColor"), 0.5f, 0.55f, 0.6f);  // navy grey
    glUniform3f(glGetUniformLocation(shader, "uLightColor"),  1.0f, 1.0f, 1.0f);
    glUniform1f(glGetUniformLocation(shader, "uAmbientStr"),  0.1f);
    glUniform1f(glGetUniformLocation(shader, "uShininess"),   64.0f);

    std::cout << "Lab 2: Phong lighting active. ESC=quit.\n";
    std::cout << "Experiment: change uShininess (8=glow, 256=tight dot)\n";

    while (!glfwWindowShouldClose(win)) {
        if (glfwGetKey(win, GLFW_KEY_ESCAPE) == GLFW_PRESS)
            glfwSetWindowShouldClose(win, true);

        glClearColor(0.04f, 0.06f, 0.10f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        float t = (float)glfwGetTime();
        glm::mat4 model = glm::rotate(glm::mat4(1.0f),
            glm::radians(t * 12.0f), glm::vec3(0, 1, 0));
        glUniformMatrix4fv(glGetUniformLocation(shader, "uModel"), 1, GL_FALSE, glm::value_ptr(model));

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glfwSwapBuffers(win); glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO);
    glDeleteProgram(shader);
    glfwTerminate();
    return 0;
}
🔬 Break-to-Learn Experiments
  • Change uShininess to 8: The specular highlight spreads across a large portion of the face. Change to 256: it becomes a tiny bright dot. Shininess controls how "polished" the surface appears — lower = matte/rough, higher = mirror-like.
  • Set uAmbientStr to 0.0: The dark side of the cube goes completely black. Set it to 0.5: the whole cube is uniformly bright. The 0.1 value is the standard for indoor lighting that is realistic but preserves shape readability.
  • Move the light: Change uLightPos to (0, 5, 0) for overhead lighting. To (-3, 0, 0) for side lighting. Observe which faces become the bright face and which face goes dark.
✓ Lab 2 Deliverable — Show Your Instructor
Lit 3D Vessel Silhouette
Rotating cube with visible Phong lighting — the 3D shape reads clearly as a solid object. All three lighting components demonstrably active.
  • Dark faces dim but not black — ambient working
  • Faces brighten as they rotate toward the light — diffuse working
  • Specular highlight visible on the most lit face
  • Can change shininess and demonstrate the difference
Lab 3 of 3 — Capstone ⬛ Intermediate · ~40 min · Builds on Lab 2
Apply Surface Texture — Final Vessel Scene
Add a texture to the lit vessel. UV coordinates are added to the vertex data (8 floats per vertex: position + normal + UV). The fragment shader multiplies Phong lighting output by the texture sample — lighting tells you how much light, texture tells you the surface colour. The final scene combines all Day 2 concepts: rotation, depth, lighting, and texture.
📋 Concepts: UV coords · stb_image · glTexImage2D · sampler2D · Phong × texture ⚡ Do Demo 10 (Textures) first 💾 Requires: metal_plate.jpg in project folder
➕ What You Add Over Lab 2
  • Vertex data extended from 6 to 8 floats per vertex: position + normal + UV
  • Attribute 2: UV coords (2 floats, stride 8, offset 6)
  • stb_image loads metal_plate.jpg, glTexImage2D uploads to GPU
  • Vertex shader: passes UV to fragment shader via out vec2 vTexCoord
  • Fragment shader: result = (ambient + diffuse + specular) * texColor
🎯 What You Will See When This Runs
Screen: Rotating vessel with hull plating texture visible on all 6 faces
Lighting: Phong still active — textured surface responds to light
Lit face: Texture bright and detailed. Dark face: texture dim but still readable
💡 Why Multiply Lighting by Texture Colour

The Phong calculation produces a scalar value: how much light reaches this surface point (0.0 = none, 1.0 = full). The texture provides the surface colour: what colour the material is when fully lit. Multiplying them gives you the final colour: the material's colour, darkened or brightened by the amount of light. This is the standard PBR-adjacent approach used in all real-time rendering.

⚠ stb_image Setup

Add #define STB_IMAGE_IMPLEMENTATION before #include "stb_image.h" in exactly one source file. The header is at C:\Resources\headers\stb_image.h on the VM — copy it to your project's src\ folder and add the include path to CMakeLists if needed.

Objectives — Check Off As You Complete
  • Vertex data rebuilt with 8 floats per vertex (pos + normal + UV)
  • Attribute 2 added: UV, stride 8, offset 6
  • stb_image.h included and STB_IMAGE_IMPLEMENTATION defined
  • Texture loaded from metal_plate.jpg, uploaded with glTexImage2D
  • Vertex shader: aTexCoord in, vTexCoord out
  • Fragment shader: texColor = texture(uTexture, vTexCoord).rgb
  • Final line: result = (ambient + diffuse + specular) * texColor
  • All 6 cube faces show correct texture mapping
  • Complete src/main.cpp — Lab 3
    src/main.cpp — Lab 3 Complete File
    C++
    // ═══════════════════════════════════════════════════════════════════════════
    //  RR GRAPHICS LAB — DAY 2 · LAB 3 of 3  [CAPSTONE]
    //  3D Vessel Briefing System — Texture + Phong = Full Final Scene
    //  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
    //
    //  WHAT THIS LAB ADDS OVER LAB 2:
    //  - Vertex data: 8 floats/vertex (pos + normal + UV)
    //  - stb_image loads metal_plate.jpg
    //  - Fragment shader multiplies Phong result by texture colour
    //  - All Day 2 concepts active simultaneously
    //
    //  REQUIRES: metal_plate.jpg in the same folder as the .exe
    //            stb_image.h in src/ (from C:\Resources\headers\)
    // ═══════════════════════════════════════════════════════════════════════════
    
    #define STB_IMAGE_IMPLEMENTATION
    #include "stb_image.h"
    
    #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>
    
    const int W = 900, H = 600;
    
    // 8 floats per vertex: x,y,z  nx,ny,nz  u,v
    // UV 0,0=bottom-left  1,0=bottom-right  1,1=top-right  0,1=top-left
    float cubeVerts[] = {
        // FRONT (+Z)
        -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 (-Z)
         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,
        // LEFT (-X)
        -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,
        -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,
        // RIGHT (+X)
         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,
         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,
        // TOP (+Y)
        -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,
        // BOTTOM (-Y)
        -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,
    };
    
    // Vertex shader: adds UV passthrough
    const char* vertSrc = R"(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aNormal;
    layout(location = 2) in vec2 aTexCoord;
    out vec3 FragPos;
    out vec3 Normal;
    out vec2 vTexCoord;
    uniform mat4 uModel, uView, uProj;
    void main() {
        FragPos     = vec3(uModel * vec4(aPos, 1.0));
        Normal      = mat3(transpose(inverse(uModel))) * aNormal;
        vTexCoord   = aTexCoord;
        gl_Position = uProj * uView * vec4(FragPos, 1.0);
    }
    )";
    
    // Fragment shader: Phong lighting multiplied by texture colour
    const char* fragSrc = R"(
    #version 330 core
    in vec3 FragPos;
    in vec3 Normal;
    in vec2 vTexCoord;
    out vec4 FragColor;
    
    uniform sampler2D uTexture;
    uniform vec3  uLightPos, uViewPos, uLightColor;
    uniform float uAmbientStr, uShininess;
    
    void main() {
        // Sample texture — this replaces the hardcoded object colour from Lab 2
        vec3 texColor = texture(uTexture, vTexCoord).rgb;
    
        vec3 ambient  = uAmbientStr * uLightColor;
        vec3 norm     = normalize(Normal);
        vec3 lightDir = normalize(uLightPos - FragPos);
        float diff    = max(dot(norm, lightDir), 0.0);
        vec3 diffuse  = diff * uLightColor;
        vec3 viewDir  = normalize(uViewPos - FragPos);
        vec3 halfDir  = normalize(lightDir + viewDir);
        float spec    = pow(max(dot(norm, halfDir), 0.0), uShininess);
        vec3 specular = 0.5 * spec * uLightColor;
    
        // KEY LINE: Phong tells us how much light, texture tells us the colour
        vec3 result = (ambient + diffuse + specular) * texColor;
        FragColor = vec4(result, 1.0);
    }
    )";
    
    static unsigned int makeShader(const char* vs, const char* fs) {
        auto compile = [](GLenum t, const char* src) {
            unsigned int s = glCreateShader(t);
            glShaderSource(s, 1, &src, nullptr);
            glCompileShader(s);
            int ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
            if (!ok) { char buf[512]; glGetShaderInfoLog(s,512,nullptr,buf); std::cerr << buf << "\n"; }
            return s;
        };
        unsigned int v = compile(GL_VERTEX_SHADER, vs), f = compile(GL_FRAGMENT_SHADER, fs);
        unsigned int p = glCreateProgram();
        glAttachShader(p, v); glAttachShader(p, f); glLinkProgram(p);
        glDeleteShader(v); glDeleteShader(f);
        return p;
    }
    
    static unsigned int loadTexture(const char* path) {
        unsigned int tex; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        stbi_set_flip_vertically_on_load(true);
        int w,h,ch; unsigned char* data = stbi_load(path, &w, &h, &ch, 0);
        if (data) {
            GLenum fmt = (ch == 4) ? GL_RGBA : GL_RGB;
            glTexImage2D(GL_TEXTURE_2D, 0, fmt, w, h, 0, fmt, GL_UNSIGNED_BYTE, data);
            glGenerateMipmap(GL_TEXTURE_2D);
            std::cout << "Texture loaded: " << path << " (" << w << "x" << h << ")\n";
        } else {
            std::cerr << "WARNING: Could not load " << path << " — using solid grey fallback\n";
            // 1x1 grey pixel as fallback so the programme still runs
            unsigned char grey[] = {128, 130, 140};
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, grey);
        }
        stbi_image_free(data);
        return tex;
    }
    
    int main() {
        glfwInit();
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        GLFWwindow* win = glfwCreateWindow(W, H, "Vessel Briefing System — Lab 3: Textured", nullptr, nullptr);
        glfwMakeContextCurrent(win);
        glewExperimental = GL_TRUE; glewInit();
        glViewport(0, 0, W, H);
        glfwSetFramebufferSizeCallback(win, [](GLFWwindow*, int w, int h){ glViewport(0,0,w,h); });
    
        unsigned int VAO, VBO;
        glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO);
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW);
        // stride = 8 floats (pos3 + normal3 + uv2)
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(3*sizeof(float)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(6*sizeof(float)));
        glEnableVertexAttribArray(2);
    
        unsigned int shader = makeShader(vertSrc, fragSrc);
        unsigned int tex    = loadTexture("metal_plate.jpg");
        glEnable(GL_DEPTH_TEST);
    
        glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)W/H, 0.1f, 100.0f);
        glm::vec3 camPos(2.0f, 1.5f, 3.0f);
        glm::mat4 view = glm::lookAt(camPos, glm::vec3(0.0f), glm::vec3(0,1,0));
    
        glUseProgram(shader);
        glUniformMatrix4fv(glGetUniformLocation(shader, "uProj"), 1, GL_FALSE, glm::value_ptr(proj));
        glUniformMatrix4fv(glGetUniformLocation(shader, "uView"), 1, GL_FALSE, glm::value_ptr(view));
        glUniform3f(glGetUniformLocation(shader, "uLightPos"),   3.0f, 3.0f, 3.0f);
        glUniform3f(glGetUniformLocation(shader, "uViewPos"),    camPos.x, camPos.y, camPos.z);
        glUniform3f(glGetUniformLocation(shader, "uLightColor"), 1.0f, 1.0f, 1.0f);
        glUniform1f(glGetUniformLocation(shader, "uAmbientStr"), 0.15f);   // slightly brighter for texture
        glUniform1f(glGetUniformLocation(shader, "uShininess"),  64.0f);
        glUniform1i(glGetUniformLocation(shader, "uTexture"),    0);  // texture unit 0
    
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, tex);
    
        std::cout << "Lab 3 capstone — all Day 2 concepts active. ESC=quit.\n";
    
        while (!glfwWindowShouldClose(win)) {
            if (glfwGetKey(win, GLFW_KEY_ESCAPE) == GLFW_PRESS)
                glfwSetWindowShouldClose(win, true);
    
            glClearColor(0.04f, 0.06f, 0.10f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            float t = (float)glfwGetTime();
            glm::mat4 model = glm::rotate(glm::mat4(1.0f),
                glm::radians(t * 12.0f), glm::vec3(0, 1, 0));
            glUniformMatrix4fv(glGetUniformLocation(shader, "uModel"), 1, GL_FALSE, glm::value_ptr(model));
    
            glBindVertexArray(VAO);
            glDrawArrays(GL_TRIANGLES, 0, 36);
            glfwSwapBuffers(win); glfwPollEvents();
        }
    
        glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO);
        glDeleteTextures(1, &tex);
        glDeleteProgram(shader);
        glfwTerminate();
        return 0;
    }
    🔬 Break-to-Learn Experiments
    • Change the texture file: Replace metal_plate.jpg with any image file — rename it and update the path. A checkerboard pattern or hull camo makes the UV mapping immediately visible on all 6 faces.
    • Disable texture — restore Lab 2: In the fragment shader, replace texture(uTexture, vTexCoord).rgb with vec3(0.5, 0.55, 0.6). The result is identical to Lab 2. This proves the only change between labs was the texture line.
    • Add UV scaling: Change vTexCoord = aTexCoord to vTexCoord = aTexCoord * 2.0 in the vertex shader. The texture tiles twice per face. Change to 0.5 and the texture zooms in. This demonstrates UV scaling without touching C++ code.
    ✓ Lab 3 Deliverable — Day 2 Complete
    3D Vessel Briefing Display — Fully Complete
    Rotating vessel with hull texture, Phong lighting, depth ordering, and perspective projection all active simultaneously. All Day 2 concepts demonstrated in one scene.
    • Texture maps correctly across all 6 cube faces
    • Phong lighting still active — lit faces brighter than dark faces
    • Rotation smooth — no jitter or flickering
    • Can explain to instructor: what the uModel does, what glm::lookAt does, and what the final multiply line in the fragment shader does
    ← Lab 2
    By Raushan Ranjan (MCT | Educator)
    Koenig Original AI-Courseware · Day 2 Labs Complete
    Day 3 Labs →