UV映射!加入纹理!
本小节图片来源:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/
上小节我们将着色器独立出来,并且利用着色器类对着色器文件进行初始化,将检查着色器源码编译是否出错集成进该类里。在主类里我们引入了Shader类,并用其加载了两个三角形面片。这一小节,我主要介绍纹理相关知识。
对于细节方面,如果用建模软件或者数字编程的方法费力不讨好,效果可能还不尽人意。这里就引出纹理(Texture)的概念,使用纹理,可以将一张或多张图片附加到面片上,这样可以有很多细节,而不需要额外消耗很多计算资源。
为了能够将纹理映射到我们的三角形面片上,我们使用纹理坐标(Texture Coordinate)来标明改动纹理图的哪个部分来进行采样。如果2D纹理图像,那么纹理坐标只分为x和y轴,范围在0,1之间。使用纹理坐标获取纹理颜色叫做采样(Sampling),纹理坐标原点为图片的左下角为(0,0),终止于图片右上角坐标为(1,1),下图展示了如何将纹理坐标映射到三角形上。
纹理坐标不是独立出来的,它通常是跟随顶点坐标一起输入顶点着色器,所以上面uv映射纹理坐标应该是这样的,每行最后两个是uv值:
// 顶点格式:x, y, z, r, g, b, u, v
static 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, 0.0f, 1.0f,0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f
};
纹理环绕方式
纹理坐标的范围通常是从(0,0)到(1,1),那么当我们把纹理坐标设置到范围外的时候辉发射什么呢?OpenGL的默认行为是重复这个图像,但是OpenGL提供了为我们提供了更多的选择。
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴进行设置(s, t, r对应u, v, w),其中“*”为函数名的待定部分,根据不同用途填写不同字符,引用不同函数。
// 为当前绑定的纹理对象设置环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
- 第一个参数指定了纹理目标,由于我们使用的是2D纹理,所以该参数填写为GL_TEXTURE_2D;
- 第二个参数指定了纹理轴,即我们需要在横轴还是纵轴配置环绕方式,s为横轴,t为纵轴;
- 第三个参数制定了环绕方式,即上述四种环绕方式之一。
需要注意的是,如果环绕方式我们选择了GL_CLAMP_TO_BORDER,那么我们还需要指定一个边缘颜色,此时glTexParameter的函数后缀就变为fv:
float boardColor = {1.0f, 0.0f, 0.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel)映射到纹理坐标,即,当画面很大或画面很小的时候,贴图里的像素最终该怎样呈现在显示器上。OpenGL中有许多关于纹理过滤(Texture Filtering)的选项,在这里介绍最重要的两个:GL_NEAREST和GL_LINEAR。
GL_NEAREST(临近过滤,Nearest Neighbor Filtering)是OpenGL默认的过滤方式,设置为此值时OpenGL会选择中心点最接近纹理坐标的那个像素。
GL_LINEAR(线性过滤,Linear Filtering),它会基于纹理坐标附近的纹理像素,计算出一个插值(加权平均,距离越近权值越大),来近似出这些纹理像素之间的颜色。
上图表明GL_NEAREST产生了颗粒状图案,而GL_LINEAR能够产生出更平滑的图案,图片的真实感会增加。当我们进行放大(Magnify)和缩小(Minify)操作的时候,可以设置纹理过滤的选项,比如,可以在纹理被缩小的时候使用临近过滤,在纹理被放大时使用线性过滤。
// 生成纹理过滤
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有一个内置函数,让我们可以免于手动创建一系列Mipnap:glGenerateMipmap函数,在创建完一个纹理后调用它OpenGL会承担接下来的所有工作。
这会导致一个问题,在渲染中切换多级渐远纹理级别时,OpenGL在两个不同级别的多级渐远纹理层之间会出现不真实的生硬边界。简单一点的理解就是:
想象一个场景,你渲染了一个地面平面,它从离相机很近的地方延伸到很远的地方。在近处的地面,GPU会使用较高的Mipmap级别,如level 1;在远处的地面GPU会使用较低的Mipmap级别,如level 3。那么理论上就存在一个中距离,这这里一部分像素应该用level 2,而一线之隔的另一部分应该用level 3,这个生硬边界就是两个不同级别的多级渐远纹理层之间的边界。因为两个不同级别的Mipmap在颜色细节上会存在些许差异,这在人眼中是很容易被捕捉到的。
你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
---|---|
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错误代码。
加载与创建纹理
首先我们先添加一个stb_image.h头文件,这个库可以很方便的让我们加载图片文件。下载地址为:https://github.com/nothings/stb/blob/master/stb_image.h,随后按照我们第一节所做的,选择添加现有项将其加入到头文件中。
// 引用该头文件
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
当你没有定义STB_IMAGE_IMPLEMENTATION时,而只是包含了stb_image.h,那么预处理器看到的只是文件中的函数原型、类型等,而具体的实现方法则被包含在定义STB_IMAGE_IMPLEMENTATION这个宏定义中:
// ... 函数原型、类型#ifdef STB_IMAGE_IMPLEMENTATION
// ... 函数实现代码
#endif
然后我们创建一个文件夹供我们放置纹理文件,并向其中放入一个.jpg格式的文件,这里放入了一张名为Screenshot.jpg图片:
下面我们使用stb_image.h来加载图片,我们需要使用到其中的stbi_load函数:
int weidth, height, nrChannels;
unsigned char* data = stbi_load("texture/Screenshot.jpg", &width, &height, &nrChannels, 0);
第一个参数接受一个图像文件的位置作为输入,后面需要三个int变量的指针来填充图片的宽、高和颜色通道数。这样纹理数据就被存储在data指针中。
生成纹理
生成纹理与生成着色器程序、着色器对象以及缓冲对象差不多,都需要一个unsigned int作为绑定对象,然后通过函数生成并绑定。绑定后就需要载入一个图片文件,这里通过函数glTexImage2D来实现,生成纹理的过程大致如下:
// 生成纹理对象并绑定
unsigned int texture;
glGenTextures(1, &texture);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
...
// 加载图片
glTexImage2D(GL_TETXURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_USIGNED_BYTE, data);
// 生成多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
// 释放图像内存
stbi_image_free(data);
函数原型:glTexImage2D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* data);
- target(目标):指定纹理绑定的目标,即纹理类型,常用值:
- GL_TETURE_2D:用于创建标准的2D纹理;
- GL_TEXTURE_CUBE_MAP_POSITION_X,GL_TEXTURE_CUBE_MAP_NEGATIVE_X, ...用于创建立方体贴图的六个面,通常与GL_TEXTURE_CUBE_MAP一起使用。
- GL_PROXY_TEXTURE_2D:用于代理查询,检查纹理是否能够被载入。
- level(层级):指定多级渐远纹理的级别,0时基本图像级别,n是第n级的缩小图像,如果不使用Mipmap,则始终设置为0。
- internalFormat(内部格式):指定显卡在内部如何存储纹理数据,它定义了纹理中颜色分量的数量和布局,常用值:
- GL_RGB:存储红、绿、蓝三通道;
- GL_RGBA:存储红、绿、蓝、透明度四通道;
- GL_RED:存储一个通道(常用于单色纹理或高度图);
- GL_DEPTH_COMPONENT:用于深度纹理;
- GL_SRGB/GL_SRGB_ALPHA:支持伽马矫正的sRGB空间;
- 注意:还有更多压缩格式和特定大小格式。
- width(宽度):纹理图像的宽度。在OpenGL3+中,必须是2的整数次方,如16、32等,除非使用特定扩展。
- height(高度):纹理图像的高度,同width。
- border(边框):这个参数是为了向后兼容,在现代OpenGL中必须始终为0,这是历史遗留问题。
- format(格式):指定你提供的源图像数据格式,即你传入的数据是如何组织的。GL_RGB、GL_RGBA、GL_RED、GLBGR/GL_BGRA(常见于windows的BMP等格式,需要拓展)、GL_DEPTH_COMPONENT。
- type(类型):指定源图像数据(data参数)中每个像素分量的数据类型,常用值:
- GL_UNSIGNED_BYTE:unsigned byte
- GL_FLOAT:float
- GL_UNSIGNED_SHORT:unsigned short
- GL_UNSIGNED_INT:unsigned int
- data(数据):指向内存中图像数据源的指针,这个指针是CPU内存中的地址,函数会将这些数据从CPU内存复制到显卡的显存中,如果指向分配显存空间(为以后填充数据,例如渲染到纹理),可以将此参数设置为NULL,此时纹理内容是未定义的。
应用纹理
首先更新顶点相关属性,为顶点说组添加纹理坐标;然后更新顶点属性的解释函数,并添加纹理坐标的解释函数,并绑定相应的逻辑索引;接下来要为顶点着色器添加相关的传入参数和传出参数,因为纹理坐标不在顶点着色器中使用,而是在片段着色器中使用;接下来为片段着色器添加输入参数,并改变颜色输出赋值。
GLSL内建的texture()函数来采样纹理颜色,第一个参数是纹理采样器,第二个参数是对应的纹理坐标。
// 顶点数据如下
static 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, 0.0f, 1.0f, // 左上0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f // 左下
};// 更新顶点属性解释函数:
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);// vertexShader.txt, 为顶点着色器添加一个新的传入参数和传出参数
#version 330 corelayout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexture;out vec3 outColor;
out vec2 outTexCoord;void main(){gl_Position = vec4(aPosition, 1.0f);outColor = aColor;outTexCoord = aTexture;
}// fragmentShader.txt, 为片段着色器添加纹理坐标传入
#version 330 corein vec3 outColor;
in vec2 outTexCoord;out vec4 FragColor;uniform vec4 timeColor;
uniform sampler2D texture;void main(){// FragColor = vec4(outColor, 1.0f);// FragColor = vec4(timeColor.x, timeColor.y, timeColor.z, timeColor.w);FragColor = texture(texture, outTexCoord)*vec4(outColor, 1.0f);
}
最后需要在glDrawElements之前绑定纹理,它会自动把纹理赋值给片段着色器:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, sizeof(indices)/4, GL_SIGNED_INT, (void*)0);
如果一切顺利,那么会得到运行结果:
纹理单元
可以注意到我们在片段着色器中定义了一个uniform sampler2D的变量类型,但是我们却没有在主程序或其他地方为其赋值。这是因为可以同时把多个纹理单元赋值给采样器,那么这样就可以一次性绑定多个纹理,但是我们上面只使用了一个纹理,而一个纹理默认纹理单元为0,它是默认的激活单元,所以前面我们并没有分配一个纹理单元,却可以正常运行。
通过纹理单元赋值给采样器,我们可以使用多于一个的纹理,只要我们首先激活对应的纹理单元,我可以使用glActiveTexture()激活纹理单元,并传入我们需要使用的纹理。激活纹理单元之后,接下来glBindTexture函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活的。
OpenGL至少保证有16个纹理单元可以使用,也就是说可以激活从GL_TEXTURE0到GL_TEXTURE15,由于他们都是按顺序定义的,所以我们可以通过加减得到纹理单元,比如GL_TEXTURE0 + 3可以获得GL_TEXTURE3。
// 激活纹理单元并绑定,将texture绑定到GL_TEXTURE0上
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
假设我们现在需要绑定两个纹理到我们的面片上,那么需要修改我们的片段着色器,为其添加一个uniform,并且使用GLSL内置的mix()函数来混合两个纹理,最终的输出结果是两个纹理的结合:
#version 330 corein vec2 outTexCoord;out vec4 FragColor;uniform vec4 timeColor;
uniform sampler2D texture0;
uniform sampler2D texture1;
uniform float mixin;void main(){FragColor = mix(texture(texture0, outTexCoord), texture(texture1, outTexCoord), mixin)*timeColor;
}
mix(in1, in2, power)函数接受三个参数:它通过第三个参数来对前两个参数进行加权平均,即out = in1*(1-power) + in2*power;
由于我们需要加载多个纹理,每次加载纹理都需要进行一长串的初始化和绑定操作,十分繁琐,我们将纹理初始化和绑定过程写到函数中,包含这些函数:
- std::vector<unsigned int> loadTexture(std::vector<std::string> paths); 加载图像文件
- std::string imageType(std::string path); 返回图像扩展名
- unsigned int textureInit(const char* path, unsigned short count, std::string type); 纹理初始化
- void bindTextureUniform(unsigned int shaderProgram, std::vector<unsigned int> textures);绑定纹理到对应的纹理单元
下面是函数实现:
unsigned int textureInit(const char* path, unsigned short count, std::string type) {unsigned int texture;glGenTextures(1, &texture);glActiveTexture(GL_TEXTURE0 + count);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_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);// 加载并生成纹理int width, height, nrChannels;unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);if (data){ if (type._Equal("png")) {// 加载.png文件需要把第二个GL_RGB参数改为GL_RGBA,因为.png文件的图片是有一个额外的alpha参数glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);}else{// 加载没有alpha通道的图像文件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);return texture;
}std::string imageType(std::string path) {std::string type = "";for (int i = path.length() - 1; i >= 0 && path[i] != '.'; i--) {type = path[i] + type;}return type;
}std::vector<unsigned int> loadTexture(std::vector<std::string> paths) {std::vector<unsigned int> textures;if (paths.empty()) {std::cout << "Paths is empty! Cann't load texture!!!" << std::endl;}// 加载图片反转y轴stbi_set_flip_vertically_on_load(true);// 便利容器加载纹理for (int i = 0; i < paths.size(); i++) {// 取出纹理路径const char* path = paths[i].c_str();// 取出纹理拓展名std::string type = imageType(path);// 加载纹理,并返回textures.push_back(textureInit(path, i, type));}return textures;
}void bindTextureUniform(unsigned int shaderProgram, std::vector<unsigned int> textures) {// 绑定纹理到对应的纹理单元for (int i = 0; i < textures.size(); i++) {// 设置采样器uniformstd::string textureName = "texture" + std::to_string(i);unsigned int textureLocation = glGetUniformLocation(shaderProgram, textureName.c_str());glUniform1i(textureLocation, i);}
}
更新渲染函数render(),加入对函数bindTextureUniform()的调用:
void render(unsigned int shaderProgram, unsigned int VAO, const std::vector<unsigned int> textures) {// 设置清空颜色(重要!)glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 清空颜色缓冲区和深度缓冲区glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glUseProgram(shaderProgram);glBindVertexArray(VAO);updateUniformTimeColor(shaderProgram);bindTextureUniform(shaderProgram, textures);// 使用EBO数据绘制glDrawElements(GL_TRIANGLES, sizeof(indices)/4, GL_UNSIGNED_INT, (void*)0);glBindVertexArray(0);
}
在主程序中加入:
// 加载图像文件到纹理单元中
std::vector<std::string> paths;
paths.push_back("texture/Screenshot.jpg");
paths.push_back("texture/awesomeface.png");
const std::vector<unsigned int> textures = loadTexture(paths);// main()中render()函数更新
render(shader->programId, VAO, textures);
一切准备就绪,运行会得到一个混合了两个图像的贴图:
如果你想通过键盘的上下键来控制是显示第一个图片还是第二个图片,可以参考下面的完整程序:
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <vector>#define STB_IMAGE_IMPLEMENTATION#include "stb/stb_image.h"
#include "shader/shader.h"int mixin = 20;static 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, 0.0f, 1.0f,0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f
};static unsigned int indices[] = {0, 1, 2,1, 2, 3
};// 当用户改变窗口的大小的时候,视口也应该被调整。
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {glViewport(0, 0, width, height);
}void registerFunction(GLFWwindow* window) {glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
}// 读取用户输入,当程序运行时,按下ESC时退出进程
void processInput(GLFWwindow* window, unsigned int shaderProgram) {if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {glfwSetWindowShouldClose(window, true);}else if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) {mixin += 1;if (mixin > 100) mixin = 100;glUniform1f(glGetUniformLocation(shaderProgram, "mixin"), float(mixin)/100);}else if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) {mixin -= 1;if (mixin < 0) mixin = 0;glUniform1f(glGetUniformLocation(shaderProgram, "mixin"), float(mixin)/100);}
}GLFWwindow* windowInit() {// 实例化窗口// 初始化GLFWglfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// 创建一个窗口对象,参数:宽、高、名称、*、*,函数返回一个GLFWwindow对象GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Window", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();}// 通知GLFW将创建的窗口的上下文设置为当前线程的主上下文glfwMakeContextCurrent(window);// GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {std::cout << "Failed to initialize GLAD" << std::endl;}// 通知OpenGL渲染窗口的尺寸大小——视口(Viewport)glViewport(0, 0, 800, 600);return window;
}unsigned int VAOInit() {unsigned int VAO;glGenVertexArrays(1, &VAO);return VAO;
}unsigned int VBOInit() {unsigned int VBO;glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, 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);return VBO;
}unsigned int EBOInit() {unsigned int EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);return EBO;
}unsigned int textureInit(const char* path, unsigned short count, std::string type) {unsigned int texture;glGenTextures(1, &texture);glActiveTexture(GL_TEXTURE0 + count);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_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);// 加载并生成纹理int width, height, nrChannels;unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);if (data){ if (type._Equal("png")) {// 加载.png文件需要把第二个GL_RGB参数改为GL_RGBA,因为.png文件的图片是有一个额外的alpha参数glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);}else{// 加载没有alpha通道的图像文件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);return texture;
}std::string imageType(std::string path) {std::string type = "";for (int i = path.length() - 1; i >= 0 && path[i] != '.'; i--) {type = path[i] + type;}return type;
}std::vector<unsigned int> loadTexture(std::vector<std::string> paths) {std::vector<unsigned int> textures;if (paths.empty()) {std::cout << "Paths is empty! Cann't load texture!!!" << std::endl;}// 加载图片反转y轴stbi_set_flip_vertically_on_load(true);// 便利容器加载纹理for (int i = 0; i < paths.size(); i++) {// 取出纹理路径const char* path = paths[i].c_str();// 取出纹理拓展名std::string type = imageType(path);// 加载纹理,并返回textures.push_back(textureInit(path, i, type));}return textures;
}void bindTextureUniform(unsigned int shaderProgram, std::vector<unsigned int> textures) {// 绑定纹理到对应的纹理单元for (int i = 0; i < textures.size(); i++) {// 设置采样器uniformstd::string textureName = "texture" + std::to_string(i);unsigned int textureLocation = glGetUniformLocation(shaderProgram, textureName.c_str());glUniform1i(textureLocation, i);}
}Shader* shaderInit(const std::string vertexPath, const std::string fragmentPath) {// 初始化着色器和着色器程序 Shader* ptrShader = new Shader(vertexPath, fragmentPath);ptrShader->shaderInit();return ptrShader;
}void updateUniformTimeColor(unsigned int shaderProgram) {float pi = 3.14;float time = glfwGetTime();float red = sin(time)/2.0f + 0.5f;float green = sin(time + pi / 6) / 2.0f + 0.5f;float blue = sin(time + pi / 3) / 2.0f + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "timeColor");glUniform4f(vertexColorLocation, red, green, blue, 1.0f);
}void render(unsigned int shaderProgram, unsigned int VAO, const std::vector<unsigned int> textures) {// 设置清空颜色(重要!)glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 清空颜色缓冲区和深度缓冲区glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glUseProgram(shaderProgram);glBindVertexArray(VAO);updateUniformTimeColor(shaderProgram);bindTextureUniform(shaderProgram, textures);// 使用EBO数据绘制glDrawElements(GL_TRIANGLES, sizeof(indices)/4, GL_UNSIGNED_INT, (void*)0);glBindVertexArray(0);
}int main()
{GLFWwindow* window = windowInit();// 注册函数framebuffer_size_callback,告知GLFW当窗口调整大小时调用这个函数registerFunction(window);// VAO必须要先初始化,否则在VBO初始化的时候不能通过VAO来记录unsigned int VAO = VAOInit();// 记录配置信息glBindVertexArray(VAO);unsigned int VBO = VBOInit();unsigned int EBO = EBOInit();std::vector<std::string> paths;paths.push_back("texture/Screenshot.jpg");paths.push_back("texture/awesomeface.png");const std::vector<unsigned int> textures = loadTexture(paths);glBindVertexArray(0);const std::string vertexPath = "shader/vertexShader.txt";const std::string fragmentPath = "shader/fragmentShader.txt";Shader* shader = shaderInit(vertexPath, fragmentPath);// glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出while (!glfwWindowShouldClose(window)) {// 输入processInput(window, shader->programId);// 渲染指令render(shader->programId, VAO, textures);// 检查并调用事件,交换缓冲// glfwSwapBuffers函数会交换颜色缓冲glfwSwapBuffers(window);// glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状并调用对应的回调函数glfwPollEvents();}// 渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。glfwTerminate();return 0;
}