|
| 1 | +# Lab 02: Texture and Shader |
| 2 | + |
| 3 | +In the last session, you created your first shape. Now, it's time to give it some life and detail. We'll be exploring how to wrap 2D images, called **textures**, onto our 3D objects. |
| 4 | + |
| 5 | +Let's return to our analogy of the GPU as a high-tech factory. You've already built the raw chassis of a car (the triangle). Now, you're going to hire a specialized painter robot (the **Fragment Shader**) and give it a detailed decal sheet (a **Texture**) to apply to the car's body, making it look realistic and interesting. |
| 6 | + |
| 7 | +### Part 1: Expanding the Blueprint (Adding Texture Coordinates) |
| 8 | + |
| 9 | +To apply a decal to a car, the factory robot needs a blueprint that shows exactly where each part of the decal goes. Similarly, to apply a texture to a triangle, we need to tell the GPU which part of the 2D image maps to each corner (vertex) of our triangle. |
| 10 | + |
| 11 | +These mapping instructions are called **Texture Coordinates**. They are 2D coordinates that range from `(0, 0)` (bottom-left corner of the image) to `(1, 1)` (top-right corner). |
| 12 | + |
| 13 | +*How texture coordinates map a 2D image onto a 3D object (a quad in this case).* |
| 14 | + |
| 15 | +#### Step 1.1: Updating the Raw Materials (Vertices) |
| 16 | + |
| 17 | +We now need to add this new texture coordinate data to our `vertices` array. Each vertex will now have a 3D position and a 2D texture coordinate. We'll draw a rectangle (made of two triangles) this time to make the texture more visible. |
| 18 | + |
| 19 | +```cpp |
| 20 | +// Each vertex now has: position (x, y, z) and texture coordinate (s, t) |
| 21 | +float vertices[] = { |
| 22 | + // positions // texture coords |
| 23 | + 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // top right |
| 24 | + 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // bottom right |
| 25 | + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // bottom left |
| 26 | + -0.5f, 0.5f, 0.0f, 0.0f, 1.0f // top left |
| 27 | +}; |
| 28 | + |
| 29 | +// We also define an Element Buffer Object (EBO) to reuse vertices |
| 30 | +unsigned int indices[] = { |
| 31 | + 0, 1, 3, // first triangle |
| 32 | + 1, 2, 3 // second triangle |
| 33 | +}; |
| 34 | +``` |
| 35 | + |
| 36 | +#### Step 1.2: Updating the GPU's Blueprint (The VAO) |
| 37 | + |
| 38 | +Since we've added new data, we must update our VAO "blueprint" to tell the GPU how to read it. Before, it just read positions. Now, it needs to know how to find both the position and the texture coordinate within the data stream. |
| 39 | + |
| 40 | +```cpp |
| 41 | +// 1. Position attribute (location = 0) |
| 42 | +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); |
| 43 | +glEnableVertexAttribArray(0); |
| 44 | + |
| 45 | +// 2. Texture coordinate attribute (location = 1) |
| 46 | +glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); |
| 47 | +glEnableVertexAttribArray(1); |
| 48 | +``` |
| 49 | +* The first line is for the position, just like before, but notice the "stride" is now `5 * sizeof(float)` because each vertex takes up 5 float values. |
| 50 | +* The second line is new. It tells the GPU that our second attribute (`location = 1`) is the texture coordinate. It's 2 floats long, and it starts after the 3 position floats. |
| 51 | +
|
| 52 | +### Part 2: Upgrading the Artists (The Shaders) |
| 53 | +
|
| 54 | +Our old shaders only knew how to position vertices and draw a solid color. We need to give them an upgrade to handle textures. |
| 55 | +
|
| 56 | +```mermaid |
| 57 | +graph TD |
| 58 | + A["Start: Vertex Data<br/>(Positions + Texture Coords)"] --> B["Vertex Shader<br/>(Position vertices AND pass TexCoords)"]; |
| 59 | + B --> C["Shape Assembly<br/>(Connect dots to form triangles)"]; |
| 60 | + C --> D["Fragment Shader<br/>(Receives TexCoords and uses a Texture to color pixels)"]; |
| 61 | + D --> E["End: Final Image<br/>(Textured object on screen)"]; |
| 62 | +``` |
| 63 | + |
| 64 | +#### The Vertex Shader (Passing the Blueprint) |
| 65 | + |
| 66 | +The Vertex Shader's new job is to receive the texture coordinate from the VAO and simply pass it along to the next stage in the pipeline. |
| 67 | + |
| 68 | +```glsl |
| 69 | +// Vertex Shader Code (shader.vs) |
| 70 | +#version 330 core |
| 71 | +layout (location = 0) in vec3 aPos; // Input: vertex position |
| 72 | +layout (location = 1) in vec2 aTexCoord; // Input: texture coordinate |
| 73 | +
|
| 74 | +out vec2 TexCoord; // Output the texture coordinate to the fragment shader |
| 75 | +
|
| 76 | +void main() { |
| 77 | + gl_Position = vec4(aPos, 1.0); |
| 78 | + TexCoord = aTexCoord; // Pass the coordinate along |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +#### The Fragment Shader (The Master Painter) |
| 83 | + |
| 84 | +This is where the real magic happens. The Fragment Shader receives the texture coordinate for each pixel. It also gets a new special variable, a `sampler2D`, which holds the actual texture image. Its job is to use the coordinate to look up the correct color from the texture and apply it to the pixel. |
| 85 | + |
| 86 | +```glsl |
| 87 | +// Fragment Shader Code (shader.fs) |
| 88 | +#version 330 core |
| 89 | +out vec4 FragColor; // Output: the final color for a pixel |
| 90 | +
|
| 91 | +in vec2 TexCoord; // Input: the coordinate from the Vertex Shader |
| 92 | +
|
| 93 | +uniform sampler2D ourTexture; // The actual texture image from our C++ code |
| 94 | +
|
| 95 | +void main() { |
| 96 | + // Look up the color from the texture at the given coordinate |
| 97 | + FragColor = texture(ourTexture, TexCoord); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +### Part 3: Preparing the Decal Sheet (Loading the Texture Image) |
| 102 | + |
| 103 | +We've told our shaders *how* to use a texture, but we haven't actually given them one yet. We need to load an image file (like a `.png` or `.jpg`) from our disk into the GPU's memory. For this, we'll use a popular, easy-to-use library called **`stb_image.h`**. |
| 104 | + |
| 105 | +1. **Generate a Texture Object**: We ask OpenGL to create an empty texture object for us. |
| 106 | +2. **Load the Image Data**: We use `stb_image` to load the pixel data from the file. |
| 107 | +3. **Send Data to GPU**: We bind our texture object and send the pixel data we just loaded to the GPU. |
| 108 | + |
| 109 | +Here's the C++ code to load a texture named `container.jpg`: |
| 110 | + |
| 111 | +```cpp |
| 112 | +#include <stb_image.h> // A library to load images |
| 113 | + |
| 114 | +// 1. Generate and bind the texture object |
| 115 | +unsigned int texture; |
| 116 | +glGenTextures(1, &texture); |
| 117 | +glBindTexture(GL_TEXTURE_2D, texture); |
| 118 | + |
| 119 | +// Optional: Set texture wrapping and filtering options |
| 120 | +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); |
| 121 | +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); |
| 122 | +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| 123 | +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| 124 | + |
| 125 | +// 2. Load the image data from a file |
| 126 | +int width, height, nrChannels; |
| 127 | +unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); |
| 128 | + |
| 129 | +// 3. Send the image data to the GPU |
| 130 | +if (data) { |
| 131 | + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); |
| 132 | + glGenerateMipmap(GL_TEXTURE_2D); |
| 133 | +} else { |
| 134 | + std::cout << "Failed to load texture" << std::endl; |
| 135 | +} |
| 136 | + |
| 137 | +// Free the CPU memory, as the data is now on the GPU |
| 138 | +stbi_image_free(data); |
| 139 | +``` |
| 140 | +
|
| 141 | +### Part 4: The Final Render! |
| 142 | +
|
| 143 | +We're all set! All that's left is to tell OpenGL to use our texture when drawing. We do this inside the main render loop. |
| 144 | +
|
| 145 | +```cpp |
| 146 | +// The updated Render Loop |
| 147 | +while (!glfwWindowShouldClose(window)) { |
| 148 | + // 1. Clear the canvas |
| 149 | + glClear(GL_COLOR_BUFFER_BIT); |
| 150 | +
|
| 151 | + // 2. Activate our shader program |
| 152 | + glUseProgram(shaderProgram); |
| 153 | +
|
| 154 | + // 3. IMPORTANT: Bind the texture so the shader can use it |
| 155 | + glBindTexture(GL_TEXTURE_2D, texture); |
| 156 | +
|
| 157 | + // 4. Use our vertex data's blueprint |
| 158 | + glBindVertexArray(VAO); |
| 159 | +
|
| 160 | + // 5. Draw the textured rectangle! |
| 161 | + // We use glDrawElements because we have an EBO |
| 162 | + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); |
| 163 | +
|
| 164 | + // Swap buffers and check for events |
| 165 | + glfwSwapBuffers(window); |
| 166 | + glfwPollEvents(); |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +And there you have it! When you run the complete program, you will see your image beautifully wrapped around a rectangle. You've just mastered one of the most fundamental and powerful concepts in computer graphics. |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +# References |
| 175 | + |
| 176 | +- [Textures - Learn OpenGL](https://learnopengl.com/Getting-started/Textures) |
0 commit comments