OpenGL入门——纹理

发布时间 2023-09-24 11:19:57作者: 一只小瓶子

已知,我们是对每个顶点去添加对应颜色,如果想要图像更加细节真实,就必须有足够多的点,点的密度要很大,而每个点都有个颜色属性,这样很浪费资源。

这时候我们就可以引入纹理,它可以用来添加图像的细节,类似于皮肤。每个顶点对应一个纹理坐标(表明从纹理图像的哪个位置采样,即获得颜色),其他片段进行插值采样(非顶点位置的颜色也是从纹理图像中采样获得),让物体非常精细又不需要指定额外的顶点。

 

纹理环绕方式

纹理坐标的范围0到1,如果是2D纹理图像,纹理坐标起始于左下角(0,0),终止于右上角(1,1)。如果超出这个范围,openGL使用纹理环绕方式进行填充。环绕方式分为以下四种:

 

  1)GL_REPEAT:重复纹理图像,默认行为;

  2)GL_MIRRORED_REPEAT:和GL_REPEAT一样,但是重复的图片是镜像放置的;

  3)GL_CLAMP_TO_EDGE:重复纹理坐标的边缘,产生一种边缘被拉伸的效果;

  4)GL_CLAMP_TO_BORDER:用户指定的颜色填充超出部分

使用glTexParameteri函数对单个坐标轴设置环绕方式(s,t,r对应x,y,z)

/*第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D;
第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴;
最后一个参数需要我们传递一个环绕方式
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

//如果选择GL_CLAMP_TO_BORDER选项,还需要使用glTexParameterfv指定一个边缘的颜色
//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

 

纹理过滤

因为纹理坐标范围是0到1的浮点值,不依赖于分辨率,所有怎么将纹理像素映射到纹理坐标呢?OpenGL使用纹理过滤,其中最重要的两种是GL_NEAREST和GL_LINEAR:

 

  1)GL_NEAREST:邻近过滤,默认的过滤方式,其选择中心点最接近纹理坐标的那个像素。产生的是颗粒状的图案,能够清晰看到组成纹理的像素,一般在纹理被缩小时使用。

  2)GL_LINEAR:线性过滤,基于纹理坐标附近的纹理像素,计算一个插值,相当于临近像素的混合色,距离越近的纹理像素影响越大。产生的是平滑的图案,很难看出单个的纹理像素,一般在纹理被放大时使用。
使用glTexParameteri函数对放大和缩小两种情况设置过滤方式

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//被缩小时的选项
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//被放大时的选项

 

多级渐远纹理

 

 

 物体有远近之分,远处的物体看起来小,产生的片段少。为了节约内存且让小物体更真实,OpenGL使用不同的多级渐远纹理(就是一系列的纹理图像,后一个纹理是前一个纹理的二分之一),根据不同的距离选择不同的纹理图像取样。

在使用多级渐远纹理的时候,不同级别的纹理层之间会产生不真实的生硬边界,这时候可以使用纹理过滤方式,指定不同多级渐远纹理级别之间的过滤方式有4种:

  1)GL_NEAREST_MIPMAP_NEAREST:使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样;

  2)GL_LINEAR_MIPMAP_NEAREST:使用最邻近的多级渐远纹理级别,并使用线性插值进行采样;

  3)GL_NEAREST_MIPMAP_LINEAR:在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样;

  4)GL_LINEAR_MIPMAP_LINEAR:在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样。

使用方式与纹理过滤一样:

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错误代码,因为多级渐远纹理是使用在纹理被缩小的情况下。

 

使用纹理

使用stb_image.h库(下载地址stb/stb_image.h at master · nothings/stb · GitHub),下载这个头文件,将它以stb_image.h的名字加入工程,并另创建一个新的C++文件,输入以下代码:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

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

 

使用纹理的方式也是需要先创建一个纹理对象,然后绑定它进行配置,如下所示:

    //创建纹理,和之前生成的OpenGL对象一样,纹理也是使用ID引用的
    unsigned int texture;
    glGenTextures(1, &texture);//生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中

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

 

OpenGL如何采样纹理呢?

首先在顶点着色器传入顶点的纹理坐标

texture_triangle.vs

//vertex shader source
#version 330 core
layout(location = 0) in vec3 position;     //位置X,Y,Z
layout(location = 1) in vec3 color;        //颜色R,G,B
layout(location = 2) in vec2 texture;    //纹理S,T

out vec3 vertexColor;    //顶点颜色
out vec2 textureCoord;    //顶点对应纹理坐标

void main()
{
    gl_Position = vec4(position, 1.0);    //顶点坐标
    vertexColor = color;                //从顶点数据那里得到的输入颜色
    textureCoord = texture;                //从顶点数据那里得到的对应纹理坐标
}

然后片段着色器使用采样器(Sampler)进行采样,这个采样器是uniform类型,因为它是用于程序把纹理对象传给片段着色器的。最后使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。

texture_triangle.fs

//fragment shader source
#version 330 core
in vec3 vertexColor;    //顶点颜色
in vec2 textureCoord;    //顶点对应纹理坐标

out vec4 fragColor;        //像素的最终颜色

uniform sampler2D ourTexture;    //纹理采样器,通过源码中绑定纹理glBindTexture赋值

void main()
{
    fragColor = texture(ourTexture, textureCoord);
}

 

完整示例

//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "shader.h"

#include "stb_image.h"

//改变窗口大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

//输入
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制
        glfwSetWindowShouldClose(window, true);
}

GLFWwindow* init_window()
{
    ///窗口初始化
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数

    //创建glfw窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "failed to create GLFW window" << std::endl;
        glfwTerminate();//释放/删除之前的分配的所有资源
        return nullptr;
    }
    glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数

    //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数
    {
        std::cout << "failed to intialize GLAD" << std::endl;
        return nullptr;
    }

    glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)

    return window;
}

int texture_triangle()
{
    ///初始化窗口
    GLFWwindow* window = init_window();

    ///定义着色器
    CShader shader("texture_triangle.vs", "texture_triangle.fs");

    ///定义顶点对象
    float vertices[] = {
        // 顶点坐标X,Y,Z      // 顶点颜色R,G,B    // 纹理坐标S,T
         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  // 左上
    };
    unsigned int indices[] = {
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
    };

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

    // 顶点坐标属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 顶点颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    // 纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);


    //创建纹理,和之前生成的OpenGL对象一样,纹理也是使用ID引用的
    unsigned int texture;
    glGenTextures(1, &texture);//生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中

    glBindTexture(GL_TEXTURE_2D, texture);//绑定纹理

    //为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);//X轴环绕方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//Y轴环绕方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//被缩小时的过滤选项
    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时,当前绑定的纹理对象就会被附加上纹理图像
        /*
        第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
        第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
        第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
        第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
        下个参数应该总是被设为0(历史遗留的问题)。
        第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
        最后一个参数是真正的图像数据。
        */
        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);

    while (!glfwWindowShouldClose(window))
    {
        processInput(window);


        //清空屏幕
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);


        ///绘制物体
        shader.run();

        glBindVertexArray(VAO);
        //使用VEO绘制
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制图元为三角形,绘制顶点数量6,索引类型uint,偏移量0

        glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲)
        glfwPollEvents();//检查有没有触发什么事件
    }

    //释放资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

    glfwTerminate();//释放/删除之前的分配的所有资源
    return 0;
}

int main()
{
    texture_triangle();

    return 0;
}

运行效果

附上纹理图像下载地址container.jpg (512×512) (learnopengl-cn.github.io)