美文网首页
LearnOpenGL 纹理

LearnOpenGL 纹理

作者: li_礼光 | 来源:发表于2020-09-16 00:48 被阅读0次

    纹理 :

    纹理也是顶点数据的一种. 纹理的添加也是处理VAO

    配置纹理的操作方式

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

    glTexParameteri(GLenum target, GLenum pname, GLint param)
    第一个参数target : 指定了纹理目标, 我们使用的是2D的内容, 所以这里使用的是GL_TEXTURE_2D
    第二个参数pname  : 需要我们指定设置的选项与应用的纹理轴. 我们打算配置的是WRAP,并且指定s和t.
    第三个参数param  : 选择纹理环绕方式.
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_MIRRORED_REPEAT);
    
    纹理坐标

    纹理的环绕方式

    环绕方式
    GL_REPEAT 默认行为,重复纹理图像
    GL_MIRRORED_REPEAT 和GL_REPEAT一样, 但每次重复都是镜像放置
    GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间, 超出的部分会重复纹理坐标的边缘, 产生一种边缘被拉伸的效果
    GL_CLAMP_TO_BOARD 超出的坐标为用户指定的边缘颜色
    环绕方式
    如果我们选择GL_CLAMP_TO_BOARD, 我们需要指定一个边缘颜色
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, {1.0f,1.0f, 0.0f, 1.0f} );
    

    纹理过滤

    纹理过滤很多选项,这里讨论重要的两种 : GL_NEAREST 和 GL_LINEAR

    邻近过滤

    GL_NEAREST 是OpenGL默认的纹理过滤方式.当设置为GL_NEAREST的时候 : OpenGL会选择中心点最接近纹理坐标的那个像素.

    GL_NEAREST
    线性过滤

    GL_LINEAR 它会基于纹理坐标的纹理像素, 计算出一个插值, 近似出这些纹理像素之间的颜色. 一个纹理像素的中心距离纹理坐标越近, 那么这个纹理像素的颜色对最终的样本颜色的贡献越大.

    GL_LINEAR

    GL_NEAREST 会产生颗粒感.
    GL_LINEAR 会羽化.产生更平滑图案.

    纹理过滤

    当进行放大和缩小操作的时候, 可以设置纹理过滤的选项, 比如
    放大时 , 选择GL_LINEAR
    缩小时 , 选择GL_NEAREST

    通过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有一个glGenerateMipmap函数,
    在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。后面的教程中你会看到该如何使用它。

    glGenerateMipmap(GLenum target) 只有一个参数. 过滤方式
    GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
    GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
    GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
    GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
    glGenerateMipmap(GL_NEAREST_MIPMAP_NEAREST);
    

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





    纹理过滤加载与创建纹理

    创建纹理之前解决一个问题, 就是怎么把图片资源转成纹理数据传给OpenGL

    STB

    先添加一个库 stb ,
    把对应的源文件加载到项目中

    STB

    这里使用这个stbi_load 加载在项目中的纹理资源

        int width, height, nrChannels;
        unsigned char *data = stbi_load("本地的纹理图片资源地址", &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);
    
    

    stbi_load 第一个参数是获取文件地址, 这里直接去一个本地的地址就好, 也可以根据自己的方式把图片资源路径设置成在项目里面的.

    值得注意 :

    stb_image.h 使用前需要 #define STB_IMAGE_IMPLEMENTATION

       Do this:
          #define STB_IMAGE_IMPLEMENTATION
       before you include this file in *one* C or C++ file to create the implementation.
    

    在#include "stb_image.h"中的函数都是STBIDEF开头. 这里定义了两种方式, static或extern.

    #ifndef STBIDEF
    #ifdef STB_IMAGE_STATIC
    #define STBIDEF static
    #else
    #define STBIDEF extern
    #endif
    #endif
    

    如果在多个文件中导入了stb_image.h的头文件. 需要STB_IMAGE_STATIC, 不然可能有编译不通过的情况.

    #define STB_IMAGE_IMPLEMENTATION
    #define STB_IMAGE_STATIC
    #include "stb_image.h"
    

    关于SOIL

    SOIL是简易OpenGL图像库(Simple OpenGL Image Library)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从他们的主页下载。像其它库一样,你必须自己生成.lib。你可以使用/projects文件夹内的任意一个解决方案(Solution)文件(不用担心他们的Visual Studio版本太老,你可以把它们转变为新的版本,这一般是没问题的。译注:用VS2010的时候,你要用VC8而不是VC9的解决方案,想必更高版本的情况亦是如此)来生成你自己的.lib文件。你还要添加src文件夹里面的文件到你的includes文件夹;对了,不要忘记添加SOIL.lib到你的链接器选项,并在你代码文件的开头加上#include <SOIL.h>

    PS : 在LearnOpenGL里面介绍添加一个SOIL的库, 并没有很好的介绍怎么配置在项目中 , 这里配置的方式好像不是特别的方便, 所以我这里加载纹理的数据使用上面的stb文件内容. 相当于源文件吧.

    下面的教程中,我们会使用一张木箱的图片。要使用SOIL加载图片,我们需要使用它的SOIL_load_image函数:

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

    函数首先需要输入图片文件的路径。然后需要两个int指针作为第二个和第三个参数,SOIL会分别返回图片的宽度高度到其中。后面我们在生成纹理的时候会用图像的宽度和高度。第四个参数指定图片的通道(Channel)数量,但是这里我们只需留为0。最后一个参数告诉SOIL如何来加载图片:我们只关注图片的RGB值。结果会储存为一个很大的char/byte数组。

    注意 : 虽然代码中用的是stbi_load函数, 但是实现的效果是一样的




    实践


    顶点数据

    MyTexturesVertices.hpp

    #ifndef MyTexturesVertices_h
    #define MyTexturesVertices_h
    
    //纹理坐标 : 以左下角 (0.0, 0.0) 为原点
    static float MyTextureVertices[] = {
    //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
         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    // 左上
    };
    
    //索引
    static unsigned int  MyTextureVerticesIndices[] = { // 注意索引从0开始!
        0, 1, 2, // 第一个三角形
        0, 3, 2  // 第二个三角形
    };
    #endif /* MyTexturesVertices_h */
    

    由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:


    顶点数据

    顶点数据和之前的有点不一样, 所以这里需要调整一下Shader程序

    Shader程序

    MyTexturesShader.hpp

    #ifndef MyTexturesShader_h
    #define MyTexturesShader_h
    
    #define STRINGIZE(x) #x
    #define SHADER(shader) STRINGIZE(shader)
    
    /// 着色器程序之间的数据传递
    static char *MyTextureVertexShaderStr = SHADER(
        \#version 330 core\n
                                   
        layout (location = 0) in vec3 position; //顶点数据源输入
        layout (location = 1) in vec3 color; //颜色数据源输入
        layout (location = 2) in vec2 texCoords; //纹理数据源输入(2D)
    
        out vec2 vertexTexCoords;//纹理输出
        out vec4 vertexColor;//颜色输出
    
        void main()
        {
            gl_Position = vec4(position, 1.0f);//坐标位置
            vertexColor = vec4(color, 1.0f); //输出给片元着色器
            vertexTexCoords = texCoords;
        }
    );
    
    //片元着色器程序
    static char *MyTextureFragmentShaderSrc = SHADER(
        \#version 330 core\n
                                            
        in vec2 vertexTexCoords;//纹理输入(从顶点)
        in vec4 vertexColor;//颜色输入(从顶点)
                            
        uniform sampler2D myTexture;//全局myTexture
    
        out vec4 color;//颜色输出
        out vec4 FragColor;//纹理输出
                                                     
        void main()
        {
            color = vertexColor;
            FragColor = texture(myTexture, vertexTexCoords);
        }
    );
    
    #endif /* MyTexturesShader_h */
    

    因为需要加载纹理, 在顶点着色器Shader程序中添加纹理的输入输出

        layout (location = 2) in vec2 texCoords; //纹理数据源输入(2D)
        out vec2 vertexTexCoords;//纹理输出
        main {   
          vertexTexCoords = texCoords; // 
        }
    

    在纹理着色器SHader程序中也需要添加处理

        in vec2 vertexTexCoords;//纹理输入(从顶点)
        uniform sampler2D myTexture;//全局myTexture
        out vec4 FragColor;//纹理输出
        main {   
          FragColor = texture(myTexture, vertexTexCoords);
        }
    

    注意 : uniform sampler2D myTexture; 这里创建了一个uniform修饰的myTexture , 暂时先忽略. 窗口程序中暂时没有用到

    窗口程序

    MyTextures.cpp

    #include <iostream>
    #include "MyTextures.hpp"
    #include "MyProgram.hpp"
    #include "MyTexturesVertices.hpp"
    #include "MyTexturesShader.hpp"
    
    #define STB_IMAGE_IMPLEMENTATION
    #include "stb_image.h"
    
    int runMyTextureOpenGlWindow() {
        int result = glfwInit();
        if (result == GL_FALSE) {
            printf("glfwInit 初始化失败");
            return -1;
        }
    
        //这里的宏不好提示出来, 根据LearnOpenGL的文档提示, 用这三个
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    
        //创建一个Window
        GLFWwindow *window = glfwCreateWindow(600, 400, "My Opengl Window", NULL, NULL);
        if(!window) {
            printf("window 创建失败");
    
        }
    
        glfwMakeContextCurrent(window);
        gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
    
        //----------------------------------------------------------------------
        MyProgram myProgram = MyProgram(MyTextureVertexShaderStr, MyTextureFragmentShaderSrc);
    
    
    
        GLuint VBO , VAO , EBO;
        unsigned int squareIndicesCount = 0;
    
        //创建VBO
        glGenBuffers(1, &VBO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(MyTextureVertices), MyTextureVertices, GL_STATIC_DRAW);
    
        //创建VAO
        glGenVertexArrays(1, &VAO);
        glBindVertexArray(VAO);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
        glEnableVertexAttribArray(1);
        //配置纹理的VAO
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
        glEnableVertexAttribArray(2);
    
        //创建EBO, 这里的EBO相当于索引的作用
        glGenBuffers(1, &EBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(MyTextureVerticesIndices), MyTextureVerticesIndices, GL_STATIC_DRAW);
    
        //解绑VAO
        glBindVertexArray(0);
        squareIndicesCount = sizeof(MyTextureVerticesIndices)/sizeof(MyTextureVerticesIndices[0]);
        
        
     
    
    
        //生成纹理
        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("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/dizhuan.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);
        
    
        
        //进行绘制
        while(!glfwWindowShouldClose(window)){
           //检查事件
            glfwPollEvents();
    
            //渲染指令
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
           
            glUseProgram(myProgram.program);
            glBindVertexArray(VAO);
            glBindTexture(GL_TEXTURE_2D, texture);//加载纹理
            glDrawElements(GL_TRIANGLES, squareIndicesCount, GL_UNSIGNED_INT, 0);
            glBindVertexArray(0);
    
            //交换缓冲
            glfwSwapBuffers(window);
        }
    
        //程序销毁
        glfwTerminate();
        return 1;
    }
    

    最终效果 :


    添加纹理

    更多效果

    因为着色器程序本身有颜色. 颜色跟纹理做一个混合 , 修改片元着色器程序

    FragColor = texture(myTexture, vertexTexCoords) * color;
    
    混合

    PS : 留个思考怎么做一些前面介绍的效果的处理.

    • 纹理过滤
    • 纹理的环绕方式



    纹理单元

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

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

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

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

    修改着色器程序
    //片元着色器程序
    static char *MyTextureFragmentShaderSrc = SHADER(
        \#version 330 core\n
                                            
        in vec2 vertexTexCoords;//从顶点着色器中拿纹理的值
        in vec4 vertexColor;//从顶点着色器中拿color的值
                            
        uniform sampler2D myTexture0;//全局myTexture
        uniform sampler2D myTexture1;//全局myTexture
    
        out vec4 color;
        out vec4 FragColor;
                                                     
        void main()
        {
            FragColor = mix(texture(myTexture0, vertexTexCoords), texture(myTexture1, vertexTexCoords), 0.5);
        }
    );
    
    
    修改窗口程序
    //生成纹理0
    unsigned int texture0, texture1;
    unsigned char *data0 ;
    int width0, height0, nrChannels0;
    
    glGenTextures(1, &texture0);
    glBindTexture(GL_TEXTURE_2D, texture0);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    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);
    data0 = stbi_load("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/dizhuan.jpg", &width0, &height0, &nrChannels0, 0);
    if (data0)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width0, height0, 0, GL_RGB, GL_UNSIGNED_BYTE, data0);
        glGenerateMipmap(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, 0);
    
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data0);
    
    //生成纹理1
    unsigned char *data1 ;
    int width1, height1, nrChannels1;
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    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);
    data1 = stbi_load("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/smile.png", &width1, &height1, &nrChannels1, 0);
    if (data1)
    {
        //这里用了PNG, 有透明度, 使用GL_RGBA.
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width1, height1, 0, GL_RGBA, GL_UNSIGNED_BYTE, data1);
        glGenerateMipmap(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, 0);
    
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data1);
    
    
    //进行绘制
    while(!glfwWindowShouldClose(window)){
      //检查事件
      glfwPollEvents();
    
      //渲染指令
      glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
      glClear(GL_COLOR_BUFFER_BIT);
    
      //激活纹理0
      glActiveTexture(GL_TEXTURE0);
      glBindTexture(GL_TEXTURE_2D, texture0);
      glUniform1i(glGetUniformLocation(myProgram.program, "myTexture0"), 0);
            
      //激活纹理1
      glActiveTexture(GL_TEXTURE1);
      glBindTexture(GL_TEXTURE_2D, texture1);
      glUniform1i(glGetUniformLocation(myProgram.program, "myTexture1"), 1);
           
      glUseProgram(myProgram.program);
      glBindVertexArray(VAO);
      glDrawElements(GL_TRIANGLES, squareIndicesCount, GL_UNSIGNED_INT, 0);
      glBindVertexArray(0);
    
      //交换缓冲
      glfwSwapBuffers(window);
    }
    
    
    纹理重叠


    最后, 重叠的效果有点奇怪. 先这样吧, 先理解这个纹理单元的概念.

    注意找的素材的格式是PNG还是JPG, PNG有Alpha通道

    这里还是有很多需要弄明白的细节, 比如着色器程序的texture() 和mix()

    体会这里的Uniform和之前的Uniform的区别

    PS : 思考, 怎么改变这个纹理贴图的大小.
    PS : 思考, 实现纹理题图大小的改变之后, 怎么设置纹理过滤和纹理环绕方式

    相关文章

      网友评论

          本文标题:LearnOpenGL 纹理

          本文链接:https://www.haomeiwen.com/subject/ohkxyktx.html