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

不羁的心 提交于 2020-12-12 13:11:17

前言

上一篇博客回顾: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, GL_UNSIGNED_BYTE, image);   // 生成纹理

参数很多,但是前人已经帮我们总结好了 引自【learn OpenGL】:

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

至此,我们离绘制纹理还差最后一步,我们需要在着色器中,根据

着色器贴纹理

在这之后,我们的着色器需要接收两个参数,即顶点位置和顶点纹理坐标。我们编写顶点着色器(因为基于上一篇博客的代码,我们还行要接收 m,v,p 矩阵以完成投影变换)。

下面是顶点着色器的代码:

#version 330 core

in vec3 vPosition;  // cpu传入的顶点坐标
in vec2 vTexcoord;     // cpu传入的顶点纹理坐标

out vec2 texcoord;   // 传顶点纹理坐标给片元着色器

uniform mat4 model; // 模型变换矩阵
uniform mat4 view;      // 模型变换矩阵
uniform mat4 projection;    // 模型变换矩阵

void main()
{
   
   
    gl_Position = projection * view * model * vec4(vPosition, 1.0); // 指定ndc坐标
    texcoord = vTexcoord;   // 传递纹理坐标到片段着色器
}

片段着色器则直接接收来自顶点着色器的纹理坐标,这个坐标是线性插值过后的,于是我们直接用它来访问纹理。我们调用 texture2D 函数即可完成对 2D 图像纹理的访问。

下面是片元着色器的代码:

#version 330 core

in vec3 vColorOut;  // 顶点着色器传递的颜色
in vec2 texcoord;    // 纹理坐标

out vec4 fColor;    // 片元输出像素的颜色

uniform sampler2D Texture;  // 纹理图片

void main()
{
   
   
    fColor.rgb =  texture2D(Texture, texcoord.st).rgb;
}

值得注意的是,我们并没有指定纹理图像变量(就是那个 sampler2D 变量)的名字。事实上在不涉及多纹理的时候,我们通过 glBindTexture 直接绑定后,就可以在着色器中,以任意变量名,对该纹理进行访问。

一切即将就绪。重新加载程序之后我们可以看到一张贴了纹理的正方形:
在这里插入图片描述

我们绘制的四方形,纹理坐标刚好涵盖图像的四个顶点,这意味着我们实现了在三维空间(的地板上)显示一张图片!事实上你可以换成任意你喜欢的图片:

在这里插入图片描述

上面那张罗恩的 p 站 id 是:85397623

读取obj文件

正方形是一个易于理解的模型。对于正方形的纹理,我们像盖被子一样,将图片的四个顶点给予正方形的顶点纹理坐标即可。

但是对于一些复杂的模型,我们无法建立有效的数学表达式,纹理图像的纹理坐标赋给顶点。于是我们有一种名叫 obj 的模型格式,该格式指定了模型的一些顶点属性,包括:

  1. 顶点位置
  2. 顶点纹理坐标
  3. 顶点法向量

于是,我们通过阅读 obj 格式的模型,就可以确定模型顶点的纹理坐标了!

obj文件格式

和 off 文件格式类似,obj 文件格式也是一种文本。obj 文件的每一行以一个 type 字符串开头,其中不同的 type 字符串,阐述了该行所表达的信息类型:

type名称 该行数据格式 解释
v 0.114 0.514 0.191 三个以空格分隔的浮点数 表示该顶点的位置
vt 0.114 0.514 0.191 三个以空格分隔的浮点数 表示该顶点纹理坐标
vn 0.114 0.514 0.191 三个以空格分隔的浮点数 表示该顶点的法向量方向
f 1/1/1 2/2/2 3/3/3 三组(或者四组)以斜杠分隔的整数
表示该面片第 i 个顶点的 位置索引/纹理坐标索引/法向量索引
# 注释 zsbd

注:还有其他的 type,只是我们暂时用不到。今天先读取顶点位置和纹理坐标

下面给出 obj 模型的文本示例:

v  0.4366 -0.3235 0.0973

# ...
vn -0.0018 0.0043 -1.0000

# ...
vt 0.1159 0.3127 0.0000

# ...
f 1/1/1 2/2/2 3/3/3 

注:因为纹理坐标(vt)一般为 2D 图片的坐标,其第三个数都是 0 所以我们一般只读取前两个数字即可

编写readObj函数进行读取

读取 obj 文件也很简单。首先我们遍历文件:

  1. 对于以 v,vt,vn 开头的顶点属性,我们直接存储。我们利用三个数组,分别是 vertexPosition,vertexTexcoord,vertexNormal 来存储。
  2. 对于以 f 开头的面片信息,我们也用三个数组存储他们的索引,分别是 positonIndex,texcoordIndex,normalIndex
  3. 读取完文件之后,遍历 2 中的索引数组,根据索引去 1 中的临时数组取数据,并且存储到目的数组(函数形参中给出的数组)

索引和 obj 给出的顶点属性之间的关联是这样的:

在这里插入图片描述

注:我们暂时用不到法线数据,但是我们先读进来,下次博客就会用到

于是我们可以编写一个函数 readObj 来读取这些变量。值得注意的是,所有的索引都是以 1 开始的下标,所以我们要减一。此外,使用 istringstream 解析一行的字符串的时候,要注意读取 f 开头的信息时,数据用斜杠分隔。我们要通过读取一个字符 slash 来消除斜杠。

此外,我们需要额外的头文件,这些都是 std c++ 标准头文件:

#include <fstream>
#include <sstream>
#include <iostream>

下面是 readObj 函数的代码:

/ 读取off文件并且生成最终传递给顶点着色器的 顶点位置 / 顶点纹理坐标 / 顶点法线
void readObj(
    std::string filepath, 
    std::vector<glm::vec3>& points, 
    std::vector<glm::vec2>& texcoords,
    std::vector<glm::vec3>& normals
)
{
   
    
    // 顶点属性
    std::vector<glm::vec3> vectexPosition;
    std::vector<glm::vec2> vertexTexcoord;
    std::vector<glm::vec3> vectexNormal;

    // 面片索引信息
    std::vector<glm::ivec3> positionIndex;
    std::vector<glm::ivec3> texcoordIndex;
    std::vector<glm::ivec3> normalIndex;

    // 打开文件流
    std::ifstream fin(filepath);
    std::string line;
    if (!fin.is_open())
    {
   
    
        std::cout << "文件 " << filepath << " 打开失败" << std::endl;
        exit(-1);
    }

    // 按行读取
    while (std::getline(fin, line))
    {
   
    
        std::istringstream sin(line);   // 以一行的数据作为 string stream 解析并且读取
        std::string type;
        GLfloat x, y, z;
        int v0, vt0, vn0;   // 面片第 1 个顶点的【位置,纹理坐标,法线】索引
        int v1, vt1, vn1;   // 2
        int v2, vt2, vn2;   // 3
        char slash;

        // 读取obj文件
        sin >> type;
        if (type == "v") {
   
    
            sin >> x >> y >> z;
            vectexPosition.push_back(glm::vec3(x, y, z));
        }
        if (type == "vt") {
   
    
            sin >> x >> y;
            vertexTexcoord.push_back(glm::vec2(x, y));
        }
        if (type == "vn") {
   
    
            sin >> x >> y >> z;
            vectexNormal.push_back(glm::vec3(x, y, z));
        }
        if (type == "f") {
   
    
            sin >> v0 >> slash >> vt0 >> slash >> vn0;
            sin >> v1 >> slash >> vt1 >> slash >> vn1;
            sin >> v2 >> slash >> vt2 >> slash >> vn2;
            positionIndex.push_back(glm::ivec3(v0 - 1, v1 - 1, v2 - 1));
            texcoordIndex.push_back(glm::ivec3(vt0 - 1, vt1 - 1, vt2 - 1));
            normalIndex.push_back(glm::ivec3(vn0 - 1, vn1 - 1, vn2 - 1));
        }
    }

    // 根据面片信息生成最终传入顶点着色器的顶点数据
    for (int i = 0; i < positionIndex.size(); i++)
    {
   
    
        // 顶点位置
        points.push_back(vectexPosition[positionIndex[i].x]);
        points.push_back(vectexPosition[positionIndex[i].y]);
        points.push_back(vectexPosition[positionIndex[i].z]);

        // 顶点纹理坐标
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].x]);
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].y]);
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].z]);

        // 顶点法线
        normals.push_back(vectexNormal[normalIndex[i].x]);
        normals.push_back(vectexNormal[normalIndex[i].y]);
        normals.push_back(vectexNormal[normalIndex[i].z]);
    }
}

渲染一张桌子

现在我们知晓了如何读取 obj 格式的文件,我们开始着手渲染一张带纹理的桌子。首先我们准备如下的 obj 文件和他们的贴图,我们放置于 models/obj 目录下:

在这里插入图片描述

然后我们再多准备一个全局变量,用来存储顶点法向量:

在这里插入图片描述

虽然这一篇博客中,我们用不到法向量,但是我们读取 obj 的时候先读上,以后有用。

然后在 init 中,我们删掉刚刚的一大段生成正方形的代码,因为我们要通过 obj 模型自动生成顶点属性了。我们添加一句:

// 读取 obj 文件
readObj("models/obj/table.obj", points, texcoords, normals);

即可。

在这里插入图片描述

随后我们改变读取的纹理图片的路径,我们读取 table.png 即桌子对应的纹理:

在这里插入图片描述

然后在 display 函数中,传递模型变换矩阵之前,我们偷偷让桌子旋转一下:

在这里插入图片描述

其他的改动就没有了。重启代码,我们看到的是 唔。。。一张炸裂的桌子?

出现这个情况的原因,是因为 OpenGL 认为图片的 y 坐标的原点应该在图像底部,而图片的 y 坐标是在图像顶部的。于是我们的纹理坐标反了。。。

一般的图片加载器,都有翻转图片的选项,SOIL ?算了,我们在片段着色器中,手动翻转一下坐标罢(我是懒狗)

我们将片段着色器中,读取纹理的代码:

fColor.rgb =  texture2D(Texture, texcoord.st).rgb;

改为:

fColor.rgb =  texture2D(Texture, vec2(texcoord.s, 1.0 - texcoord.t)).rgb;

注:因为纹理坐标范围 [0, 1] 我们用 1 减去原来的 y 坐标即可实现翻转。

然后再次运行程序,好耶,我们利用 obj 模型生成了一张带纹理的桌子!这意味着我们的程序能够读取标准化的 obj 模型,能够和现代艺术家接轨辣

在这里插入图片描述

完整代码

c++

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <sstream>
#include <iostream>

#include <GL/glew.h>
#include <GL/freeglut.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <SOIL2/SOIL2.h>

std::vector<glm::vec3> points;      // 顶点坐标
std::vector<glm::vec2> texcoords;   // 顶点纹理坐标
std::vector<glm::vec3> normals;     // 顶点法线

GLuint program; // 着色器程序对象  

// 相机参数
glm::vec3 cameraPosition(0, 0, 0);      // 相机位置
glm::vec3 cameraDirection(0, 0, -1);    // 相机视线方向
glm::vec3 cameraUp(0, 1, 0);            // 世界空间下竖直向上向量
float pitch = 0.0f;
float roll = 0.0f;
float yaw = 0.0f;

// 视界体参数
float left = -1, right = 1, bottom = -1, top = 1, zNear = 0.1, zFar = 100.0;

int windowWidth = 512;  // 窗口宽
int windowHeight = 512; // 窗口高

bool keyboardState[1024];   // 键盘状态数组 keyboardState[x]==true 表示按下x键

// --------------- end of global variable definition --------------- //

// 读取文件并且返回一个长字符串表示文件内容
std::string readShaderFile(std::string filepath)
{
   
     
    std::string res, line;
    std::ifstream fin(filepath);
    if (!fin.is_open())
    {
   
     
        std::cout << "文件 " << filepath << " 打开失败" << std::endl;
        exit(-1);
    }
    while (std::getline(fin, line))
    {
   
     
        res += line + '\n';
    }
    fin.close();
    return res;
}

// 获取着色器对象
GLuint getShaderProgram(std::string fshader, std::string vshader)
{
   
     
    // 读取shader源文件
    std::string vSource = readShaderFile(vshader);
    std::string fSource = readShaderFile(fshader);
    const char* vpointer = vSource.c_str();
    const char* fpointer = fSource.c_str();

    // 容错
    GLint success;
    GLchar infoLog[512];

    // 创建并编译顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, (const GLchar**)(&vpointer), NULL);
    glCompileShader(vertexShader);
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);   // 错误检测
    if (!success)
    {
   
     
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "顶点着色器编译错误\n" << infoLog << std::endl;
        exit(-1);
    }

    // 创建并且编译片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, (const GLchar**)(&fpointer), NULL);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);   // 错误检测
    if (!success)
    {
   
     
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "片段着色器编译错误\n" << infoLog << std::endl;
        exit(-1);
    }

    // 链接两个着色器到program对象
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // 删除着色器对象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

// 读取obj文件并且生成最终传递给顶点着色器的 顶点位置 / 顶点纹理坐标 / 顶点法线
void readObj(
    std::string filepath, 
    std::vector<glm::vec3>& points, 
    std::vector<glm::vec2>& texcoords,
    std::vector<glm::vec3>& normals
)
{
   
     
    // 顶点属性
    std::vector<glm::vec3> vectexPosition;
    std::vector<glm::vec2> vertexTexcoord;
    std::vector<glm::vec3> vectexNormal;

    // 面片索引信息
    std::vector<glm::ivec3> positionIndex;
    std::vector<glm::ivec3> texcoordIndex;
    std::vector<glm::ivec3> normalIndex;

    // 打开文件流
    std::ifstream fin(filepath);
    std::string line;
    if (!fin.is_open())
    {
   
     
        std::cout << "文件 " << filepath << " 打开失败" << std::endl;
        exit(-1);
    }

    // 按行读取
    while (std::getline(fin, line))
    {
   
     
        std::istringstream sin(line);   // 以一行的数据作为 string stream 解析并且读取
        std::string type;
        GLfloat x, y, z;
        int v0, vt0, vn0;   // 面片第 1 个顶点的【位置,纹理坐标,法线】索引
        int v1, vt1, vn1;   // 2
        int v2, vt2, vn2;   // 3
        char slash;

        // 读取obj文件
        sin >> type;
        if (type == "v") {
   
     
            sin >> x >> y >> z;
            vectexPosition.push_back(glm::vec3(x, y, z));
        }
        if (type == "vt") {
   
     
            sin >> x >> y;
            vertexTexcoord.push_back(glm::vec2(x, y));
        }
        if (type == "vn") {
   
     
            sin >> x >> y >> z;
            vectexNormal.push_back(glm::vec3(x, y, z));
        }
        if (type == "f") {
   
     
            sin >> v0 >> slash >> vt0 >> slash >> vn0;
            sin >> v1 >> slash >> vt1 >> slash >> vn1;
            sin >> v2 >> slash >> vt2 >> slash >> vn2;
            positionIndex.push_back(glm::ivec3(v0 - 1, v1 - 1, v2 - 1));
            texcoordIndex.push_back(glm::ivec3(vt0 - 1, vt1 - 1, vt2 - 1));
            normalIndex.push_back(glm::ivec3(vn0 - 1, vn1 - 1, vn2 - 1));
        }
    }

    // 根据面片信息生成最终传入顶点着色器的顶点数据
    for (int i = 0; i < positionIndex.size(); i++)
    {
   
     
        // 顶点位置
        points.push_back(vectexPosition[positionIndex[i].x]);
        points.push_back(vectexPosition[positionIndex[i].y]);
        points.push_back(vectexPosition[positionIndex[i].z]);

        // 顶点纹理坐标
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].x]);
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].y]);
        texcoords.push_back(vertexTexcoord[texcoordIndex[i].z]);

        // 顶点法线
        normals.push_back(vectexNormal[normalIndex[i].x]);
        normals.push_back(vectexNormal[normalIndex[i].y]);
        normals.push_back(vectexNormal[normalIndex[i].z]);
    }
}

// 初始化
void 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)
    };
    // 根据顶点属性生成两个三角面片顶点位置 -- 共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]);
    */

    // 读取 obj 文件
    readObj("models/obj/table.obj", points, texcoords, normals);

    // ---------------------------------------------------------------------//

    // 生成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对象并且绑定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参数

    // 生成纹理
    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);
    // 读取图片纹理
    int textureWidth, textureHeight;
    unsigned char* image = SOIL_load_image("models/obj/table.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);   // 生成纹理

    glEnable(GL_DEPTH_TEST);  // 开启深度测试

    glClearColor(0.0, 0.0, 0.0, 1.0);   // 背景颜色 -- 黑
}

// 鼠标滚轮函数
void mouseWheel(int wheel, int direction, int x, int y)
{
   
     
    // zFar += 1 * direction * 0.1;
    glutPostRedisplay();    // 重绘
}

// 鼠标运动函数
void mouse(int x, int y)
{
   
     
    // 调整旋转
    yaw += 35 * (x - float(windowWidth) / 2.0) / windowWidth;
    yaw = glm::mod(yaw + 180.0f, 360.0f) - 180.0f;    // 取模范围 -180 ~ 180

    pitch += -35 * (y - float(windowHeight) / 2.0) / windowHeight;
    pitch = glm::clamp(pitch, -89.0f, 89.0f);

    glutWarpPointer(windowWidth / 2.0, windowHeight / 2.0);
    glutPostRedisplay();    // 重绘
}

// 键盘回调函数
void keyboardDown(unsigned char key, int x, int y)
{
   
     
    keyboardState[key] = true;
}
void keyboardDownSpecial(int key, int x, int y)
{
   
     
    keyboardState[key] = true;
}
void keyboardUp(unsigned char key, int x, int y)
{
   
     
    keyboardState[key] = false;
}
void keyboardUpSpecial(int key, int x, int y)
{
   
     
    keyboardState[key] = false;
}
// 根据键盘状态判断移动
void move()
{
   
     
    if (keyboardState['w']) cameraPosition += 0.0005f * cameraDirection;
    if (keyboardState['s']) cameraPosition -= 0.0005f * cameraDirection;
    if (keyboardState['a']) cameraPosition -= 0.0005f * glm::normalize(glm::cross(cameraDirection, cameraUp));
    if (keyboardState['d']) cameraPosition += 0.0005f * glm::normalize(glm::cross(cameraDirection, cameraUp));
    if (keyboardState[GLUT_KEY_CTRL_L]) cameraPosition.y -= 0.0005;
    if (keyboardState[' ']) cameraPosition.y += 0.0005;
    glutPostRedisplay();    // 重绘
}

// 显示回调函数
void display()
{
   
     
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);       // 清空窗口颜色缓存

    // 模型变换矩阵
    glm::mat4 model(    // 单位矩阵
        glm::vec4(1, 0, 0, 0),
        glm::vec4(0, 1, 0, 0),
        glm::vec4(0, 0, 1, 0),
        glm::vec4(0, 0, 0, 1)
    );
    model = glm::rotate(model, glm::radians(-90.0f), glm::vec3(1, 0, 0));   // 绕 x 轴转90度
    GLuint mlocation = glGetUniformLocation(program, "model");    // 名为model的uniform变量的位置索引
    glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model));   // 列优先矩阵

    // 视图矩阵 -- 世界坐标转相机坐标
    move(); // 移动控制 -- 控制相机位置

    // 计算欧拉角以确定相机朝向
    cameraDirection.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
    cameraDirection.y = sin(glm::radians(pitch));
    cameraDirection.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 相机看向z轴负方向

    // 传视图矩阵
    glm::mat4 view = glm::lookAt(cameraPosition, cameraPosition + cameraDirection, cameraUp);
    GLuint vlocation = glGetUniformLocation(program, "view");
    glUniformMatrix4fv(vlocation, 1, GL_FALSE, glm::value_ptr(view));

    // 传投影矩阵
    glm::mat4 projection = glm::perspective(glm::radians(70.0f), (GLfloat)windowWidth / (GLfloat)windowHeight, zNear, zFar);
    GLuint plocation = glGetUniformLocation(program, "projection");
    glUniformMatrix4fv(plocation, 1, GL_FALSE, glm::value_ptr(projection));

    glDrawArrays(GL_TRIANGLES, 0, points.size());   // 绘制n个点

    glutSwapBuffers();                  // 交换缓冲区
}

int main(int argc, char** argv)
{
   
     
    glutInit(&argc, argv);              // glut初始化
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(windowWidth, windowHeight);// 窗口大小
    glutCreateWindow("5 - texture"); // 创建OpenGL上下文

#ifdef __APPLE__
#else
    glewInit();
#endif

    init();

    // 绑定鼠标移动函数 -- 
    //glutMotionFunc(mouse);  // 左键按下并且移动
    glutPassiveMotionFunc(mouse);   // 鼠标直接移动
    //glutMouseWheelFunc(mouseWheel); // 滚轮缩放

    // 绑定键盘函数
    glutKeyboardFunc(keyboardDown);
    glutSpecialFunc(keyboardDownSpecial);
    glutKeyboardUpFunc(keyboardUp);
    glutSpecialUpFunc(keyboardUpSpecial);

    glutDisplayFunc(display);           // 设置显示回调函数 -- 每帧执行
    glutMainLoop();                     // 进入主循环

    return 0;
}

顶点着色器

#version 330 core

in vec3 vPosition;  // cpu传入的顶点坐标
in vec2 vTexcoord;     // cpu传入的顶点纹理坐标

out vec2 texcoord;   // 传顶点纹理坐标给片元着色器

uniform mat4 model; // 模型变换矩阵
uniform mat4 view;      // 模型变换矩阵
uniform mat4 projection;    // 模型变换矩阵

void main()
{
   
     
    gl_Position = projection * view * model * vec4(vPosition, 1.0); // 指定ndc坐标
    texcoord = vTexcoord;   // 传递纹理坐标到片段着色器
}

片元着色器

#version 330 core

in vec3 vColorOut;  // 顶点着色器传递的颜色
in vec2 texcoord;    // 纹理坐标

out vec4 fColor;    // 片元输出像素的颜色

uniform sampler2D Texture;  // 纹理图片

void main()
{
   
     
    //fColor.rgb =  texture2D(Texture, texcoord.st).rgb;
    fColor.rgb =  texture2D(Texture, vec2(texcoord.s, 1.0 - texcoord.t)).rgb;
    //fColor.rgb = vec3(1, 0, 0);
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!