前面的材質(zhì)系統(tǒng)對于除了最簡單的模型以外都是不夠的,所以我們需要擴展前面的系統(tǒng),我們要介紹diffuse和specular貼圖。它們允許你對一個物體的diffuse(而對于簡潔的ambient成分來說,它們幾乎總是是一樣的)和specular成分能夠有更精確的影響。
漫反射貼圖(Diffuse maps)
使用一張圖片覆蓋住物體,以便我們?yōu)槊總€原始像素索引獨立顏色值。在光照場景中,通過紋理來呈現(xiàn)一個物體的diffuse顏色,這個做法被稱做漫反射貼圖(Diffuse map)(因為3D建模師就是這么稱呼這個做法的)。注:它基本就是一個紋理。我們其實是使用同一個潛在原則下的不同名稱。
為了演示漫反射貼圖,我們將會使用下面的圖片,它是一個有一圈鋼邊的木箱:

在著色器中使用漫反射貼圖和紋理教程介紹的一樣。這次我們把紋理以sampler2D類型儲存在Material結構體中。我們使用diffuse貼圖替代早期定義的vec3類型的diffuse顏色。
要記住的是sampler2D也叫做模糊類型,這意味著我們不能以某種類型對它實例化,只能用uniform定義它們。如果我們用結構體而不是uniform實例化(就像函數(shù)的參數(shù)那樣),GLSL會拋出奇怪的錯誤;這同樣也適用于其他模糊類型。
我們也要移除amibient材質(zhì)顏色向量,因為ambient顏色絕大多數(shù)情況等于diffuse顏色,所以不需要分別去儲存它:
struct Material // 儲存物體的材質(zhì)屬性
{
sampler2D diffuse;
vec3 specular;
float shininess;
};
in vec2 TexCoords;
如果你非把ambient顏色設置為不同的值不可(不同于diffuse值),你可以繼續(xù)保留ambient的vec3,但是整個物體的ambient顏色會繼續(xù)保持不變。為了使每個原始像素得到不同ambient值,你需要對ambient值單獨使用另一個紋理。(???)
注意,在片段著色器中我們將會再次需要紋理坐標,所以我們聲明一個額外輸入變量。然后我們簡單地從紋理采樣,來獲得原始像素的diffuse顏色值:
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
同樣,不要忘記把ambient材質(zhì)的顏色設置為diffuse材質(zhì)的顏色:
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
這就是diffuse貼圖的全部內(nèi)容了。就像你看到的,這不是什么新的東西,但是它卻極大提升了視覺品質(zhì)。為了讓它工作,我們需要用到紋理坐標更新頂點數(shù)據(jù),把它們作為頂點屬性傳遞到片段著色器,把紋理加載并綁定到合適的紋理單元。
更新的頂點數(shù)據(jù)可以從這里找到。頂點數(shù)據(jù)現(xiàn)在包括了頂點位置,法線向量和紋理坐標,每個立方體的頂點都有這些屬性。讓我們更新頂點著色器來接受紋理坐標作為頂點屬性,然后發(fā)送到片段著色器:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
...
out vec2 TexCoords;
void main()
{
...
TexCoords = texCoords;
}
要保證更新的頂點屬性指針,不僅是VAO匹配新的頂點數(shù)據(jù),也要把箱子圖片加載為紋理。在繪制箱子之前,我們希望首選紋理單元被賦為material.diffuse這個uniform采樣器,并綁定箱子的紋理到這個紋理單元:
glActiveTexture (GL_TEXTURE0); // 激活紋理單元
glBindTexture (GL_TEXTURE_2D, diffuseMap); // 綁定紋理diffuseMap到當前激活的紋理單元
glUniform1i (glGetUniformLocation (lightingShader.Program, "material.diffuse"), 0); // 定義采樣器material.diffuse對應這個紋理單元
現(xiàn)在,使用一個diffuse貼圖,我們在細節(jié)上再次獲得驚人的提升,這次添加到箱子上的光照開始閃光了(名符其實)。你的箱子現(xiàn)在可能看起來像這樣:

你可以在這里得到應用的全部代碼。
鏡面貼圖
由于我們的物體是個箱子,大部分是木頭,我們知道木頭是不應該有鏡面高光的。我們通過把物體設置specular材質(zhì)設置為vec3(0.0f)來修正它。但是這樣意味著鐵邊會不再顯示鏡面高光,我們知道鋼鐵是會顯示一些鏡面高光的。我們會想要控制物體部分地顯示鏡面高光,它帶有修改了的亮度。這個問題看起來和diffuse貼圖的討論一樣。這是巧合嗎?我想不是。
我們同樣用一個紋理貼圖,來獲得鏡面高光。這意味著我們需要生成一個黑白(或者你喜歡的顏色)紋理來定義specular亮度,把它應用到物體的每個部分。下面是一個specular貼圖的例子:

、
一個specular高光的亮度可以通過圖片中每個紋理的亮度來獲得。specular貼圖的每個像素可以顯示為一個顏色向量,比如:在那里黑色代表顏色向量vec3(0.0f),灰色是vec3(0.5f)。在片段著色器中,我們采樣相應的顏色值,把它乘以光的specular亮度。像素越“白”,乘積的結果越大,物體的specualr部分越亮。
由于箱子幾乎是由木頭組成,木頭作為一個材質(zhì)不會有鏡面高光,整個木頭部分的diffuse紋理被用黑色覆蓋:黑色部分不會包含任何specular高光。箱子的鐵邊有一個修改的specular亮度,它自身更容易受到鏡面高光影響,木紋部分則不會。
從技術上來講,木頭也有鏡面高光,盡管這個閃亮值很小(更多的光被散射),影響很小,但是為了學習目的,我們可以假裝木頭不會有任何specular光反射。
使用Photoshop或Gimp之類的工具,通過將圖片進行裁剪,將某部分調(diào)整成黑白圖樣,并調(diào)整亮度/對比度的做法,可以非常容易將一個diffuse紋理貼圖處理為specular貼圖。
鏡面貼圖采樣
一個specular貼圖和其他紋理一樣,所以代碼和diffuse貼圖的代碼也相似。確保合理的加載了圖片,生成一個紋理對象。由于我們在同樣的片段著色器中使用另一個紋理采樣器,我們必須為specular貼圖使用一個不同的紋理單元(參見紋理),所以在渲染前讓我們把它綁定到合適的紋理單元
glActiveTexture (GL_TEXTURE1);
glBindTexture (GL_TEXTURE_2D, specularMap);
glUniform1i (glGetUniformLocation (lightingShader.Program, "material.specular"), 1);
然后更新片段著色器材質(zhì)屬性,接受一個sampler2D作為這個specular部分的類型,而不是vec3:
struct Material
{
sampler2D diffuse;
sampler2D specular;
float shininess;
};
最后我們希望采樣這個specular貼圖,來獲取原始像素相應的specular亮度:
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
通過使用一個specular貼圖我們可以定義極為精細的細節(jié),物體的這個部分會獲得閃亮的屬性,我們可以設置它們相應的亮度。specular貼圖給我們一個在diffuse貼圖上的控制層。
如果你不想成為主流,你可以在specular貼圖里使用顏色,不單單為每個原始像素設置specular亮度,同時也設置specular高光的顏色。從真實角度來說,specular的顏色基本是由光源自身決定的,所以它不會生成真實的圖像(這就是為什么圖片通常是黑色和白色的:我們只關心亮度)。
如果你現(xiàn)在運行應用,你可以清晰地看到箱子的材質(zhì)現(xiàn)在非常類似真實的鐵邊的木頭箱子了:

你可以在這里找到全部源碼。
使用diffuse和specular貼圖,我們可以給相關但簡單物體添加一個極為明顯的細節(jié)。我們可以使用其他紋理貼圖,比如法線/bump貼圖或者反射貼圖,給物體添加更多的細節(jié)。但是這些在后面教程才會涉及。把你的箱子給你所有的朋友和家人看,有一天你會很滿足,我們的箱子會比現(xiàn)在更漂亮!
練習
調(diào)整光源的ambient,diffuse和specular向量值,看看它們?nèi)绾斡绊憣嶋H輸出的箱子外觀。
嘗試在片段著色器中反轉(zhuǎn)鏡面貼圖(Specular Map)的顏色值,然后木頭就會變得反光而邊框不會反光了(由于貼圖中鋼邊依然有一些殘余顏色,所以鋼邊依然會有一些高光,不過反光明顯小了很多)。
解答:
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); // texture函數(shù)使用前面設置的紋理參數(shù)對相應顏色值進行采樣,返回vec4類型

- Try creating a specular map from the diffuse texture that uses actual colors instead of black and white and see that the result doesn't look too realistic. 如果你不會處理圖片,你可以使用這個帶顏色的鏡面貼圖。

最終效果:

-添加一個叫做放射光貼圖(Emission Map)的東西,即記錄每個片段發(fā)光值(Emission Value)大小的貼圖,發(fā)光值是(模擬)物體自身發(fā)光(Emit)時可能產(chǎn)生的顏色。這樣的話物體就可以忽略環(huán)境光自身發(fā)光。通常在你看到游戲里某個東西(比如 機器人的眼,或是箱子上的小燈)在發(fā)光時,使用的就是放射光貼圖。使用這個貼圖(作者為 creativesam)作為放射光貼圖并使用在箱子上,你就會看到箱子上有會發(fā)光的字了。

項目代碼。
實驗結果如下:
