Skip to content

Commit 715c7a0

Browse files
committed
add: 02 texture and shader
1 parent c246296 commit 715c7a0

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

.vitepress/config.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export default withMermaid({
2121
text: "Labs",
2222
items: [
2323
{ text: "01 - Hello Triangle", link: "/labs/01-hello-triangle" },
24+
{
25+
text: "02 - Texture and Shader",
26+
link: "/labs/02-texture-and-shader",
27+
},
2428
],
2529
},
2630
],

labs/02-texture-and-shader.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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

Comments
 (0)