Skip to content

Lecture 8: Shading 2(Shading, Pipeline and Texture Mapping)

Blinn-Phong Shading Model

Specular Reflection

镜面反射(Specular Reflection):光线会在物体表面形成高光。

当视角接近反射光线的方向时,会产生高光效果,此时\(l\)\(v\)的半程向量\(\mathbf{H}\)与法向量\(\mathbf{N}\)的夹角较小。

Blinn-Phong模型中的镜面反射计算公式为:

\[ h = bisector(\mathbf{L}, \mathbf{V}) = \frac{\mathbf{L} + \mathbf{V}}{\left\lVert \mathbf{L} + \mathbf{V}\right\rVert}\qquad L_s = k_s \cdot (\frac{I}{r^2}) \cdot \max(0, \mathbf{N} \cdot \mathbf{H})^\mathbf{p} \]

其中,\(k_s\)是镜面反射系数,\(I\)是光源强度,\(r\)是光源到表面的距离,\(\mathbf{p}\)是高光指数。

Ambient Reflection

环境光(Ambient Reflection):物体表面在没有直接光照时的颜色。环境光通常是一个常数值,用于模拟环境光对物体的影响。

\[ L_a = k_a \cdot I_a \]

其中,\(k_a\)是环境光系数,\(I_a\)是环境光强度。

Final Color Calculation

最终颜色计算公式为:

\[ L = L_d + L_s + L_a = k_d \cdot I_L \cdot \max(0, \mathbf{N} \cdot \mathbf{L}) + k_s \cdot (\frac{I}{r^2}) \cdot \max(0, \mathbf{N} \cdot \mathbf{H})^\mathbf{p} + k_a \cdot I_a \]

Shading Frequencies

着色频率是指在渲染过程中对物体表面进行着色的频率。如果着色频率过低,会导致颜色出现明显的块状或条纹状分布,影响渲染效果。

1755892815044

Flat Shading(平面着色)

Flat Shading是一种简单的着色方法,它对每个多边形面使用单一颜色进行着色。其优点是计算量小,但缺点是无法表现物体表面的细节和高光效果。

Gouraud Shading(逐顶点着色)

Gouraud Shading是一种逐顶点着色方法,它在每个顶点计算颜色,然后在三角形的边界上进行插值。其优点是计算量小,但缺点是高光效果不明显。

如何计算顶点的法线呢?答案是通过顶点周围的面法线进行平均。公式如下:

\[ N_v = \frac{\sum_{i=1}^{n} \mathbf{N}_i}{\left\lVert \sum_{i=1}^{n} \mathbf{N}_i \right\rVert} \]

以上公式还可以通过考虑各面的面积来进行加权平均。

Phong Shading(逐像素着色)

Phong Shading是一种逐像素着色方法,它在每个像素计算颜色,能够更好地模拟高光效果。其计算量较大,但渲染效果更好。

如何计算像素的法线呢?答案是通过顶点的法线进行插值,使用重心坐标进行插值。

Graphics Pipeline

图形渲染管线(Graphics Pipeline)是将3D模型转换为2D图像的过程。它通常分为以下几个阶段:

  1. 顶点处理(Vertex Processing):处理顶点数据,投影到屏幕空间
  2. 三角形处理(Triangle Processing):将顶点组装成三角形
  3. 光栅化(Rasterization):将三角形转换为像素
  4. 片段处理(Fragment Processing):处理每个像素的颜色和深度信息
  5. 帧缓冲操作(Framebuffer Operations):将片段颜色写入帧缓冲区

Texture Mapping

纹理映射(Texture Mapping)是将图像(纹理)应用到3D模型表面的技术。它可以增加模型的细节和真实感。

需要定义一个纹理坐标系(Texture Coordinate System)来映射纹理到模型表面。纹理坐标通常使用二维坐标系表示,范围从(0, 0)到(1, 1)。


附录

用AI生产了一段代码,用于理解OpenGL:(编译环境:VS2022,使用glad2、GLFW和glm库)

C++
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
/**
 * @file main.cpp
 * @brief A demonstration of a 3D lit, tumbling cube using modern OpenGL.
 *
 * @details
 * This program serves as a comprehensive example of modern OpenGL rendering.
 * Key features include:
 * - **Window and Context Management**: Uses GLFW to create a window and an OpenGL 3.3 Core Profile context.
 * - **OpenGL Function Loading**: Employs GLAD to load required OpenGL functions at runtime.
 * - **3D Mathematics**: Leverages the GLM library for all matrix operations (Model, View, Projection),
 *   avoiding legacy fixed-function pipeline matrix stacks.
 * - **Shader-Based Rendering**: Implements a vertex and fragment shader pair to control the rendering pipeline.
 *   - The **Vertex Shader** transforms vertices from object space to clip space.
 *   - The **Fragment Shader** calculates per-pixel lighting using the Phong reflection model.
 * - **Dynamic Animation**: The cube rotates around a smoothly changing axis, and its color cycles through
 *   the spectrum, both based on elapsed time.
 * - **Performance Optimization**: VSync is enabled to prevent excessive GPU usage, and uniform locations
 *   are cached to minimize redundant API calls.
 *
 * @author Written by AI Assistant
 * @date 2025-08-22
 */

// --- 1. HEADERS AND GLOBAL CONSTANTS ---

// GLAD: A loader-generator for OpenGL. It's crucial to include GLAD's header before
// GLFW's, as GLAD defines the necessary OpenGL headers which GLFW might also include.
#include <glad/gl.h>

// GLFW: A library for creating windows, contexts, and surfaces, and for receiving
// input and events. It provides a simple, platform-independent API.
#include <GLFW/glfw3.h>

// GLM (OpenGL Mathematics): A header-only C++ mathematics library for graphics software
// based on the GLSL specification.
#include <glm/glm.hpp>                  // Core GLM types like glm::vec3 and glm::mat4
#include <glm/gtc/matrix_transform.hpp> // Functions to create transformation matrices (translate, rotate, scale, perspective)
#include <glm/gtc/type_ptr.hpp>         // Function to convert GLM types to raw pointers for OpenGL

// Standard C++ Libraries
#include <iostream> // For printing messages to the console (e.g., error reporting)
#include <vector>   // For dynamically-sized arrays, used here for error logs

// Global constants for window dimensions. Using constants makes it easy to change
// the window size in one place.
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 800;

// --- 2. FUNCTION PROTOTYPES ---
// Forward-declaring functions helps organize the code and allows `main` to be at the top
// without causing "identifier not found" compiler errors.

// Initialization functions
GLFWwindow* initWindow();
void setupOpenGL();

// Shader management functions
GLuint compileShader(GLenum type, const char* src);
GLuint createShaderProgram(const char* vs, const char* fs);

// Geometry creation functions
GLuint createCubeVAO(GLuint& VBO);

// Callback and input handling functions
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);


// --- 3. MAIN FUNCTION ---
// The entry point of the application.

int main()
{
 // --- INITIALIZATION PHASE ---
 // Set up the environment (window, OpenGL context, etc.).
 GLFWwindow* window = initWindow();
 if (!window) {
  // If window creation fails, `initWindow` will have already printed an error.
  // Terminate the program gracefully.
  return -1;
 }

 // --- PERFORMANCE OPTIMIZATION: ENABLE VSYNC ---
 // This synchronizes the application's framerate with the monitor's refresh rate (e.g., 60 Hz).
 // It is the most effective way to prevent the GPU from running at 100% load
 // by rendering frames that will never be displayed. A value of 1 enables VSync.
 glfwSwapInterval(1);

 // Load OpenGL functions and configure global OpenGL state.
 setupOpenGL();

 // --- SHADER CREATION PHASE ---
 // Shaders are small programs that run on the GPU. They are essential for modern OpenGL.

 // Source code for the Vertex Shader, written in GLSL (OpenGL Shading Language).
 // The vertex shader's primary job is to process each vertex and determine its final
 // position in clip space, which is then used for rasterization.
 const char* vertexShaderSrc = R"(
        #version 330 core

        // INPUT: Vertex attributes from the VBO.
        // `layout (location = 0)` links this attribute to the first slot in the VAO's configuration.
        layout (location = 0) in vec3 aPos;    // Vertex position in object (local) space
        layout (location = 1) in vec3 aNormal; // Vertex normal vector in object space

        // OUTPUT: Data to be passed to the fragment shader.
        // These values will be interpolated across the surface of the triangle.
        out vec3 FragPos; // The vertex's position in world space
        out vec3 Normal;  // The vertex's normal vector in world space

        // UNIFORMS: Global variables passed from the CPU to the shader.
        // They are constant for all vertices processed in a single draw call.
        uniform mat4 model;      // Model matrix: transforms from object space to world space
        uniform mat4 view;       // View matrix: transforms from world space to view (camera) space
        uniform mat4 projection; // Projection matrix: transforms from view space to clip space

        void main()
        {
            // 1. Transform vertex position to world space. We pass this to the fragment shader
            //    so it knows the pixel's position in the world for lighting calculations.
            FragPos = vec3(model * vec4(aPos, 1.0));

            // 2. Transform the normal vector to world space.
            //    We use the inverse transpose of the model matrix to ensure that normals are
            //    correctly oriented even if the model is non-uniformly scaled. For simple
            //    rotation/translation, just `mat3(model)` would suffice, but this is more robust.
            Normal = mat3(transpose(inverse(model))) * aNormal;

            // 3. Calculate the final position of the vertex in clip space.
            //    This is the mandatory output of the vertex shader. OpenGL will use this
            //    to figure out where on the screen the vertex lies.
            gl_Position = projection * view * vec4(FragPos, 1.0);
        }
    )";

 // Source code for the Fragment Shader, also in GLSL.
 // The fragment shader runs for every pixel (fragment) of a rendered triangle and
 // determines its final color. This is where lighting and texturing are typically handled.
 const char* fragmentShaderSrc = R"(
        #version 330 core

        // OUTPUT: The final color of the fragment.
        out vec4 FragColor;

        // INPUT: Data received from the vertex shader, interpolated for this specific fragment.
        in vec3 FragPos; // This fragment's position in world space
        in vec3 Normal;  // This fragment's normal vector in world space

        // UNIFORMS: Global variables for lighting calculations.
        uniform vec3 objectColor; // The base color of the object material
        uniform vec3 lightColor;  // The color of the light source
        uniform vec3 lightPos;    // The position of the light in world space
        uniform vec3 viewPos;     // The position of the camera/viewer in world space

        void main()
        {
            // --- Phong Lighting Model ---
            // This model simulates lighting by combining three components: ambient, diffuse, and specular.

            // 1. Ambient Lighting: Simulates indirect light that bounces around the scene,
            // ensuring that even parts in shadow are not completely black.
            float ambientStrength = 0.1;
            vec3 ambient = ambientStrength * lightColor;

            // 2. Diffuse Lighting: Simulates the directional impact of a light source.
            // The closer the angle between the surface normal and the light direction is to 0,
            // the brighter the surface.
            vec3 norm = normalize(Normal); // Ensure the normal vector is a unit vector
            vec3 lightDir = normalize(lightPos - FragPos); // Vector from this fragment to the light source
            float diff = max(dot(norm, lightDir), 0.0);    // `max(..., 0.0)` prevents light from appearing on the back side
            vec3 diffuse = diff * lightColor;

            // 3. Specular Lighting: Simulates the bright highlight that appears on shiny surfaces.
            // It depends on the viewing angle and the reflection of the light.
            float specularStrength = 0.5;
            vec3 viewDir = normalize(viewPos - FragPos);    // Vector from this fragment to the viewer
            vec3 reflectDir = reflect(-lightDir, norm);     // Calculate the reflection vector for the light
            float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); // 32 is the "shininess" factor. Higher means a smaller, sharper highlight.
            vec3 specular = specularStrength * spec * lightColor;

            // Combine all three lighting components and multiply by the object's base color.
            vec3 result = (ambient + diffuse + specular) * objectColor;
            FragColor = vec4(result, 1.0); // Set the final color with full alpha (1.0)
        }
    )";

 // Compile the GLSL source code and link the shaders into a single shader program.
 GLuint shaderProgram = createShaderProgram(vertexShaderSrc, fragmentShaderSrc);

 // --- PERFORMANCE OPTIMIZATION: CACHE UNIFORM LOCATIONS ---
 // It's much more efficient to query uniform locations once after creating the
 // shader program, rather than querying them every single frame inside the render loop.
 // `glGetUniformLocation` involves a string comparison and is a relatively slow operation.
 // We store the integer "location" IDs for use in the render loop.
 GLint lightColorLoc = glGetUniformLocation(shaderProgram, "lightColor");
 GLint lightPosLoc = glGetUniformLocation(shaderProgram, "lightPos");
 GLint viewPosLoc = glGetUniformLocation(shaderProgram, "viewPos");
 GLint objectColorLoc = glGetUniformLocation(shaderProgram, "objectColor");
 GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
 GLint viewLoc = glGetUniformLocation(shaderProgram, "view");
 GLint projectionLoc = glGetUniformLocation(shaderProgram, "projection");

 // --- GEOMETRY SETUP PHASE ---
 // Define the cube's vertices and upload them to the GPU.
 GLuint VBO, VAO; // VBO: Vertex Buffer Object, VAO: Vertex Array Object
 VAO = createCubeVAO(VBO);

 // --- RENDER LOOP ---
 // This is the heart of the application. It runs continuously until the user
 // signals to close the window (e.g., by clicking the 'X' button or pressing ESC).
 while (!glfwWindowShouldClose(window))
 {
  // 1. INPUT: Check for and process any user input.
  processInput(window);

  // 2. RENDERING: All drawing commands go here.
  // Set a background color (a dark grey).
  glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
  // Clear the color and depth buffers from the previous frame.
  // The depth buffer must be cleared to ensure correct depth testing in the new frame.
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Activate our shader program. All subsequent rendering calls will use this program.
  glUseProgram(shaderProgram);

  // 3. UPDATE UNIFORMS: Send updated data to the shaders for this frame.
  float timeValue = (float)glfwGetTime(); // Get the elapsed time since GLFW started.

  // Update lighting uniforms (these could also be set once outside the loop if they don't change).
  glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f); // White light
  glUniform3f(lightPosLoc, 1.2f, 1.0f, 2.0f);   // Position of the light source
  glUniform3f(viewPosLoc, 0.0f, 0.0f, 3.0f);    // The camera is at (0,0,3) in world space

  // Update the object color dynamically over time using sine waves for a pleasing effect.
  float redValue = sin(timeValue * 2.0f) / 2.0f + 0.5f;   // Sways between 0.0 and 1.0
  float greenValue = sin(timeValue * 1.5f) / 2.0f + 0.5f;
  float blueValue = sin(timeValue * 2.5f) / 2.0f + 0.5f;
  glUniform3f(objectColorLoc, redValue, greenValue, blueValue);

  // --- Set up Model-View-Projection (MVP) matrices for this frame ---

  // View Matrix (Camera): Defines the position and orientation of the camera.
  // We create it by starting with an identity matrix and translating it backward.
  // Moving the entire scene *away* from the camera is equivalent to moving the camera *backward*.
  glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -3.0f));

  // Projection Matrix (Lens): Defines the viewing volume (frustum).
  // We use a perspective projection, which makes distant objects appear smaller.
  // - 45.0f: Field of View (FOV)
  // - (float)SCR_WIDTH / (float)SCR_HEIGHT: Aspect ratio
  // - 0.1f, 100.0f: Near and Far clipping planes
  glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);

  // Model Matrix (Object Transform): Defines the position, rotation, and scale of the object.
  // We make the rotation axis change over time to create a "tumbling" effect.
  float axisX = cos(timeValue * 0.5f);
  float axisY = sin(timeValue * 0.7f);
  float axisZ = cos(timeValue * 0.3f);
  glm::vec3 rotationAxis = glm::normalize(glm::vec3(axisX, axisY, axisZ)); // Normalize for stable rotation

  // Create the rotation matrix. The angle of rotation also increases with time.
  glm::mat4 model = glm::rotate(glm::mat4(1.0f), timeValue * glm::radians(50.0f), rotationAxis);

  // Send the matrices to the vertex shader using the cached locations.
  glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
  glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
  glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

  // 4. DRAW: Issue the draw call to render the object.
  glBindVertexArray(VAO); // Bind the VAO that contains our cube's vertex data and attribute pointers.
  // Draw the triangles. We have 36 vertices in total (6 faces * 2 triangles/face * 3 vertices/triangle).
  glDrawArrays(GL_TRIANGLES, 0, 36);

  // 5. SWAP BUFFERS AND POLL EVENTS: Finalize the frame.
  // Swaps the back buffer (which we were drawing to) with the front buffer (which is on screen).
  glfwSwapBuffers(window);
  // Checks for any events (like keyboard input, mouse movement, window resizing) and calls their callbacks.
  glfwPollEvents();
 }

 // --- 7. CLEANUP ---
 // De-allocate all resources once the application is exiting.
 glDeleteVertexArrays(1, &VAO);
 glDeleteBuffers(1, &VBO);
 glDeleteProgram(shaderProgram);

 // Terminate GLFW, cleaning up all of its resources.
 glfwTerminate();
 return 0;
}

// --- 4. INITIALIZATION FUNCTIONS ---

/**
 * @brief Initializes GLFW, creates a window, and prepares the OpenGL context.
 * @return A pointer to the created GLFWwindow, or nullptr if an error occurred.
 */
GLFWwindow* initWindow() {
 // Initialize the GLFW library.
 if (!glfwInit()) {
  std::cout << "Failed to initialize GLFW" << std::endl;
  return nullptr;
 }

 // Configure GLFW with window hints before creating the window.
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // We want OpenGL 3.3
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // We want the modern, core profile

 // Create a window object.
 GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Tumbling Lit Cube (GLM)", NULL, NULL);
 if (window == NULL) {
  std::cout << "Failed to create GLFW window" << std::endl;
  glfwTerminate();
  return nullptr;
 }
 // Make the window's context the current context on the calling thread.
 glfwMakeContextCurrent(window);

 // Register our callback function for window resize events.
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 return window;
}

/**
 * @brief Loads OpenGL function pointers using GLAD and sets initial OpenGL state.
 */
void setupOpenGL() {
 // Initialize GLAD. We pass it the function to get the address of OpenGL functions,
 // which is provided by GLFW.
 if (!gladLoadGL(glfwGetProcAddress)) {
  std::cout << "Failed to initialize GLAD" << std::endl;
  // A real application should handle this failure more gracefully.
 }

 // Enable the depth test. This ensures that OpenGL draws fragments based on their
 // distance to the camera, so closer objects correctly obscure farther ones.
 glEnable(GL_DEPTH_TEST);
}


// --- 5. SHADER FUNCTIONS ---

/**
 * @brief Compiles a single shader from source code.
 * @param type The type of shader (e.g., GL_VERTEX_SHADER or GL_FRAGMENT_SHADER).
 * @param src The shader's source code as a C-style string.
 * @return The ID of the compiled shader object. Returns 0 on failure.
 */
GLuint compileShader(GLenum type, const char* src) {
 GLuint shader = glCreateShader(type);
 glShaderSource(shader, 1, &src, nullptr);
 glCompileShader(shader);

 // Check for compilation errors.
 GLint ok = 0;
 glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
 if (!ok) {
  GLint len = 0;
  glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
  std::vector<char> log(len);
  glGetShaderInfoLog(shader, len, nullptr, log.data());
  const char* shaderTypeStr = (type == GL_VERTEX_SHADER) ? "VERTEX" : "FRAGMENT";
  std::cout << "ERROR::SHADER::" << shaderTypeStr << "::COMPILATION_FAILED\n" << log.data() << std::endl;
  glDeleteShader(shader); // Don't leak the shader.
  return 0;
 }
 return shader;
}

/**
 * @brief Creates a shader program by compiling and linking vertex and fragment shaders.
 * @param vs Source code for the vertex shader.
 * @param fs Source code for the fragment shader.
 * @return The ID of the linked shader program. Returns 0 on failure.
 */
GLuint createShaderProgram(const char* vs, const char* fs) {
 // Compile the vertex and fragment shaders.
 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vs);
 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fs);
 if (vertexShader == 0 || fragmentShader == 0) {
  return 0;
 }

 // Create a shader program object.
 GLuint program = glCreateProgram();
 // Attach the compiled shaders to the program.
 glAttachShader(program, vertexShader);
 glAttachShader(program, fragmentShader);
 // Link the shaders together.
 glLinkProgram(program);

 // Check for linking errors.
 GLint ok = 0;
 glGetProgramiv(program, GL_LINK_STATUS, &ok);
 if (!ok) {
  GLint len = 0;
  glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
  std::vector<char> log(len);
  glGetProgramInfoLog(program, len, nullptr, log.data());
  std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << log.data() << std::endl;
  glDeleteProgram(program);
  program = 0;
 }

 // The individual shaders are no longer needed after they've been linked into the program,
 // so we can (and should) delete them to free up resources.
 glDeleteShader(vertexShader);
 glDeleteShader(fragmentShader);

 return program;
}


// --- 6. BUFFER/VAO FUNCTIONS ---

/**
 * @brief Creates and configures the Vertex Array Object (VAO) and Vertex Buffer Object (VBO) for the cube.
 * @param[out] VBO The ID of the created VBO will be stored in this reference variable.
 * @return The ID of the configured VAO.
 */
GLuint createCubeVAO(GLuint& VBO) {
 // Define the vertex data for a cube.
 // Each line represents a vertex with its position (x, y, z) and normal vector (nx, ny, nz).
 // We need 36 vertices because each of the 6 faces needs its own vertices to have distinct normals.
 // If we shared vertices, the normals would be averaged, resulting in a smooth-shaded look instead of a sharp-edged cube.
 float vertices[] = {
  // positions          // normals
  // Back face
  -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,
  // Front face
  -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
  -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
   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,
   // Bottom face
   -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,
   // Top face
   -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
 };

 GLuint VAO_id;
 // Generate one VAO and one VBO.
 glGenVertexArrays(1, &VAO_id);
 glGenBuffers(1, &VBO);

 // --- Configure the VAO and VBO ---
 // A VAO stores the configuration of vertex attributes. By binding a VAO,
 // all subsequent VBO bindings and attribute pointer configurations are saved to it.
 glBindVertexArray(VAO_id);

 // Bind the VBO to the GL_ARRAY_BUFFER target.
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // Copy the vertex data from our `vertices` array into the currently bound VBO.
 // GL_STATIC_DRAW is a hint that the data will not change often.
 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

 // --- Set up Vertex Attribute Pointers ---
 // Tell OpenGL how to interpret the vertex data in the VBO.

 // Attribute 0: Position
 // Corresponds to `layout (location = 0)` in the vertex shader.
 glVertexAttribPointer(0,         // Attribute location index
  3,         // Number of components per attribute (x, y, z)
  GL_FLOAT,  // Type of the components
  GL_FALSE,  // Should data be normalized?
  6 * sizeof(float), // Stride: byte offset between consecutive vertices
  (void*)0); // Offset of the first component in the buffer
 glEnableVertexAttribArray(0);    // Enable this vertex attribute

 // Attribute 1: Normal
 // Corresponds to `layout (location = 1)` in the vertex shader.
 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float),
  (void*)(3 * sizeof(float))); // Offset: starts after the 3 position floats
 glEnableVertexAttribArray(1);

 // Unbind the VBO and VAO to prevent accidental modification.
 // This is good practice, though not strictly necessary in this simple example.
 glBindBuffer(GL_ARRAY_BUFFER, 0);
 glBindVertexArray(0);

 return VAO_id;
}


// --- 8. UTILITY AND CALLBACK FUNCTIONS ---

/**
 * @brief Processes user input. This function is called once per frame.
 * @param window The active GLFW window.
 */
void processInput(GLFWwindow* window) {
 // If the user presses the ESCAPE key, set the window's "should close" flag to true.
 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
  glfwSetWindowShouldClose(window, true);
 }
}

/**
 * @brief A GLFW callback function that is executed whenever the window is resized.
 * @param window The window that was resized.
 * @param width The new width of the window, in pixels.
 * @param height The new height of the window, in pixels.
 */
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
 // When the window is resized, we need to update OpenGL's viewport to match
 // the new dimensions. The viewport specifies the area of the window where
 // rendering will occur.
 glViewport(0, 0, width, height);
}