筆者介紹:姜雪偉,IT公司技術(shù)合伙人,IT高級(jí)講師,CSDN社區(qū)專家,特邀編輯,暢銷書作者;已出版書籍:《手把手教你架構(gòu)3D游戲引擎》電子工業(yè)出版社和《Unity3D實(shí)戰(zhàn)核心技術(shù)詳解》電子工業(yè)出版社等。
CSDN視頻網(wǎng)址:http://edu.csdn.net/lecturer/144
游戲畫面中的美術(shù)品質(zhì)對(duì)產(chǎn)品來(lái)說(shuō)非常重要,這決定了產(chǎn)品是否能吸引玩家。美術(shù)品質(zhì)的好壞主要體現(xiàn)在材質(zhì)的渲染上,材質(zhì)的渲染不僅是美術(shù)的事情也是程序的事情,二者要互相配合才能得到想要的效果。本篇博客主要介紹的是材質(zhì)的法線渲染,本篇博文也適合美術(shù)人員學(xué)習(xí),當(dāng)然對(duì)于程序更重要,它從法線的原理講起,逐步深入,博文最后會(huì)給出源代碼。
游戲場(chǎng)景中會(huì)擺滿很多物體,其中每個(gè)物體都可能由成百上千平坦的三角形組成。我們以向三角形上附加紋理的方式來(lái)增加額外細(xì)節(jié),提升真實(shí)感,隱藏多邊形幾何體是由無(wú)數(shù)三角形組成的事實(shí)。紋理確有助益,然而當(dāng)你近看它們時(shí),這個(gè)事實(shí)便隱藏不住了。現(xiàn)實(shí)中的物體表面并非是平坦的,而是表現(xiàn)出無(wú)數(shù)(凹凸不平的)細(xì)節(jié)。
例如,磚塊的表面。磚塊的表面非常粗糙,顯然不是完全平坦的:它包含著接縫處水泥凹痕,以及非常多的細(xì)小的空洞。如果我們?cè)谝粋€(gè)有光的場(chǎng)景中看這樣一個(gè)磚塊的表面,問(wèn)題就出來(lái)了。下圖中我們可以看到磚塊紋理應(yīng)用到了平坦的表面,并被一個(gè)點(diǎn)光源照亮。
光照并沒(méi)有呈現(xiàn)出任何裂痕和孔洞,完全忽略了磚塊之間凹進(jìn)去的線條;表面看起來(lái)完全就是平的。我們可以使用specular貼圖根據(jù)深度或其他細(xì)節(jié)阻止部分表面被照的更亮,以此部分地解決問(wèn)題,但這并不是一個(gè)好方案。我們需要的是某種可以告知光照系統(tǒng)給所有有關(guān)物體表面類似深度這樣的細(xì)節(jié)的方式。
如果我們以光的視角來(lái)看這個(gè)問(wèn)題:是什么使表面被視為完全平坦的表面來(lái)照亮?答案會(huì)是表面的法線向量。以光照算法的視角考慮的話,只有一件事決定物體的形狀,這就是垂直于它的法線向量。磚塊表面只有一個(gè)法線向量,表面完全根據(jù)這個(gè)法線向量被以一致的方式照亮。如果每個(gè)fragment都是用自己的不同的法線會(huì)怎樣?這樣我們就可以根據(jù)表面細(xì)微的細(xì)節(jié)對(duì)法線向量進(jìn)行改變;這樣就會(huì)獲得一種表面看起來(lái)要復(fù)雜得多的幻覺(jué):
每個(gè)fragment使用了自己的法線,我們就可以讓光照相信一個(gè)表面由很多微小的(垂直于法線向量的)平面所組成,物體表面的細(xì)節(jié)將會(huì)得到極大提升。這種每個(gè)fragment使用各自的法線,替代一個(gè)面上所有fragment使用同一個(gè)法線的技術(shù)叫做法線貼圖(normal mapping)或凹凸貼圖(bump mapping)。應(yīng)用到磚墻上,效果像這樣:
你可以看到細(xì)節(jié)獲得了極大提升,開(kāi)銷卻不大。因?yàn)槲覀冎恍枰淖兠總€(gè)fragment的法線向量,并不需要改變所有光照公式。現(xiàn)在我們是為每個(gè)fragment傳遞一個(gè)法線,不再使用插值表面法線。這樣光照使表面擁有了自己的細(xì)節(jié)。
為使法線貼圖工作,我們需要為每個(gè)fragment提供一個(gè)法線。像diffuse貼圖和specular貼圖一樣,我們可以使用一個(gè)2D紋理來(lái)儲(chǔ)存法線數(shù)據(jù)。2D紋理不僅可以儲(chǔ)存顏色和光照數(shù)據(jù),還可以儲(chǔ)存法線向量。這樣我們可以從2D紋理中采樣得到特定紋理的法線向量。
由于法線向量是個(gè)幾何工具,而紋理通常只用于儲(chǔ)存顏色信息,用紋理儲(chǔ)存法線向量不是非常直接。如果你想一想,就會(huì)知道紋理中的顏色向量用r、g、b元素代表一個(gè)3D向量。類似的我們也可以將法線向量的x、y、z元素儲(chǔ)存到紋理中,代替顏色的r、g、b元素。法線向量的范圍在-1到1之間,所以我們先要將其映射到0到1的范圍:
vec3 rgb_normal = normal * 0.5 + 0.5; // 從 [-1,1] 轉(zhuǎn)換至 [0,1]將法線向量變換為像這樣的RGB顏色元素,我們就能把根據(jù)表面的形狀的fragment的法線保存在2D紋理中。在博客文章開(kāi)頭展示的那個(gè)磚塊的例子的法線貼圖如下所示:
這會(huì)是一種偏藍(lán)色調(diào)的紋理(你在網(wǎng)上找到的幾乎所有法線貼圖都是這樣的)。這是因?yàn)樗蟹ň€的指向都偏向z軸(0, 0, 1)這是一種偏藍(lán)的顏色。法線向量從z軸方向也向其他方向輕微偏移,顏色也就發(fā)生了輕微變化,這樣看起來(lái)便有了一種深度。例如,你可以看到在每個(gè)磚塊的頂部,顏色傾向于偏綠,這是因?yàn)榇u塊的頂部的法線偏向于指向正y軸方向(0, 1, 0),這樣它就是綠色的了。
在一個(gè)簡(jiǎn)單的朝向正z軸的平面上,我們可以用這個(gè)diffuse紋理和這個(gè)法線貼圖來(lái)渲染前面部分的圖片。要注意的是這個(gè)鏈接里的法線貼圖和上面展示的那個(gè)不一樣。原因是OpenGL讀取的紋理的y(或V)坐標(biāo)和紋理通常被創(chuàng)建的方式相反。鏈接里的法線貼圖的y(或綠色)元素是相反的(你可以看到綠色現(xiàn)在在下邊);如果你沒(méi)考慮這個(gè),光照就不正確了(使用SOIL載入紋理會(huì)上下顛倒,它也會(huì)把法線在y方向上顛倒)。加載紋理,把它們綁定到合適的紋理單元,然后使用下面的改變了的像素著色器來(lái)渲染一個(gè)平面:
uniform sampler2D normalMap; void main(){ // 從法線貼圖范圍[0,1]獲取法線 normal = texture(normalMap, fs_in.TexCoords).rgb; // 將法線向量轉(zhuǎn)換為范圍[-1,1] normal = normalize(normal * 2.0 - 1.0); [...] // 像往常那樣處理光照}這里我們將被采樣的法線顏色從0到1重新映射回-1到1,便能將RGB顏色重新處理成法線,然后使用采樣出的法線向量應(yīng)用于光照的計(jì)算。在例子中我們使用的是Blinn-Phong著色器。
通過(guò)慢慢隨著時(shí)間慢慢移動(dòng)光源,你就能明白法線貼圖是什么意思了。運(yùn)行這個(gè)例子你就能得到本篇博客開(kāi)始的那個(gè)效果:
實(shí)現(xiàn)上述效果的源代碼,頂點(diǎn)著色器代碼如下所示:
#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec3 normal;layout (location = 2) in vec2 texCoords;// Declare an interface block; see 'Advanced GLSL' for what these are.out VS_OUT { vec3 FragPos; vec3 Normal; vec2 TexCoords;} vs_out;uniform mat4 PRojection;uniform mat4 view;uniform mat4 model;void main(){ gl_Position = projection * view * model * vec4(position, 1.0f); vs_out.FragPos = vec3(model * vec4(position, 1.0)); vs_out.TexCoords = texCoords; mat3 normalMatrix = transpose(inverse(mat3(model))); vs_out.Normal = normalMatrix * normal;}片段著色器代碼如下所示:
#version 330 coreout vec4 FragColor;in VS_OUT { vec3 FragPos; vec3 Normal; vec2 TexCoords;} fs_in;uniform sampler2D diffuseMap;uniform sampler2D normalMap; uniform vec3 lightPos;uniform vec3 viewPos;uniform bool normalMapping;void main(){ vec3 normal = normalize(fs_in.Normal); if(normalMapping) { // Obtain normal from normal map in range [0,1] normal = texture(normalMap, fs_in.TexCoords).rgb; // Transform normal vector to range [-1,1] normal = normalize(normal * 2.0 - 1.0); } // Get diffuse color vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb; // Ambient vec3 ambient = 0.1 * color; // Diffuse vec3 lightDir = normalize(lightPos - fs_in.FragPos); float diff = max(dot(lightDir, normal), 0.0); vec3 diffuse = diff * color; // Specular 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.0f);} 然而有個(gè)問(wèn)題限制了剛才講的那種法線貼圖的使用。我們使用的那個(gè)法線貼圖里面的所有法線向量都是指向正z方向的。上面的例子能用,是因?yàn)槟莻€(gè)平面的表面法線也是指向正z方向的??墒?,如果我們?cè)诒砻娣ň€指向正y方向的平面上使用同一個(gè)法線貼圖會(huì)發(fā)生什么?
光照看起來(lái)完全不對(duì)!發(fā)生這種情況是平面的表面法線現(xiàn)在指向了y,而采樣得到的法線仍然指向的是z。結(jié)果就是光照仍然認(rèn)為表面法線和之前朝向正z方向時(shí)一樣;這樣光照就不對(duì)了。下面的圖片展示了這個(gè)表面上采樣的法線的近似情況:
你可以看到所有法線都指向z方向,它們本該朝著表面法線指向y方向的。一個(gè)可行方案是為每個(gè)表面制作一個(gè)單獨(dú)的法線貼圖。如果是一個(gè)立方體的話我們就需要6個(gè)法線貼圖,但是如果模型上有無(wú)數(shù)的朝向不同方向的表面,這就不可行了。
注意事項(xiàng):實(shí)際上對(duì)于復(fù)雜模型可以把朝向各個(gè)方向的法線儲(chǔ)存在同一張貼圖上,你可能看到過(guò)不只是藍(lán)色的法線貼圖,不過(guò)用那樣的法線貼圖有個(gè)問(wèn)題是你必須記住模型的起始朝向,如果模型運(yùn)動(dòng)了還要記錄模型的變換,這是非常不方便的;如果把一個(gè)diffuse紋理應(yīng)用在同一個(gè)物體的不同表面上,就像立方體那樣的,就需要做6個(gè)法線貼圖,這也不可取。
另一個(gè)稍微有點(diǎn)難的解決方案是,在一個(gè)不同的坐標(biāo)空間中進(jìn)行光照,這個(gè)坐標(biāo)空間里,法線貼圖向量總是指向這個(gè)坐標(biāo)空間的正z方向;所有的光照向量都相對(duì)與這個(gè)正z方向進(jìn)行變換。這樣我們就能始終使用同樣的法線貼圖,不管朝向問(wèn)題。這個(gè)坐標(biāo)空間叫做切線空間(tangent space)。
下篇博客介紹切線空間。。。。。。。。。
|
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注