当前位置: 首页 > news >正文

第三章 opengl之纹理

OpenGL

  • 纹理
    • 纹理环绕方式
  • 纹理过滤
    • 多级渐远纹理
  • 加载和创建纹理
    • stb_image.h
  • 生成纹理
  • 纹理的应用
    • 纹理单元

纹理

用stb_image.h库,原先用SOIL库也可以实现。

可以为每个顶点添加颜色来增加图形的细节。但是想得到一个真实的图形,需要足够多的顶点,从而指定足够多的颜色。
会产生很多额外开销,每个模型都有更多的顶点,每个顶点都需要一个颜色属性。纹理是一个2D图片,可以用来添加物体的细节。(除了图像以外,纹理页可以被用来存储大量的数据,数据可以发送到着色器上)

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。纹理坐标如下:

float texCoords[] = {0.0f, 0.0f, // 左下角1.0f, 0.0f, // 右下角0.5f, 1.0f // 上中
};

纹理环绕方式

纹理坐标范围是(0,0)-(1,1)。如果把纹理坐标设置在范围之外,OpenGL会重复这个纹理图像:

GL_REPEAT——对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT——和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE——纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER——超出的坐标为用户指定的边缘颜色。

前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

参数解释如下:
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴。最后一个参数需要我们传递一个环绕方式(Wrapping),在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为GL_MIRRORED_REPEAT。

如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:

float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

纹理过滤

纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。你可能已经猜到了,OpenGL也有对于纹理过滤(Texture Filtering)的选项。纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
1。GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素
2。GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。

在一个很大的物体上应用一个分辨率低的纹理(则:纹理被放大了,每个纹理像素都能看到):
GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出
当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多级渐远纹理

一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一
多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。

OpenGL有一个glGenerateMipmaps函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作。在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
GL_NEAREST_MIPMAP_NEAREST——使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST——使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR——在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR——在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样

就像纹理过滤一样,我们可以使用glTexParameteri将过滤方式设置为前面四种提到的方法之一:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

常见的一个错误是:将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果**,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码**。

加载和创建纹理

纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列,一个解决方案是选一个需要的文件格式,比如.PNG,然后自己写一个图像加载器,把图像转化为字节序列。写自己的图像加载器虽然不难,但仍然挺麻烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的stb_image.h库。

stb_image.h

stb_image.h是Sean Barrett的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中。下载这一个头文件,将它以stb_image.h的名字加入你的工程,并另创建一个新的C++文件,输入以下代码:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了。

要使用stb_image.h加载图片,我们需要使用它的stbi_load函数:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

这个函数首先接受一个图像文件的位置作为输入。接下来它需要三个int作为它的第二、第三和第四个参数,stb_image.h将会用图像的宽度、高度和颜色通道的个数填充这三个变量。我们之后生成纹理的时候会用到的图像的宽度和高度的。最后那个0表示你期望的通道数

生成纹理

纹理跟之前生成的OpenGL对象类似,用ID引用生成:

unsigned int texture;
glGenTextures(1, &texture);

glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中(我们的例子中只是单独的一个unsigned int),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:

glBindTexture(GL_TEXTURE_2D, texture);

现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
  1. 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
  2. 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
  3. 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
  4. 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
  5. 下个参数应该总是被设为0(历史遗留的问题)。
  6. 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
  7. 最后一个参数是真正的图像数据。

当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。

生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个很好的习惯。

stbi_image_free(data);

生成一个纹理的过程如下:

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);

纹理的应用

使用glDrawElements绘制最后一部分的矩形。
需要告诉OpenGL采样纹理。需要用纹理坐标更新顶点数据:

float vertices[] = {
//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
};

因为新添加了一个顶点属性,需要告诉OpenGL新的顶点格式:
在这里插入图片描述

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:

#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;
}

片段着色器应该接下来会把输出变量TexCoord作为输入变量。

片段着色器也应该能访问纹理对象,需要我们把纹理对象传给片段着色器:GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。例子如下:我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。

#version 330 core
out vec4 FragColor;in vec3 ourColor;
in vec2 TexCoord;uniform sampler2D ourTexture;void main()
{FragColor = texture(ourTexture, TexCoord);
}

使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。

现在只剩下在调用glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

综上,源码如下:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>#include <learnopengl/shader_s.h>#include <iostream>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, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#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("4.1.texture.vs", "4.1.texture.fs"); // set up vertex data (and buffer(s)) and configure vertex attributes// ------------------------------------------------------------------float vertices[] = {// positions          // colors           // texture coords0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // top right0.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 triangle1, 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 attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1);// texture coord attributeglVertexAttribPointer(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 parametersglTexParameteri(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 parametersglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// load image, create texture and generate mipmapsint 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(FileSystem::getPath("resources/textures/container.jpg").c_str(), &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 TextureglBindTexture(GL_TEXTURE_2D, texture);// render containerourShader.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);
}

纹理单元

sampler2D变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以前面部分我们没有分配一个位置值。

纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:

glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);

接下来的glBindTexture函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活,所以我们在前面的例子里当我们使用glBindTexture的时候,无需激活任何纹理单元。

补充:OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTRUE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8,这在当我们需要循环一些纹理单元的时候会很有用。
下面需要编辑片段着色器来接收另一个采样器:

#version 330 core
...uniform sampler2D texture1;
uniform sampler2D texture2;void main()
{FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

最终输出颜色现在是两个纹理的结合。GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。

我们现在需要载入并创建另一个纹理;记得创建另一个纹理对象,载入图片,使用glTexImage2D生成最终纹理。
为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:

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);

还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面:

ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置while(...) 
{[...]
}

通过使用glUniform1i设置采样器,我们保证了每个uniform采样器对应着正确的纹理单元。

注意:OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。很幸运,stb_image.h能够在图像加载时帮助我们翻转y轴,只需要在加载任何图像前加入以下语句即可:

stbi_set_flip_vertically_on_load(true);

综上,源码如下:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>#include <learnopengl/shader_s.h>#include <iostream>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, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#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("4.2.texture.vs", "4.2.texture.fs");// set up vertex data (and buffer(s)) and configure vertex attributes// ------------------------------------------------------------------float vertices[] = {// positions          // colors           // texture coords0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // top right0.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 triangle1, 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 attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1);// texture coord attributeglVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));glEnableVertexAttribArray(2);// load and create a texture // -------------------------unsigned int texture1, texture2;// texture 1// ---------glGenTextures(1, &texture1);glBindTexture(GL_TEXTURE_2D, texture1); // set the texture wrapping parametersglTexParameteri(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 parametersglTexParameteri(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 mipmapsint width, height, nrChannels;stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.// 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(FileSystem::getPath("resources/textures/container.jpg").c_str(), &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);// texture 2// ---------glGenTextures(1, &texture2);glBindTexture(GL_TEXTURE_2D, texture2);// set the texture wrapping parametersglTexParameteri(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 parametersglTexParameteri(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 mipmapsdata = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0);if (data){// note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBAglTexImage2D(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);// tell opengl for each sampler to which texture unit it belongs to (only has to be done once)// -------------------------------------------------------------------------------------------ourShader.use(); // don't forget to activate/use the shader before setting uniforms!// either set it manually like so:glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);// or set it via the texture classourShader.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 textures on corresponding texture unitsglActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, texture1);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, texture2);// render containerourShader.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);
}

相关文章:

第三章 opengl之纹理

OpenGL纹理纹理环绕方式纹理过滤多级渐远纹理加载和创建纹理stb_image.h生成纹理纹理的应用纹理单元纹理 用stb_image.h库&#xff0c;原先用SOIL库也可以实现。 可以为每个顶点添加颜色来增加图形的细节。但是想得到一个真实的图形&#xff0c;需要足够多的顶点&#xff0c;…...

【Flink】FlinkSQL中执行计划以及如何用代码看执行计划

FilnkSQL怎么查询优化 Apache Flink 使用并扩展了 Apache Calcite 来执行复杂的查询优化。 这包括一系列基于规则和成本的优化,例如: • 基于 Apache Calcite 的子查询解相关 • 投影剪裁 • 分区剪裁 • 过滤器下推 • 子计划消除重复数据以避免重复计算 • 特殊子查询重写,…...

从业者必读,一篇文章轻松掌握DevOps核心概念和最佳技能实践!

文章目录前言一. DevOps的定义及由来二. DevOps的价值三. devops工具有哪些3.1 devops工程师的硬实力3.2 devops工程师的软实力总结前言 大家好&#xff0c;又见面了&#xff0c;我是沐风晓月&#xff0c;本文是对DevOps的总结&#xff0c;一篇文章告诉你什么是DevOps. 对很多…...

2023爱分析·一体化HR SaaS市场厂商评估报告:北森

目录 1.研究范围定义 2. 一体化HR SaaS市场分析 3.厂商评估&#xff1a;北森 4.入选证书 1.研究范围定义 研究范围 伴随数字化转型走向深入&#xff0c;企业人力资源数字化也进入快速发展阶段&#xff0c;人力资源的价值也得到了重新审视和定义。政策层面&#xff0c;《…...

JAVA练习67-二叉树的中序遍历

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、题目-二叉树的中序遍历 1.题目描述 2.思路与代码 2.1 思路 2.2 代码 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 3月3日练习…...

【JeecgBoot-Vue3】第1节 源码下载和环境安装与启动

目录 一. 资料 1. 源码下载 2. 官网启动文档 二、 前端开发环境安装 2.1 开发工具 2.2 前后端代码下载 2.3 前端启动 Step 1&#xff1a;安装nodejs npm Step 2&#xff1a;配置国内镜像&#xff08;这里选阿里&#xff09; Step 3&#xff1a;安装yarn Step 4&…...

WebAPI

WebAPI知识详解day11.Web API 基本认知作用和分类什么是DOM&#xff1f;DOM树的概念DOM对象2.获取DOM对象通过css选择器获取dom对象通过其他方法获取dom3.设置/修改DOM元素内容方法1. document.write() 方法方法2. 对象.innerText 属性方法3. 对象.innerHTML4.设置/修改DOM元素…...

Shell命令——date的用法

date命令可以用来显示或设定系统的日期与时间。 一、显示系统的日期与时间 &#xff08;1&#xff09;如果date命令后面不加任何参数&#xff0c;则会按照固定的格式显示时间信息&#xff1a; 星期几 月份 日 时:分:秒 时区 年xjhubuntu:~/iot/tmp$ date Fri Mar 3 16:56:4…...

XSS跨站脚本

XSS跨站脚本XSS简介XSS验证XSS危害XSS简介 XSS被称为跨站脚本攻击(Cross-site scripting)&#xff0c;由于和CSS(Cascading Style Sheets)重名&#xff0c;所以改为XSS。XSS主要基于javascript语言完成恶意的攻击行为&#xff0c;因为javascript可以非常灵活的操作html、css和…...

【强烈建议收藏:MySQL面试必问系列之慢SQL优化专题】

一.知识回顾 学习本篇文章之前呢&#xff0c;我们可以先看一下【强烈建议收藏:MySQL面试必问系列之SQL语句执行专题】&#xff0c;看完这篇文章再来学习本篇文章可谓是如虎添翼。好的&#xff0c;那我们也不讲太多的废话&#xff0c;直接开始。 二.如何做慢SQL查询优化呢&…...

windows,liunx,java实现apk解压,去签名、重新签名,重新打包apk

背景&#xff1a;由于项目需要&#xff0c;需要将apk包加入服务端返回的静态资源文件到apk中&#xff0c;形成离线apk包供下载安装。经过调查研究&#xff0c;决定使用apktool实现。关于apktool的资料可以参考 https://blog.csdn.net/quantum7/article/details/124060620 htt…...

【Linux】进程信号

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;信号入门&…...

SpringBoot 集成Junit单元测试

学习文章: https://www.cnblogs.com/ysocean/p/6889906.html 开发工具: IDEA 2022.1.4 目录 目录 1. 概述 2. 实现步骤 2.1 maven导入依赖 2.2 随意代码演示(不推荐) 2.3 规范代码演示(推荐) 3. Junit相关其他注解 4. 注意事项 5. 结语 1. 概述 接触到Junit&#xff0c;…...

Android开发之简单控件

文章目录一 文本显示1.1 文本设置的两种方式1.2 常见字号单位类型2.2 设置文本的颜色三 视图基础3.1 设置视图的宽高3.2 设置视图的间距3.3 设置视图的对齐方式四常用布局4.1 线性布局LinearLayout4.2 相对布局RelativeLayout4.3 网格布局GridLayout4.4 滚动视图ScrollView五 按…...

树状数组讲解

树状数组 文章目录树状数组引入例题AcWing241.楼兰图腾思路代码AcWing 242. 一个简单的整数问题思路代码AcWing 244. 谜一样的牛思路代码总结引入 树状数组主要维护的是这样一个数据结构&#xff1a; tr[x]表示以x为终点的长度为lowbit(x)的前缀和、最大值、最小值、最大公约数…...

每个Android开发都应需知的性能指标~

无论你是发布一个新的 Android 应用&#xff0c;还是希望提高现有应用的性能&#xff0c;你都可以使用 Android 应用性能指标来帮助你。 在这篇文章中&#xff0c;我将解释什么是 Android 应用性能指标&#xff0c;并列出8个需要考虑跟踪的维度和建议的基线。 什么是 Android…...

MSYS2安装

最近在学习windows上编译FFmpeg&#xff0c;需要用到msys2&#xff0c;在此记录一下安装和配置过程。 点击如下链接&#xff0c;下载安装包&#xff1a; Index of /msys2/distrib/x86_64/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 我下载的是&#xff1a;ms…...

3/3考试总结

时间安排 7:30–7:50 看题&#xff0c;怎么感觉三道构造&#xff0c;T3 貌似有网络流背景。 7:50–8:30 T1,有一些简单的性质&#xff0c;缩减两端点后枚举一下翻转的区间就可以了。然后花了一点时间写 spj 调试。 8:30–10:20 T2,比较纯粹的构造题。有网络流做法&#xff0c;…...

Spark Streaming DStream转换

DStream上的操作与RDD的类似&#xff0c;分为Transformations&#xff08;转换&#xff09;和Output Operations&#xff08;输出&#xff09;两种&#xff0c;此外转换操作中还有一些比较特殊的算子&#xff0c;如&#xff1a;updateStateByKey()、transform()以及各种Window相…...

水果商城,可运行

文章目录项目介绍一、技术栈二、本项目分为前后台&#xff0c;有管理员与用户两种角色&#xff1b;1、管理员角色包含以下功能&#xff1a;2、用户角色包含以下功能&#xff1a;三、用户功能页面展示四、管理员功能页面展示五、部分代码展示六、获取整套项目源码项目介绍 一、…...

LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像

LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像1、报警信息1.1、报警查询1.2、配置开启报警订阅1.2.1、国标设备编辑1.2.2、选择开启报警订阅1.3、配置摄像头报警1.3.1、配置摄像头报警通道ID1.3.2、配置摄像头开启侦测1.3.3、尝试触发摄像头…...

软件测试---测试分类

一 : 按测试对象划分 1.1 可靠性测试 可靠性&#xff08;Availability&#xff09;即可用性&#xff0c;是指系统正常运行的能力或者程度&#xff0c;一般用正常向用户提供软件服务的时间占总时间的百分比表示。 1.2 容错性测试 行李箱 , 四个轮子 , 坏了一个 , 说明这个容错…...

剑指 Offer II 015. 字符串中的所有变位词

题目链接 剑指 Offer II 015. 字符串中的所有变位词 mid 题目描述 给定两个字符串 s和 p&#xff0c;找到 s中所有 p的 变位词 的子串&#xff0c;返回这些子串的起始索引。不考虑答案输出的顺序。 变位词 指字母相同&#xff0c;但排列不同的字符串。 示例 1&#xff1a; 输…...

【SpringCloud】SpringCloud详细教程之微服务比较

目录前言一.什么是微服务&#xff1f;为什么要使用微服务二.微服务对比三.企业开发场景前言 我会通过实际代码来给展示每个组件的用法 一.什么是微服务&#xff1f;为什么要使用微服务 分布式&#xff0c;把一个项目拆分成多个模块&#xff0c;每一个模块相当于一个服务。 微…...

二.项目使用vue-router,引入ant-design-vue的UI框架,引入less

根据前文《使用Vue脚手架工具搭建vue项目》搭建好脚手架后使用 1.vue-router 2.引入UI框架ant design vue 3.引入less 1.vue-router vue-router分为两种模式(默认为hash模式)&#xff1a; hash history hash&#xff1a; 特征&#xff1a; 1.hash会在浏览器路径里带#号&#…...

网络安全怎么学?20年白帽子老江湖告诉你

很多人都知道龙叔是个老程序员&#xff0c;但却不知道其实我也是个H客&#xff0c;20年前我就开始痴迷于H客技术&#xff0c;可以说是网络安全方面的老江湖了。 到现在&#xff0c;我还依然会去研究这一块&#xff0c;偶尔会和一些网安的朋友交流技术&#xff0c;比如说红盟的…...

药房管理系统;药库管理系统

第一&#xff0c;主要功能&#xff1a;  本系统集日常销售、药品进销存、会员积分、GSP管理等药店所需的所有功能于一体&#xff0c;实现店铺管理的全部自动化。第二、新功能&#xff1a;  增加了“按功能查询药品”的功能&#xff0c;使软件用户可以根据客户的症状推荐合适…...

深眸科技|机器视觉提升制造性能,焕发传统企业智造新活力!

随着机器视觉技术的成熟与发展&#xff0c;其在工业制造中得到越来越广泛的应用。机器视觉在工业制造领域的应用朝着智能识别、智能检测、智能测量以及智能互联的完整智能体系方向发展。此外&#xff0c;快速变化的市场需求&#xff0c;不断涌入行业的竞争对手&#xff0c;让传…...

ubuntu安装SSH的方法

Ubuntu安装SSH的方法。14版的ubuntu经过测试&#xff0c;默认没有开启SSH&#xff0c;所以需要安装。 1、虚拟机设置网卡为桥接模式&#xff0c;即NAT。12版虚拟机默认的。 2、查看ubuntu使用的ip。 ifconfig即可查看&#xff0c;14版的ubuntu自带这个命令。 3、查看是否pi…...

哪种蓝牙耳机通话效果好?通话清晰的蓝牙耳机推荐

出门的时候&#xff0c;如果戴耳机和别人通话&#xff0c;就不必把耳机摘下来&#xff0c;接电话变得前所未有的简单。现在的蓝牙耳机&#xff0c;已经不是单纯的用来听音乐了&#xff0c;而是一种更好的功能。下面这四款蓝牙耳机不仅适合听歌&#xff0c;通话还清晰&#xff0…...

做网站不赚钱了/app制作一个需要多少钱

题目&#xff1a;子线程循环10次&#xff0c;接着主线程循环100次&#xff0c;接着又回到子线程循环10次&#xff0c;接着再回到主线程100次&#xff0c;如此循环50次&#xff0c;请写出程序。 1 public class TraditionalThreadSynchronized2 {2 public static void main(…...

购物网站建设机构/深圳百度关键

1 概述 函数式编程是一种抽象程序很高的编程范式&#xff0c;纯粹的函数式编程语言编写的函数没有变量&#xff0c;因此&#xff0c;任意一个函数&#xff0c;只要输入是确定的&#xff0c;输出就是确定的&#xff0c;这种纯函数称之为没有副作用。而允许使用变量的程序设计语…...

wordpress更换域名所有页面404/企业网站的搜索引擎推广与优化

概述&#xff1a;Spark 程序开发&#xff0c;调试和运行&#xff0c;intellij idea开发Spark java程序。 分两部分&#xff0c;第一部分基于intellij idea开发Spark实例程序并在intellij IDEA中运行Spark程序.第二部分&#xff0c;将开发程序提交到Spark local或者hadoop YARN…...

泗水网站建设ys178/百度首页排名优化价格

整理来源|网络背调&#xff0c;可以很精准的检验应聘者简历中所写和所讲的是否属实&#xff0c;已经成为招聘企业检验员工是否合格的重要手段之一&#xff0c;也是应聘者在面试过程中十分反感的一点。我们经常会看到有人在网络上吐槽自己因为背调错失了高薪offer&#xff0c;而…...

重庆网站推广哪家好/拓客引流推广

JetCache学习笔记 - lwh147 - 博客园JetCache学习笔记和问题记录https://www.cnblogs.com/lwh147/p/15176574.html 在分布式系统中&#xff0c;使用redis的原子性操作实现分布式锁&#xff0c;不过在自己编辑代码的时候&#xff0c;会出现一些代码上的问题&#xff0c;这个工具…...

wordpress ios shared/河北软文搜索引擎推广公司

数着星星 盼着月亮2021国考笔试成绩终于出了&#xff01; 成绩今日公布中公锦鲤给大家带来好运哭惹&#xff01;也太快了吧&#xff01;只能骂骂咧咧地打开查询入口▽▽▽扫码进入成绩查询入口国考成绩对手成绩&同岗分差万人报名产生千人大岗的XXX局究竟会有多少大神产生…...