OpenGL光照渲染技術(shù)

1 簡介

本系列的文章到目前為止已經(jīng)介紹完了OpenGL的基礎(chǔ)知識州藕,你應該已經(jīng)了解OpenGL中的大部分特性睦优,也在示例程序中見到過利用它們來實現(xiàn)圖像渲染算法抗俄。本文對其中部分算法深入講解,特別時在實時渲染環(huán)境下應該關(guān)注的算法疟赊。

首先我們將會學習一些基本的光照技術(shù),它們使得我們可以在場景中應用有趣的陰影效果煞聪。然后我們會認識一些不以寫實照片(photo-realism)為目的的渲染方法。最后我們將會討論一些只有非傳統(tǒng)前向渲染幾何管道才適用的算法姐军,最終來的本文講解的高級特性础废,如何不使用頂點和三角形渲染整個場景。

總的來說躯砰,文章將會圍繞如下3個知識點詳細展開:

  • 如何照亮場景中的像素频轿。
  • 如何將渲染延遲到最后一刻。
  • 如何不使用三角形渲染整個場景。

2 光照模型

辨證的看回溺,所有圖像渲染程序的工作都是光線的模擬茴晋。無論是最簡單的旋轉(zhuǎn)幾何體逼友,還是最復雜的電影特性,我們都努力使用戶相信他們看見的就是真實的場景针饥,或者說是真實世界的模擬巧号。為了達到這個目的,我們必須為光與表面的交互方式建立模型旺罢。最高級的模型能夠最真實和準確的反映我們所理解的光的物理屬性正卧。然而大多數(shù)這些模型在實時環(huán)境下效率都很低,因此我們必須接受近似值跪解,或者說即使這些模型產(chǎn)生的結(jié)果和物理性質(zhì)相比并不是非常精確炉旷,但是它是可以接受的。下面將介紹在實時場景中如何使用可用的光照模型叉讥。

2.1 馮氏照明模型

最常見的一個照明模型是馮氏照明模型(Phong Lighting Model)窘行。它的原理很簡單,物體都有3個材質(zhì)屬性图仓,分別是環(huán)境反射率罐盔,漫反射率和鏡面反射率。這些屬性都用顏色值表示透绩,更亮的顏色表示更高的反射率翘骂。光源有相似的三個屬性,它們同樣使用顏色值表示光源的環(huán)境光顏色帚豪,漫射光顏色和反射光顏色碳竟。最終計算出的顏色是光源和模型這三個屬性交互的和。

2.1.1 環(huán)境光

環(huán)境光(Ambient Light)不來自于特定的方向狸臣。盡管它源于真實位置的光源莹桅,但是由于它在房間或者場景中經(jīng)過多次漫反射,因此它可以被認為是無方向的。環(huán)境光會均勻的從所有方向照亮物體的所有表面诈泼《埃可用將環(huán)境光看成是一個全局的明亮因子,它又所有光源共同產(chǎn)生铐达。這個照明分量實際上很很接近光灑落在環(huán)境中的效果岖赋。

在計算環(huán)境光對最終像素顏色貢獻的時候,將材質(zhì)的環(huán)境光反射率和每個光源的環(huán)境光相乘瓮孙。在GLSL編寫的著色器中唐断,可以使用如下的方式定義材質(zhì)環(huán)境光反射率。

uniform vec3 ambient = vec3(0.1, 0.1, 0.1);
2.1.2 漫射光

漫射光(Diffuse Light)是光源中帶方向的分量杭抠,也是我們之前的示例程序中在計算像素顏色時主要使用的分量脸甘。在馮氏光照模型中,材質(zhì)的漫射光反射率和光源的漫射光顏色相乘后得到最大的漫射光顏色偏灿,也就是光照逆方向和像素所處平面的法向量同向時的漫射光顏色丹诀。因此我們要通過該平面法向量和光照逆方向(也就是被照射到片段到光源的方向)單位向量的點積計算出有效因子,最后和最大漫反射顏色相乘得到實際的漫反射顏色翁垂。在著色器中铆遭,其計算的示例代碼如下。

uniform vec3 vDiffuseMaterial; 
uniform vec3 vDiffuseLight;
float fDotProduct = max(0.0, dot(vNormal, vLightDir));
vec3 vDiffuseColor = vDiffuseMaterial * vDiffuseLight * fDotProduct;

這里需要注意我們對平面法向量和光照逆方向單位向量的點積做了額外處理沮峡,將其和0取最大值疚脐,這是因為如果光從平面背面照射時其該值為負,但是很明顯此時漫反射光產(chǎn)生的顏色應該為0邢疙,所以這里有這步額外的處理棍弄。

2.1.3 鏡面反射光

和漫反射光一樣,鏡面反射光具有極強的方向?qū)傩耘庇危遣煌氖撬捅砻娴慕换ジ劢乖谝粋€特定的方向上呼畸。強鏡面反射光(在現(xiàn)實世界中表面具有高鏡面反射材質(zhì)屬性時)通常都會在表面形成一個亮點,這被稱為鏡面高光(specular highlight)颁虐。因為鏡面反射蛮原,因此鏡面反射光對最終像素的顏色貢獻取決于觀察方向和光照方向的夾角相關(guān)。點光源和太陽是具有高鏡面反射光的很好的例子另绩,當被照射物體具有高鏡面反射屬性的時候就能形成非常明亮的光斑儒陨。

材料鏡面反射材質(zhì)屬性和光源的鏡面反射光相乘后還需要經(jīng)過一次縮放,這個縮放因子可以理解為是一個全局的材質(zhì)屬性-反光度(shininess)笋籽,需要注意這里的計算方式是使用算出的向量點積作為底蹦漠,材質(zhì)反光因子作為指數(shù),因此反光因子越大车海,得到的鏡面反色光顏色越暗笛园。其計算的主要代碼如下。

uniform vec3 vSpecularMaterial; 
uniform vec3 vSpecularLight; 
float shininess = 128.0;
vec3 vReflection = reflect(-vLightDir, vNormal);
float EyeReflectionAngle = max(0.0, dot(vEyeNormal, vReflection)); 
fSpec = pow(EyeReflectionAngle, shininess);
vec3 vSpecularColor = vSpecularLight * vSpecularMaterial * fSpec;

反光因子shininess可以通過統(tǒng)一變量的方式傳入,從OpenGL固定管道時代開始研铆,其最大值通常被設置為128埋同。

現(xiàn)在我們就能得到完整的公式計算光照在模型表面的顏色】煤欤考慮模型的環(huán)境材質(zhì)屬性為ka凶赁,漫反射材質(zhì)屬性為kd,鏡面反射材質(zhì)為ks窄赋,反光因子為α哟冬,環(huán)境光為ia,漫反射光為id忆绰,鏡面反射光為is,則像素最終的顏色可以由如下公式得出可岂。

等式中的向量N?错敢、L?、R?缕粹、和V? 都是單位向量稚茅,分別表示像素所處平面的法向量,像素到光源的向量平斩,像素到光源的向量的負向量被像素平面反射向量亚享,像素到觀察者的向量。它們表示如下圖绘面。

圖中欺税,-L?為像素到光源的向量的負向量,圖中應該和L?位于同一直線(畫的有點失真)揭璃。向量R?越偏離觀察者晚凿,反射光是越暗,當向量R?直接指向觀察著時瘦馍,它和向量V?的點積最大歼秽,其值為1,此時鏡面反射光最強情组,稱為鏡面高亮燥筷。

同樣的,在上圖中漫射光也很容易理解院崇。光源垂直照射像素時肆氓,單位向量L?和像素平面垂直,和平面法向量N?共線亚脆,此時點積最大做院,漫射光最強。光源驗證像素平面方向照射像素時,單位向量L?和平面法向量N?垂直键耕,此時點積最小寺滚,漫射光最弱。

2.1.4 高洛德著色

示例程序phonglighting的著色器使用了上面的公式來計算片段的顏色屈雄,其中使用了高洛德著色(Gouraud shading)技術(shù)村视。在該技術(shù)中,首先計算每個頂點的顏色值酒奶,然后再通過簡單的插值計算得到所有片段的顏色值蚁孔,頂點著色器代碼如下。

#version 420 core
// Per-vertex inputs
layout (location = 0) in vec4 position; 
layout (location = 1) in vec3 normal;

// Matrices we’ll need
layout (std140) uniform constants {
    mat4 mv_matrix;
    mat4 view_matrix;
    mat4 proj_matrix; 
};

// Light and material properties惋嚎,默認光源的漫反射和鏡面反射光都為白色
uniform vec3 light_pos = vec3(100.0, 100.0, 100.0); 
// 通常直接設置所有模型的環(huán)境光反射率和光源環(huán)境光的交互得到的最終環(huán)境光顏色
uniform vec3 ambient = vec3(0.1, 0.1, 0.1);
uniform vec3 diffuse_albedo = vec3(0.5, 0.2, 0.7); 
uniform vec3 specular_albedo = vec3(0.7);
uniform float specular_power = 128.0;

// Outputs to the fragment shader
out VS_OUT {
    vec3 color; 
} vs_out;

void main(void) {
    // Calculate view-space coordinate
    vec4 P = mv_matrix * position;
    // Calculate normal in view space
    vec3 N = mat3(mv_matrix) * normal;

    // Calculate view-space light vector
    vec3 L = light_pos - P.xyz;
    // Calculate view vector (simply the negative of the view-space position)
    vec3 V = -P.xyz;
        
    // Normalize all three vectors
    N = normalize(N);
    L = normalize(L);
    V = normalize(V);

    // Calculate R by reflecting -L around the plane defined by N
    vec3 R = reflect(-L, N);
    
    // Calculate the diffuse and specular contributions
    vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
    vec3 specular = pow(max(dot(R, V), 0.0), specular_power) * specular_albedo;
        
    // Send the color output to the fragment shader
    vs_out.color = ambient + diffuse + specular;
    
    // Calculate the clip-space position of each vertex
    gl_Position = proj_matrix * P;
}

除非你使用了很高等級的曲面細分杠氢,否則對于一個指定的三角形,它只有3個頂點另伍,同時會產(chǎn)生大量的片段填充這個三角形圖元鼻百。在這種情況下逐頂點的光照計算方式以及高洛德著色技術(shù)變得十分高效,只需要對每個頂點計算一次光照形成的顏色值摆尝。下圖為示例程序PhongLighting的渲染效果温艇。源碼傳送門

2.1.5 馮氏著色

在上圖中,可以很清晰的看出高洛德著色的一個缺點堕汞,即鏡面光亮顯示出明顯的星形光斑勺爱。在靜態(tài)圖像中,這僅僅是一個星形光斑讯检,但是對于運動的模型琐鲁,當球體旋轉(zhuǎn)時這個光斑會十分晃眼,也是不愿意被看見的视哑。這種現(xiàn)象是由于在三角形頂點中進行線性插值計算顏色時绣否,球面上相鄰的兩個三角形之間的顏色不連續(xù)所造成的。三角形內(nèi)的亮線在單個三角形內(nèi)部具有相同的顏色挡毅。一種解決這種光斑的方案是將三角形劃分得更小蒜撮。

另外一個能夠取得更好效果的方法稱為馮氏著色(Phong Shading)。需要注意的是馮氏著色和馮氏光照模型是兩個不同的概念跪呈,盡管他們都是有同一個人在同一時期所發(fā)明的段磨。馮氏著色法不再是對每個頂點進行顏色插值計算,而是對每個頂點的法向量進行插值計算耗绿,再用得到的結(jié)果在對每個像素計算光照顏色值苹支。示例程序phonglighting經(jīng)過一定修改,使用馮氏著色得到如下的圖像误阻。源碼傳送門

這種做法的代價是我們需要在片段著色器中執(zhí)行更多的計算操作债蜜,這樣會使得程序渲染同一個模型花費更多的時間晴埂。新的頂點著色器代碼如下。

#version 420 core
// Per-vertex inputs
layout (location = 0) in vec4 position; 
layout (location = 1) in vec3 normal;

// Matrices we’ll need
layout (std140) uniform constants {
    mat4 mv_matrix;
    mat4 view_matrix;
    mat4 proj_matrix; 
};

// Inputs from vertex shader
out VS_OUT {
    vec3 N; 
    vec3 L; 
    vec3 V;
} vs_out;
    
// Position of light
uniform vec3 light_pos = vec3(100.0, 100.0, 100.0); 

void main(void) {
    // Calculate view-space coordinate
    vec4 P = mv_matrix * position;
    // Calculate normal in view-space
    vs_out.N = mat3(mv_matrix) * normal; 
    // Calculate light vector
    vs_out.L = light_pos - P.xyz;
    // Calculate view vector
    vs_out.V = -P.xyz;
    // Calculate the clip-space position of each vertex
    gl_Position = proj_matrix * P;
}

每個像素的顏色都根據(jù)插值后的平面法向量寻定,光源向量和視口向量計算儒洛,而不是通過每個頂點的顏色進行插值計算。在頂點著色器中輸出每個頂點的這三個向量分別為vs_out.N狼速,vs_out.L和vs_out.V琅锻。在片段著色中就可以得到插值后到向量,其源碼如下向胡。

#version 420 core 
// Output
layout (location = 0) out vec4 color;

// Input from vertex shader
in VS_OUT {
    vec3 N; 
    vec3 L; 
    vec3 V;
} fs_in;
    
// Material properties
uniform vec3 diffuse_albedo = vec3(0.5, 0.2, 0.7); 
uniform vec3 specular_albedo = vec3(0.7);
uniform float specular_power = 128.0;

void main(void) {
    // Normalize the incoming N, L, and V vectors
    vec3 N = normalize(fs_in.N); 
    vec3 L = normalize(fs_in.L); 
    vec3 V = normalize(fs_in.V);

    // Calculate R locally
    vec3 R = reflect(-L, N);
    
    // Compute the diffuse and specular components for each fragment
    vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
    vec3 specular = pow(max(dot(R, V), 0.0), specular_power) * specular_albedo;
        
    // Write final color to the framebuffer
    color = vec4(diffuse + specular, 1.0); 
}

在現(xiàn)代的圖像硬件上恼蓬,通常使用的是類似馮氏著色方法的高質(zhì)量渲染方法。視覺效果通常是我們更關(guān)注的僵芹,因此需要在性能上做一定的讓步处硬。當然,在低性能低設備上拇派,或者場景中已經(jīng)于很多耗性能的任務需要執(zhí)行時郁油,高洛德著色法依然是最好的選擇。正如我們所見攀痊,一個通用的著色器優(yōu)化法制就是盡可能的將片段著色器的任務移動到頂點著色器執(zhí)行,但是有時為了更高質(zhì)量的渲染效果也不得不做性能低妥協(xié)拄显。

無論是逐頂點或者逐片段顏色計算苟径,馮氏光照計算公式中的主要參數(shù)是漫反射率,鏡面反射率和鏡面反光度躬审。前兩個參數(shù)可以計算出模擬材質(zhì)漫反射和鏡面反射的顏色值棘街。通常情況下他們的值相同,或者漫反射率反映的是材質(zhì)的顏色承边,而鏡面反射率為1遭殉,即全反射。當然你也可以設置鏡面反射率完全不同于漫反射率博助。鏡面反光度控制了鏡面高光的收斂速度险污。下圖顯示了設置不同鏡面反射率和鏡面反光度時得到的渲染結(jié)果。場景中具有單一的白光源富岳。從左到右蛔糯,鏡面反光率從0變化到1,從上到下窖式,鏡面反光度從4增加到256蚁飒,每隔1行,該值增加1倍萝喘。左上角球體的鏡面反光看上去暗淡并且均勻淮逻,然而右下角的球體鏡面反光看上去更亮并且收斂琼懊。源碼傳送門

盡管上圖僅僅模擬了白色的點光源,對于其他顏色的光源爬早,像素顏色的計算方式也類似哼丈。

2.2 賓氏-馮氏光照模型

賓氏-馮氏光照模型(Blinn-Phong Lighting Model)可以看作是馮氏光照模型的一個擴展和優(yōu)化。在馮氏光照模型中凸椿,我們計算了每個頂點或者片段的R?和N?向量點積削祈。然而我們可以使用向量N?和H?的點積計算出的點積替代,這里向量H?是光源向量L?和視口向量E?的中間向量脑漫,可以通過如下方式計算髓抑。

技術(shù)上講,在應用馮氏光照公式的地方都需要做這樣的計算优幸,并且在計算的每一步都需要對向量執(zhí)行標準化操作(向量需要除以自身的模)吨拍。雖然這看上去計算任務較大,但是我們卻不需要再計算向量R?网杆,避免了調(diào)用計算反射向量的函數(shù)〗ト埽現(xiàn)代的圖像處理器都足夠強大,計算標準向量H?的成本和計算反射向量的成本差異幾乎可以忽略讲竿。然而翅娶,如果三角形圖元所在曲面的曲率足夠小,三角形圖元的大小相對于其與光源和觀察者的距離足夠小昼浦,向量H?在圖元內(nèi)部的變化并不會太明顯馍资,因此可以在頂點、幾何或者曲面細分計算著色器中計算出向量H?关噪,并將其作為flat類型的變量傳入到片段著色器中鸟蟹。即便這樣做會有一些誤差,但是也可以通過加大鏡面反光度來彌補使兔。示例程序BlinnPhong的片段著色器代碼如下建钥,其中使用賓氏-馮氏光照模型對每個片段的顏色進行計算。

#version 420 core 
// Output 
layout (location = 0) out vec4 color;

// Input from vertex shader
in VS_OUT {
    vec3 N; 
    vec3 L; 
    vec3 V;
} fs_in;
    
// Material properties
uniform vec3 diffuse_albedo = vec3(0.5, 0.2, 0.7); 
uniform vec3 specular_albedo = vec3(0.7);
uniform float specular_power = 128.0;

void main(void) {
    // Normalize the incoming N, L, and V vectors
    vec3 N = normalize(fs_in.N); 
    vec3 L = normalize(fs_in.L); 
    vec3 V = normalize(fs_in.V);
        
    // Calculate the half vector, H
    vec3 H = normalize(L + V);
    // Compute the diffuse and specular components for each fragment
    vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo; 
    // Replace the R.V calculation (as in Phong) with N.H
    vec3 specular = pow(max(dot(N, H), 0.0), specular_power) * specular_albedo;
    // Write final color to the framebuffer
    color = vec4(diffuse + specular, 1.0); 
}

下圖是分別使用馮氏光照模型(左側(cè))和賓氏-馮氏光照模型(右側(cè))得到的圖片虐沥,其中馮氏光照模型使用的反光度為128熊经,而賓氏-馮氏光照模型使用的反光度為200。在調(diào)整該參數(shù)后置蜀,兩種光照模型得到了相似的結(jié)果奈搜。源碼傳送門

2.3 邊緣光照

邊緣光照(Rim Lighting)也稱為背光(Back Lighting),它指被觀察模型在觀察者和光源之間時盯荤,光從模型的邊緣向模型內(nèi)部滲透的效果馋吗,或者模型的陰暗面沒有光照的效果。邊緣光照是由被照射的模型輪廓發(fā)光而得名秋秤。在攝影中宏粤,可以通過將被觀察到物體置于相機和光源之間獲得這種效果脚翘,而在計算機圖形學中,可以通過模擬觀察方向獲得近似的效果绍哎。

模擬邊緣光照需要視點向量和表面法向量来农,在前面描述的兩個光照模型中我們已經(jīng)描述過著兩個向量。當觀察方向時面對平面時崇堰,視點向量和平面法向量共線沃于,邊緣光照效果最不明顯。當觀察方向和平面法向量垂直時海诲,邊緣光照效果最明顯繁莹。

下圖演示了邊緣光照和視點向量V?和平面法向量N?之間的關(guān)系。圖中特幔,向量N?1和V?1幾乎垂直咨演,此時從光源繞著物體的輪廓投射到被觀測片段的光線最多。然而向量N?2和V?2幾乎是共線同向的蚯斯,此時光源幾乎完全被物體所遮擋薄风,投射到該片段上的輪廓光最少。

向量的點積計算方便拍嵌,它與兩個向量的角度成比例遭赂。當兩個標準向量平行且同向時,它們的點積為0横辆,當它們正交時嵌牺,點積為0。因此我們可以通過計算視點向量和平面法向量的點擊龄糊,再通過對其取反比,從而計算邊緣光募疮。為了更好的模擬邊緣光照炫惩,我們引入了一個亮度系數(shù)和一個收斂指數(shù),邊緣光最終的計算公式如下阿浓。

在上面的公式中他嚷,向量N?和V? 分別為平面法向量和視點向量,Crim和Prim分別是邊緣光的顏色和亮度芭毙,Lrim為最終計算出某個片段的邊緣光筋蓖。下面的著色器代碼片段使用該公式計算邊緣光。

// Uniforms controlling the rim light effect
uniform vec3 rim_color; 
uniform float rim_power;
vec3 calculate_rim(vec3 N, vec3 V) {
    // Calculate the rim factor
    float f = 1.0 - dot(N, V);
    // Constrain it to the range 0 to 1 using a smooth step function
    f = smoothstep(0.0, 1.0, f);
    // Raise it to the rim exponent
    f = pow(f, rim_power);
    // Finally, multiply it by the rim color
    return f * rim_color; 
}

示例程序RimLight使用馮氏光照模型退敦,應用了邊緣光照效果粘咖,其渲染結(jié)果如下圖。左上角的圖禁用了邊緣光照侈百,右上角的圖應用了中等強度的邊緣光和中等級的光強度瓮下,可以看見龍的肚子邊緣處有環(huán)境光效果形成的白光翰铡。左下角的圖增加了邊緣光的顏色和光強度,能夠看到邊緣光變得更加收斂讽坏,或者更聚焦了锭魔。右下角的圖調(diào)低了邊緣光顏色和強度,這使得邊緣光向模型內(nèi)部滲透得更遠路呜,看上去像環(huán)境光的效果迷捧。Demo傳送門

對于一個給定的場景,邊緣光的顏色通常是固定的胀葱,或者隨著物體在世界坐標系中的位置不同產(chǎn)生變化(或者是不同的物體被不同顏色的光源照量漠秋,當然這看上去十分怪異)。然而巡社,邊緣光的能量本質(zhì)上可以理解為是邊緣光向物體中滲透的強度膛堤,它隨著材質(zhì)的不同而發(fā)生變化。例如在頭發(fā)晌该、毛皮等軟材質(zhì)肥荔,或者大理石等半透明材質(zhì)上,邊緣光的滲透能力更強朝群,在如木頭燕耿、巖石等硬材質(zhì)上,邊緣光的滲透能力更弱姜胖。

2.4 法線貼圖

到目前為止計算片段光照顏色的示例中誉帅,我們使用了高洛德著色和馮氏著色兩種顏色計算方法。前者對每個頂點的顏色進行計算右莱,然后在各個頂點中插值從而得到每個片段的顏色蚜锨。后者通過每個頂點的屬性計算出一些向量,然后將向量在頂點中插值從而計算出每個片段的近似向量慢蜓,如法向量亚再,最后再計算出每個片段的顏色。為了使物體表面更逼真晨抡,也為了使頂點間插值產(chǎn)生的誤差更小氛悬,大多數(shù)情況下我們需要在OpenGL管道中使用大量幾何圖形,這樣三角形就會足夠小耘柱,誤差就會更新如捅,但這樣每個三角形只能覆蓋少量的像素。

一種不需要增加頂點數(shù)量调煎,仍能提高模型表面細節(jié)的方式是法線(Normal Mapping)貼圖镜遣,有時也稱為凹凸貼圖(Bump Mapping)。想要應用法線貼圖士袄,我們首先需要一個能存儲模型表面每個紋素法向量的紋理烈涮。然后將該紋理應用到模型上朴肺,在片段著色器中計算出每個片段的法向量,再利用某個選定的光照模型計算出每個片段經(jīng)光照后應該呈現(xiàn)的顏色坚洽。一個法線貼圖使用的包含法向量的紋理如下戈稿。

需要注意的是,盡管頂點屬性也能定義法線讶舰,但是這樣只能對一個圖元的特定幾個頂點定義鞍盗,并且它定義的是曲面的法向量。而通過法線貼圖跳昼,我們可以對每個紋素的法線定義般甲,這樣就能定義更多的細節(jié)。

上圖中每個紋素的顏色值RGB都可以轉(zhuǎn)換成為一個XYZ的三維單位向量鹅颊,其中顏色的取值為[0, 1]敷存,而向量坐標的取值為[-1, 1],只需要做簡單的映射即可堪伍。另外在上圖中锚烦,我們將整個蟲子的所有模型的法線貼圖都集成到了一個紋理中,這樣只需要在頂點屬性中傳入正確的紋理坐標就能取到對應紋素的法向量了帝雇,當然獲取頂點屬性的模型通常是由專業(yè)的軟件生成的涮俄。

在法線貼圖中最常用的是切線空間,它是一個局部的坐標系尸闸,其中z軸和曲面的法向量同向彻亲。在切線空間中的兩外兩個軸的向量被稱為切向量(Tangent Vectors)和副切向量(Bitangent Vectors),它們的選擇組合有很多吮廉,為了方便計算和統(tǒng)一苞尝,通常將它們和紋理的U、V向量對齊宦芦。如下圖中野来,藍色向量為曲面法向量,紅色向量為選定的切向量踪旷,綠色向量為副切向量。這里紋素的法向量并未列出是因為對于圖中的紋素豁辉,其紋素切向量和曲面切向量相同令野。

需要注意的是因為每個紋素的法向量都是是描述在它們自己的曲面切線空間內(nèi)部的,而每個紋素的曲面切線空間并不相同徽级,因此每個紋素的法向量都在不同空間內(nèi)气破。由于大多數(shù)的紋素法向量都和曲面法向量差異不大,因此它們的z軸分量都遠遠大于其他兩個分量餐抢,這也是為什么上面的法線紋理整體看上去呈現(xiàn)藍色的原因现使。

通常情況下每個頂點的切向量作為頂點的一個屬性低匙,已經(jīng)和位置等屬性一起被包含在模型文件中。這樣我們在頂點著色器中就可以知道切空間的法向量碳锈,切向量顽冶,由于我們建立切空間時使用的是正交坐標系,因此其副切向量可以由法向量和切向量的叉乘得到售碳。??由于OpenGL使用了右手坐標系强重,向量的叉乘不遵守乘法交換律,不能交換順序贸人。

通常的做法是在頂點坐標系中計算出光源向量间景、視點向量等變量后,將其傳遞到片段著色器中艺智,OpenGL會計算每個片段插值后的結(jié)果倘要,此時我們再用這些插值后的向量,和從顏色貼圖中取得的材質(zhì)漫射色十拣,和從法線貼圖中取得的片段法向量封拧,共同計算出每個片段的顏色。但是這里有一個問題父晶,光源向量哮缺、視點向量都是定義在視口空間內(nèi),而紋素法向量是定義在切空間內(nèi)甲喝,頂點屬性的向量定義在模型空間內(nèi)的尝苇,它們不能直接計算,因此我們不能直接計算埠胖。通常的做法是在頂點著色器內(nèi)將相關(guān)的向量都轉(zhuǎn)換到切空間內(nèi)計算糠溜,得到轉(zhuǎn)換后的視點向量和光源向量再傳入到頂點著色器中。

要將視圖空間中的向量轉(zhuǎn)換到切線空間內(nèi)直撤,首先需要使用曲面法向量非竿、切線向量、副切線向量構(gòu)建一個旋轉(zhuǎn)矩陣谋竖,只需要將這三個單位向量作為矩陣中的三行(如需將切線空間內(nèi)的點向視圖空間轉(zhuǎn)化红柱,則將這三個單位向量表示為三列)即可。其構(gòu)造方式如下蓖乘。

這樣在片段著色器中锤悄,我們得到轉(zhuǎn)換后的視點向量和光源向量,以及發(fā)現(xiàn)貼圖中的片段法線向量就都位于切線空間內(nèi)嘉抒,這樣我們就能使用馮氏照明模型正確的計算出片段的顏色零聚。

示例程序BumpMapping演示了上述的計算邏輯,其頂點著色器代碼如下。

#version 420 core
layout (location = 0) in vec4 position; 
layout (location = 1) in vec3 normal; 
layout (location = 2) in vec3 tangent; 
layout (location = 4) in vec2 texcoord;

out VS_OUT {
    vec2 texcoord; 
    vec3 eyeDir; 
    vec3 lightDir;
} vs_out;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform vec3 light_pos = vec3(0.0, 0.0, 100.0);

void main(void) {
    // Calculate vertex position in view space.
    vec4 P = mv_matrix * position;

    // Calculate normal (N) and tangent (T) vectors in view space from 
    // incoming object space vectors.
    vec3 N = normalize(mat3(mv_matrix) * normal);
    vec3 T = normalize(mat3(mv_matrix) * tangent);
    // Calculate the bitangent vector (B) from the normal and tangent vectors.
    vec3 B = cross(N, T);

    // The light vector (L) is the vector from the point of interest to 
    // the light. Calculate that and multiply it by the TBN matrix. 
    vec3 L = light_pos - P.xyz;
    vs_out.lightDir = normalize(vec3(dot(V, T), dot(V, B), dot(V, N)));

    // The view vector is the vector from the point of interest to the
    // viewer, which in view space is simply the negative of the position. 
    // Calculate that and multiply it by the TBN matrix.
    vec3 V = -P.xyz;
    vs_out.eyeDir = normalize(vec3(dot(V, T), dot(V, B), dot(V, N)));

    // Pass the texture coordinate through unmodified so that the fragment 
    // shader can fetch from the normal and color maps.
    vs_out.texcoord = texcoord;

    // Calculate clip coordinates by multiplying our view position by 
    // the projection matrix.
    gl_Position = proj_matrix * P;
}

該示例程序片段著色器代碼如下隶症。

#version 420 core 
out vec4 color;
// Color and normal maps
layout (binding = 0) uniform sampler2D tex_color; 
layout (binding = 1) uniform sampler2D tex_normal;

in VS_OUT {
    vec2 texcoord; 
    vec3 eyeDir; 
    vec3 lightDir;
} fs_in;

void main(void) {
    // Normalize our incoming view and light direction vectors.
    vec3 V = normalize(fs_in.eyeDir);
    vec3 L = normalize(fs_in.lightDir);
    // Read the normal from the normal map and normalize it.
    vec3 N = normalize(texture(tex_normal, fs_in.texcoord).rgb * 2.0 - vec3(1.0));
    // Calculate R ready for use in Phong lighting.
    vec3 R = reflect(-L, N);
   
    // Fetch the diffuse albedo from the texture.
    vec3 diffuse_albedo = texture(tex_color, fs_in.texcoord).rgb; 
    // Calculate diffuse color with simple N dot L.
    vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
    // Uncomment this to turn off diffuse shading
    // diffuse = vec3(0.0);
    
    // Assume that specular albedo is white - it could also come from a texture
    vec3 specular_albedo = vec3(1.0);
    // Calculate Phong specular highlight
    vec3 specular = max(pow(dot(R, V), 5.0), 0.0) * specular_albedo; 
    // Uncomment this to turn off specular highlights
    // specular = vec3(0.0);
    
    // Final color is diffuse + specular
    color = vec4(diffuse + specular, 1.0); }

上面的著色器計算結(jié)果會根據(jù)法線貼圖中的細節(jié)來表現(xiàn)鏡面高光政模,計算過程不再依靠模型數(shù)據(jù)提供的幾何細節(jié)。在下圖中左上角的圖片顯示的是添加漫射光后的結(jié)果蚂会,右上角的圖片是添加反射光的結(jié)果淋样,左下角的圖片是這兩種效果的組合。作為對照颂龙,右下角的圖是使用表面法向量在頂點之間插值后再計算各個片段顏色的方式得到的結(jié)果习蓬。我們可以明顯看出左下角使用了法線貼圖的計算方式比右下角的圖片多出了更多的細節(jié)。Demo傳送門

2.5 環(huán)境貼圖

前面的幾個小節(jié)已經(jīng)介紹了如何在模型的表面應用光照效果計算片段顏色措嵌。但是對于現(xiàn)實世界中的一個具體環(huán)境躲叼,想要在著色器中模擬環(huán)境光照可能使其內(nèi)部邏輯變得十分復雜,最終影響程序的性能企巢。另外枫慷,實際上我們也不能建立一個能表示任意環(huán)境的公式。環(huán)境貼圖(Environment Mapping)很好的解決了這個問題浪规,它在避免復雜的光學效果計算同時或听,仍能很好的模擬光滑表面(如鏡子等)對周圍環(huán)境的反射效果。在實時圖像處理程序中常見的環(huán)境貼圖類型有球面環(huán)境貼圖(Spherical Environment Map)笋婿,等矩形球面環(huán)境貼圖(Equirectangular Map)誉裆,和立方體環(huán)境貼圖(Cube Map)。球面環(huán)境貼圖模擬環(huán)境投影到一個半球體上缸濒,再將其投影到一個圓形來表示不同法向量下投影的環(huán)境顏色足丢。相對于球面環(huán)境貼圖只能使用一個半球表示被投影的環(huán)境,等矩形球面貼圖將球面坐標投影到一個矩形上庇配,從而能夠表示360度全方位環(huán)境細節(jié)斩跌。立方體環(huán)境貼圖是一個由6個平面組成的特殊紋理,它可以被看成是一個玻璃盒子捞慌,當觀察點位于立方體中心時能夠完整的看到整個環(huán)境耀鸦。后面的小節(jié)會詳細介紹這三種類型的環(huán)境貼圖。

2.5.1 球面環(huán)境貼圖

前面已經(jīng)講到使用要模擬的材質(zhì)建立一個球體模型啸澡,然后將模擬環(huán)境產(chǎn)生的光投影到這個球體上袖订,最終將其投影到一個平面的圓形圖片上就得到了球面環(huán)境貼圖。在渲染場景時嗅虏,通過計算渲染模型的每個片段視點向量和表面法向量的關(guān)系計算出正確的紋理坐標洛姑,再到環(huán)境貼圖中查詢正確的光照系數(shù),從而模擬環(huán)境光照效果旋恼。盡管這個系數(shù)可以存儲任意和光照相關(guān)的系數(shù),但是在最簡單的場景中,這個系數(shù)就是在模擬的環(huán)境光照下的顏色值冰更。下圖是一些球面環(huán)境貼圖的例子产徊。

圖中,法向量為和屏幕垂直蜀细,正方向朝外舟铜,由于球面被投影到了xy平面上,圓心到球面每一個點都是三維空間中的單位向量奠衔,因此當我們計算出視點單位向量時谆刨,直接使用其xy值就能計算出對應的光照系數(shù)。

實現(xiàn)球面環(huán)境貼圖的第一步是在頂點著色器中將輸入的頂點法向量轉(zhuǎn)換到視圖空間中归斤,并計算出每個頂點的觀察向量痊夭。在片段著色器中將會使用這兩個變量計算出紋理的坐標從而在環(huán)境貼圖中查詢出具體紋素。頂點著色器的代碼如下脏里。

#version 420 core 

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;

out VS_OUT {
    vec3 normal;
    vec3 view; 
} vs_out;

void main(void) {
    vec4 pos_vs = mv_matrix * position;
    vs_out.normal = mat3(mv_matrix) * normal; 
    vs_out.view = pos_vs.xyz;
    gl_Position = proj_matrix * pos_vs;
}

在頂點著色器運行后她我,我們在片段著色器中就能夠得到每個片段的法向量和視點單位向量,從而就能夠計算出正確的坐標迫横,再到環(huán)境貼圖中查詢到正確的紋素番舆。對應的片段著色器代碼如下。

#version 420 core

layout (binding = 0) uniform sampler2D tex_envmap;

in VS_OUT {
    vec3 normal;
    vec3 view; 
} fs_in;

out vec4 color;

void main(void) {
    // 計算標準觀測向量
    vec3 u = normalize(fs_in.view);

    // 計算觀測向量于法向量的反射向量
    vec3 r = reflect(u, normalize(fs_in.normal));
    
    // 假設球面貼圖上存在點A矾踱,由于球面貼圖的制作原理恨狈,認為球面上的
    // 每個點都是無窮遠處觀察到的反射景色,因此從觀測點到球面任意一點
    // 的單位觀測向量都可以認為是(0呛讲,0禾怠,-1),即對于每個興趣點視點向
    // 量都為(0, 0, 1)圣蝎,片段的單位反射向量和該向量之和標準化后即可得
    // 到球面貼圖中的法向量刃宵,即可以用于確定片段的顏色
    vec3 sphericalR = normalize(r + vec3(0, 0, 1));

    // 標準向量的三個分量的取值范圍為[-1,1],需要將其值映射到[0,1]的
    // 區(qū)間才能正確的計算出紋理坐標對于單位球面上的點A徘公,其在球形紋理貼
    // 圖中的投影坐標可以由其x和y值表示
    color = texture(tex_envmap, sphericalR.xy * 0.5 + vec2(0.5));
}

使用上圖球面環(huán)境貼圖的最后一張圖像渲染后的結(jié)果如下圖牲证,源碼傳送門

2.5.2 等矩形球面環(huán)境貼圖

等矩形球面環(huán)境貼圖(Equirectangular Environment Maps)和球面環(huán)境貼圖類似关面,但是它更不容易出現(xiàn)圖像變形坦袍,這種變形在球體環(huán)境貼圖的極點比較明顯。下圖是一個等矩形球面環(huán)境貼圖的例子等太。

這里仍然需要在頂點著色器中計算出視圖空間內(nèi)的法向量和觀察向量捂齐,在片段著色器中得到插值結(jié)果后,計算出觀察向量的反射向量缩抡。

在說明紋理坐標方式的時候先簡要說明如何將球面坐標投影到等矩形坐標之上奠宜,即等距球面投影(Equidistant Cylindrical Projection)。將單位球體放入到一個圓柱體中,將其經(jīng)線映射為投影后的x坐標压真,將其緯線映射為投影后的y坐標娩嚼,這樣在投影變換后的紋理中,等距離經(jīng)線和緯線距離仍不會發(fā)生改變滴肿。值得關(guān)注的是我們平時看到的世界地圖也有采用類似的投影技術(shù)岳悟。另外這種投影方式會發(fā)生變形,緯度越高泼差,形變越大贵少。

因此在計算紋理坐標時,我們首先提取出y分量堆缘,這可以理解為高度滔灶,然后將
其設置為0,再對向量進行標準化操作套啤,從而將反射向量投影在xz平面上宽气。在這個標準化向量中提取出x分量從而得到第二個紋理坐標,這可以理解為其方位潜沦。這樣便能組合出高度和方位角萄涯,就能夠在等矩形球面環(huán)境貼圖中查詢到正確的紋素。

下面的片段著色器演示了示例程序Equirectangular中如何使用等矩形球面環(huán)境貼圖唆鸡。

#version 410 core

// 統(tǒng)一變量涝影,環(huán)境紋理采樣器
uniform sampler2D tex_envmap;

// 輸入變量
in VS_OUT {
    vec3 normal;
    vec3 view;
} fs_in;

// 輸出變量
out vec4 color;

void main(void) {
    // 計算標準觀察向量
    vec3 u = normalize(fs_in.view);

    // 計算視圖坐標系中的觀察向量沿法向量的反射向量,即為“光源向量”
    vec3 r = reflect(u, normalize(fs_in.normal));

    // 計算環(huán)境紋理貼圖中的采樣坐標
    vec2 tc;
    // 反射向量的y被直接投影到環(huán)境紋理中的t坐標争占,可以理解為高度
    tc.y = r.y;
    // 將反射向量y軸分量設置為0燃逻,在求其標準向量,從而將其投影到xz平面臂痕,再通過其x和z值計算出環(huán)境紋理貼圖中的采樣坐標s分量
    r.y = 0.0;
    tc.x = normalize(r).x;

    // 1. 等矩形球面貼圖的制作方式?jīng)Q定了其坐標的原點和被投影模型在視圖空間中的(x:0伯襟,z:-1)對應
    // 2. 并且x軸和s軸是線性相關(guān),也就是直接將模型投影到xy平面上握童,因此從被投影模型(xz)坐標到紋理坐標(st)的計算方式如下
    // 當z > 0時姆怪,tc.s = 0.5 + 0.25tc.s    => tc.s = 0.75 - 0.25 * sign(r.z) + 0.25 * tc.s * sign(r.z)
    // 當z < 0時,tc.s = 1 - 0.25tc.s      => tc.s = 0.75 - 0.25 * sign(r.z) + 0.25 * tc.s * sign(r.z)
    // 合并上面兩個情況 tc.s = 0.75 - sign(r.z) * 0.25 (1 - tc.s) = 0.75 - s * 0.25 * (1 - tc.s)
    
    // 需要注意在被投影模型的(x: 0, z: -1) -> (x: -1, z: 0) -> (x: 0, z: 1) -> (x: 1, z: 0) -> (x: 0, z: -1)
    // 變化過程中其紋理坐標tc.s分別被投影成了0 -> 1.25/0.25(跳躍間斷點) -> 0.5 -> 0.75 -> 1
    // 由于我們設置了紋理過濾模式為Repeat澡绩,因此上述變化可以被映射到0 -> 0.25 -> 0.5 -> 0.75 -> 1稽揭,因此能夠正確采樣
    float s = sign(r.z);
    tc.s = 0.75 - s * 0.25 * (1 - tc.s);
    // 將從被投影模型的到的y軸坐標分量取值區(qū)間為[-1, 1]映射至[0, 1]之間
    tc.t = 0.5 + 0.5 * tc.t;

    // 從環(huán)境紋理采樣確定片段最終顏色
    color = texture(tex_envmap, tc);
}

其渲染結(jié)果如下圖,Demo傳送門

2.5.3 立方體環(huán)境貼圖

盡管立方體環(huán)境貼圖(Cube Map)被當作是單個紋理對象肥卡,但是它是由6個正方體的2維紋理組成溪掀,它們構(gòu)成正方體的每一個面。從3維光照貼圖步鉴,到反射光貼圖揪胃,和高精度環(huán)境貼圖都可以看見它的應用璃哟。下圖是展示了一個立方體環(huán)境貼圖的六個面,我們將在示例程序CubeMap中用到喊递。

在繼續(xù)討論使用立方體環(huán)境貼圖時紋理坐標的計算方式之前沮稚,簡單說明下如何從球體投影到立方體貼圖。假設有一個單位球體册舞,其表面反射除周圍的環(huán)境。在球體中心存在一個光源障般,其發(fā)散出的光投影在立方體的六個內(nèi)表面调鲸,單位球面上所有的紋素就能夠被投影到立方體貼圖中。

加載立方體紋理首先需要創(chuàng)建一個紋理對象挽荡,并將其綁定到靶點GL_TEXTURE_CUBE_MAP上藐石,然后調(diào)用函數(shù)glTexStorage2D()為紋理對象分配內(nèi)存空間,再對立方體的每個面調(diào)用函數(shù)glTexSubImage2D()向紋理中填充數(shù)據(jù)定拟。在該函數(shù)中我們會用到6個特殊的靶點GL_TEXTURE_CUBE_MAP_POSITIVE_X,GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 和GL_TEXTURE_CUBE_MAP_NEGATIVE_Z于微。他們是連續(xù)的數(shù)字,因此我們可以通過一個簡單的循環(huán)來填充紋理數(shù)據(jù)青自。其代碼如下株依。

GLuint texture;

glGenTextures(1, &texture); 
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);

glTexStorage2D(GL_TEXTURE_CUBE_MAP, levels, internalFormat, width, height);
for (face = 0; face < 6; face++) {
    glTexSubImage2D(GL_TEXURE_CUBE_MAP_POSITIVE_X + face,
                    0,
                    0, 0,
                    width, height,
                    format, type,
                    data + face * face_size_in_bytes);
}

立方體環(huán)境貼圖同樣支持多分辨率貼圖,如果要使用這種類型的紋理延窜,上面的代碼只需要進行簡單的修改恋腕。Khronos的紋理文件格式都支持立方體環(huán)境貼圖,本書中的.KTX文件加載器也可以幫組你完成這部分工作逆瑞。

盡管立方體紋理貼圖是一系列二維紋理的集合荠藤,但是在使用時仍需要三維的紋理坐標。在真實的三維紋理中获高,由S吵冒、T和R分量構(gòu)成的一個有向向量以紋理中心為原點镣隶,向外延伸至一個具體的點。而在立方體紋理中,這個向量將會和立方體的某個面相交吴裤,在焦點附近的紋素將被采樣并計算出最終的顏色值。

立方體紋理最常用的場景是創(chuàng)建一個模型再悼,其表面反射了周圍環(huán)境馅袁。利用立方體貼圖還可以創(chuàng)建出一個天空盒(Sky box),它可以完全反射出周圍的環(huán)境严就。天空盒可以理解為一個盒子包含了整個場景总寻,在游戲中應用使得玩家站在盒子中心能夠看到非常理想的環(huán)境,盒子模型的每個內(nèi)表面分別投影出從屏幕中心向六個方向的風景梢为。

要渲染立方體紋理貼圖渐行,我們需要以觀察者為中心繪制一個大的立方體轰坊,并對其應用環(huán)境貼圖。然而我們有更簡單的方法實現(xiàn)這個目的祟印,模擬的立方體模型超出視野的部分都將會被裁剪掉肴沫,另外我們還需要觀察的整個場景都是周圍的環(huán)境,從而獲得沉浸式的體驗蕴忆。假設我們只觀察環(huán)境中的一個面颤芬,我們可以簡單的繪制一個全屏幕的四邊形,接下來需要做的事情就是計算視野的四個角上的紋理坐標套鹅,從而就能渲染出立方體貼圖站蝠。

在本例子中,立方體紋理直接映射到模擬的立方體中卓鹿,因此模擬立方體的頂點位置就是紋理坐標菱魔,再經(jīng)過觀察矩陣的子矩陣(因為無需考慮投影問題,也就不需要使用齊次坐標吟孙,選取左上3??3子矩陣)對這些坐標進一步處理澜倦,使其和視圖空間內(nèi)的模型對其。這些操作都在頂點著色器中進行杰妓,其代碼如下藻治。

#version 410 core

// 統(tǒng)一變量,仿射矩陣
uniform mat4 view_matrix;

// 輸出變量
out VS_OUT {
    vec3    tc;
} vs_out;

void main(void) {
    // 1. 定義一個全窗口的矩形模型
    vec3[4] vertices = vec3[4](vec3(-1.0, -1.0, 1.0),
                               vec3( 1.0, -1.0, 1.0),
                               vec3(-1.0,  1.0, 1.0),
                               vec3( 1.0,  1.0, 1.0));
    // 2. 計算采樣紋理坐標
    // 立方體紋理使用的采樣紋理坐標是觀察向量巷挥,這個向量沒有必要是標準向量栋艳,
    // 只要指定了方向,OpenGL的紋理采樣器就能夠獲取到正確的像素顏色
    vs_out.tc = mat3(view_matrix) * vertices[gl_VertexID];

    // 3. 計算頂點在投影空間的位置
    // 繪制全窗口的矩形不涉及到投影變換句各,因此這里直接使用設置好的頂點坐標
    gl_Position = vec4(vertices[gl_VertexID], 1.0);
}

因為我們使用硬編碼的方式定義了立方體某個平面的坐標吸占,因此我們不需要額外的頂點屬性,也不需要額外的緩存對象來存儲這些數(shù)據(jù)凿宾。另外矾屯,可以通過調(diào)整每個頂點的z軸分量來實現(xiàn)對該平面的縮放,z軸分量越大初厚,最后模型經(jīng)過透視投影到屏幕空間內(nèi)的圖像就會越小件蚕。渲染立方體環(huán)境貼圖的片段著色器也很簡單,其源碼如下产禾。

#version 410 core

// 統(tǒng)一變量:立方體環(huán)境貼圖采樣器
uniform samplerCube tex_cubemap;

// 輸入變量
in VS_OUT {
    vec3    tc;
} fs_in;

// 輸出變量
layout (location = 0) out vec4 color;

void main(void) {
    // 計算該片段的最終顏色
    color = texture(tex_cubemap, fs_in.tc);
}

當渲染好天空盒后排作,再渲染一個小的模型在場景中心,使其反射出天空盒構(gòu)建出的環(huán)境亚情。前面已經(jīng)講過妄痪,用于在立方體環(huán)境貼圖中查詢紋理的坐標是以其中心為原點,朝向外的一個有向向量楞件,通過和立方體紋理表面相交從而獲得最終的顏色衫生,我們需要做的只是計算出這些向量裳瘪,剩下的OpenGL都會幫我們完成。同學我們需要計算出每個片段的觀察向量和法向量罪针。

這些工作都在頂點著色器中完成彭羹,計算好的數(shù)據(jù)將會被傳遞到片段著色器中,并被標準化處理泪酱。和之前的很多例子一樣派殷,我們需要根據(jù)片段法向量定義的平面計算出觀察向量的反射向量。假定天空盒中的場景都足夠遠墓阀,反射向量都被認為是從圓心發(fā)射而出愈腾,則這些反射向量可以被看作是紋理坐標。頂點著色器的代碼如下岂津。

#version 410 core

// 輸入變量
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;

// 統(tǒng)一變量:仿射矩陣
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

// 輸出變量
out VS_OUT {
    vec3 normal;
    vec3 view;
} vs_out;

void main(void) {
    // 1. 計算頂點在視圖坐標系中的坐標
    vec4 pos_vs = mv_matrix * position;
    // 2. 將頂點法向量轉(zhuǎn)換到視圖坐標系
    vs_out.normal = mat3(mv_matrix) * normal;
    // 3. 計算興趣點點觀察矩陣
    vs_out.view = pos_vs.xyz;
    // 4. 計算頂點在投影坐標系下的坐標
    gl_Position = proj_matrix * pos_vs;
}

片段著色器的代碼如下。

#version 410 core

// 輸入變量
in VS_OUT {
    vec3 normal;
    vec3 view;
} fs_in;

// 統(tǒng)一變量:立方體環(huán)境紋理采樣器
uniform samplerCube tex_cubemap;

// 輸出變量
out vec4 color;

void main(void) {
    // 1. 計算片段的反射向量悦即,追蹤光源位置
    vec3 r = reflect(fs_in.view, normalize(fs_in.normal));

    // 2. 使用反射向量在立方體環(huán)境紋理中采樣吮成,計算出該片段應該反射的環(huán)境顏色
    // 再乘以其材質(zhì)屬性的漫反射率得到最終的顏色
    color = texture(tex_cubemap, r) * vec4(0.95, 0.80, 0.45, 1.0);
}

上面的程序會在來一個天空盒,并在其中繪制了一個模型來反應天空盒的環(huán)境辜梳,其運行效果如下粱甫。源碼傳送門

當然,場景中心的模型并不一定是直接從立方體環(huán)境貼圖中獲取作瞄。例如茶宵,你可以將環(huán)境的顏色和模型本身材質(zhì)的顏色相乘從而得到其他有趣的結(jié)果。如下圖渲染了一條金龍宗挥。

2.6 材質(zhì)屬性

在前面的例子中乌庶,我們對整個模型使用的都是同一種材質(zhì)。這使得渲染出的整只龍看上去具有相同的光澤契耿,渲染出的瓢蟲看上去塑料感十足瞒大。然而,現(xiàn)實中一個模型可以由多種不同的材質(zhì)所組成搪桂。實際上透敌,我們可以定義每個表面,每個三角形踢械,甚至每個像素的材質(zhì)酗电,只需要將這些信息存儲在一個紋理對象中就可以達到這個目的。例如通過紋理存儲高光指數(shù)内列,在渲染模型的時候應用這個紋理撵术,就可以使模型中不同部分表現(xiàn)出不同程度的反射效果。

通過預模糊環(huán)境貼圖话瞧,再通過存儲在紋理中的光澤系數(shù)混合清晰和模糊的紋素可以使模型具有有趣的光澤荷荤。在這個例子中會再次使用到簡單的球面環(huán)境貼圖退渗。下圖分別是清晰的環(huán)境貼圖,模糊的環(huán)境貼圖蕴纳,以及用做提取光澤系數(shù)的光澤紋理会油。在光澤紋理中,越亮的部分使用越多的清晰環(huán)境紋素古毛,反之則使用更多的模糊環(huán)境紋素翻翩。

我們可以將兩個環(huán)境紋理合并成一個只有兩層的三維紋理。再將光澤紋理中查詢到的亮度值作為第三個坐標分量稻薇,從而在合并后的三維紋理中查詢顏色嫂冻。將清晰的環(huán)境貼圖作為第一層,將模糊后的環(huán)境貼圖作為第二層塞椎,這樣OpenGL就能夠自動在它們之間平滑插值桨仿,從而計算出最終的顏色。

負責讀取光澤系數(shù)案狠,環(huán)境紋理服傍,并計算最終每個片段的顏色的片段著色器源碼如下。

#version 420 core

layout (binding = 0) uniform sampler3D tex_envmap;
layout (binding = 1) uniform sampler2D tex_glossmap;

in VS_OUT {
    vec3 normal; 
    vec3 view; 
    vec2 tc;
} fs_in;

out vec4 color;

void main(void) {
    // u will be our normalized view vector
    vec3 u = normalize(fs_in.view);
    
    // Reflect u about the plane defined by the normal at the fragment
    vec3 r = reflect(u, normalize(fs_in.normal));
        
    // Compute scale factor
    r.z += 1.0;
    float m = 0.5 * inversesqrt(dot(r, r));
        
    // Sample gloss factor from glossmap texture
    float gloss = texture(tex_glossmap, fs_in.tc * vec2(3.0, 1.0) * 2.0).r; 
    
    // Sample from scaled and biased texture coordinate
    vec3 env_coord = vec3(r.xy * m + vec2(0.5), gloss);
        
    // Sample from two-level environment map
    color = texture(tex_envmap, env_coord);
}

示例程序PerPixelGloss的運行結(jié)果如下[WIP: Demo工程中3D紋理采樣仍存在問題骂铁,待查明]吹零。源碼傳送門

2.7 制作陰影

目前為止的所有光照著色器算法都假定每個片段的顏色都是由光照決定的,實際上在由多個模型的場景中拉庵,片段的最終顏色還受到其他因素影響灿椅。模型會在自己以及其他模型上投射陰影,如果這些陰影在最終渲染出的場景中被忽略掉钞支,那么整個場景看上去就不是那么真實茫蛹。這小節(jié)主要介紹一些技術(shù)來模擬模型產(chǎn)生的陰影效果。

2.7.1 陰影貼圖

任何陰影計算的第一步都是計算某個點是否被光源照射烁挟。實際上麻惶,我們必須計算出從某個點到光源之間是否有任何障礙物,這種計算也可以稱為可見性計算信夫。幸運的是深度緩存能夠是我們很高效的完成這一計算窃蹋。

陰影貼圖技術(shù)通過從光源的角度去渲染一個場景,能夠得到整個場景的可見性静稻。在這個計算過程中警没,我們只需要深度信息,因此我們在創(chuàng)建幀緩存對象的時候只需要添加一個深度附件振湾。以光源的視角渲染整個場景后杀迹,我們能夠得到光源照射到場景中最近模型片段的距離。當我們在正常渲染場景時押搪,可以計算每個片段到光源的距離树酪,并將其和從之前計算好的深度緩存中取出的距離值相比較浅碾,從而知道該片段是否可見。當然续语,在比較之前我們需要將待比較的片段坐標從視圖空間轉(zhuǎn)換到光源空間垂谢,也就是之前計算深度緩存時使用的坐標系統(tǒng)。

如果計算出當前片段到光源的距離大于從深度緩存中讀取出的最近可見片段距離疮茄,當前片段位于陰影中滥朱。實際上,這種可見性計算在圖像學領(lǐng)域是一個很常見的操作力试,甚至OpenGL還提供了一個特殊的采樣器來完成這部分工作徙邻,即陰影采樣器(Shadow Sampler)。在著色器語言中畸裳,對于2D紋理的采樣器使用關(guān)鍵字sampler2DShadow聲明缰犁,這也是我們將要在下面例子中使用到的采樣器類型。你也可以使用關(guān)鍵字sampler1DShadow聲明1維的陰影采樣器怖糊,使用關(guān)鍵字samplerRectShadow聲明矩形陰影采樣器帅容,甚至你可以聲明這些類型(除矩形陰影采樣器)對應的數(shù)組類型。

下面的代碼演示了在渲染陰影貼圖之前蓬抄,如何準備只有1個深度附件的幀緩存對象。

GLuint shadow_buffer; GLuint shadow_tex;

glGenFramebuffers(1, &shadow_buffer); 
glBindFramebuffer(GL_FRAMEBUFFER, shadow_buffer);

glGenTextures(1, &shadow_tex); 
glBindTexture(GL_TEXTURE_2D, shadow_tex); 
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT32, 
               DEPTH_TEX_WIDTH, DEPTH_TEX_HEIGHT); 

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE,
                GL_COMPARE_REF_TO_TEXTURE); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadow_tex, 0);
    
glBindFramebuffer(GL_FRAMEBUFFER, 0);

在上面的代碼中夯到,多次調(diào)用了函數(shù)glTexParameteri()和參數(shù)GL_TEXTURE_COMPARE_MODE以及GL_TEXTURE_COMPARE_FUNC設置了紋理寫入時的比較模式為參考值和紋理存儲值相比較嚷缭,比較函數(shù)為小于或者等于,即如果計算出某個片段深度值比紋理中存儲的值更小耍贾,就會替代紋理中對應的值阅爽。當我們創(chuàng)建了用于渲染深度的幀緩存對象后,就可以以光源的位置為視圖原點來渲染整個場景荐开。假定光源的位置為light_pos付翁,以該點指向世界坐標系的原點,我們就可以構(gòu)建出如下的光源的模型-視口投影矩陣晃听。

vmath::mat4 model_matrix = vmath::rotate(currentTime, 0.0f, 1.0f, 0.0f); 
vmath::mat4 light_view_matrix =
        vmath::lookat(light_pos,
                      vmath::vec3(0.0f),
                      vmath::vec3(0.0f, 1.0f, 0.0f);
vmath::mat4 light_proj_matrix =
       vmath::frustum(-1.0f, 1.0f, -1.0f, 1.0f,
                      1.0f, 1000.0f);
vmath::mat4 light_mvp_matrix = light_projection_matrix * 
                               light_view_matrix *
                               model_matrix;

前文的渲染任務結(jié)束后百侧,得到的幀緩存對象中存儲了從光源到以其為視角最近的模型片段距離。通過如下灰度圖我們可以直觀的觀察到這個結(jié)果能扒,對于模型區(qū)域佣渴,其中黑色部分深度值為0,白色部分深度值為1初斑,也就是說光源照射到的片段距光源最遠辛润。

在使用這些深度信息來生成陰影效果,我們需要對渲染著色器進行一定的修改见秤。首先我們需要聲明陰影采樣器砂竖,并從中讀取數(shù)據(jù)真椿。有趣的部分是如何計算從深度緩存中讀取數(shù)據(jù)所使用到的紋理坐標。實際上這很簡單乎澄,在頂點著色器中通常會計算裁剪坐標系中的位置突硝,即先將世界坐標系中的頂點投影到模擬出的相機視圖坐標系中,最后再投影到相機的截錐體中三圆。與此同時狞换,我們可以使用光源的視圖投影矩陣執(zhí)行類似的操作,將得到的結(jié)果傳遞到片段著色器中舟肉,最后在其運算的時候就能夠獲得每個片段在光源裁剪坐標系的位置修噪,從而計算出查詢深度紋理時所需要用到的坐標。

出來執(zhí)行坐標系轉(zhuǎn)換外路媚,我們還必須對得到的裁剪坐標進一步處理黄琼。需要記住,OpenGL中標準的裁剪坐標空間其x和y軸上的取值范圍為[-1.0, 1.0]整慎,而在y軸上的取值為[0, 1.0]脏款。將頂點坐標從物體空間轉(zhuǎn)換到光源裁剪空間的矩陣成為陰影矩陣(Shadow Matrix),其計算過程如下裤园。

const vmath::mat4 scale_bias_matrix = 
      vmath::mat4(vmath::vec4(0.5f, 0.0f, 0.0f, 0.0f),
                  vmath::vec4(0.0f, 0.5f, 0.0f, 0.0f),
                  vmath::vec4(0.0f, 0.0f, 0.5f, 0.0f),
                  vmath::vec4(0.5f, 0.5f, 0.5f, 1.0f));

vmath::mat4 shadow_matrix = scale_bias_matrix * light_proj_matrix * 
                            light_view_matrix * model_matrix;

陰影矩陣可以被作為統(tǒng)一變量傳遞到頂點著色器中撤师,一個簡化版本的頂點著色器如下。

#version 420 core

uniform mat4 mv_matrix; 
uniform mat4 proj_matrix; 
uniform mat4 shadow_matrix;

layout (location = 0) in vec4 position;

out VS_OUT {
    vec4 shadow_coord;
} vs_out;

void main(void) {
    gl_Position = proj_matrix * mv_matrix * position;
    vs_out.shadow_coord = shadow_matrix * position;
}

在頂點著色器中輸出變量shadow_coord經(jīng)過插值運算后被傳遞到片段著色器中拧揽,每個片段的陰影坐標接下來會被投影到標準設備坐標系中剃盾,從而被用于紋理坐標在之前得到的陰影紋理中查詢數(shù)據(jù)。通常的方式是將這些坐標都除以w軸分量淤袜,但是OpenGL提供的函數(shù)textureProj中會自動完成這一步處理痒谴。當我們使用該函數(shù)查詢陰影貼圖的時候,OpenGL會先將坐標的x铡羡、y和z軸分量都除以w分量积蔚,然后再使用處理后的x和y軸分量在紋理中查詢數(shù)據(jù),最后使用制定的函數(shù)將查詢到的值和處理后的z軸分量比較烦周,如果通過測試則返回1.0尽爆,否則返回0.0。

如果選擇的紋理過濾模式是GL_LINEAR或者開啟了多重采樣读慎,則OpenGL會對多個片段進行上述測試教翩,最終取它們的平均值返回,也就是說此時該函數(shù)返回的值將會在區(qū)間[0.0, 1.0]中贪壳。這樣我們就可以根據(jù)函數(shù)textureProj的返回值來判斷某個片段是否位于陰影之中饱亿。一個高度簡化版的陰影計算片段著色器如下。

#version 420 core

layout (location = 0) out vec4 color;

layout (binding = 0) uniform sampler2DShadow shadow_tex;

in VS_OUT {
    vec4 shadow_coord;
} fs_in;

void main(void) {
    color = textureProj(shadow_tex, fs_in.shadow_coord) * vec4(1.0); 
}

上述簡化版代碼渲染的結(jié)果并沒有應用到光照計算,只是簡單使用黑白的圖像演示模型的陰影渲染結(jié)果彪笼。在代碼中钻注,這里將從陰影紋理查詢結(jié)果直接乘以白色,實際上我們可以使用前文所介紹到的光照計算方法配猫,或者從紋理中加載紋素得到有光照的顏色幅恋,再將其和陰影紋理查詢到的結(jié)果想乘,從而得到最終的顏色泵肄。下圖是示例程序ShadowMapping的運行結(jié)果捆交,其中左圖展示了模型的陰影信息,右圖在此基礎(chǔ)上應用了光照效果腐巢。源碼傳送門

陰影貼圖也有缺點品追,這種技術(shù)對內(nèi)存的消耗非常大,對于每一個光源都需要生成一個應用貼圖冯丙,并且對于每個光源都需要復雜的計算決定片段是否位于陰影內(nèi)肉瓦,這樣成本很大。這種計算很容易快速累積胃惜,從而影響程序的性能泞莉。陰影貼圖的分辨率應該足夠大,使得屏幕空間內(nèi)的多個像素被映射到陰影紋理中的單個紋素船殉,這在執(zhí)行光照計算時比較高效鲫趁。最后,對于陰影區(qū)域的條帶和不規(guī)則圖案可能會出現(xiàn)自遮擋效果利虫“ず瘢可以通過幾何體位移(Polygon Offset)技術(shù)在一定程度上減弱這種影響。當開啟這個特性后列吼,OpenGL會自動對所有幾何體以及三角形應用一個微小的位移幽崩,使得它們遠離或者靠近觀察者苦始。調(diào)用如下函數(shù)可以配置該特性的參數(shù)寞钥。

void glPolygonOffset(GLfloat factor, GLfloat units);

片段的深度偏移值計算公式為offset = factor * change + units * smallChange,其中change是和集合體的深度位移系數(shù)陌选,其值和其在屏幕上的大小相關(guān)理郑,smallChange是深度緩存中兩個不同深度的最小差值,和具體的實現(xiàn)相關(guān)咨油。通常這兩個參數(shù)設置為1即可您炉,我們也可以通過設置不同的值來獲得我們想要的效果。當參數(shù)配置完成以后役电,調(diào)用函數(shù)glEnable()和參數(shù)GL_POLYGON_OFFSET_FILL就可以開啟深度位移功能赚爵,當然使用同樣的參數(shù)調(diào)用函數(shù)glDisable()可以關(guān)閉該功能。

2.8 環(huán)境特效

總的來說,場景的渲染需要建立光照模型冀膝,并且考慮光線和場景所處世界的交互唁奢。目前為止,我們在渲染模型的時候并未考慮到光線傳遞的介質(zhì)窝剖。通常情況下介質(zhì)是空氣麻掸,然而空氣并不是完全透明的,其中包含有細小顆粒赐纱,水蒸氣以及一些特殊氣體脊奋,這些微粒在光傳播的路徑上會吸收和散射光。模擬這種光的散射和吸收特性疙描,我們能夠渲染出帶有深度感覺的場景诚隙,從而推斷出景物的距離,這樣渲染出的場景會更接近現(xiàn)實世界淫痰。

2.8.1 霧

我們都對霧很熟悉最楷,在霧天,我們只能看到近距離的物體待错,濃密的霧還能帶有危險的感覺籽孙。霧是由懸浮在空氣中的水蒸汽,其他氣體以及如煙塵和污染物等固體顆粒組成火俄。當光穿過霧的時候回發(fā)生兩件事情犯建,部分光線會被這些微粒吸收,另外部分光線會在這些顆粒表面折射瓜客,甚至這些顆粒自己也會發(fā)出新的光線适瓦。光線被霧吸收被稱為消光(Extinction),小消光效果最明顯的時候谱仪,所有光線都會被吸收玻熙。然而因為光線也會在這些顆粒之間折射,被吸收的光也會再次發(fā)出疯攒,因此通常情況下光總能找到一條路徑逃離霧區(qū)嗦随,這種現(xiàn)象稱為內(nèi)散射(Inscattering)。我們可以建立一個包含消光和內(nèi)散射的模型敬尺,從而簡單高效的模擬霧天的光效枚尼。

在這個例子中,我們將用到前面章節(jié)《圖元處理》中曲面細分渲染風景的例子砂吞。在前面的例子中署恍,天空是純色的,并且山脈也僅用簡單的紋理著色蜻直。這樣我們很難推斷出一處微景觀和我們的距離盯质,這本節(jié)的例子中袁串,我們將使用霧化效果優(yōu)化這個場景渲染效果。

修改曲面細分著色器呼巷,同時計算出世界坐標系和視圖坐標系中的每個頂點坐標般婆,并將其傳入到片段著色器中,其代碼如下朵逝。其中世界坐標系的頂點位置用于計算消光和散色系數(shù)蔚袍,視圖坐標系中的頂點位置用于計算片段到觀察者的位置,從而計算片段的顏色配名。

#version 420 core

layout (quads, fractional_odd_spacing) in; 

uniform sampler2D tex_displacement;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform float dmap_depth;

out vec2 tc;

in TCS_OUT {
    vec2 tc;
} tes_in[];

out TES_OUT {
    vec2 tc;
    vec3 world_coord; 
    vec3 eye_coord;
} tes_out;

void main(void) {
    vec2 tc1 = mix(tes_in[0].tc, tes_in[1].tc, gl_TessCoord.x); 
    vec2 tc2 = mix(tes_in[2].tc, tes_in[3].tc, gl_TessCoord.x); 
    vec2 tc = mix(tc2, tc1, gl_TessCoord.y);

    vec4 p1 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
    vec4 p2 = mix(gl_in[2].gl_Position, gl_in[3].gl_Position, gl_TessCoord.x);
    vec4 p = mix(p2, p1, gl_TessCoord.y);
    p.y += texture(tex_displacement, tc).r * dmap_depth;

    vec4 P_eye = mv_matrix * p;

    tes_out.tc = tc;
    tes_out.world_coord = p.xyz;
    tes_out.eye_coord = P_eye.xyz;

    gl_Position = proj_matrix * P_eye;
}

在片段著色器中啤咽,我們像往常一樣從風景紋理中查詢紋素數(shù)據(jù),然后應用簡單的霧化模型計算最終的顏色值渠脉。通過計算在視圖坐標系中的頂點位置計算出片段距離觀察點點距離宇整,這也是光線需要穿越的距離,也是霧化公式的輸入?yún)?shù)芋膘。消光因子和內(nèi)散色因子計算公式如下鳞青。

在上面點公式中,fe為消光因子为朋,fi為內(nèi)散色因子臂拓,de和di分別是消光和內(nèi)散色系數(shù),z是觀察點到需要渲染片段的距離习寸。當z趨于0時胶惰,消光因子和內(nèi)散色因子都趨于1,此時觀察到的是片段本來的顏色霞溪。當z增加時孵滞,消光因子和內(nèi)散色因子減小,最終趨于0鸯匹,此時觀察到的為霧的顏色坊饶。指數(shù)函數(shù)的部分曲線圖像如下。

片段著色器部分的源碼如下殴蓬。

#version 420 core

out vec4 color;

layout (binding = 1) uniform sampler2D tex_color;

uniform bool enable_fog = true;
uniform vec4 fog_color = vec4(0.7, 0.8, 0.9, 0.0);

in TES_OUT {
    vec2 tc;
    vec3 world_coord; 
    vec3 eye_coord;
} fs_in;

vec4 fog(vec4 c) {
    float z = length(fs_in.eye_coord);
    
    float de = 0.025 * smoothstep(0.0, 6.0, 10.0 - fs in.world coord.y);
    float di = 0.045 * smoothstep(0.0, 40.0, 20.0 - fs_in.world_coord.y);

    float extinction = exp(-z * de);
    float inscattering = exp(-z * di);
    
    return c * extinction + fog_color * (1.0 - inscattering);
}

void main(void) {
    vec4 landscape = texture(tex_color, fs_in.tc);
    
    if (enable_fog) {
        color = fog(landscape);
    } else {
        color = landscape;
    }
}

在片段著色器中定義了霧化函數(shù)vec4 fog(vec4 c)匿级,它根據(jù)輸入的片段顏色疊加霧化效果計算出模擬霧化效果后的顏色。霧化效果的顏色為元素片段顏色經(jīng)過消光后的顏色科雳,和被內(nèi)散射效果扣除后霧自己的顏色的疊加根蟹。當觀察著的距離越遠時脓杉,內(nèi)散色因子接近0糟秘,因此我們看到的時霧自己的顏色,所以我們需要在計算時使用1減去內(nèi)散射因子來計算需要疊加多少霧的顏色球散。該例子的渲染結(jié)果如下圖尿赚,其中左圖未應用場景效果,右圖應用了場景效果,在觀察右圖時凌净,可以明顯感覺到片段的景深悲龟。源碼傳送門

3 非真實渲染

通常情況下,計算機圖形學渲染目標是盡可能模擬現(xiàn)實世界的場景冰寻,然而在一些程序中须教,或者由于一些藝術(shù)原因,我們并不希望渲染出真實世界的效果斩芭。例如可能我們想要渲染出鉛筆素描效果轻腺,或者是完全使用一種抽象的方式。這種繪制方式稱為非真實渲染划乖,簡稱為NPR(Non-Photo-Realistic Rendering)贬养。

3.1 細胞著色-紋素為光

在前面的章節(jié)中,大多數(shù)的例子使用的都是2維紋理琴庵,這是最簡單也是最容易理解的误算。我們能夠很好的理解將二維的紋理貼合到一個2D或者3D的幾何體之中。一維紋理在電腦游戲中很常用迷殿,它們通常被用于渲染卡通類型的場景儿礼。卡通著色(Toon Shading)庆寺,有時也被稱為細胞著色(Cell Shading)蜘犁,它使用一維紋理作為有限顏色查詢表,使用查詢(紋理過濾函數(shù)需要設置為GL_NEAREST)到的固定顏色填充幾何圖形止邮。

卡通著色的基礎(chǔ)思想是使用一個逐漸變亮的1維顏色查詢表这橙,使用漫射光強度(可以通過在視圖空間中的單位視點向量和平面法向量的點積計算得出)作為待查詢的紋理坐標,從顏色表中查處對應的顏色导披。下圖演示了一個由4個逐漸變亮的紅色組成的一維紋理屈扎。

上圖紋理的創(chuàng)建和數(shù)據(jù)加載代碼如下。

static const GLubyte toon_tex_data[] = {
        0x44, 0x00, 0x00, 0x00,
        0x88, 0x00, 0x00, 0x00,
        0xCC, 0x00, 0x00, 0x00,
        0xFF, 0x00, 0x00, 0x00
};

glGenTextures(1, &tex_toon);
glBindTexture(GL_TEXTURE_1D, tex_toon);
glTexStorage1D(GL_TEXTURE_1D, 1, GL_RGB8, sizeof(toon_tex_data) / 4); 
glTexSubImage1D(GL_TEXTURE_1D, 0, 
                0, sizeof(toon_tex_data) / 4, 
                GL_RGBA, GL_UNSIGNED_BYTE, 
                toon_tex_data);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

示例程序toonshading中使用了這部分紋理創(chuàng)建和加載代碼撩匕,該程序渲染了一個旋轉(zhuǎn)環(huán)狀幾何體鹰晨,并使用了卡通著色技術(shù)。這里的模型文件中包含了頂點的2維紋理坐標止毕,但是我們在頂點著色器中并不需要使用這部分數(shù)據(jù)模蜡,僅僅使用位置和法線數(shù)據(jù),頂點著色器代碼如下扁凛。

#version 420 core 

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;

out VS_OUT {
    vec3 normal;
    vec3 view; 
} vs_out;

void main(void) {
    vec4 pos_vs = mv_matrix * position;
    // Calculate eye-space normal and position
    vs_out.normal = mat3(mv_matrix) * normal; 
    vs_out.view = pos_vs.xyz;

    // Send clip-space position to primitive assembly
    gl_Position = proj_matrix * pos_vs;
}

該著色器計算視圖空間中的頂點坐標和法向量忍疾,將之傳遞給片段著色器,在片段著色器中谨朝,其代碼如下卤妒。

#version 420 core

layout (binding = 0) uniform sampler1D tex_toon;

uniform vec3 light_pos = vec3(30.0, 30.0, 100.0);

in VS_OUT {
    vec3 normal;
    vec3 view; 
} fs_in;

out vec4 color;

void main(void) {
    // Calculate per-pixel normal and light vector
    vec3 N = normalize(fs_in.normal);
    vec3 L = normalize(light_pos - fs_in.view);
        
    // Simple N dot L diffuse lighting
    float tc = pow(max(0.0, dot(N, L)), 5.0);
    
    // Sample from cell shading texture
    color = texture(tex_toon, tc) * (tc * 0.8 + 0.2);
}

和之前的例子一樣甥绿,在片段著色器中計算出漫射光系數(shù),但是這次不直接使用這個系數(shù)計算顏色则披,而是將其當作紋理坐標使用共缕,從而從紋理中查詢出片段的顏色。這里對漫射光系數(shù)做了指數(shù)級的操作士复,最終查詢到的顏色也進行了縮放處理图谷,這樣可以使得顏色變化更為劇烈,另外也能夠增加一些景深感阱洪。

該示例程序的渲染結(jié)果如下蜓萄,由于使用卡通著色的方式,在圖中我們能夠看到很清晰的條帶以及高亮部分澄峰。源碼傳送門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嫉沽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子俏竞,更是在濱河造成了極大的恐慌绸硕,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魂毁,死亡現(xiàn)場離奇詭異玻佩,居然都是意外死亡邪财,警方通過查閱死者的電腦和手機珠闰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胆胰,“玉大人烦秩,你說我怎么就攤上這事垮斯。” “怎么了只祠?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵兜蠕,是天一觀的道長。 經(jīng)常有香客問我抛寝,道長熊杨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任盗舰,我火速辦了婚禮晶府,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻趋。我一直安慰自己川陆,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布爷绘。 她就那樣靜靜地躺著书劝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪土至。 梳的紋絲不亂的頭發(fā)上购对,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音陶因,去河邊找鬼骡苞。 笑死,一個胖子當著我的面吹牛楷扬,可吹牛的內(nèi)容都是我干的解幽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼烘苹,長吁一口氣:“原來是場噩夢啊……” “哼躲株!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镣衡,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤霜定,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后廊鸥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體望浩,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年惰说,在試婚紗的時候發(fā)現(xiàn)自己被綠了磨德。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡吆视,死狀恐怖典挑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啦吧,我是刑警寧澤搔弄,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站丰滑,受9級特大地震影響顾犹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜褒墨,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一炫刷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郁妈,春花似錦浑玛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽极阅。三九已至,卻和暖如春涨享,著一層夾襖步出監(jiān)牢的瞬間筋搏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工厕隧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奔脐,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓吁讨,卻偏偏與公主長得像髓迎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子建丧,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

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