This blog post explains OpenGL texture maps
1. Texture
Texture: Texture, in most cases is a 2D image (there are also 1D and 3D textures)
It can be imagined that the texture is a piece of paper with bricks painted on it, pasted on a 3D house, so that the house looks like it has a brick wall appearance
The following 512x512 brick wall image is a texture:
Next, learn how to paste this texture on the previously drawn triangle
First, you need to define a set of texture coordinates, specifying which part of the texture each vertex of the triangle corresponds to
2. Texture coordinates
The texture coordinates of the 2D texture image are on the x and y axes, the range is (0,1), the origin is the coordinates of the lower left corner (0, 0), and the coordinates of the upper right corner are (1, 1)
Using texture coordinates to obtain texture colors is called sampling (collecting fragment colors)
The following figure shows the mapping of texture coordinates and triangles:
We just pass the vertex shader the three texture coordinates that map to the triangle,
They are then passed to the fragment shader, which interpolates the texture coordinates for each fragment
Texture coordinates:
float texCoords[] = { 0.0f, 0.0f, // bottom left corner 1.0f, 0.0f, // bottom right corner 0.5f, 1.0f // top center };
After setting the texture coordinates,
Also tell OpenGL how to do texture sampling
This is the texture surround mode and texture filtering method that needs to be configured below
3. Texture Surrounding
As mentioned in the previous section, the range of texture coordinates is from (0,0) to (1,1)
Beyond this range , OpenGL will repeat the texture image by default
In fact, OpenGL is chosen by others, but we need to configure it:
Surround |
describe |
GL_REPEAT |
Default: repeat texture image |
GL_MIRRORED_REPEAT |
Texture Image Mirror Repeat |
GL_CLAMP_TO_EDGE |
Texture Image Edge Repeat |
GL_CLAMP_TO_BORDER |
User-specified edge color fill |
These options can be set for individual axes using the glTexParameter() function
2D texture coordinate axes: s, t
3D texture coordinate axes: s, t, r
Example:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
Parameter one: texture target
Parameter 2: texture coordinate axis
Parameter three: surround mode
If it is the GL_CLAMP_TO_BORDER option, you also need to call the glTexParameterfv() function to specify an edge color:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
4. Texture filtering
Texture filtering can be understood as a way for OpenGL to sample texture pixels when using textures for mapping
There are many options for texture filtering, the most important are two: GL_NEAREST and GL_LINEAR
GL_NEAREST : Nearest Neighbor Filtering, the default texture filtering method of OpenGL
OpenGL will select the pixel whose center point is closest to the texture coordinates as the sampling color
There are four pixels in the picture below, the plus sign represents the texture coordinates, the center of the texture pixel in the upper left corner is closest to the texture coordinates, so its pixel color will be sampled:
GL_LINEAR: Linear Filtering
Based on the texels near the texture coordinates, OpenGL calculates an approximate color between those texels.
The closer the center of a texel is to the texture coordinates, the greater the contribution of the texel's color to the final sample color.
In the image below you can see that the returned color is a blend of neighboring pixels:
Applying a low-resolution texture to a large object, what are the different effects of these two texture filtering methods,
Look at the picture below:
GL_NEAREST : Sharpened grainy patterns, allowing the pixels that make up the texture to be clearly seen
GL_LINEAR : smooth blurred patterns, it is difficult to see individual texels
Proximity filtering is used when textures are minified
Use linear filtering when textures are upscaled
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Five. Multi-level gradual distance texture
When texture mapping multiple objects with different distances, there is no need for distant objects to use normal resolution textures for mapping
This not only wastes memory, but also affects OpenGL rendering performance, and using large resolution textures on small objects will also produce unrealistic
For this, OpenGL uses a method called multi-level gradient texture (Mipmap)
If this concept is based on the interpretation of official documents, it is a bit difficult to understand thoroughly.
In explaining the multi-level gradient texture, first introduce a very common term in the field of graphics and images: image pyramid
It is an image of the original size that is stepwise downsampled with a scalar coefficient to create images of different scalar levels, which is called an image pyramid.
In the OpenGL multi-level gradual distance texture mode, the scalar is 1/2
After creating a texture, call the glGenerateMipmaps () function and pass in the OpenGL texture type bound to the texture ID to enable the multi-level fading mode of the texture:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
...
glGenerateMipmap(GL_TEXTURE_2D);
OpenGL will select a texture with the most suitable resolution for the object from the image pyramid of this texture according to the distance of the object.
OpenGL multi-level gradual distance texture example:
We don't need to care about the details of image pyramid creation and selection, OpenGL will automatically handle it for us, all we need to do is call glGenerateMipmaps()
When switching the multi-level gradual distance texture level (Level) in rendering, OpenGL will produce an unreal hard border between two different levels of multi-level gradual distance texture layers.
Just like normal texture filtering, you can also use NEAREST and LINEAR filtering between two different fading texture levels when switching fading texture levels.
In order to specify the filtering method between different multi-level gradient texture levels, you can use one of the following four options instead of the original filtering method:
filter method | describe |
GL_NEAREST_MIPMAP_NEAREST | Use nearest-neighbor multi-level gradient textures to match pixel size, and use proximity interpolation for texture sampling |
GL_LINEAR_MIPMAP_NEAREST | Use the nearest multi-level fader texture level and sample using linear interpolation |
GL_LINEAR_MIPMAP_NEAREST | Use the nearest multi-level fader texture level and sample using linear interpolation |
GL_LINEAR_MIPMAP_LINEAR | Use linear interpolation between two adjacent multi-level gradient textures, and use linear interpolation to sample |
Multi-level gradient textures are only used when the texture is scaled down
Using a multi-level fader texture when the texture is enlarged will have no effect and will generate a GL_INVALID_ENUM error code
6. Loading and creating textures
1.stb_image.h
Texture maps need to serialize images in different formats (png, jpg, bmp....), you can use the open source stb_image.h library
It is not so much a library, it is actually a .h file, and the function declarations and definitions for binary parsing of pictures in various formats are all in this .h file
We just need to download it and put it in inc, and then define and include it in the code
Download address: stb/stb_image.h at master nothings/stb GitHub
Code loading:
(1) Header file loading
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_IMPLEMENTATION: The preprocessor will modify the header file so that it only contains the relevant function definition source code, which is equivalent to turning this header file into a .cpp file. The #define sentence in the code should be #include "stb_image.h " before, otherwise the following error will be reported:
(2) Load pictures in the code:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
stbi_load() function:
Parameter 1: image path
Parameters 2 and 3: image width and height
Parameter four: the number of color channels
2. Generate texture
(1) Create texture ID:
unsigned int texture;
glGenTextures(1, &texture);
(2) Bind the texture ID to OpenGL:
glBindTexture(GL_TEXTURE_2D, texture);
(3) glTexImage2D() generates texture:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexImage2D() function:
Parameter one: the 2D texture currently bound to OpenGL
Parameter 2: Multi-level gradual distance texture level, if you set it manually, fill in 0
Parameter three: the format in which the texture is stored
Parameters four and five: texture width and height
Parameter six: set to 0, don't ask why, OpenGL historical issues
Parameters seven and eight: source image format and data type (we load this image using RGB values and store them as char (byte) arrays)
Parameter nine: real image data
After calling glTexImage2D(), the texture object currently bound to OpenGL will be attached with the texture image
If you do not set the multi-level gradient texture, OpenGL will only load the original texture image
The second parameter allows us to manually set the multi-level gradual distance texture level (that is, the image pyramid level), or we can call glGenerateMipmap() externally to set it separately and let OpenGL configure the level by itself
(4) Release image memory:
stbi_image_free(data);
(5) The overall process of generating a texture:
//创建、绑定纹理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
//加载纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
//纹理多级渐远
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
//回收图像内存
stbi_image_free(data);
3. Draw the texture:
(1). Define the vertex data with added texture coordinates:
float vertices[] = {
// ---- position ---- ---- color ---- - texture coordinates -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left
};
(2). Configure vertex attributes
New vertex format:
Vertex attribute configuration code:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
(3) Modify the shader code
Modify the vertex shader code to increase the texture coordinates
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
Modify the fragment shader code to use the output variable TexCoord of the texture coordinates in the vertex shader as the input variable
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
The texture object should also be accessible in the fragment shader
Sample2D: GLSL's built-in data type SampleXD, also known as a sampler , can be used to add texture objects to shaders
texture(): GLSL built-in function, the first the texture sampler, and the second parameter is the texture coordinate
The texture function will use the previously set texture parameters to sample the corresponding color value
The output of this fragment shader is the color after the texture interpolation filter
(4) draw
Bind texture, glDrawElements() draw
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
Compile and run:
Modify the fragment shader to mix the texture color with the vertex color:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
7. Blend textures
1. Texture unit
The wooden box texture map effect has been realized in the above process
One of the details is that in the fragment shader we define a uniform variable of type sampler2D for the texture object: ourTexture
As mentioned in the previous blog post, the uniform global variable in the shader can be assigned in the code using the glUniformX() type function
Then, we can define multiple textures in the fragment shader
The position values of these textures in the fragment shader (also can be understood as index values) are called texture units
Texture objects need to be activated by calling glActiveTexture() before binding
glActiveTexture(GL_TEXTURE0); // Activate the texture unit before binding the texture
glBindTexture(GL_TEXTURE_2D, texture);
OpenGL activates GL_TEXTURE0 by default
Because there is only one texture in the previous code, we did not call glActiveTexture() to activate the texture specifically
OpenGL guarantees at least 16 texture units , which means it can be activated from GL_TEXTURE0 to GL_TEXTRUE15
They are all defined in order, so we can also get GL_TEXTURE8 through GL_TEXTURE0 + 8
This is useful when we need to loop over some texture units
2. Mix rendering two textures
Two texture mixed maps, first modify the fragment shader code:
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
mix(): GLSL built-in function , using the third parameter weighted for linear interpolation
Output: parameter 1*(1-parameter 3) + parameter 2*parameter 3
After adding a texture, in addition to the fragment shader code needs to be modified, the rendering process code also needs to be changed accordingly
This is reflected in two places:
(1). Before the texture object is bound to OpenGL, it must be activated
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
(2). Set the texture unit of the texture sampler to OpenGL
Ensure that each uniform sampler corresponds to the correct texture unit
// 不要忘记在设置uniform变量之前激活着色器程序!
ourShader.use();
// 手动设置
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
// 或者使用我们前文中封装好的着色器类的api设置
ourShader.setInt("texture2", 1);
while(...)
{
[...]
}
Another thing to note is that
OpenGL requires that the y-axis 0.0 coordinate is at the bottom of the picture, but the y-axis 0.0 coordinate of the picture is usually at the top
So the code also needs to flip the y-axis of the image through the api of stb_image.h:
stbi_set_flip_vertically_on_load(true);