從0開(kāi)始的OpenGL學(xué)習(xí)(三十二)-法線貼圖

本文主要解決一個(gè)問(wèn)題:

如何使用法線貼圖給物體添加更多的細(xì)節(jié)?

引言

學(xué)了這么多技巧罐呼,也能顯示非骋酷炫的畫(huà)面蚯妇,是不是覺(jué)得自己已經(jīng)非常強(qiáng),有點(diǎn)飄飄然了?哈哈崭捍,還差的遠(yuǎn)呢。不信蕾各?來(lái)看下面一張對(duì)比圖就知道了:


對(duì)比圖

左圖明顯比右圖感覺(jué)更真實(shí)冶忱,細(xì)節(jié)更多,更帶感登馒。你也許會(huì)這樣想:這兩張紋理貼圖不一樣匙握,這是美術(shù)的事情。那你可就錯(cuò)了陈轿,這兩張圖用的是同一張紋理圈纺。當(dāng)然也不是左邊的模型更加精細(xì),因?yàn)槲艺也坏竭@么精細(xì)的模型麦射,而且自己也不會(huì)做蛾娶。那么,為什么兩張圖的差距這么大呢潜秋?

答案是:左邊的場(chǎng)景在渲染時(shí)用了法線貼圖蛔琅。

酷不酷,贊不贊峻呛, 想不想學(xué)罗售?別急,我們慢慢來(lái)杀饵。

法線貼圖

原理

法線貼圖莽囤,顧名思義,就是將每一個(gè)片元(像素)的法線值保存成一張圖片切距。法向量的xyz坐標(biāo)對(duì)應(yīng)到圖的rgb值朽缎。這樣會(huì)產(chǎn)生了一個(gè)問(wèn)題,就是我們的法線xyz的取值范圍是[-1,1]谜悟,而rgb值的取值范圍[0,1]话肖,我們?cè)撛趺礃影褁yz值轉(zhuǎn)換成rgb值呢?其實(shí)葡幸,這也很簡(jiǎn)單最筒,只要將xyz值加1,然后除以2就可以了蔚叨。翻譯成方程式就是:rgb = (xyz + 1) / 2床蜘。下面我們就來(lái)看看法線貼圖長(zhǎng)什么樣:


磚墻的法線貼圖

怎么藍(lán)不拉嘰的辙培?別急,這個(gè)問(wèn)題會(huì)在下面解答邢锯。

解決了轉(zhuǎn)換成rgb值的問(wèn)題扬蕊,我們還有一個(gè)非常重要的問(wèn)題要解決。思考一下丹擎,物體的法線在世界空間的不同位置是不同的尾抑,我們?nèi)绾尾拍苡靡粡垐D來(lái)表示物體的法線,使它能夠在所有的情況下都適用呢蒂培?仔細(xì)想想再愈,法線貼圖是貼在物體表面的,我在一個(gè)固定的坐標(biāo)系中生成法線貼圖护戳,再通過(guò)一個(gè)轉(zhuǎn)換矩陣將法線轉(zhuǎn)換到世界空間中翎冲,這不就行了嗎?

思路沒(méi)錯(cuò)灸异,關(guān)鍵是如何計(jì)算出這個(gè)轉(zhuǎn)換矩陣府适。首先介紹一下,我們生成法線貼圖的坐標(biāo)系稱作TBN坐標(biāo)系肺樟,坐標(biāo)系的三個(gè)軸是t軸檐春,b軸和n軸,對(duì)應(yīng)我們熟悉的x軸么伯,y軸和z軸疟暖。之所以把這個(gè)坐標(biāo)系稱為T(mén)BN坐標(biāo)系,是因?yàn)閚軸表示的是圖元三角形表面法向量田柔。有了法向量之后俐巴,我們還需要兩個(gè)軸來(lái)確定這個(gè)坐標(biāo)系。這兩個(gè)軸是切線軸(tangent )和副切線軸(bitangent )硬爆,這三個(gè)軸一起組成的坐標(biāo)系就是TBN坐標(biāo)系欣舵。理論上,T軸可以是圖元平面上的任意軸缀磕,B軸只需要垂直T軸和N軸就可以了缘圈。但是,TBN坐標(biāo)系不是隨隨便便弄出來(lái)玩的袜蚕,它的存在價(jià)值就是簡(jiǎn)化計(jì)算法向量的步驟糟把。所以,我們通常采用的是和表面紋理坐標(biāo)一致的軸作為T(mén)軸和B軸牲剃,即U軸對(duì)應(yīng)T軸遣疯,V軸對(duì)應(yīng)B軸,這樣凿傅,我們就可以用UV值來(lái)計(jì)算轉(zhuǎn)換矩陣了缠犀。

注意TBN坐標(biāo)系的存在意義就是簡(jiǎn)化計(jì)算變換矩陣的数苫,所以我們的TBN軸就必須按照約定俗成的規(guī)范來(lái)

TBN坐標(biāo)系

上圖是法線貼圖在TBN坐標(biāo)系中的狀態(tài)。

接下來(lái)夭坪,我們就要來(lái)計(jì)算在模型空間中的向量T和向量B了文判。來(lái)看下面一張?jiān)韴D,我們從這張?jiān)韴D上推導(dǎo)出計(jì)算的公式:


原理圖

假設(shè)我們的向量T和B都是單位向量室梅,根據(jù)向量共面定理,我們可以列出一個(gè)方程式:


方程式

介紹一下向量共面定理疚宇。
如果e1和e2是同一個(gè)平面內(nèi)兩個(gè)不共線向量亡鼠,那么對(duì)于平面內(nèi)的任一向量a,有且只有一對(duì)實(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)依次類(lèi)推。這樣我們就非常容易地將這個(gè)等式轉(zhuǎn)換成矩陣的形式:


矩陣格式

要計(jì)算TB的值壶硅,我們只需要在等式兩邊乘以UV矩陣的逆矩陣就行了:
計(jì)算TB向量的矩陣方程

逆矩陣的計(jì)算方法我們不用去考究威兜,直接使用下面的最終計(jì)算方程式就行:


最終計(jì)算方程式

有了這個(gè)方程式,我們就可以計(jì)算轉(zhuǎn)換矩陣了庐椒。

講點(diǎn)小歷史知識(shí):
在以前椒舵,法線貼圖需要用高精度模型通過(guò)特定的算法計(jì)算出來(lái),非常的不方便约谈。后來(lái)改進(jìn)之后才有了我們現(xiàn)在的貼圖與模型分離的方法笔宿。應(yīng)用上,PS2不支持法線貼圖窗宇,XBox360以及之后的主機(jī)都支持法線貼圖措伐,這已經(jīng)成為了一種標(biāo)配。

為什么是藍(lán)兮兮的军俊?

你肯定已經(jīng)在這個(gè)這問(wèn)題上糾結(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é),看起來(lái)像高精度模型那樣烤礁。來(lái)看下面這張圖:


法線貼圖的優(yōu)點(diǎn)

只需要500個(gè)三角形的簡(jiǎn)單網(wǎng)格加上法線貼圖就能有媲美4M個(gè)三角形的精細(xì)網(wǎng)格模型的效果讼积,不得不說(shuō)法線貼圖的優(yōu)勢(shì)巨大,處理4M個(gè)三角形可不是500個(gè)三角形那么簡(jiǎn)單的事情脚仔。

當(dāng)然法線貼圖也不是萬(wàn)能的勤众,它也有它的缺點(diǎn)。因?yàn)樗皇歉淖兞宋矬w表面的光照計(jì)算方式玻侥,所以它不適合用在凹凸起伏較大的物體上决摧,這些物體會(huì)有遮擋的效果,這是法線貼圖無(wú)法實(shí)現(xiàn)的凑兰。另外掌桩,使用法線貼圖的物體經(jīng)不起特寫(xiě)放大操作,如果攝像機(jī)離的很近波岛,或者攝像機(jī)的角度刁鉆,很容易就會(huì)穿幫音半。

實(shí)現(xiàn)

理解原理后则拷,自然就到了實(shí)現(xiàn)的過(guò)程〔莛基本上煌茬,我們最容易想到的方法有兩種:

其一、將法線通過(guò)TBN矩陣變換后彻桃,在世界坐標(biāo)空間中計(jì)算光照效果坛善。

其二、將光源、視點(diǎn)眠屎、片元的位置經(jīng)過(guò)TBN矩陣的逆矩陣變換后剔交,在TBN空間中計(jì)算光照效果。

兩種方法都可行改衩,我們都會(huì)進(jìn)行嘗試岖常。不過(guò)要先來(lái)看看如何計(jì)算T和B向量。

要計(jì)算T和B向量葫督,我們首先要知道三角形的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)竭鞍。如果我們要顯示上面的那一面磚墻,我們就需要4個(gè)頂點(diǎn)坐標(biāo)和4個(gè)紋理坐標(biāo)橄镜,外加一個(gè)法向量笼蛛,先來(lái)定義這些數(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ù)领追,跟著公式走他膳,我們的代碼出來(lái)了:

float coefficient = 1 / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);

最后,計(jì)算出T向量和B向量绒窑。向量的乘法還記得嗎棕孙?用前一個(gè)矩陣的一行去乘上后一個(gè)矩陣的每一列,得到最終矩陣上一行的元素值些膨,以此類(lèi)推:

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)));

這樣洼哎,在主流程中的工作就完成了烫映,接下來(lái)就是著色器的工作了,我們先來(lái)看第一種方法噩峦。

方法一:切線空間->世界空間

在頂點(diǎn)著色器中锭沟,我們需要把T向量和B向量都接收進(jìn)來(lái),所以在著色器開(kāi)頭要加上這兩行代碼:

layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;

然后识补,計(jì)算得到的TBN矩陣(就是上面說(shuō)的轉(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矩陣的方式十分簡(jiǎn)單较幌,直接調(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)代碼槐瑞,如果你看過(guò)我前面的文章熙涤,肯定很熟悉這套代碼了。

再寫(xiě)一些邊邊角角的代碼困檩,讓模型動(dòng)起來(lái)方便我們觀察效果祠挫。將模型在光源的位置也顯示出來(lái),這樣我們就能知道光照效果對(duì)不對(duì)了:

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();

好悼沿,完成之后我們就可以看看效果了:


運(yùn)行效果

8錯(cuò)8錯(cuò)等舔,是我們要的效果。

方法二:世界空間->切線空間

使用這個(gè)方法糟趾,我們不需要將TBN矩陣傳遞給片元著色器了慌植,而是要在頂點(diǎn)著色器中,將光源位置义郑、視點(diǎn)位置蝶柿、片元位置通過(guò)TBN矩陣的逆矩陣轉(zhuǎn)換好之后,將轉(zhuǎn)換過(guò)后的坐標(biāo)傳遞給片元著色器讓它使用魔慷。

好了只锭,動(dòng)手!新建一個(gè)頂點(diǎn)著色器和片元著色器院尔,取名method2Shader.vs和method2Shader.fs蜻展,將方法一中的代碼都復(fù)制過(guò)來(lái),我們?cè)谒幕A(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)置矩陣來(lái)算出光源曹仗、視點(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)來(lái)怎茫,有了T和N向量之后,我們完全可以通過(guò)叉乘來(lái)計(jì)算出B向量妓灌。

在大量的計(jì)算過(guò)程中轨蛤,TBN向量可能會(huì)變得不兩兩垂直,這就會(huì)導(dǎo)致我們的模型有瑕疵虫埂。因此祥山,一種名叫Gram-Schmidt正交化的方法就被創(chuàng)造出來(lái)。通過(guò)一點(diǎn)很小的代價(jià)掉伏,讓TBN向量繼續(xù)兩兩垂直缝呕,這樣,我們的模型顯示就完美無(wú)瑕了斧散。上面的代碼中岳颇,T = normalize(T - dot(T, N) * N);就是正交化過(guò)程。

片元著色器中颅湘,我們將原本用來(lái)接收的結(jié)構(gòu)改成頂點(diǎn)著色器傳過(guò)來(lái)的結(jié)構(gòu):

in VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;

計(jì)算光照時(shí),去掉用TBN矩陣轉(zhuǎn)換法向量的過(guò)程栗精,只將采樣的法向量轉(zhuǎn)換回[-1,1]空間就行了闯参。然后,在計(jì)算光照的時(shí)候悲立,必須使用從頂點(diǎn)傳過(guò)來(lái)的光源鹿寨、片元和視點(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)行效果

添加一些控制功能

每次總是運(yùn)行一個(gè)著色器文件原献,完全沒(méi)法看出對(duì)比效果是不是馏慨?不要急,我們這就來(lái)添加一些控制的功能姑隅。我們想要的功能有兩個(gè):1写隶、在方法一、方法二和使用法線貼圖的著色器之間切換讲仰。2慕趴、暫停旋轉(zhuǎn),方便切換比較。

切換功能

實(shí)現(xiàn)切換功能很容易冕房,在全局空間中添加一個(gè)渲染類(lèi)型變量躏啰,初始值設(shè)置成1,表示方法1

int renderType = 1;         //繪制方式:1耙册、不使用法線貼圖给僵;2、切線空間->世界空間觅玻;3想际、世界空間->切線空間

然后,在鍵盤(pán)控制的處理函數(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)位置的工作請(qǐng)自行完成守屉。

完成之后,我們就可以在3種方式中之間隨意切換了蒿辙。


切換效果

暫停功能

暫停功能的實(shí)現(xiàn)更加簡(jiǎn)單拇泛。第一步先在全局變量中添加暫停變量以及當(dāng)前角度變量(用來(lái)保存當(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;

最后熄守,在生成模型矩陣之前,判斷是否暫停耗跛,如果暫停裕照,就不對(duì)當(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)平面觀察不同角度的效果

寫(xiě)完這些代碼后,編譯運(yùn)行一下调塌,看看是否有效牍氛。(當(dāng)然有效,不然我上面的圖是咋截的~)

最后烟阐,附上完整的代碼以供參考搬俊。

講點(diǎn)題外話

首先我要對(duì)那些一直等待我寫(xiě)教程的讀者鄭重地說(shuō)一句:對(duì)不起紊扬,你們久等了!

在跨年的時(shí)候唉擂,我忽然就有一種不同的覺(jué)悟餐屎,那就是我想做的是一個(gè)有趣有料的opengl教程,而不是單純的翻譯玩祟。所以腹缩,我放棄了翻譯,選擇了靜下心來(lái)多找資料空扎,多嘗試藏鹊,多研究。期間我也經(jīng)歷了一個(gè)概念死活搞不懂帶來(lái)的煩躁和郁悶转锈,那時(shí)候想盘寡,如果我只是翻譯該多好啊,那樣我就能寫(xiě)的很快撮慨。但是竿痰,每當(dāng)這種想法出現(xiàn)的時(shí)候,我就靜下心來(lái)問(wèn)問(wèn)自己砌溺,我是想要一篇翻譯的東西還是要一個(gè)卓越的教程影涉?

經(jīng)過(guò)掙扎,我還是選擇了后者规伐,于是便繼續(xù)死磕蟹倾。不過(guò),我還只是一個(gè)初級(jí)圖形程序員猖闪,要做一個(gè)有趣有料的教程并不是我一個(gè)人能完成的喊式。在這里,希望看到這里的讀者能幫我一個(gè)忙萧朝,如果你也想把學(xué)習(xí)的心得記錄下來(lái)的話,非常歡迎對(duì)這教程補(bǔ)充或者指正夏哭,感謝大家的理解與支持检柬,謝謝!

總結(jié)

本章最重要的知識(shí)是法線貼圖的原理竖配。通過(guò)向量共面定理計(jì)算出模型空間中的T向量和B向量何址。然后生成TBN矩陣,這個(gè)矩陣可以將法向量從TBN空間轉(zhuǎn)換到模型空間中进胯,再通過(guò)模型變換矩陣轉(zhuǎn)換到世界空間就非常簡(jiǎn)單了用爪。

下一篇
目錄
上一篇

參考資料

www.learnopengl.com
維基百科
應(yīng)用歷史
Tutorial 26: Normal Mapping

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市胁镐,隨后出現(xiàn)的幾起案子偎血,更是在濱河造成了極大的恐慌诸衔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颇玷,死亡現(xiàn)場(chǎng)離奇詭異笨农,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)帖渠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)谒亦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人空郊,你說(shuō)我怎么就攤上這事份招。” “怎么了狞甚?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵锁摔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我入愧,道長(zhǎng)鄙漏,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任棺蛛,我火速辦了婚禮怔蚌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旁赊。我一直安慰自己桦踊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布终畅。 她就那樣靜靜地躺著籍胯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪离福。 梳的紋絲不亂的頭發(fā)上杖狼,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音妖爷,去河邊找鬼蝶涩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛絮识,可吹牛的內(nèi)容都是我干的绿聘。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼次舌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熄攘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起彼念,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挪圾,失蹤者是張志新(化名)和其女友劉穎浅萧,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體洛史,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惯殊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了也殖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土思。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖忆嗜,靈堂內(nèi)的尸體忽然破棺而出己儒,到底是詐尸還是另有隱情,我是刑警寧澤捆毫,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布闪湾,位于F島的核電站,受9級(jí)特大地震影響绩卤,放射性物質(zhì)發(fā)生泄漏途样。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一濒憋、第九天 我趴在偏房一處隱蔽的房頂上張望何暇。 院中可真熱鬧,春花似錦凛驮、人聲如沸裆站。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宏胯。三九已至,卻和暖如春本姥,著一層夾襖步出監(jiān)牢的瞬間肩袍,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工婚惫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氛赐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓辰妙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親甫窟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子密浑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容