【(一)OpenGl入门】4.纹理

目录

1.基础知识

1-1 什么是纹理

1-2 什么是纹理采样

2.如何创建并使用纹理-画箱子

2-1.主要步骤

2-2 完整代码

3.纹理环绕方式

4.纹理过滤(Texture Filtering)

5.多级渐远纹理(Mipmap)

6.纹理单元--纹理叠加

6-1.纹理叠加实例--箱子与笑脸

6-2.完整代码

7.练习

1.基础知识

1-1 什么是纹理

        我们之前只画了一个三角形,而更加复杂的物体是更多的三角形组成的,并且我们通过为每个顶点添加颜色来增加物体的细节。如果我们想让物体更加真实,就得有足够多的顶点,且指定足够多的颜色,这样会产生很多额外开销,因为每个模型需要很多顶点,而每个顶点又需要颜色属性。

        为了方便且减少开销,我们更习惯使用纹理来绘制更多的模型,纹理就像一张2D的贴图,把它贴在图像或模型上来增加物体的细节。这样物体细节取决于设计师的美工,而程序员则不必指定更多的顶点。

1-2 什么是纹理采样

        比如为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate)。用来表面该从纹理的哪个部分采样(采集片段颜色),之后在图形的其它片段上进行片段插值。2D纹理坐标范围在(0,1)之间,纹理坐标始于(0,0),终于(1,1),使用纹理坐标获取纹理颜色叫做纹理的采样(Sampling)。如下图展示了如何把纹理坐标映射到第一讲画的三角形上。

                                            

        上图中,我们指定了三个纹理坐标点。注意这和我们三角形的标准设备坐标不吻合,不要混淆。然后我们只需要给顶点着色器里传递这三个纹理坐标,它会被传到片段着色器中为每个片段进行纹理坐标插值。纹理有几种插值方式,我们需要告诉opengl该如何对纹理采样(纹理环绕方式),纹理坐标如下:

2.如何创建并使用纹理-画箱子

2-1.主要步骤

准备工作:添加stb_image.h库文件 

        纹理图像可能不储存为各种各样的格式,每种都有自己的数据结构和排列,我们如何把纹理图像加载到应用中呢?一种就是自己写一个图像加载器把图像转化为字节序列。另一种就是使用图像加载库stb_image.h。 

        使用加载纹理的库文件,需要使用stb_image.h库文件,下载完直接当头文件加进来,下载网址:https://github.com/nothings/stb/blob/master/stb_image.h

        然后把#define STB_IMAGE_IMPLEMENTATION这条代码加在stb_image.h的最上面。通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。

正式工作:编写代码

(一)首先我们需要在顶点数组中再定义一个纹理坐标属性。

(二)我们得增加一个VAO属性,然后修改VAO的格式,因为又多了2个folat类型的属性,所以步长为8*sizeof(float)。为什么是2个?因为采样的是2D纹理,就是xy两个坐标值。

(三)我们需要把纹理坐标传到顶点着色器里,并输出到片段着色器。

(四)在片段着色器中加入采样器(GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler)) uniform sampler2D ourTexture。  

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

(五)然后我们要使用EBO把上节课的三角形改成一个长方形,参考<2.画一个三角形>中的4-2修改。这里有遗忘没做出来没关系,可以看后面的完整代码。

(六)把加载纹理的代码添加到主函数的while循环上面。

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

(七)其实这个时候已经可以运行了,但是如果我们有很多纹理的话,为了方便管理还是创建纹理对象比较好。我们把创建纹理对象的代码加到第六步的上面。

    //创建纹理对象
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

2-2 完整代码

shader.vs

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
uniform float offsetx;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord; 
}

shader.fs

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture; 

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

shader.h (未做修改)

#ifndef SHADER_H
#define SHADER_H

#include <glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader
{
public:
    unsigned int ID;//着色器程序ID,即流水线ID

    // 构造函数生成着色器对象
    Shader(const char* vertexPath, const char* fragmentPath)
    {
        // 1.从文件路径中获取顶点/片段源代码
        std::string vertexCode;
        std::string fragmentCode; 
        std::ifstream vShaderFile;
        std::ifstream fShaderFile;
        
        // 确保ifstream对象可以引发异常:
        vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        try
        {
            // 打开文件
            vShaderFile.open(vertexPath);         
            fShaderFile.open(fragmentPath);
            std::stringstream vShaderStream, fShaderStream;
            // 将文件的缓冲区内容读入流
            vShaderStream << vShaderFile.rdbuf();
            fShaderStream << fShaderFile.rdbuf();
            // 关闭文件
            vShaderFile.close();
            fShaderFile.close();
            // 将流转换为字符串
            vertexCode = vShaderStream.str();
            fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure& e)
        {
            std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
        }
        //将字符串转换为C风格字符串
        const char* vShaderCode = vertexCode.c_str();
        const char* fShaderCode = fragmentCode.c_str();
        // 2. 编译着色器
        unsigned int vertex, fragment;
        // 创建并编译顶点着色器
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        checkCompileErrors(vertex, "VERTEX");
        // 创建并编译片段着色器
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        checkCompileErrors(fragment, "FRAGMENT");
        // 链接着色器程序
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        checkCompileErrors(ID, "PROGRAM");
        // 删除着色器,因为它们现在链接到着色器程序中,不再需要了
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    
    // 选择着色器程序
    void use()
    {
        glUseProgram(ID);
    } 
    
    // uniform的实用函数 
    void setBool(const std::string& name, bool value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
    }
    
    void setInt(const std::string& name, int value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
    }
   
    void setFloat(const std::string& name, float value) const
    {
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
    }

private:
    // 用于检查着色器编译/链接错误的实用函数。
    void checkCompileErrors(GLuint shader, std::string type)
    {
        GLint success;
        GLchar infoLog[1024];
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
    }
};
#endif

main.cpp

#include <glad.h>   //glad.h需要在glfw.h之前
#include <glfw3.h>
#include <iostream>
#include <cmath> 
#include "shader.h"
#include "stb_image.h"

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  // 左上 
};

unsigned int indices[] = {  //两个三角形拼接成一个长方形
    0, 1, 3,
    1, 2, 3
};

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

int main()
{
    //初始化glfw:选择opengl 3.3~4.2核心模式,取决于你的显卡驱动
    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

    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); //创建窗体对象:参数1:opengl窗口大小;参数2:窗口标题
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                     //设置当前窗口上下文设置为当前线程的上下文,之后才能在窗口进行绘制

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))            //glad:加载所有OpenGL函数指针,固定写法就这一行
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    glViewport(0, 0, 600, 400);                                         //设置opengl实际渲染的区域大小;0,0为左下角坐标

    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);  //注册窗口大小改变回调函数

    Shader shaderobject("shaders/shader.vs", "shaders/shader.fs");

    //2.创建VBO和VAO、EBO对象并赋予ID值
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    //3.绑定VBO,VAO、EBO对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //填充EBO缓冲
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //4.开辟并填充数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //5.告知Shader如何解析缓冲里的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);

    //6.开启VAO管理的第一个属性值,第一个VAO编号为0
    glEnableVertexAttribArray(0);

    //新增VAO颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3*sizeof(float))); //颜色属性开始偏移3个顶点属性
    glEnableVertexAttribArray(1);

    //新增VAO纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); //纹理坐标属性开始偏移了6个顶点属性
    glEnableVertexAttribArray(2);

    //7.解绑VBO,VAO,EBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    //创建纹理对象
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    while (!glfwWindowShouldClose(window))                              //检查一次GLFW是否被要求退出,循环一次就是刷新一帧
    {
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);                           //设置颜色:深绿色
        glClear(GL_COLOR_BUFFER_BIT);                                   //刷新屏幕,使用设置的颜色

        //d.选择使用着色器给三角形上色,即上面创建链接好的流水线
        shaderobject.use();

        //9.上面把VAO解绑了,这里必须重新绑定否则绘制不出来三角形
        glBindVertexArray(VAO);

        //11.绑定EBO,注意在绑定VAO之后才绑定EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

        //10.绘制元素
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);                                        //交换颜色缓冲(双缓冲):储存着GLFW窗口每一个像素颜色值作为输出显示在屏幕上
        glfwPollEvents();                                               //轮训检查输入设备触发事件
    }
    //e.渲染结束后把VAO,VBO和着色器都释放掉
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);//销毁的顺序没有规定
    glDeleteProgram(shaderobject.ID);

    glfwTerminate();                                                    //释放分配的所有glfw资源
    return 0;
}

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

stb_image.h

#define STB_IMAGE_IMPLEMENTATION
/* stb_image - v2.27 - public domain image loader - http://nothings.org/stb
                                  no warranty implied; use at your own risk
   Do this:

3.纹理环绕方式

        纹理坐标的范围通常是(0,0)到(0,1),如果把纹理坐标设置在这个范围之外,opengl会默认该行为是重复绘制纹理图像。opengl为环绕方式提供了多种选择。

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

        我们把上面箱子代码的纹理坐标改大试试看有什么效果,不出意外的话它会以默认重复图像的现象出现。下面的纹理坐标把1.0都改为2.0,相当于扩大4倍的面积,最终出现四个箱子。

float vertices[] = {
    // 位置              // 颜色           // 纹理坐标
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 2.0f, // 右上
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 2.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, 2.0f  // 左上 
};

除了上面默认方式,推荐下面使用函数设置纹理环绕方式。

        参数一:指定纹理目标;参数二:指定应用纹理轴S、T(如果是3D纹理还会有个R轴,它们和x、y、z是等价的);参数三:设置纹理环绕方式。

        我们换一张不对称的图片,并设置纹理环绕方式为镜像环绕。运行看一下效果。

    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/small.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

        显示的有问题,原因是我们换了的纹理图片是png格式,而glTexmage2D函数的第七个参数 然是jpg图片的RGB格式,我们得改成png格式的RGBA。修改后如下。

    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/small.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

       

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

        修改glTexParameteri第三个参数为GL_CLAMP_TO_BORDER,并紧挨着在下面添加以上代码。

    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/small.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

4.纹理过滤(Texture Filtering)

        纹理坐标不依赖于分辨率,可以是任意浮点值。所以opengl需要知道怎样将纹理像素(Texture Pixel)映射到纹理坐标。这对于物体很大但是纹理分辨率很低的时候就变得很重要了。

        GL_NEAREST(近邻过滤):opengl默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色。

                                                

        GL_LINEAR(线性过滤)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色。

                                                

两种过滤方式的视觉效果如下:

                                

        看见左图产生颗粒状、右图更平滑。建议在纹理被放大的时候采用线性过滤,纹理被缩小时采用近邻过滤。使用glTexParameter*函数指定过滤方式。这看起来和纹理环绕方式的设置很相似。

        在工程中直接粘贴以上代码如下:

    // 加载并生成纹理
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/small.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

        运行后把窗口拖大时看到纹理过渡比较平滑,看起来模糊,8bit风格得到改善,说明放大纹理使用了线性过滤;窗口放小纹理过渡尖锐,看起来清晰,说明缩小纹理使用了近邻过滤。

5.多级渐远纹理(Mipmap)

        根据透视中近大远小的道理,距离越远的纹理也应该逐渐变得模糊。因此我们将使用多级渐远纹理来解决空间中距离我们远近不同的物体纹理不真实的问题。如下就是多级渐远纹理的样子。

                                        

        OpenGL通过glGenerateMipmap函数就可以产生多级渐远纹理,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。因为是多级渐远,它就会有级别Level,最近的是0级,不同的多级渐远纹理间就会产生生硬的边界,处理方法就是使用之前讲过的纹理过滤。

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

         注意,多级渐远纹理在纹理缩小的场景下,放大时使用会报错,所以如上代码其实就是修改了前面代码中纹理缩小时的第三个参数,而该参数可以修改为上面表格中的四种之一。这里就不演示了,该知识点以理解为主。

6.纹理单元--纹理叠加

        前面我们只需要一张纹理所以使用的是默认纹理单元0(有的显卡驱动不支持默认纹理单元),故而没有介绍纹理单元,纹理单元的引入可以帮助我们叠加绘制多张纹理,opengl中默认至少有16个纹理单元。之前在片段着色器中定义了uniform sampler2D,但是还没用,先前介绍过uniform变量可以直接在代码中使用而不需要将元素从顶点着色器一步一步传进来。

        下面我们基于<2-2>来修改代码并实现两张纹理重叠的效果。

6-1.纹理叠加实例--箱子与笑脸

(一)main.cpp文件中新增一个纹理对象,并加载第二张纹理,注意变量重定义问题。

    //创建纹理对象1和2
    unsigned int texture1, texture2;
    glGenTextures(1, &texture1);
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture1);

    // 加载并生成纹理texture1
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    glBindTexture(GL_TEXTURE_2D, texture2);
    // 加载并生成纹理texture2
    data = stbi_load("./pics/awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

(2)修改片段着色器shader.fs,添加一个纹理单元。

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;

void main()
{
    //FragColor = texture(ourTexture1, TexCoord);  //使用1号纹理进行采样
  
    FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2); //纹理混合采样
    //参数3:0.2代表第二个纹理图像透明度为20%
}

        但是运行后效果是笑脸覆盖了箱子,因为按照main.cpp中的逻辑是笑脸的data后来覆盖了箱子的data。

(3)我们前两步已经在main.cpp中创建好了两个纹理对象并在shader.fs中添加好了两个纹理单元,为了解决上面的问题我们应该在main函数的绘制逻辑中添加激活纹理单元的代码。

//激活纹理单元
glActiveTexture(GL_TEXTURE0); 
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1); 
glBindTexture(GL_TEXTURE_2D, texture2); 

        这次运行结果变成了箱子了,说明此时没有被后绑定的笑脸覆盖了,但是还没达到我们想要的效果。

(四)GPU端的shader没有设置导致笑脸没有传过来,我们还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元,注意传入的字符串是shader.fs里定义的纹理单元的名字,不是我们在main.cpp中创建的纹理对象的变量名。

shaderobject.use();//不要忘记在设置uniform变量之前激活着色器程序!

glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture1"), 0); // 手动设置
glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture2"), 1); // 手动设置

        或者可以使用我们自己在shader.h里面封装的setInt函数来设置,里面还是调用glUniform1i函数。

shaderobject.use(); //设置uniform变量之前激活着色器程序

//glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture1"), 0); // 手动设置
//glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture2"), 1); // 手动设置

shaderobject.setInt("ourTexture1", 0); //使用自己封装好的setInt,里面还是实现了glUniform1i函数
shaderobject.setInt("ourTexture2", 1);

运行结果如下:

(五)笑脸是上下颠倒的,因为opengl的y坐标的(0, 0)点在左下角,而图片y坐标的原点在左上角。我们可以在加载纹理图片之前的任何位置加上下面这行代码让纹理正常显示。

stbi_set_flip_vertically_on_load(true);

6-2.完整代码

shader.vs(未修改)

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
uniform float offsetx;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord; 
}

shader.fs(定义俩纹理单元并使用mix函数进行纹理叠加)

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;

void main()
{
    //FragColor = texture(ourTexture1, TexCoord);  //使用1号纹理进行采样
  
    FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2); //纹理混合采样
    //参数3:0.2代表第二个纹理图像透明度为20%
}

main.cpp(激活纹理单元、设置两个纹理采样器对应几号级别的纹理:0~15)

#include <glad.h>   //glad.h需要在glfw.h之前
#include <glfw3.h>
#include <iostream>
#include <cmath> 
#include "shader.h"
#include "stb_image.h"

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  // 左上 
};

unsigned int indices[] = {  //两个三角形拼接成一个长方形
    0, 1, 3,
    1, 2, 3
};

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

int main()
{
    //初始化glfw:选择opengl 3.3~4.2核心模式,取决于你的显卡驱动
    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

    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); //创建窗体对象:参数1:opengl窗口大小;参数2:窗口标题
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                     //设置当前窗口上下文设置为当前线程的上下文,之后才能在窗口进行绘制

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))            //glad:加载所有OpenGL函数指针,固定写法就这一行
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    glViewport(0, 0, 600, 400);                                         //设置opengl实际渲染的区域大小;0,0为左下角坐标

    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);  //注册窗口大小改变回调函数

    Shader shaderobject("shaders/shader.vs", "shaders/shader.fs");

    //2.创建VBO和VAO、EBO对象并赋予ID值
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    //3.绑定VBO,VAO、EBO对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //填充EBO缓冲
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //4.开辟并填充数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //5.告知Shader如何解析缓冲里的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);

    //6.开启VAO管理的第一个属性值,第一个VAO编号为0
    glEnableVertexAttribArray(0);

    //新增VAO颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3*sizeof(float))); //颜色属性开始偏移3个顶点属性
    glEnableVertexAttribArray(1);

    //新增VAO纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); //纹理坐标属性开始偏移了6个顶点属性
    glEnableVertexAttribArray(2);

    //7.解绑VBO,VAO,EBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //创建纹理对象1和2
    unsigned int texture1, texture2;
    glGenTextures(1, &texture1);
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture1);

    stbi_set_flip_vertically_on_load(true);//放于纹理加载之前,防止颠倒

    // 加载并生成纹理texture1
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    glBindTexture(GL_TEXTURE_2D, texture2);
    // 加载并生成纹理texture2
    data = stbi_load("./pics/awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    while (!glfwWindowShouldClose(window))                              //检查一次GLFW是否被要求退出,循环一次就是刷新一帧
    {
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);                           //设置颜色:深绿色
        glClear(GL_COLOR_BUFFER_BIT);                                   //刷新屏幕,使用设置的颜色

        //d.选择使用着色器给三角形上色,即上面创建链接好的流水线
        shaderobject.use(); //设置uniform变量之前激活着色器程序

        //9.上面把VAO解绑了,这里必须重新绑定否则绘制不出来三角形
        glBindVertexArray(VAO);

        //11.绑定EBO,注意在绑定VAO之后才绑定EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

        //激活纹理单元
        glActiveTexture(GL_TEXTURE0);           //注意宏是从0号纹理开始
        glBindTexture(GL_TEXTURE_2D, texture1);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);

        //glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture1"), 0); // 手动设置
        //glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture2"), 1); // 手动设置

        shaderobject.setInt("ourTexture1", 0); //使用自己封装好的setInt,里面还是实现了glUniform1i函数
        shaderobject.setInt("ourTexture2", 1);

        //10.绘制元素
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);                                        //交换颜色缓冲(双缓冲):储存着GLFW窗口每一个像素颜色值作为输出显示在屏幕上
        glfwPollEvents();                                               //轮训检查输入设备触发事件
    }
    //e.渲染结束后把VAO,VBO和着色器都释放掉
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);//销毁的顺序没有规定
    glDeleteProgram(shaderobject.ID);

    glfwTerminate();                                                    //释放分配的所有glfw资源
    return 0;
}

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

shader.h(未做修改)

#ifndef SHADER_H
#define SHADER_H

#include <glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader
{
public:
    unsigned int ID;//着色器程序ID,即流水线ID

    // 构造函数生成着色器对象
    Shader(const char* vertexPath, const char* fragmentPath)
    {
        // 1.从文件路径中获取顶点/片段源代码
        std::string vertexCode;
        std::string fragmentCode; 
        std::ifstream vShaderFile;
        std::ifstream fShaderFile;
        
        // 确保ifstream对象可以引发异常:
        vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        try
        {
            // 打开文件
            vShaderFile.open(vertexPath);         
            fShaderFile.open(fragmentPath);
            std::stringstream vShaderStream, fShaderStream;
            // 将文件的缓冲区内容读入流
            vShaderStream << vShaderFile.rdbuf();
            fShaderStream << fShaderFile.rdbuf();
            // 关闭文件
            vShaderFile.close();
            fShaderFile.close();
            // 将流转换为字符串
            vertexCode = vShaderStream.str();
            fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure& e)
        {
            std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
        }
        //将字符串转换为C风格字符串
        const char* vShaderCode = vertexCode.c_str();
        const char* fShaderCode = fragmentCode.c_str();
        // 2. 编译着色器
        unsigned int vertex, fragment;
        // 创建并编译顶点着色器
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        checkCompileErrors(vertex, "VERTEX");
        // 创建并编译片段着色器
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        checkCompileErrors(fragment, "FRAGMENT");
        // 链接着色器程序
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        checkCompileErrors(ID, "PROGRAM");
        // 删除着色器,因为它们现在链接到着色器程序中,不再需要了
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    
    // 选择着色器程序
    void use()
    {
        glUseProgram(ID);
    } 
    
    // uniform的实用函数 
    void setBool(const std::string& name, bool value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
    }
    
    void setInt(const std::string& name, int value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
    }
   
    void setFloat(const std::string& name, float value) const
    {
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
    }

private:
    // 用于检查着色器编译/链接错误的实用函数。
    void checkCompileErrors(GLuint shader, std::string type)
    {
        GLint success;
        GLchar infoLog[1024];
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
    }
};
#endif

7.练习

(1)修改片段着色器,仅让笑脸图案朝另一个方向看,参考解答

        修改方法就是把x轴的纹理坐标反一下,比如说之前的左眼x坐标是0.2,那么反过去后就是0.8,公式为1 - x。

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;

void main()
{
    //FragColor = texture(ourTexture1, TexCoord);  //使用1号纹理进行采样
  
    FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2); //纹理混合采样
    //参数3:0.2代表第二个纹理图像透明度为20%
}

(2)尝试用不同的纹理环绕方式,设定一个从0.0f2.0f范围内的(而不是原来的0.0f1.0f)纹理坐标。试试看能不能在箱子的角落放置4个笑脸:参考解答结果。记得一定要试试其它的环绕方式。

        纹理环绕方式那里已经做过了,这里直接看参考答案就行。

(3)尝试在矩形上只显示纹理图像的中间一部分,修改纹理坐标,达到能看见单个的像素的效果。尝试使用GL_NEAREST的纹理过滤方式让像素显示得更清晰:参考解答

        题目意思是我们把纹理坐标改小,只取一部分纹理,然后在纹理过滤部分把线性过滤改成近邻过滤,增加颗粒感。

(4)使用一个uniform变量作为mix函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度:参考解答

        该题目意思是我们需要在shader中再添加一个变量用来存储纹理的混合值,然后通过键盘上下键来改变混合值。我们直接来修改代码。

shader.fs

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
uniform float mixValue;

void main()
{
    //FragColor = texture(ourTexture1, TexCoord);  //使用1号纹理进行采样
  
    FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), mixValue); //纹理混合采样
    //参数3:0.2代表第二个纹理图像透明度为20%
}

main.cpp

float mixValue = 0.2f;  //初次运行就是0.2的混合值
......
while (!glfwWindowShouldClose(window))
{
    // set the texture mix value in the shader
    shaderobject.setFloat("mixValue", mixValue);
}
......
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)              //按Esc键关闭界面
        glfwSetWindowShouldClose(window, true);
    if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
    {
        mixValue += 0.001f; // change this value accordingly (might be too slow or too fast based on system hardware)
        if (mixValue >= 1.0f)
            mixValue = 1.0f;
    }
    if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
    {
        mixValue -= 0.001f; // change this value accordingly (might be too slow or too fast based on system hardware)
        if (mixValue <= 0.0f)
            mixValue = 0.0f;
    }
}

效果展示:

Video_2024-04-07_224300

main.cpp完整代码

#include <glad.h>   //glad.h需要在glfw.h之前
#include <glfw3.h>
#include <iostream>
#include <cmath> 
#include "shader.h"
#include "stb_image.h"

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  // 左上 
};

unsigned int indices[] = {  //两个三角形拼接成一个长方形
    0, 1, 3,
    1, 2, 3
};

float mixValue = 0.2f;  //初次运行就是0.2的混合值

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

int main()
{
    //初始化glfw:选择opengl 3.3~4.2核心模式,取决于你的显卡驱动
    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

    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); //创建窗体对象:参数1:opengl窗口大小;参数2:窗口标题
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                     //设置当前窗口上下文设置为当前线程的上下文,之后才能在窗口进行绘制

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))            //glad:加载所有OpenGL函数指针,固定写法就这一行
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    glViewport(0, 0, 600, 400);                                         //设置opengl实际渲染的区域大小;0,0为左下角坐标

    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);  //注册窗口大小改变回调函数

    Shader shaderobject("shaders/shader.vs", "shaders/shader.fs");

    //2.创建VBO和VAO、EBO对象并赋予ID值
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    //3.绑定VBO,VAO、EBO对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //填充EBO缓冲
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //4.开辟并填充数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //5.告知Shader如何解析缓冲里的属性值
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);

    //6.开启VAO管理的第一个属性值,第一个VAO编号为0
    glEnableVertexAttribArray(0);

    //新增VAO颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3*sizeof(float))); //颜色属性开始偏移3个顶点属性
    glEnableVertexAttribArray(1);

    //新增VAO纹理坐标属性
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); //纹理坐标属性开始偏移了6个顶点属性
    glEnableVertexAttribArray(2);

    //7.解绑VBO,VAO,EBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //创建纹理对象1和2
    unsigned int texture1, texture2;
    glGenTextures(1, &texture1);
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture1);

    stbi_set_flip_vertically_on_load(true);//放于纹理加载之前,防止颠倒

    // 加载并生成纹理texture1
    int width, height, nrChannels;
    unsigned char* data = stbi_load("./pics/container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    glBindTexture(GL_TEXTURE_2D, texture2);
    // 加载并生成纹理texture2
    data = stbi_load("./pics/awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);//把data的纹理数据加载到GL_TEXTURE_2D
        glGenerateMipmap(GL_TEXTURE_2D);//自动生成多级渐远纹理
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);//data已经加载进缓冲了就可以释放

    while (!glfwWindowShouldClose(window))                              //检查一次GLFW是否被要求退出,循环一次就是刷新一帧
    {
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);                           //设置颜色:深绿色
        glClear(GL_COLOR_BUFFER_BIT);                                   //刷新屏幕,使用设置的颜色

        // set the texture mix value in the shader
        shaderobject.setFloat("mixValue", mixValue);

        //d.选择使用着色器给三角形上色,即上面创建链接好的流水线
        shaderobject.use(); //设置uniform变量之前激活着色器程序

        //9.上面把VAO解绑了,这里必须重新绑定否则绘制不出来三角形
        glBindVertexArray(VAO);

        //11.绑定EBO,注意在绑定VAO之后才绑定EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

        //激活纹理单元
        glActiveTexture(GL_TEXTURE0);           //注意宏是从0号纹理开始
        glBindTexture(GL_TEXTURE_2D, texture1);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);

        //glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture1"), 0); // 手动设置
        //glUniform1i(glGetUniformLocation(shaderobject.ID, "ourTexture2"), 1); // 手动设置

        shaderobject.setInt("ourTexture1", 0); //使用自己封装好的setInt,里面还是实现了glUniform1i函数
        shaderobject.setInt("ourTexture2", 1);

        //10.绘制元素
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);                                        //交换颜色缓冲(双缓冲):储存着GLFW窗口每一个像素颜色值作为输出显示在屏幕上
        glfwPollEvents();                                               //轮训检查输入设备触发事件
    }
    //e.渲染结束后把VAO,VBO和着色器都释放掉
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);//销毁的顺序没有规定
    glDeleteProgram(shaderobject.ID);

    glfwTerminate();                                                    //释放分配的所有glfw资源
    return 0;
}

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);
    if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
    {
        mixValue += 0.001f; // change this value accordingly (might be too slow or too fast based on system hardware)
        if (mixValue >= 1.0f)
            mixValue = 1.0f;
    }
    if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
    {
        mixValue -= 0.001f; // change this value accordingly (might be too slow or too fast based on system hardware)
        if (mixValue <= 0.0f)
            mixValue = 0.0f;
    }
}

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值