目录
前言
上一篇博客回顾:OpenGL学习(五)相机变换,透视投影与FPS相机
在上一篇博客中,我们利用相机变换矩阵,对场景进行透视投影,同时我们实现了可以自由飞翔的 FPS 相机。
迄今为止我们的渲染都是非常单调并且过时的,今天我们来引入一些现代化的东西,来丰富我们的场景。
首先我们会利用一张图片生成纹理,随后我们将这张图片贴在我们的物体上。这就像现代计算机游戏中,我们可以让艺术家们人为的制定一些图片,而不是由程序员大费周章的生成它。
在最后我们通过读取 obj 格式的模型并且创建对应的纹理,来绘制一些精美的模型。
⚠
该部分的绘制代码基于上一篇博客:OpenGL学习(五)相机变换,透视投影与FPS相机
博客内容因为篇幅关系,不会完整的列出所有的代码 完整代码会放在文章末尾
纹理映射
在正式开始之前,我们需要了解纹理映射的知识。在计算机游戏中,我们往往见到很多精美的模型,比如下图的水果摊,就有很多个🍎。
通过模型实际上还原这些🍎的几何细节是非常困难的。而且我们还要确定他们的颜色,这更加是难上加难。
于是我们想出了一个曲线救国的方式:我们将一张图片贴上去,不就可以达到逼真的效果了吗?
你通过观察不难发现,原本的柜台就是一个平面,我们将图片贴上去就达到了 “近似” 的效果。
你不得不承认这样看上去很假,因为我们没有考虑到从各个角度观察的情况,但是事实上这是聪明的图形程序员一种非常高效的解决方案
在之后的博客中,我们会利用视差贴图来进一步丰富该效果
纹理的本质就是一张图片。一张图片,那么他就有坐标。纹理的坐标通常称之为 uv 坐标。
该坐标的原点位于左下角,为 (0, 0) 而右上角的坐标为 (1, 1),这是约定俗成的,因为不同的纹理有不同的大小,我们必须归一化!
我们在 GLSL 中,引入一个新的变量类型,叫做 sampler2D
,这就是一张 2D 的纹理对象,一般以 uniform 的形式传入。
和一般的编程语言中进行图像处理不同,我们不能通过下标索引来取像素。相反,我们通过:
uniform sampler2D image;
vec3 color = texture2D(image, 坐标).rgb;
其中 texture2D 是纹理采样函数,第一个参数是 sampler2D 纹理对象,第二个参数是纹理的坐标,即一个位于 [0, 1] 之间的二维向量。
如果我们传入 (0, 0) 那么我们会取纹理左下角的像素颜色,如果是 (0.5, 0.5) 那么我们会取纹理图像中心的像素颜色。
我们想要将纹理贴到物体上,可是物体的几何形状非常不规则,我们难以通过数学的方式描述这些变换,于是我们要引入一个新的东西,叫做纹理坐标。
纹理坐标
纹理坐标,顾名思义就是纹理的坐标。纹理坐标是一种顶点属性,就和顶点的位置,颜色,法线一样,理论上每个顶点都必须拥有纹理坐标。
纹理坐标描述了该顶点的颜色,应该从纹理图上的哪个位置去取。
比如我们渲染一个正方形平面,它有 4 个顶点,那么我们应该去纹理图像上的四个顶点取颜色,这样我们就能够显示整张图片!
因为纹理坐标是顶点属性,我们在片段着色器中,采样纹理的时候,得到的纹理坐标是经过线性插值的,我们可以连续地取像素。
值得注意的是,纹理坐标也是人为指定的,一般模型信息里面会附代它的纹理坐标(就如同顶点位置信息一样)
映射到简单正方形
我们试图按照上文的思路来。正方形一共四个顶点,我们将其映射到纹理图像的四个角上,他们的坐标分别是:
(0, 0)
(0, 1)
(1, 0)
(1, 1)
于是我们需要向顶点着色器中传递的纹理坐标就是这四个点(实际上正方形是 6 个点组成的,我们要传递 6 个顶点位置,和 6 个纹理坐标)
读取图像
我们通过 SOIL 库进行图像的读取。通过
vcpkg install SOIL2
可以利用 vcpkg 进行安装。如果安装遇到问题,那么尝试阅读:vcpkg安装SOIL2库报错及其解决方案
在成功安装之后,我们可以通过
#include <SOIL2/SOIL2.h>
int textureWidth, textureHeight;
unsigned char* image = SOIL_load_image("textures/wall.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);
来进行图像的读取。其中 textureWidth, textureHeight
是图像的宽高,单位为像素。我们传入其引用(地址),函数就会自动给他们赋值。
生成正方形数据
我们将 init 函数中的 readOff 注释掉,因为我们现在不再依赖 off 格式的模型,而是手动创建一个正方形。
事实上这里大改了整个 init 详情请见【完整代码】部分
为了为正方形贴上图片,我们需要确定两个顶点属性:
- 正方形顶点位置
- 正方形顶点的纹理坐标
故,我们添加如下的顶点数据:
// 手动指定正方形的 4 个顶点位置和其纹理坐标
std::vector<glm::vec3> vectexPosition = {
glm::vec3(-1,-0.2,-1), glm::vec3(-1,-0.2,1), glm::vec3(1,-0.2,-1),glm::vec3(1,-0.2,1)
};
std::vector<glm::vec2> vertexTexcoord = {
glm::vec2(0, 0), glm::vec2(0, 1), glm::vec2(1, 0), glm::vec2(1, 1)
};
同时我们创建一个新的全局变量 texcoords,用以存储每个顶点的纹理坐标。事实上 texcoord 就是 texture coord,纹理坐标。
然后我们需要绘制两个三角形(共 6 个点)来填充正方形。我们在 init 函数中添加:
// 根据顶点属性生成两个三角面片顶点位置 -- 共6个顶点
points.push_back(vectexPosition[0]);
points.push_back(vectexPosition[2]);
points.push_back(vectexPosition[1]);
points.push_back(vectexPosition[2]);
points.push_back(vectexPosition[3]);
points.push_back(vectexPosition[1]);
// 根据顶点属性生成三角面片的纹理坐标 -- 共6个顶点
texcoords.push_back(vertexTexcoord[0]);
texcoords.push_back(vertexTexcoord[2]);
texcoords.push_back(vertexTexcoord[1]);
texcoords.push_back(vertexTexcoord[2]);
texcoords.push_back(vertexTexcoord[3]);
texcoords.push_back(vertexTexcoord[1]);
如图:
然后我们生成 vbo 对象,我们将数据传递进去:
// 生成vbo对象并且绑定vbo
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 先确定vbo的总数据大小 -- 传NULL指针表示我们暂时不传数据
GLuint dataSize = sizeof(glm::vec3) * points.size() + sizeof(glm::vec2) * texcoords.size();
glBufferData(GL_ARRAY_BUFFER, dataSize, NULL, GL_STATIC_DRAW);
// 传送数据到vbo 分别传递 顶点位置 和 顶点纹理坐标
GLuint pointDataOffset = 0;
GLuint texcoordDataOffset = sizeof(glm::vec3) * points.size();
glBufferSubData(GL_ARRAY_BUFFER, pointDataOffset, sizeof(glm::vec3) * points.size(), &points[0]);
glBufferSubData(GL_ARRAY_BUFFER, texcoordDataOffset, sizeof(glm::vec2) * texcoords.size(), &texcoords[0]);
然后我们生成 vao 对象,指定这些参数该如何读取。这部分在之前 OpenGL学习(二)渲染流水线与三角形绘制 已经细🔒过了,这里直接粘贴代码:
这里我们只需要传递顶点位置和顶点纹理坐标,他们分别对应着色器变量 vPositon
和 vTexture
// 生成vao对象并且绑定vao
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 生成着色器程序对象
std::string fshaderPath = "shaders/fshader.fsh";
std::string vshaderPath = "shaders/vshader.vsh";
program = getShaderProgram(fshaderPath, vshaderPath);
glUseProgram(program); // 使用着色器
// 建立顶点变量vPosition在着色器中的索引 同时指定vPosition变量的数据解析格式
GLuint vlocation = glGetAttribLocation(program, "vPosition"); // vPosition变量的位置索引
glEnableVertexAttribArray(vlocation);
glVertexAttribPointer(vlocation, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0); // vao指定vPosition变量的数据解析格式
// 建立颜色变量vTexcoord在着色器中的索引 同时指定vTexcoord变量的数据解析格式
GLuint tlocation = glGetAttribLocation(program, "vTexcoord"); // vTexcoord变量的位置索引
glEnableVertexAttribArray(tlocation);
glVertexAttribPointer(tlocation, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(sizeof(glm::vec3) * points.size())); // 注意指定offset参数
生成纹理
和大多数 OpenGL 对象一样,纹理对应也是通过引用来创建的。我们调用 glGenxxx 函数就行了。我们创建一个纹理对象,并且绑定它,这意味着之后所有的纹理操作都会执行在其上面:
// 生成纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
随后我们设置纹理的一些参数:
// 参数设置 -- 过滤方式与越界规则
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
这些参数决定了纹理是如何取值的,比如线性过滤。此外,还决定了一个纹理坐标超出返回,该如何取纹理图像的像素:
然后我们利用 SOIL 库读取图片,并且利用 glTexImage2D 生成一张纹理。我们读取路径 textures/wall.png
下的一张图片:
// 读取图片纹理
int textureWidth, textureHeight;
unsigned char* image = SOIL_load_image("textures/wall.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB