本文主要解決一個(gè)問題:
如何使用法線貼圖給物體添加更多的細(xì)節(jié)?
引言
學(xué)了這么多技巧,也能顯示非常酷炫的畫面,是不是覺得自己已經(jīng)非常強(qiáng),有點(diǎn)飄飄然了?哈哈,還差的遠(yuǎn)呢。不信?來看下面一張對比圖就知道了:
左圖明顯比右圖感覺更真實(shí),細(xì)節(jié)更多,更帶感。你也許會(huì)這樣想:這兩張紋理貼圖不一樣,這是美術(shù)的事情。那你可就錯(cuò)了,這兩張圖用的是同一張紋理。當(dāng)然也不是左邊的模型更加精細(xì),因?yàn)槲艺也坏竭@么精細(xì)的模型,而且自己也不會(huì)做。那么,為什么兩張圖的差距這么大呢?
答案是:左邊的場景在渲染時(shí)用了法線貼圖。
酷不酷,贊不贊, 想不想學(xué)?別急,我們慢慢來。
法線貼圖
原理
法線貼圖,顧名思義,就是將每一個(gè)片元(像素)的法線值保存成一張圖片。法向量的xyz坐標(biāo)對應(yīng)到圖的rgb值。這樣會(huì)產(chǎn)生了一個(gè)問題,就是我們的法線xyz的取值范圍是[-1,1],而rgb值的取值范圍[0,1],我們該怎么樣把xyz值轉(zhuǎn)換成rgb值呢?其實(shí),這也很簡單,只要將xyz值加1,然后除以2就可以了。翻譯成方程式就是:rgb = (xyz + 1) / 2。下面我們就來看看法線貼圖長什么樣:
怎么藍(lán)不拉嘰的?別急,這個(gè)問題會(huì)在下面解答。
解決了轉(zhuǎn)換成rgb值的問題,我們還有一個(gè)非常重要的問題要解決。思考一下,物體的法線在世界空間的不同位置是不同的,我們?nèi)绾尾拍苡靡粡垐D來表示物體的法線,使它能夠在所有的情況下都適用呢?仔細(xì)想想,法線貼圖是貼在物體表面的,我在一個(gè)固定的坐標(biāo)系中生成法線貼圖,再通過一個(gè)轉(zhuǎn)換矩陣將法線轉(zhuǎn)換到世界空間中,這不就行了嗎?
思路沒錯(cuò),關(guān)鍵是如何計(jì)算出這個(gè)轉(zhuǎn)換矩陣。首先介紹一下,我們生成法線貼圖的坐標(biāo)系稱作TBN坐標(biāo)系,坐標(biāo)系的三個(gè)軸是t軸,b軸和n軸,對應(yīng)我們熟悉的x軸,y軸和z軸。之所以把這個(gè)坐標(biāo)系稱為TBN坐標(biāo)系,是因?yàn)閚軸表示的是圖元三角形表面法向量。有了法向量之后,我們還需要兩個(gè)軸來確定這個(gè)坐標(biāo)系。這兩個(gè)軸是切線軸(tangent )和副切線軸(bitangent ),這三個(gè)軸一起組成的坐標(biāo)系就是TBN坐標(biāo)系。理論上,T軸可以是圖元平面上的任意軸,B軸只需要垂直T軸和N軸就可以了。但是,TBN坐標(biāo)系不是隨隨便便弄出來玩的,它的存在價(jià)值就是簡化計(jì)算法向量的步驟。所以,我們通常采用的是和表面紋理坐標(biāo)一致的軸作為T軸和B軸,即U軸對應(yīng)T軸,V軸對應(yīng)B軸,這樣,我們就可以用UV值來計(jì)算轉(zhuǎn)換矩陣了。
注意TBN坐標(biāo)系的存在意義就是簡化計(jì)算變換矩陣的,所以我們的TBN軸就必須按照約定俗成的規(guī)范來
上圖是法線貼圖在TBN坐標(biāo)系中的狀態(tài)。
接下來,我們就要來計(jì)算在模型空間中的向量T和向量B了。來看下面一張?jiān)韴D,我們從這張?jiān)韴D上推導(dǎo)出計(jì)算的公式:
假設(shè)我們的向量T和B都是單位向量,根據(jù)向量共面定理,我們可以列出一個(gè)方程式:
介紹一下向量共面定理。
如果e1和e2是同一個(gè)平面內(nèi)兩個(gè)不共線向量,那么對于平面內(nèi)的任一向量a,有且只有一對實(shí)數(shù)(x,y)使得a = x*e1 + y * e2。
在這里,我們的T和B是互相垂直的向量,滿足不共線前提,E1和E2也是在TB平面內(nèi),其平面坐標(biāo)我們也知道,所以可以列出上面的等式,這是最重要的一個(gè)等式。
在模型空間中,我們可以把這個(gè)等式轉(zhuǎn)換成下面的形式:
其中,(delta)U1表示(U1-U0),(delta)V1表示(V1-V0)依次類推。這樣我們就非常容易地將這個(gè)等式轉(zhuǎn)換成矩陣的形式:
要計(jì)算TB的值,我們只需要在等式兩邊乘以UV矩陣的逆矩陣就行了:
逆矩陣的計(jì)算方法我們不用去考究,直接使用下面的最終計(jì)算方程式就行:
有了這個(gè)方程式,我們就可以計(jì)算轉(zhuǎn)換矩陣了。
講點(diǎn)小歷史知識(shí):
在以前,法線貼圖需要用高精度模型通過特定的算法計(jì)算出來,非常的不方便。后來改進(jìn)之后才有了我們現(xiàn)在的貼圖與模型分離的方法。應(yīng)用上,PS2不支持法線貼圖,XBox360以及之后的主機(jī)都支持法線貼圖,這已經(jīng)成為了一種標(biāo)配。
為什么是藍(lán)兮兮的?
你肯定已經(jīng)在這個(gè)這問題上糾結(jié)了很久,為什么法線貼圖是這樣藍(lán)兮兮的?是不是所有的法線貼圖都這樣,還是只有這張是這樣?
這里我可以負(fù)責(zé)任地告訴你,所有的法線貼圖都是這樣藍(lán)兮兮的。原因很好理解,因?yàn)槲覀冑N圖保存的是切線空間中的法向量值。在切線空間中,法向量的n坐標(biāo)始終指向+n的方向,它的值就在[0,1]范圍內(nèi),而t坐標(biāo)和b坐標(biāo)的范圍是[-1,1]。這樣,將tbn三個(gè)坐標(biāo)值映射到貼圖的rgb值(通常范圍是[0,255])時(shí),貼圖中的blue分量就會(huì)比較大,從而造成了整張圖看上去藍(lán)兮兮的效果。
優(yōu)缺點(diǎn)
法線貼圖的優(yōu)點(diǎn),是我們可以用一個(gè)低精度模型表現(xiàn)出非常高的細(xì)節(jié),看起來像高精度模型那樣。來看下面這張圖:
只需要500個(gè)三角形的簡單網(wǎng)格加上法線貼圖就能有媲美4M個(gè)三角形的精細(xì)網(wǎng)格模型的效果,不得不說法線貼圖的優(yōu)勢巨大,處理4M個(gè)三角形可不是500個(gè)三角形那么簡單的事情。
當(dāng)然法線貼圖也不是萬能的,它也有它的缺點(diǎn)。因?yàn)樗皇歉淖兞宋矬w表面的光照計(jì)算方式,所以它不適合用在凹凸起伏較大的物體上,這些物體會(huì)有遮擋的效果,這是法線貼圖無法實(shí)現(xiàn)的。另外,使用法線貼圖的物體經(jīng)不起特寫放大操作,如果攝像機(jī)離的很近,或者攝像機(jī)的角度刁鉆,很容易就會(huì)穿幫。
實(shí)現(xiàn)
理解原理后,自然就到了實(shí)現(xiàn)的過程。基本上,我們最容易想到的方法有兩種:
其一、將法線通過TBN矩陣變換后,在世界坐標(biāo)空間中計(jì)算光照效果。
其二、將光源、視點(diǎn)、片元的位置經(jīng)過TBN矩陣的逆矩陣變換后,在TBN空間中計(jì)算光照效果。
兩種方法都可行,我們都會(huì)進(jìn)行嘗試。不過要先來看看如何計(jì)算T和B向量。
要計(jì)算T和B向量,我們首先要知道三角形的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)。如果我們要顯示上面的那一面磚墻,我們就需要4個(gè)頂點(diǎn)坐標(biāo)和4個(gè)紋理坐標(biāo),外加一個(gè)法向量,先來定義這些數(shù)據(jù):
//位置
glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
//紋理坐標(biāo)
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
//法向量
glm::vec3 nm(0.0, 0.0, 1.0);
接著,觀察公式,計(jì)算公式中要用到的數(shù)據(jù),這些數(shù)據(jù)包括E1,E2,deltaU1,deltaV1,deltaU2,deltaV2:
glm::vec3 e1 = pos2 - pos1;
glm::vec3 e2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;
然后,計(jì)算前面的分?jǐn)?shù)系數(shù),跟著公式走,我們的代碼出來了:
float coefficient = 1 / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
最后,計(jì)算出T向量和B向量。向量的乘法還記得嗎?用前一個(gè)矩陣的一行去乘上后一個(gè)矩陣的每一列,得到最終矩陣上一行的元素值,以此類推:
glm::vec3 tangent1, bitangent1;
tangent1.x = (deltaUV2.y * e1.x - deltaUV1.y * e2.x) * coefficient;
tangent1.y = (deltaUV2.y * e1.y - deltaUV1.y * e2.y) * coefficient;
tangent1.z = (deltaUV2.y * e1.z - deltaUV1.y * e2.z) * coefficient;
tangent1 = glm::normalize(tangent1);
bitangent1.x = (-deltaUV2.x * e1.x + deltaUV1.x * e2.x) * coefficient;
bitangent1.y = (-deltaUV2.x * e1.y + deltaUV1.x * e2.y) * coefficient;
bitangent1.z = (-deltaUV2.x * e1.z + deltaUV1.x * e2.z) * coefficient;
bitangent1 = glm::normalize(bitangent1);
記得最后要把向量規(guī)范化成單位向量。這樣,第一個(gè)三角形的TB向量就計(jì)算好了,第二個(gè)三角形就留給讀者自己完成吧。
將兩個(gè)三角形的TB向量都算完之后,我們就可以把這些數(shù)據(jù)統(tǒng)統(tǒng)放到頂點(diǎn)結(jié)構(gòu)中傳遞給著色器了:
float quadVertices[] = {
// 位置 // 法線 // 紋理坐標(biāo) // 切線 // 副切線
pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
pos2.x, pos2.y, pos2.z, nm.x, nm.y, nm.z, uv2.x, uv2.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
pos4.x, pos4.y, pos4.z, nm.x, nm.y, nm.z, uv4.x, uv4.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z
};
...
//把T向量和B向量都傳遞給著色器
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(8 * sizeof(float)));
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(11 * sizeof(float)));
這樣,在主流程中的工作就完成了,接下來就是著色器的工作了,我們先來看第一種方法。
方法一:切線空間->世界空間
在頂點(diǎn)著色器中,我們需要把T向量和B向量都接收進(jìn)來,所以在著色器開頭要加上這兩行代碼:
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;
然后,計(jì)算得到的TBN矩陣(就是上面說的轉(zhuǎn)換矩陣)要傳遞給片元著色器,因此,我們要在輸出塊中加入一個(gè)TBN矩陣作為輸出:
out VS_OUT {
vec3 FragPos;
vec2 TexCoords;
mat3 TBN;
} vs_out;
然后計(jì)算TBN矩陣:
void main (){
vs_out.TexCoords = aTexCoords;
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * aTangent);
vec3 B = normalize(normalMatrix * aBitangent);
vec3 N = normalize(normalMatrix * aNormal);
vs_out.TBN = mat3 (T,B,N);
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
由于我們的TBN都是模型空間中的坐標(biāo),在轉(zhuǎn)換到世界空間中時(shí)需要乘上"模型變換矩陣"(這里的變換矩陣當(dāng)然是要和之前一樣的),當(dāng)然,TBN向量都需要規(guī)范化一下才能避免出錯(cuò)。組成TBN矩陣的方式十分簡單,直接調(diào)用mat3(T,B,N)就行了。
完成頂點(diǎn)著色器的計(jì)算后,片元著色器中就能直接使用頂點(diǎn)著色器的計(jì)算結(jié)果了:
//輸入TBN矩陣
in VS_OUT {
vec3 FragPos;
vec2 TexCoords;
mat3 TBN;
} fs_in;
...
void main()
{
// 采樣法線貼圖
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
// 轉(zhuǎn)換法向量到[-1,1]范圍
normal = normalize(normal * 2.0 - 1.0); // 此向量是切線空間中的向量
normal = normalize(fs_in.TBN * normal);
// 采樣漫反射
vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
// 環(huán)境光
vec3 ambient = 0.1 * color;
// 漫反射
vec3 lightDir = normalize(lightPos - fs_in.FragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// 鏡面高光
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
vec3 specular = vec3(0.2) * spec;
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
獲取法向量的方式和采樣顏色值一樣,采樣完成后,需要將值轉(zhuǎn)換回[-1,1]的區(qū)間內(nèi),然后,用TBN矩陣乘上法向量再規(guī)范化,我們要的世界空間中的法向量就橫空出世了。其余的部分是Blinn-Phong光照模型的實(shí)現(xiàn)代碼,如果你看過我前面的文章,肯定很熟悉這套代碼了。
再寫一些邊邊角角的代碼,讓模型動(dòng)起來方便我們觀察效果。將模型在光源的位置也顯示出來,這樣我們就能知道光照效果對不對了:
model = glm::rotate(model, currentRadians, glm::normalize(glm::vec3(1.0, 0.0, 1.0))); // 旋轉(zhuǎn)平面觀察不同角度的效果
...
//在光源位置顯示一個(gè)小磚墻
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.1f));
method1shader.setMat4("model", glm::value_ptr(model));
renderQuad();
好,完成之后我們就可以看看效果了:
8錯(cuò)8錯(cuò),是我們要的效果。
方法二:世界空間->切線空間
使用這個(gè)方法,我們不需要將TBN矩陣傳遞給片元著色器了,而是要在頂點(diǎn)著色器中,將光源位置、視點(diǎn)位置、片元位置通過TBN矩陣的逆矩陣轉(zhuǎn)換好之后,將轉(zhuǎn)換過后的坐標(biāo)傳遞給片元著色器讓它使用。
好了,動(dòng)手!新建一個(gè)頂點(diǎn)著色器和片元著色器,取名method2Shader.vs和method2Shader.fs,將方法一中的代碼都復(fù)制過來,我們在它的基礎(chǔ)上-改。首先,去掉VS_OUT塊中的TBN矩陣,加入我們計(jì)算好的光源、視點(diǎn)和片元位置:
out VS_OUT {
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} vs_out;
然后,在代碼中,我們計(jì)算出的TBN矩陣要進(jìn)行一下轉(zhuǎn)置計(jì)算(這里的矩陣比較特殊,轉(zhuǎn)置操作就是取當(dāng)前矩陣的逆矩陣),用這個(gè)轉(zhuǎn)置矩陣來算出光源、視點(diǎn)和片元的位置。
void main()
{
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
vs_out.TexCoords = aTexCoords;
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * aTangent);
vec3 N = normalize(normalMatrix * aNormal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
mat3 TBN = transpose(mat3(T, B, N));
vs_out.TangentLightPos = TBN * lightPos;
vs_out.TangentViewPos = TBN * viewPos;
vs_out.TangentFragPos = TBN * vs_out.FragPos;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
可以看到,我們其實(shí)并不需要將T和B向量都傳遞進(jìn)來,有了T和N向量之后,我們完全可以通過叉乘來計(jì)算出B向量。
在大量的計(jì)算過程中,TBN向量可能會(huì)變得不兩兩垂直,這就會(huì)導(dǎo)致我們的模型有瑕疵。因此,一種名叫Gram-Schmidt正交化的方法就被創(chuàng)造出來。通過一點(diǎn)很小的代價(jià),讓TBN向量繼續(xù)兩兩垂直,這樣,我們的模型顯示就完美無瑕了。上面的代碼中,T = normalize(T - dot(T, N) * N);就是正交化過程。
片元著色器中,我們將原本用來接收的結(jié)構(gòu)改成頂點(diǎn)著色器傳過來的結(jié)構(gòu):
in VS_OUT {
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} fs_in;
計(jì)算光照時(shí),去掉用TBN矩陣轉(zhuǎn)換法向量的過程,只將采樣的法向量轉(zhuǎn)換回[-1,1]空間就行了。然后,在計(jì)算光照的時(shí)候,必須使用從頂點(diǎn)傳過來的光源、片元和視點(diǎn)位置,我們的代碼就成了這個(gè)樣子:
void main()
{
// 采樣法線貼圖
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
// 轉(zhuǎn)換法向量到[-1,1]范圍
normal = normalize(normal * 2.0 - 1.0); // 此向量是切線空間中的向量
// 采樣漫反射
vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
// 環(huán)境光
vec3 ambient = 0.1 * color;
// 漫反射
vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// 鏡面高光
vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
vec3 reflectDir = reflect(-lightDir, normal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
vec3 specular = vec3(0.2) * spec;
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
再修改一下主函數(shù)的代碼,編譯運(yùn)行,你看到的結(jié)果應(yīng)該和上面一樣。
添加一些控制功能
每次總是運(yùn)行一個(gè)著色器文件,完全沒法看出對比效果是不是?不要急,我們這就來添加一些控制的功能。我們想要的功能有兩個(gè):1、在方法一、方法二和使用法線貼圖的著色器之間切換。2、暫停旋轉(zhuǎn),方便切換比較。
切換功能
實(shí)現(xiàn)切換功能很容易,在全局空間中添加一個(gè)渲染類型變量,初始值設(shè)置成1,表示方法1
int renderType = 1; //繪制方式:1、不使用法線貼圖;2、切線空間->世界空間;3、世界空間->切線空間
然后,在鍵盤控制的處理函數(shù)中,添加如下的處理代碼:
if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS)
renderType = 1;
if (glfwGetKey(window, GLFW_KEY_2) == GLFW_PRESS)
renderType = 2;
if (glfwGetKey(window, GLFW_KEY_3) == GLFW_PRESS)
renderType = 3;
當(dāng)我們按下1的時(shí)候,使用方法1的著色器;按下2時(shí),使用方法2著色器;按下3時(shí),使用不進(jìn)行法線貼圖計(jì)算的著色器。在渲染循環(huán)中,我們也要相應(yīng)地添加一些設(shè)置代碼:
if (renderType == 1) {
method1shader.use();
method1shader.setMat4("projection", glm::value_ptr(projection));
method1shader.setMat4("view", glm::value_ptr(view));
}
else if (renderType == 2) {
method2shader.use();
method2shader.setMat4("projection", glm::value_ptr(projection));
method2shader.setMat4("view", glm::value_ptr(view));
}
else {
shaderWithoutNormalMap.use();
shaderWithoutNormalMap.setMat4("projection", glm::value_ptr(projection));
shaderWithoutNormalMap.setMat4("view", glm::value_ptr(view));
}
//還有設(shè)置模型坐標(biāo),光源位置,視點(diǎn)位置的工作請自行完成。
完成之后,我們就可以在3種方式中之間隨意切換了。
暫停功能
暫停功能的實(shí)現(xiàn)更加簡單。第一步先在全局變量中添加暫停變量以及當(dāng)前角度變量(用來保存當(dāng)前旋轉(zhuǎn)到哪個(gè)角度):
bool isPause = false; //是否暫停
float currentRadians = 0.0f; //當(dāng)前旋轉(zhuǎn)角度
緊接著,在處理按鈕的地方添加處理。當(dāng)我們按下z鍵時(shí),暫停旋轉(zhuǎn),按下x鍵時(shí),繼續(xù)旋轉(zhuǎn):
if (glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS)
isPause = true;
if (glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS)
isPause = false;
最后,在生成模型矩陣之前,判斷是否暫停,如果暫停,就不對當(dāng)前旋轉(zhuǎn)角度進(jìn)行刷新:
if (!isPause)
currentRadians = glm::radians((float)glfwGetTime() * -10.0f);
model = glm::rotate(model, currentRadians, glm::normalize(glm::vec3(1.0, 0.0, 1.0))); // 旋轉(zhuǎn)平面觀察不同角度的效果
寫完這些代碼后,編譯運(yùn)行一下,看看是否有效。(當(dāng)然有效,不然我上面的圖是咋截的~)
最后,附上完整的代碼以供參考。
講點(diǎn)題外話
首先我要對那些一直等待我寫教程的讀者鄭重地說一句:對不起,你們久等了!
在跨年的時(shí)候,我忽然就有一種不同的覺悟,那就是我想做的是一個(gè)有趣有料的opengl教程,而不是單純的翻譯。所以,我放棄了翻譯,選擇了靜下心來多找資料,多嘗試,多研究。期間我也經(jīng)歷了一個(gè)概念死活搞不懂帶來的煩躁和郁悶,那時(shí)候想,如果我只是翻譯該多好啊,那樣我就能寫的很快。但是,每當(dāng)這種想法出現(xiàn)的時(shí)候,我就靜下心來問問自己,我是想要一篇翻譯的東西還是要一個(gè)卓越的教程?
經(jīng)過掙扎,我還是選擇了后者,于是便繼續(xù)死磕。不過,我還只是一個(gè)初級(jí)圖形程序員,要做一個(gè)有趣有料的教程并不是我一個(gè)人能完成的。在這里,希望看到這里的讀者能幫我一個(gè)忙,如果你也想把學(xué)習(xí)的心得記錄下來的話,非常歡迎對這教程補(bǔ)充或者指正,感謝大家的理解與支持,謝謝!
總結(jié)
本章最重要的知識(shí)是法線貼圖的原理。通過向量共面定理計(jì)算出模型空間中的T向量和B向量。然后生成TBN矩陣,這個(gè)矩陣可以將法向量從TBN空間轉(zhuǎn)換到模型空間中,再通過模型變換矩陣轉(zhuǎn)換到世界空間就非常簡單了。
參考資料
www.learnopengl.com
維基百科
應(yīng)用歷史
Tutorial 26: Normal Mapping