目錄
- 前言
- 紋理映射
- 紋理坐標
- 映射到簡單正方形
- 讀取影像
- 生成正方形資料
- 生成紋理
- 著色器貼紋理
- 讀取obj檔案
- obj檔案格式
- 撰寫readObj函式進行讀取
- 渲染一張桌子
- 完整代碼
- c++
- 頂點著色器
- 片元著色器
前言
上一篇博客回顧: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, 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 的模型格式,該格式指定了模型的一些頂點屬性,包括:
- 頂點位置
- 頂點紋理坐標
- 頂點法向量
于是,我們通過閱讀 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 檔案也很簡單,首先我們遍歷檔案:
- 對于以 v,vt,vn 開頭的頂點屬性,我們直接存盤,我們利用三個陣列,分別是 vertexPosition,vertexTexcoord,vertexNormal 來存盤,
- 對于以 f 開頭的面片資訊,我們也用三個陣列存盤他們的索引,分別是 positonIndex,texcoordIndex,normalIndex
- 讀取完檔案之后,遍歷 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);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/233068.html
標籤:其他

