OpenGL學習(六)紋理與obj格式模型的讀取

前言

上一篇博客回顧: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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章