一、纹理相关
1.纹理概念
用简单光照明模型生成真实感图像,由于表面过于光滑单调,反而显得不真实。现实物体表面有各种表面细节,这些细节就叫纹理。
纹理:体现物体表面的细节
2.纹理类型:
颜色纹理:物体表面(平面或者曲面)花纹、图案
几何纹理:基于物体表面的微观几何形状
3.生成纹理的一般方法,是预先定义纹理模式,然后建立物体表面的点与纹理模式的点之间的对应。
4.纹理映射
当物体表面的可见点确定之后,以纹理模式的对应点参与光照模型进行计算,就可把纹理模式附加到物体表面上。这种方法称为纹理映射。
5.纹理模式
图像纹理:将二维纹理图案映射到三维物体表面,绘制物体表面上的一点时,采用相应的纹理图案中相应的颜色值。
函数纹理:用数学函数定义简单的二维纹理图案,如方格地毯;或用数学函数定义随机高度场,生成表面粗糙纹理即几何纹理。
6.纹素(texel)和纹理坐标
使用纹素而不是像素来表示纹理对象中的显示元素,主要是为了强调纹理对象的应用方式。纹理对象通常是通过纹理图片读取到的,这个数据保存到一个二维数组中,这个数组中的元素称为纹素(texel),纹素包含颜色值和alpha值。纹理对象的宽度和高度应该是2的整数幂(如16,32,64,128,256)。要想获得纹理对象中的纹素,需要使用纹理坐标指定。
纹理坐标与纹理对象的大小无关,这样指定的纹理坐标当纹理对象大小变更时,依然能够工作。
纹理坐标使用规范化的值,大小范围为[0,1],纹理坐标使用uv表示,u轴从左到右,v轴从底向上,右上角为(1,1),左下角为(0,0)。
通过指定纹理坐标,可以映射到纹素,如一个256*256大小的二维纹理,坐标(0.5,1.0)对应的纹素即是(128,256).
7.纹理映射方法:
建立物体空间表面和纹理空间之间的对应关系。
第一步:根据物体空间的表面坐标(x,y,z)计算其纹理空间坐标(u,v)值:对物体表面坐标(x,y,z)用u、v进行参数化
第二步:反求出参数u、v用物体表面坐标(x,y,z)的表达
第三步:根据纹理空间定义的纹理(u,v)得到该处的纹理值,并用此值取代光照模型中的相应项,实现纹理映射。
二、基于OpenGL的颜色纹理
1.加载与创建纹理
根据learn OpenGL CN教程,我们使用stb_image.h库(stb_image.h下载地址)来实现图像加载,把该库文件放到我们的工程文件目录底下后,将stb_image.h加入到我们的工程中,并在我们的.cpp中加入
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
然后我们可以找一张.jpg格式的图片(如木箱),使用它的stb_load函数。
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
2.指定纹理坐标
// 指定顶点属性数据 顶点位置 颜色 纹理
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,0.0f, 0.0f, // 0
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,1.0f, 0.0f, // 1
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f,1.0f, 1.0f, // 2
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f,0.0f, 1.0f // 3
};
GLushort indices[] = {
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
};
3.生成纹理
unsigned int texture;
glGenTextures(1, &texture);
//绑定纹理
glBindTexture(GL_TEXTURE_2D, texture);
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
void GL_APIENTRY glTexImage2D(GLenum target, GLint level, GLenum internalformat,
GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels);
第一个参数指定了纹理目标(Target),这个值必须是GL_TEXTURE_2D;
level执行细节级别,0是最基本的图像级别,n表示第N级贴图细化级别
internalformat指定纹理中的颜色组件。可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,GL_LUMINANCE_ALPHA等几种。
width指定纹理图像的宽度,必须是2的n次方。纹理图片至少要支持64个材质元素的宽度
height:指定纹理图像的高度,必须是2的m次方。纹理图片至少要支持64个材质元素的高度
border:指定边框的宽度,必须为0
format:像素数据的颜色格式,不需要和internalformatt取值相同
倒数第二个type:指定像素数据的数据类型。可以使用的值有:GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1
pixel:指定内存中指向图像数据的指针
4.设置纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
5.顶点着色器
#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;
}
6.片元着色器
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
7.纹理环绕方式
环绕方式 | 描述 |
GL_REPEAT | 对纹理的默认行为,重复图像 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色 |
当纹理坐标超出默认范围时,每个选项都有不同的的视觉效果。
可以使用glTexParamenter*函数对单独的一个坐标轴设置(s、t(三维纹理有r),和x,y,z等价)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
第一个参数指定了纹理目标,由于使用的是2D纹理,则为GL_TEXTURE_2D
第二个参数需要我们指定设置的选项与应用的纹理轴,我们打算配置的是WRAP选项,并且指定s和t轴
第三个参数为纹理环绕方式
8.纹理过滤
当使用纹理坐标映射到纹素坐标时,正好得到对应纹素的中心位置的情况很少出现。当纹理坐标映射到一个浮点位的纹素位置时,我们需要进行纹理过滤
一种方式是对这个坐标取整,使用最佳逼近点来获取纹素,这种方式即点采样,也就是最近邻滤波(nearest neighbor filtering),这种方式容易导致走样误差,明显有像素块的感觉。其示意图如下(其中目标纹素位置距离左上角的纹素最近,因此它被选为样本颜色):
GL_LINEAR(线性过滤)会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色贡献越大。下图返回邻近像素的混合色
下图为两种滤波方法的效果图
可以看出,最近邻方法获取的纹素有明显的像素块,而线性滤波方法获取的纹素看起来比较平滑。两种方法各自应用于不同的场合,没有绝对的好坏,当需要制作8位图形效果(8 bit graphic,每个像素用8位字节表示)需要使用最近邻滤波。
当进行放大(Magnify)和缩小(Minify)操作的时候可以设置过滤的选项。
使用glTexParameter*函数为放大和缩小指定过滤方式。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
9.多级渐进纹理
当物体在场景中离观察者很远,最终只用一个屏幕像素来显示时,这个像素纹理确定成为一个问题。OpenGL使用多级渐进纹理(Mipmap)来解决这个问题,即提前计算一系列的纹理图像,后一个纹理是前一个的二分之一。
原始纹理 256×256 |
Mip 1 = 128×128 |
Mip 2 = 64×64 |
Mip 3 = 32×32 |
Mip 4 = 16×16 |
Mip 5 = 8×8 |
Mip 6 = 4×4 |
Mip 7 = 2×2 |
Mip 8 = 1×1 |
多级渐进纹理的理念是:距观察者的距离超过一定的阈值,OpenGL会使用不同的渐进纹理,即最适合物体距离的那个。
Mipmap纹理示意图如下:
OpenGL中通过函数glGenerateMipmap(GL_TEXTURE_2D);来生成Mipmap,前提是已经指定了原始纹理。原始纹理必须自己通过读取纹理图片来加载,上面第一步我们已经实现了。
如果直接在不同等级的MipMap之间切换,会形成明显的边缘,因此对于Mipmap也可以同纹素一样使用滤波方法在不同等级的Mipmap之间滤波。要在不同等级的MipMap之间滤波,需要将之前设置的GL_TEXTURE_MIN_FILTER选项更改为以下选项之一:
过滤方式 | 描述 |
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
使用设置glTexParameteri过滤方式(主要用于纹理缩小,用于纹理放大时会出现GL_INVALID_ENUM错误):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
10.main函数(shader.cpp和shader.h不变,同OpenGL学习之着色器下)
#include
#include
#include
#include"Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader zprogram
// ------------------------------------
Shader ourShader("VertexSource.txt", "fragmentSource.txt");
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
// positions // colors // texture coords
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
};
unsigned int indices[] = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// texture coord attribute
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// load and create a texture
// -------------------------
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture); // all upcoming GL_TEXTURE_2D operations now have effect on this texture object
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
int width, height, nrChannels;
// The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path.
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);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// bind Texture
glBindTexture(GL_TEXTURE_2D, texture);
// render container
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
11.结果图
12. 纹理与颜色混合
在顶点着色器中修改
FragColor = texture(texture1, TexCoord) * vec4(ourColor, 1.0);
结果如下
13.多个纹理单元
在一个程序中可以使用多个纹理单元加载多个2D纹理。
(1)纹理单元
一个纹理的位置通常称为纹理单元,一个纹理的默认激活纹理单元是0。通过把纹理单元赋值给采样器,一次可以绑定多个纹理。
ourShader.use();
// 使用0号纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Id, "texture1"), 0);
// 使用1号纹理单元
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Id, "texture2"), 1);
(2)着色器
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
上面用到的GLSL内建函数mix()需要接受两个值作为参数,并对它们根据第三个参数进行线性插值,此处的0.2表示返回80%第一个输入颜色和20%第二个输入颜色。
(3)main函数
在释放第一张纹理图片后绑定第二张纹理(awesomeface.png)
unsigned int texture2;
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2); // all upcoming GL_TEXTURE_2D operations now have effect on this texture object
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_set_flip_vertically_on_load(true);
// load image, create texture and generate mipmaps
data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
ourShader.use();
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
ourShader.setInt("texture2", 1);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// bind Texture
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
以上代码中
stbi_set_flip_vertically_on_load(true);
使用的原因是:OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部,所以我们在加载图片时翻转y轴。
源代码中加载的第二张图片如果是一张没有透明成分的图就会报错,而用有透明成分的png图片甚至是不存在的图片都可以通过。所以说若要正确运行,第二张图片不仅要用png格式而且图片中要有透明的成分。由于我们加的第二个纹理图片是png格式,有RGBA四个通道,所以这一句修改为
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
(4)结果图
参考文章: https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/#_8
https://blog.csdn.net/wangdingqiaoit/article/details/51457675