OpenGL学习(六)纹理与obj格式模型的读取

本文介绍了如何在OpenGL中进行纹理映射,通过读取图像生成纹理并将其贴在简单的正方形上。接着讲解了如何读取obj格式的模型文件,以及如何在OpenGL中渲染一张带有纹理的桌子。内容包括纹理坐标、映射到正方形、读取图像、生成纹理、着色器应用等,并提供了相关代码示例。
摘要由CSDN通过智能技术生成

前言

上一篇博客回顾: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 详情请见【完整代码】部分

为了为正方形贴上图片,我们需要确定两个顶点属性:

  1. 正方形顶点位置
  2. 正方形顶点的纹理坐标

故,我们添加如下的顶点数据:

// 手动指定正方形的 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学习(二)渲染流水线与三角形绘制 已经细🔒过了,这里直接粘贴代码:

这里我们只需要传递顶点位置和顶点纹理坐标,他们分别对应着色器变量 vPositonvTexture

// 生成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
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值