/**
* @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);
}