• 友链

  • 首页

  • 文章归档
h u a n b l o g
h u a n b l o g

欢

HI,Friend

05月
14
Shader
C++

OpenGL笔记4-纹理

发表于 2024-05-14 • 字数统计 29446 • 被 2,284 人看爆

序言

以glew、glfw库
OpenGL学习网站
glfw官网
OpenGL-API文档
glew官网

纹理

纹理:是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。

三角形纹理.png

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

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。

下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
纹理坐标.png

我们为三角形指定了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。

纹理坐标看起来就像这样:

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

对纹理采样的解释非常宽松,它可以采用几种不同的插值方式。所以我们需要自己告诉OpenGL该怎样对纹理采样。

纹理环绕方式

纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:

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

当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。下面是示例:
纹理环绕方式示例.png

可使用glTexParameter*函数对环绕方式对单独的坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);     //GL_TEXTURE_WRAP_S是S轴,对应X轴 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);     //GL_TEXTURE_WRAP_T是T轴,对应Y轴 

glTexParameter*:设置纹理的坐标轴。

  • 第一个参数:指定了纹理目标。示例是2D纹理,因此纹理目标是GL_TEXTURE_2D。
  • 第二个参数:需要我们指定设置的选项与应用的纹理轴。我们配置的是WRAP选项,并且指定S和T轴。
  • 第三个参数:传递环绕方式。

如果我们选择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需要知道怎样将纹理像素映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。

纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。

Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。

临近过滤

GL_NEAREST:也叫临近过滤(Nearest Neighbor Filtering)。是OpenGL默认的纹理过滤方式。

当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。

下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
临近过滤.png

线性过滤

GL_LINEAR:也叫线性过滤((Bi)linear Filtering)。

它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。

一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。

下图中你可以看到返回的颜色是邻近像素的混合色:
线性过滤.png

那么两种过滤效果怎样呢?我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):

两种过滤效果.png

GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出,但有些开发者更喜欢8-bit风格,所以他们会用GL_NEAREST选项。

有些图形处理后会出现锯齿状,可以通过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会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:

多级渐远纹理.png

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错误代码。

加载与创建纹理

用于将图像加载到我们应用中。即图像加载库(图像库)。这里以SOIL2为例(SOIL太老了)。

SOIL2

SOIL2是简易OpenGL图像库(Simple OpenGL Image Library2)的缩写,它支持大多数流行的图像格式,使用起来也很简单。

soli2下载地址

将soil2编译出lib库,可使用premake工具进行。

premake生成lib库教程

将导出lib库和SOIL2.h按照之前配置的OpenGL配置即可。
OpenGL环境搭建(GLEW版)

使用SOIL2加载图片,我们需要使用它的SOIL_load_image函数:

int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);

SOIL_load_image:记载图片。

  • 第一个参数:图片地址。
  • 第二个参数:图片宽和高,SOIL2会返回宽和高到其中。后面我们在生成纹理的时候会用图像的宽度和高度。
  • 第三个参数:指定图片的通道(Channel)数量,但是这里我们只需留为0。
  • 第四个参数:告诉SOIL2如何来加载图片:我们只关注图片的RGB值。结果会储存为一个很大的char/byte数组。

生成纹理

和之前生成的OpenGL对象一样,纹理也是使用ID引用的。

GLuint texture;
glGenTextures(1, &texture);

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

glBindTexture(GL_TEXTURE_2D, texture);

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

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);    //生成所有需要的多级渐远纹理

glTexImage2D:生成纹理。

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

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

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

SOIL_free_image_data(image);        //释放图像内存
glBindTexture(GL_TEXTURE_2D, 0);    //解除绑定,和解除VBO一样

生成纹理过程完整代码:

int textureWidht, textureHeight;
unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

//生成纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

//环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

//过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);

//解除绑定
SOIL_free_image_data(image);          //释放图像资源
glBindTexture(GL_TEXTURE_2D, 0);      //解除纹理绑定

应用纹理

纹理是矩形形状,所以顶点坐标也应该是绘制矩形,可以采用OpenGL笔记2-绘制三角形最后绘制矩形的方法,只不过顶点数据里还行要加上纹理坐标。

GLfloat 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我们新的顶点格式:
纹理顶点属性.png

是不是已经想到之前经常提到的两个函数glVertexAttribPointer和glEnableVertexAttribArray了。

不记得了,可以去看看OpenGL笔记2-绘制三角形。

glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);

注意,我们同样需要调整前面两个顶点属性的步长参数为8 * sizeof(GLfloat),因为一个顶点包括8个数据(3个顶点,3个颜色,2个纹理坐标)。

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

#version 330 core
layout (location = 0) in vec3 position;     //顶点坐标
layout (location = 1) in vec3 color;        //颜色
layout (location = 2) in vec2 texCoord;     //纹理坐标

out vec3 ourColor;      //输出颜色
out vec2 TexCoord;      //输出纹理坐标

void main()
{
    gl_Position = vec4(position, 1.0f);
    ourColor = color;
    TexCoord = texCoord;
}

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

片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?

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

片段着色器

#version 330 core
in vec3 ourColor;     //顶点着色器传进来的颜色参数
in vec2 TexCoord;     //顶点着色器传进来的纹理坐标

out vec4 color;       //输出的颜色值

uniform sampler2D ourTexture;   //程序传进来的纹理采样器

void main()
{
    color = texture(ourTexture, TexCoord);     
}

我们使用GLSL内建的texture函数来采样纹理的颜色。

texture:采样纹理颜色。

  • 第一个参数:纹理采样器。
  • 第二个参数:纹理坐标。

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

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

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

是不是很奇怪,有在着色器里定义uniform,为啥没在程序中调用glGetUniformLocation查找,glUniformXX更新呢,一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元。所以不需要查找,但如果有多个的话,就需要了。详情可看下面的纹理单元。

所有工作都弄完啦,现在可以展示下效果了
纹理加载效果.png

加载纹理完整代码

#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>

//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 color;\n"
"layout (location = 2) in vec2 texCoord;\n"

"out vec3 ourColor;\n"
"out vec2 TexCoord;\n"

"void main()\n"
"{\n"
"   gl_Position = vec4(position, 1.0);\n"
"   ourColor = color;\n"
"   TexCoord = texCoord;\n"
"}\n";

//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor;\n"
"in vec2 TexCoord;\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture;\n"

"void main()\n"
"{\n"
"   color = texture(ourTexture, TexCoord);\n"
"}\n";

int main()
{
    glfwInit();     //必须要将glfw初始化
    //告诉GLFW使用OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  //使用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);   //不允许调整窗口大小

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW Window" << std::endl;

        glfwTerminate();    //销毁窗口与数据

        return -1;
    }

    glfwMakeContextCurrent(window);     //将OpenGL指向为当前窗口

    glewExperimental = GL_TRUE;     //用于告知GLEW使用现化OpenGL技术

    //glew初始化
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;

        return -1;
    }

    //视口
    int width = 800, height = 600;
    glfwGetFramebufferSize(window, &width, &height);        //设置OpenGL渲染窗口的尺寸
    glViewport(0, 0, width, height);    //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度

    //顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);
    
    GLint vertexSuccess;
    GLchar vertexInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLint fragmentSuccess;
    GLchar fragmentInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &fragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, fragmentInfoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
    }


    //着色器链接程序
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint programSuccess;
    GLchar programInfoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
    }

    GLfloat vertices[] = {
        //-----位置          -----颜色          ---纹理坐标
        0.5f, 0.5f, 0.0f,    1.0f, 0.f, 0.f,    1.0f, 1.0f,     //右上
        0.5f, -0.5f, 0.f,    0.0f, 1.0f, 0.0f,  1.0f, 0.0f,     //右下
        -0.5f, -.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      //右上

    };

    //顶点索引
    GLuint indices[] = {
            0, 1, 3,
            1, 2, 3
    };

    //顶点数据
    GLuint VBO, EBO, VAO;
    glGenBuffers(1, &VBO);      //顶点缓冲对象
    glGenBuffers(1, &EBO);      //索引缓冲数组
    glGenVertexArrays(1, &VAO); //顶点数组对象

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

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //颜色
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    //纹理坐标
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
    glEnableVertexAttribArray(2);
 
    //加载纹理
    int textureWidht, textureHeight;
    unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);

    //解除绑定
    SOIL_free_image_data(image);          //释放图像资源
    glBindTexture(GL_TEXTURE_2D, 0);      //解除纹理绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    while (!glfwWindowShouldClose(window)) {
        //检查GLFW是否退出,即窗口是否关闭了,true代表结束了

        glfwPollEvents();       //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数  键盘事件:glfwSetKeyCallback(window, key_callback);  key_callback即设定的回调函数
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);       //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        glClear(GL_COLOR_BUFFER_BIT);       //清空屏幕缓冲,这里是颜色缓冲

        //渲染指令
        glBindTexture(GL_TEXTURE_2D, texture);
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
        glfwSwapBuffers(window);  //交换颜色缓冲,用来绘制,输出显示在屏幕上
    }

    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteVertexArrays(1, &VAO);

    glfwTerminate();
    return 0;
}


我们还可以把得到的纹理颜色与顶点颜色混合,来获得更有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:

片段着色器

color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);

纹理颜色顶点颜色混合效果
纹理加载混合效果.png

纹理单元

你可能会奇怪为什么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 ourTexture1;      //采样器1
uniform sampler2D ourTexture2;      //采样器2

void main()
{
    color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}

最终输出颜色现在是两个纹理的结合。

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

我们现在需要载入并创建另一个纹理;记得创建另一个纹理对象,载入图片,使用glTexImage2D生成最终纹理。对于第二个纹理我们使用一张你学习OpenGL时的面部表情图片。

为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元(只有一个无需绑定):

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);

glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

注意,我们使用glUniform1i设置uniform采样器的位置值,或者说纹理单元。通过glUniform1i的设置,我们保证每个uniform采样器对应着正确的纹理单元。你应该能得到下面的结果:

多个纹理加载效果.png

多个纹理加载原代码

#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>

//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 color;\n"
"layout (location = 2) in vec2 texCoord;\n"

"out vec3 ourColor;\n"
"out vec2 TexCoord;\n"

"void main()\n"
"{\n"
"   gl_Position = vec4(position, 1.0);\n"
"   ourColor = color;\n"
"   TexCoord = texCoord;\n"
"}\n";

//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor;\n"
"in vec2 TexCoord;\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture1;\n"
"uniform sampler2D ourTexture2;\n"

"void main()\n"
"{\n"
"     color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);\n"      
"}\n";

int main()
{
    glfwInit();     //必须要将glfw初始化
    //告诉GLFW使用OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  //使用的是OpenGL核心模式
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);   //不允许调整窗口大小

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr) {
        std::cout << "Failed to create GLFW Window" << std::endl;

        glfwTerminate();    //销毁窗口与数据

        return -1;
    }

    glfwMakeContextCurrent(window);     //将OpenGL指向为当前窗口

    glewExperimental = GL_TRUE;     //用于告知GLEW使用现化OpenGL技术

    //glew初始化
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;

        return -1;
    }

    //视口
    int width = 800, height = 600;
    glfwGetFramebufferSize(window, &width, &height);        //设置OpenGL渲染窗口的尺寸
    glViewport(0, 0, width, height);    //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度

    //顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);
    
    GLint vertexSuccess;
    GLchar vertexInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
    if (!vertexSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
    }

    //片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLint fragmentSuccess;
    GLchar fragmentInfoLog[512];

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &fragmentSuccess);
    if (!fragmentSuccess) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, fragmentInfoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
    }


    //着色器链接程序
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint programSuccess;
    GLchar programInfoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
    if (!programSuccess) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
    }

    GLfloat vertices[] = {
        //-----位置          -----颜色          ---纹理坐标
        0.5f, 0.5f, 0.0f,    1.0f, 0.f, 0.f,    1.0f, 1.0f,     //右上
        0.5f, -0.5f, 0.f,    0.0f, 1.0f, 0.0f,  1.0f, 0.0f,     //右下
        -0.5f, -.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      //右上

    };

    //顶点索引
    GLuint indices[] = {
            0, 1, 3,
            1, 2, 3
    };

    //顶点数据
    GLuint VBO, EBO, VAO;
    glGenBuffers(1, &VBO);      //顶点缓冲对象
    glGenBuffers(1, &EBO);      //索引缓冲数组
    glGenVertexArrays(1, &VAO); //顶点数组对象

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

    //顶点属性
    //顶点坐标
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    //颜色
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    //纹理坐标
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
    glEnableVertexAttribArray(2);
 

    //纹理1--------------
    //加载纹理
    int textureWidht, textureHeight;
    unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    GLuint texture1;
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);


    //加载纹理2---------------
    image = SOIL_load_image("awesomeface.png", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);

    //生成纹理
    GLuint texture2;
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);

    //环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);       //对应X轴  
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);       //对应Y轴

    //过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);       //缩小
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);       //放大

    //生成纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

    //多级渐远纹理
    glGenerateMipmap(GL_TEXTURE_2D);

    //解除绑定
    SOIL_free_image_data(image);          //释放图像资源
    glBindTexture(GL_TEXTURE_2D, 0);      //解除纹理绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    while (!glfwWindowShouldClose(window)) {
        //检查GLFW是否退出,即窗口是否关闭了,true代表结束了

        glfwPollEvents();       //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数  键盘事件:glfwSetKeyCallback(window, key_callback);  key_callback即设定的回调函数
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);       //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        glClear(GL_COLOR_BUFFER_BIT);       //清空屏幕缓冲,这里是颜色缓冲

        //渲染指令

        glUseProgram(shaderProgram);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture1"), 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);
        glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture2"), 1);

        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
        glfwSwapBuffers(window);  //交换颜色缓冲,用来绘制,输出显示在屏幕上
    }

    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteVertexArrays(1, &VAO);

    glfwTerminate();
    return 0;
}

你可能注意到纹理上下颠倒了!这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。一些图片加载器比如DevIL在加载的时候有选项重置y原点,但是SOIL没有。SOIL却有一个叫做SOIL_load_OGL_texture函数可以使用一个叫做SOIL_FLAG_INVERT_Y的标记加载并生成纹理,这可以解决我们的问题。不过这个函数用了一些在现代OpenGL中失效的特性,所以现在我们仍需坚持使用SOIL_load_image,自己做纹理的生成。

所以修复我们的小问题,有两个选择:

  • 1.我们可以改变顶点数据的纹理坐标,翻转y值(用1减去y坐标)。
  • 2.我们可以编辑顶点着色器来自动翻转y坐标,替换TexCoord的值为TexCoord = vec2(texCoord.x, 1.0f - texCoord.y);。

顶点着色器

#version 330 core
layout (location = 0) in vec3 position;     //顶点坐标
layout (location = 1) in vec3 color;        //颜色
layout (location = 2) in vec2 texCoord;     //纹理坐标

out vec3 ourColor;      //输出颜色
out vec2 TexCoord;      //输出纹理坐标

void main()
{
    gl_Position = vec4(position, 1.0f);
    ourColor = color;
    TexCoord = vec2(texCoord.x, 1.0f - texCoord.y);     //翻转位置
}

如果你编辑了顶点数据,在顶点着色器中翻转了纵坐标,你会得到下面的结果:
多个纹理加载Y轴翻转效果.png

原教程

纹理原教程

该教程源码

git地址

分享到:
OpenGL笔记5-变换
OpenGL笔记3-着色器
  • 文章目录
  • 站点概览
欢

网红 欢

你能抓到我么?

Email RSS
看爆 Top5
  • mac系统版本与Xcode版本有冲突 4,079次看爆
  • JAVA_HOME环境配置问题 3,729次看爆
  • AssetBundle使用 3,499次看爆
  • VSCode配置C++开发环境 3,256次看爆
  • Lua反射 3,133次看爆

Copyright © 2025 欢 粤ICP备2020105803号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图