目錄
前言
上一篇博客回顧: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);
}