筆者介紹:姜雪偉,IT公司技術合伙人,IT高級講師,CSDN社區專家,特邀編輯,暢銷書作者;已出版書籍:《手把手教你架構3D游戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。
CSDN視頻網址:http://edu.csdn.net/lecturer/144
游戲畫面中的美術品質對產品來說非常重要,這決定了產品是否能吸引玩家。美術品質的好壞主要體現在材質的渲染上,材質的渲染不僅是美術的事情也是程序的事情,二者要互相配合才能得到想要的效果。本篇博客主要介紹的是材質的法線渲染,本篇博文也適合美術人員學習,當然對于程序更重要,它從法線的原理講起,逐步深入,博文最后會給出源代碼。
游戲場景中會擺滿很多物體,其中每個物體都可能由成百上千平坦的三角形組成。我們以向三角形上附加紋理的方式來增加額外細節,提升真實感,隱藏多邊形幾何體是由無數三角形組成的事實。紋理確有助益,然而當你近看它們時,這個事實便隱藏不住了。現實中的物體表面并非是平坦的,而是表現出無數(凹凸不平的)細節。
例如,磚塊的表面。磚塊的表面非常粗糙,顯然不是完全平坦的:它包含著接縫處水泥凹痕,以及非常多的細小的空洞。如果我們在一個有光的場景中看這樣一個磚塊的表面,問題就出來了。下圖中我們可以看到磚塊紋理應用到了平坦的表面,并被一個點光源照亮。
光照并沒有呈現出任何裂痕和孔洞,完全忽略了磚塊之間凹進去的線條;表面看起來完全就是平的。我們可以使用specular貼圖根據深度或其他細節阻止部分表面被照的更亮,以此部分地解決問題,但這并不是一個好方案。我們需要的是某種可以告知光照系統給所有有關物體表面類似深度這樣的細節的方式。
如果我們以光的視角來看這個問題:是什么使表面被視為完全平坦的表面來照亮?答案會是表面的法線向量。以光照算法的視角考慮的話,只有一件事決定物體的形狀,這就是垂直于它的法線向量。磚塊表面只有一個法線向量,表面完全根據這個法線向量被以一致的方式照亮。如果每個fragment都是用自己的不同的法線會怎樣?這樣我們就可以根據表面細微的細節對法線向量進行改變;這樣就會獲得一種表面看起來要復雜得多的幻覺:
每個fragment使用了自己的法線,我們就可以讓光照相信一個表面由很多微小的(垂直于法線向量的)平面所組成,物體表面的細節將會得到極大提升。這種每個fragment使用各自的法線,替代一個面上所有fragment使用同一個法線的技術叫做法線貼圖(normal mapping)或凹凸貼圖(bump mapping)。應用到磚墻上,效果像這樣:
你可以看到細節獲得了極大提升,開銷卻不大。因為我們只需要改變每個fragment的法線向量,并不需要改變所有光照公式?,F在我們是為每個fragment傳遞一個法線,不再使用插值表面法線。這樣光照使表面擁有了自己的細節。
為使法線貼圖工作,我們需要為每個fragment提供一個法線。像diffuse貼圖和specular貼圖一樣,我們可以使用一個2D紋理來儲存法線數據。2D紋理不僅可以儲存顏色和光照數據,還可以儲存法線向量。這樣我們可以從2D紋理中采樣得到特定紋理的法線向量。
由于法線向量是個幾何工具,而紋理通常只用于儲存顏色信息,用紋理儲存法線向量不是非常直接。如果你想一想,就會知道紋理中的顏色向量用r、g、b元素代表一個3D向量。類似的我們也可以將法線向量的x、y、z元素儲存到紋理中,代替顏色的r、g、b元素。法線向量的范圍在-1到1之間,所以我們先要將其映射到0到1的范圍:
vec3 rgb_normal = normal * 0.5 + 0.5; // 從 [-1,1] 轉換至 [0,1]將法線向量變換為像這樣的RGB顏色元素,我們就能把根據表面的形狀的fragment的法線保存在2D紋理中。在博客文章開頭展示的那個磚塊的例子的法線貼圖如下所示:
這會是一種偏藍色調的紋理(你在網上找到的幾乎所有法線貼圖都是這樣的)。這是因為所有法線的指向都偏向z軸(0, 0, 1)這是一種偏藍的顏色。法線向量從z軸方向也向其他方向輕微偏移,顏色也就發生了輕微變化,這樣看起來便有了一種深度。例如,你可以看到在每個磚塊的頂部,顏色傾向于偏綠,這是因為磚塊的頂部的法線偏向于指向正y軸方向(0, 1, 0),這樣它就是綠色的了。
在一個簡單的朝向正z軸的平面上,我們可以用這個diffuse紋理和這個法線貼圖來渲染前面部分的圖片。要注意的是這個鏈接里的法線貼圖和上面展示的那個不一樣。原因是OpenGL讀取的紋理的y(或V)坐標和紋理通常被創建的方式相反。鏈接里的法線貼圖的y(或綠色)元素是相反的(你可以看到綠色現在在下邊);如果你沒考慮這個,光照就不正確了(使用SOIL載入紋理會上下顛倒,它也會把法線在y方向上顛倒)。加載紋理,把它們綁定到合適的紋理單元,然后使用下面的改變了的像素著色器來渲染一個平面:
uniform sampler2D normalMap; void main(){ // 從法線貼圖范圍[0,1]獲取法線 normal = texture(normalMap, fs_in.TexCoords).rgb; // 將法線向量轉換為范圍[-1,1] normal = normalize(normal * 2.0 - 1.0); [...] // 像往常那樣處理光照}這里我們將被采樣的法線顏色從0到1重新映射回-1到1,便能將RGB顏色重新處理成法線,然后使用采樣出的法線向量應用于光照的計算。在例子中我們使用的是Blinn-Phong著色器。
通過慢慢隨著時間慢慢移動光源,你就能明白法線貼圖是什么意思了。運行這個例子你就能得到本篇博客開始的那個效果:
實現上述效果的源代碼,頂點著色器代碼如下所示:
#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);} 然而有個問題限制了剛才講的那種法線貼圖的使用。我們使用的那個法線貼圖里面的所有法線向量都是指向正z方向的。上面的例子能用,是因為那個平面的表面法線也是指向正z方向的??墒牵绻覀冊诒砻娣ň€指向正y方向的平面上使用同一個法線貼圖會發生什么?
光照看起來完全不對!發生這種情況是平面的表面法線現在指向了y,而采樣得到的法線仍然指向的是z。結果就是光照仍然認為表面法線和之前朝向正z方向時一樣;這樣光照就不對了。下面的圖片展示了這個表面上采樣的法線的近似情況:
你可以看到所有法線都指向z方向,它們本該朝著表面法線指向y方向的。一個可行方案是為每個表面制作一個單獨的法線貼圖。如果是一個立方體的話我們就需要6個法線貼圖,但是如果模型上有無數的朝向不同方向的表面,這就不可行了。
注意事項:實際上對于復雜模型可以把朝向各個方向的法線儲存在同一張貼圖上,你可能看到過不只是藍色的法線貼圖,不過用那樣的法線貼圖有個問題是你必須記住模型的起始朝向,如果模型運動了還要記錄模型的變換,這是非常不方便的;如果把一個diffuse紋理應用在同一個物體的不同表面上,就像立方體那樣的,就需要做6個法線貼圖,這也不可取。
另一個稍微有點難的解決方案是,在一個不同的坐標空間中進行光照,這個坐標空間里,法線貼圖向量總是指向這個坐標空間的正z方向;所有的光照向量都相對與這個正z方向進行變換。這樣我們就能始終使用同樣的法線貼圖,不管朝向問題。這個坐標空間叫做切線空間(tangent space)。
下篇博客介紹切線空間。。。。。。。。。
新聞熱點
疑難解答