4 更多渲染技術(shù)
傳統(tǒng)的向前渲染方式需要一個完整的圖像渲染管線,以頂點著色器為起點,氣候跟隨多個后續(xù)階段,通常以片段著色器為終點。片段著色器負責計算每個片段的最終顏色,隨著每一條繪制指令的執(zhí)行,幀緩存對象的內(nèi)容逐漸變得完整。然而我們并不是只有這一種渲染場景的方式,正如你接下來將在本小節(jié)中看到的一樣,你可以只計算部分著色信息,當所有模型完成繪制后,仍然能夠得到渲染好的場景。甚至你可以繞過傳統(tǒng)的基于頂點的幾何圖形表示,將所有的幾何處理邏輯都放在片段著色器中。
4.1 延遲著色(Demo要求OpenGL4.2)
幾乎到目前為止我們所有的示例程序中片段著色器都是用于計算當前正在渲染的片段的顏色。現(xiàn)在考慮這樣一個場景,當你渲染多個模型時,某些模型的部分區(qū)域會覆蓋其他模型,而被覆蓋的部分實際上已經(jīng)執(zhí)行過完整的渲染邏輯,這種現(xiàn)象稱為覆蓋渲染(overdraw)。這種情況下需要使用新的渲染結(jié)果來覆蓋之前的渲染結(jié)果,也就意味著你之前的那部分渲染工作的結(jié)果全部被拋棄。如果片段著色器運行的成本很高,或者有大量的覆蓋渲染,這會很影響性能。為了解決這個問題,你可以使用延遲著色(Deferred Shading)技術(shù),它使得片段著色器中的高成本計算邏輯可以被推遲到最后一刻執(zhí)行。
在使用延遲著色技術(shù)時,首先我們需要使用一個非常簡單版本的片段著色器,該著色器需要將我們真正執(zhí)行渲染任務(wù)所需要使用到的參數(shù)輸入片段著色器中。在大多數(shù)場景中,我們都需要使用多個幀緩存附件。回顧前面介紹光照效果時使用過的渲染邏輯,在渲染單個片段的時候需要使用到的參數(shù)有片段的漫射系數(shù),所處曲面的法向量,在世界坐標系中的位置。盡管在世界坐標系中的位置可以通過片段在屏幕空間內(nèi)的坐標和深度緩存中的數(shù)據(jù)重建,但是我們還是將這些數(shù)據(jù)直接存儲到一個幀緩存附件中會更高效,也會更方便。用于存儲這部分數(shù)據(jù)的幀緩存對象我們通常稱為G緩存(G-buffer)。在這里G表示的是幾何體(Geometry),意為存儲的是幾何體的點位置而不是圖像數(shù)據(jù)。
當G緩存準備好后,就可以使用一個視圖窗口的四邊形來繪制整個場景。這一次渲染將執(zhí)行整個光照算法邏輯,但是我們并沒有對場景中所有模型的每個三角形圖元光柵化得到的每個片段進行處理,對整個幀緩存中的每個像素僅僅執(zhí)行了一個高成本的光照計算。這能夠很大程度的降低片段著色的性能開銷,尤其當使用的著色算法很復(fù)雜時這種性能提升更明顯。
4.1.1 生成G緩存
延遲著色的第一步是創(chuàng)建一個G緩存,具體的方法是為一個幀緩存對象添加多個附件。OpenGL最多支持為一個幀緩存對象添加8個附件,每個附件最高支持4個32位的通道,如格式GL_RGBA32F
。然而每個附件的每個通道都會消耗一定的內(nèi)存帶寬(Memory Bandwidth),如果我們完全不考慮寫入到幀緩存中的數(shù)據(jù)體積,那么盡管我們提示了渲染邏輯的效率,但是我們卻增加了數(shù)據(jù)存儲成本。
通常情況下,使用16位的浮點數(shù)據(jù)來存儲顏色和法向量信息已經(jīng)足夠。32位的浮點數(shù)據(jù)用于存儲對精度要求更高的每個片段在世界坐標系中的位置。此外又是我們還需要存儲一些材質(zhì)數(shù)據(jù),例如每個片段的高光指數(shù)(Specular Exponent),也可以稱為閃光因子(Shininess Factor)。通常對于需要存儲的數(shù)據(jù)我們需要使用不同的格式存儲,考慮內(nèi)存帶寬的高效性,一個好的方式是將這些數(shù)據(jù)打包成為相同的格式,而不是在一個幀緩存對象中使用多個不同數(shù)據(jù)格式的附件。如將2個16位的數(shù)據(jù)打包成一個32位的數(shù)據(jù),具體方法稍后演示。
在本節(jié)的示例程序中,將使用3個16位數(shù)據(jù)格式的分量來存儲每個片段的法向量,3個16位數(shù)據(jù)格式的分量來存儲每個片段自己的顏色,3個32位數(shù)據(jù)格式的分量來存儲每個片段在世界坐標系中的位置,1個32位的整形變量來存儲每個片段使用的材質(zhì)索引,以及1個32位的數(shù)據(jù)格式分量存儲每個像素的高光指數(shù)。
總的來說需要6個16位分量和5個32位的分量。我們可以將6個16位分量打包后存儲在格式為GL_RGBA32UI的幀緩存的前三個分量中,剩余的1個分量剛好能夠存儲1個32位的數(shù)據(jù)。剩下的3個32位世界空間頂點坐標以及1個32位數(shù)據(jù)可以打包存儲在一個格式為GL_RGBA32F的幀緩存中。G緩存創(chuàng)建的代碼如下。
GLuint gbuffer;
GLuint gbuffer_tex[3];
glGenFramebuffers(1, &gbuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gbuffer);
glGenTextures(3, gbuffer_tex);
glBindTexture(GL_TEXTURE_2D, gbuffer_tex[0]);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32UI,
MAX_DISPLAY_WIDTH, MAX_DISPLAY_HEIGHT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, gbuffer_tex[1]);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F,
MAX_DISPLAY_WIDTH, MAX_DISPLAY_HEIGHT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, gbuffer_tex[2]);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT32F,
MAX_DISPLAY_WIDTH, MAX_DISPLAY_HEIGHT);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, gbuffer_tex[0], 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, gbuffer_tex[1], 0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, gbuffer_tex[2], 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
當G緩存準備完畢后,接下來需要做的事情就是向其中填充數(shù)據(jù)。前面已經(jīng)提過我們需要將兩個個16位的數(shù)據(jù)打包成為1個32位的數(shù)據(jù),這可以通過在著色器語言中調(diào)用函數(shù)packHalf2x16
(該函數(shù)要求OpenGL4.2)將2個32位浮點型數(shù)據(jù)轉(zhuǎn)換為2個16位浮點型數(shù)據(jù),并按位轉(zhuǎn)換為32位整形數(shù)據(jù)實現(xiàn)。假定我們能夠在片段著色器中拿到必要的數(shù)據(jù),那么通過如下的代碼可以將這些數(shù)據(jù)通過兩個顏色輸出填充到幀緩存中。
#version 420 core
layout (location = 0) out uvec4 color0;
layout (location = 1) out vec4 color1;
in VS_OUT {
vec3 ws_coords;
vec3 normal;
vec3 tangent;
vec2 texcoord0;
flat uint material_id;
} fs_in;
layout (binding = 0) uniform sampler2D tex_diffuse;
void main(void) {
uvec4 outvec0 = uvec4(0);
vec4 outvec1 = vec4(0);
vec3 color = texture(tex_diffuse, fs_in.texcoord0).rgb;
outvec0.x = packHalf2x16(color.xy);
outvec0.y = packHalf2x16(vec2(color.z, fs_in.normal.x));
outvec0.z = packHalf2x16(fs_in.normal.yz);
outvec0.w = fs_in.material_id;
outvec1.xyz = fs_in.ws_coords;
outvec1.w = 60.0;
color0 = outvec0;
color1 = outvec1;
}
在準備好G緩存后,下一步就是計算其中所有像素的最終顏色,并將其輸出到屏幕上。
4.1.2 使用G緩存
在準備好包含漫射色,法向量,高光指數(shù),片段世界坐標系中頂點以及其他必要信息的G緩存后,需要做的事情是從這個緩存中讀取數(shù)據(jù),并解包重建原始數(shù)據(jù)。使用函數(shù)unpackHalf2x16
可以執(zhí)行和上面代碼相反的解包邏輯,該函數(shù)會將1個32位整形數(shù)據(jù)按位解包為2個16位浮點型數(shù)據(jù),再轉(zhuǎn)化為2個32位浮點型數(shù)據(jù)。重建原始數(shù)據(jù)的代碼如下。
layout (binding = 0) uniform usampler2D gbuf0;
Layout (binding = 1) uniform sampler2D gbuf1;
struct fragment_info_t {
vec3 color;
vec3 normal;
float specular_power;
vec3 ws_coord;
uint material_id;
};
void unpackGBuffer(ivec2 coord, out fragment_info_t fragment) {
uvec4 data0 = texelFetch(gbuf_tex0, ivec2(coord), 0);
vec4 data1 = texelFetch(gbuf_tex1, ivec2(coord), 0);
vec2 temp;
temp = unpackHalf2x16(data0.y);
fragment.color = vec3(unpackHalf2x16(data0.x), temp.x);
fragment.normal = normalize(vec3(temp.y, unpackHalf2x16(data0.z)));
fragment.material_id = data0.w;
fragment.ws_coord = data1.xyz;
fragment.specular_power = data1.w;
}
將從G緩存中解包重建的數(shù)據(jù)直接渲染到一個普通的顏色幀緩存中可以直觀看到G緩存的內(nèi)容。其渲染結(jié)果如下。源碼傳送門由于函數(shù)unpackHalf2x16
等需要OpenGL4.2接口才能正常工作,該Demo未驗證。
左上角的圖是直接使用漫射光顏色的渲染結(jié)果,右上角的圖表示了每個片段的曲面法向量,左下角的圖片表示每個片段在世界坐標系中的位置,右下角的圖表示了每個片段的材質(zhì)索引。
對于G緩存解包重建后的數(shù)據(jù),可以使用本章節(jié)前面的任何光照模型來計算每個片段的最終顏色。本實例中將使用標準馮氏著色模型,具體計算邏輯如下。
vec4 light_fragment(fragment_info_t fragment) {
int I;
vec4 result = vec4(0.0, 0.0, 0.0, 1.0);
if (fragment.material_id != 0) {
for (i = 0; i < num_lights; i++) {
vec3 L = fragment.ws_coord - light[i].position;
float dist = length(L);
L = normalize(L);
vec3 N = normalize(fragment.normal);
vec3 R = reflect(-L, N);
float NdotR = max(0.0, dot(N, R));
float NdotL = max(0.0, dot(N, L));
float attenuation = 50.0 / (pow(dist, 2.0) + 1.0);
vec3 diffuse_color = light[i].color * fragment.color *
NdotL * attenuation;
vec3 specular_color = light[i].color
* pow(NdotR, fragment.specular_power)
* attenuation;
result += vec4(diffuse_color + specular_color, 0.0);
}
}
return result;
}
使用延時著色最終得到的場景渲染結(jié)果如下圖。
在上圖中,通過多實例渲染的方式繪制了超過200個甲殼蟲模型,絕大部分片段都存在覆蓋渲染情況。最終片段顏色的計算考慮了64個光源,使用延遲著色后,增加或者減少光源的數(shù)量對整個程序性能影響不是很大。實際上程序中計算成本大的部分是生成G緩存,以及從G緩存中讀取并重建原始數(shù)據(jù),單這部分計算只需要執(zhí)行一次,并且和光源的數(shù)量無關(guān)。在本實例中為了使代碼更容易理解,使用到的G緩存效率并不高,它消耗的內(nèi)存帶寬仍然還有優(yōu)化的空間,程序的性能也還能進一步優(yōu)化。
4.1.3 法向量貼圖和延遲著色
在前面的章節(jié)中介紹過法向量貼圖,這種技術(shù)可以通過將片段的法向量存儲在一個紋理中,通過讀取紋理中的值獲得單個片段的法向量,從而更精細的控制光照效果,以獲得更多的圖像細節(jié)。大多數(shù)法向量貼圖算法使用的都是切線空間法向量(Tangent space normals),并在切線空間中執(zhí)行所有的光照計算。其中需要計算光照向量L和視點向量V,在頂點著色器中,使用TBN矩陣將它們轉(zhuǎn)換到切線空間中,然后將轉(zhuǎn)換后到向量傳遞到片段著色器中用于光照著色計算。然而,在延遲渲染中,在G緩存中存儲的法向量總是以世界坐標系或者視圖坐標系為參考。
為了生成存儲于G緩存中,用于延遲著色,以視圖空間為參考的法向量,我們需要從法線貼圖中讀取切線空間法向量,并將其轉(zhuǎn)換到視圖坐標系中,然后對普通的法向量貼圖算法進行微調(diào)即可。
首先,在頂點著色器中計算出視圖空間法向量N和切向量T,并將它們傳遞到片段著色器中。在片段著色器中對向量N和T進行標準化處理得到單位向量,通過它們的外積計算出副切線向量B。在片段著色器中通過這三個向量構(gòu)建出TBN矩陣,然后從法線貼圖中讀取切線空間的片段法向量,使用TBN的逆矩陣將其轉(zhuǎn)換到視圖空間內(nèi),由于TBN是正交矩陣,因此其逆矩陣就是它的轉(zhuǎn)置矩陣。被轉(zhuǎn)換到視圖空間中的片段法向量隨后被存入到G緩存中。
生產(chǎn)G緩存的頂點著色器不需要修改,和上面的延遲著色示例程序使用到的頂點著色器相同。修改后的片段著色器如下。
#version 420 core
layout (location = 0) out uvec4 color0;
layout (location = 1) out vec4 color1;
in VS_OUT {
vec3 ws_coords;
vec3 normal;
vec3 tangent;
vec2 texcoord0;
flat uint material_id;
} fs_in;
layout (binding = 0) uniform sampler2D tex_diffuse;
layout (binding = 1) uniform sampler2D tex_normal_map;
void main(void) {
vec3 N = normalize(fs_in.normal);
vec3 T = normalize(fs_in.tangent);
vec3 B = cross(N, T);
mat3 TBN = mat3(T, B, N);
vec3 nm = texture(tex_normal_map, fs_in.texcoord0).xyz * 2.0 - vec3(1.0);
nm = TBN * normalize(nm);
uvec4 outvec0 = uvec4(0);
vec4 outvec1 = vec4(0);
vec3 color = texture(tex_diffuse, fs_in.texcoord0).rgb;
outvec0.x = packHalf2x16(color.xy);
outvec0.y = packHalf2x16(vec2(color.z, nm.x));
outvec0.z = packHalf2x16(nm.yz);
outvec0.w = fs_in.material_id;
outvec1.xyz = floatBitsToUint(fs_in.ws_coords);
outvec1.w = 60.0;
color0 = outvec0;
color1 = outvec1;
}
在下圖中,左圖是使用了法向量貼圖的渲染結(jié)果,右圖是使用片段插值法向量的渲染結(jié)果。雖然不明顯,但是左側(cè)的突破包含更多細小的細節(jié)。示例程序DeferredShading源碼傳送門由于函數(shù)unpackHalf2x16
等需要OpenGL4.2接口才能正常工作,該Demo未驗證。
4.1.4 延遲著色的缺點
盡管延遲著色技術(shù)能夠減少大量復(fù)雜的光照著色技術(shù)對于程序性能的影響,但是它并不能解決所有問題。除了在生成G緩存時會額外占用大量的內(nèi)存帶寬外,它還存著一些其他缺點。通過一些努力,也許你能解決其中部分問題,但是當你準備寫一個延遲渲染器時,你都應(yīng)該考慮如下幾件事情。
首先,你應(yīng)該仔細考慮延遲渲染著色器所需要的內(nèi)存帶寬。在本小節(jié)的示例程序中,G緩存中的每個像素都消耗了256位的內(nèi)存,我們并沒有特別高效的組織數(shù)據(jù)存儲方式。我們將世界坐標系直接存儲在G緩存中,這消耗了96位內(nèi)存空間。然而我們可以在渲染階段直接獲取到屏幕坐標系下每個像素的位置,可以從片段著色器內(nèi)建變量gl_FragCoord
獲取x,y分量,從深度緩存中獲取到z分量,從而構(gòu)建片段在屏幕坐標系(采集坐標系)下的坐標。通過視口變化的逆操作,即簡單的縮放和平移操作,得到標準設(shè)備坐標系中的位置,再通過應(yīng)用投影和觀察矩陣的逆矩陣將坐標從采集坐標系中移動到世界坐標系中。觀察矩陣通常只包含平移和旋轉(zhuǎn)變化,它的逆矩陣運算較為簡單。但是投影矩陣以及齊次坐標的逆運算會比較復(fù)雜。
另外我們使用了48位來編碼表面法向量,但是實際上只需要存儲xy分量即可,由于這里使用的都是單位法向量,因此可以通過公式x2 + y2 + z2 = 1來計算z軸分量。當然這里z的符號是未確定的,但是假定我們的曲面法向量z軸都不為負,那么這種方式將不會有任何問題。
另外高光指數(shù)和材質(zhì)ID我們都分別使用了32位數(shù)據(jù)來保存,但是通常情況下在渲染的場景中材質(zhì)的數(shù)量不會大于16位能夠表示的6000個。高光指數(shù)也可以保存對數(shù)形式的數(shù)據(jù),在我們計算的時候?qū)υ偾?的指數(shù)重構(gòu)原始數(shù)據(jù)即可,這樣也能節(jié)省一部分內(nèi)存開銷。
延遲渲染了另一個問題是它在抗鋸齒的能力上較弱。通常情況下,使用多重采樣抗鋸齒的程序會取一個像素多個樣本的平均或者加權(quán)平均值來作為這個像素的最終輸出顏色。因此對于延遲渲染的程序,在啟用多重采樣特性后我們還需要為所有的數(shù)據(jù),如深度數(shù)據(jù)、法向量以及材質(zhì)索引等媒體數(shù)據(jù)準備對多重采樣紋理并綁定到G緩存上。更糟糕的是,由于最后真正圖像渲染階段使用的是一個覆蓋整個屏幕的四變形,因此對于其內(nèi)部的所有像素而言,并沒有任何邊緣像素,這破壞了傳統(tǒng)的多重采樣抗鋸齒的計算邏輯。另外在解析階段,我們也需要準備一個自定義的解析著色器為每個樣本執(zhí)行光照著色計算,這會極大的增加程序的計算成本。
最后,大多數(shù)延遲渲染算法都不能很好的處理透明問題。因為在G緩存中每個像素點我們只存儲了一個片段的數(shù)據(jù),而在處理透明問題時,我們需要從某個像素點位置離觀察者最近的片段開始直至查找到不透明的片段。有一些算法使用這種方式處理透明圖元,它們都是圖元的順序不回影響最終結(jié)果的場景中使用。另外一種方式首先處理所有不透明的圖元,然后再渲染透明的圖元。這種方式要求渲染器維護一個列表保存所有透明曲面,在穿越場景逐個渲染模型時跳過這些表面,或者穿越場景兩次。無論選擇哪種方式都是一個高成本的方案。
總的來說,如果你小心使用延遲渲染技術(shù),很好的處理算法,它將會你的程序性能帶來極大的提升。
4.2 基于屏幕空間渲染技術(shù)
到目前為止,本系列文章中所使用的渲染技術(shù)都是逐圖元渲染的。然而在前一小節(jié)中講到的延遲渲染技術(shù)并不是這樣,這意味著我們可以將一些渲染程序推遲到最后,通過渲染和屏幕空間等大的圖元去執(zhí)行。在這個小節(jié)中我們將接受其他一些能夠?qū)?zhí)行時機推遲的算法。在有些場景中,這是實現(xiàn)某些技術(shù)唯一方法,在另外一些場景中延遲計算邏輯到所有的幾何體已經(jīng)渲染完成后會極大的提升程序性能。
4.2.1 環(huán)境光遮蔽
現(xiàn)實世界中物體鏡面反射或者漫反射出的光線回到我們眼睛中,使得物體呈現(xiàn)特定的顏色。而這種現(xiàn)象根據(jù)物體接收到的光源類型分為直接光照和間接光照,其中直接光照指物體接收的光線直接來自于光源的光線,間接光照指物體接收的光線來源于物體之間反復(fù)反射后光源后的剩余光,以及物體吸收光線后再次發(fā)出的光。全局光照(Global Illumination)則指的是直接光照和間接光照結(jié)合的效果。
環(huán)境光(Ambient Light)是間接光照產(chǎn)生得到的光的近似值,它是一個小的,固定的量被添加到光照計算公式中。環(huán)境光遮蔽(Ambient Occlusion)指在很深的褶皺中或者物體之間的間隙內(nèi),附近的曲面遮蔽環(huán)境光的現(xiàn)象。實時全局光照是當前到一個研究課題,盡管目前已經(jīng)有了相當多的工作,但這個課題仍然是一個未解決的問題。然而我們?nèi)匀荒軌蚴褂靡恍┓钦椒椒ê痛致缘慕浦祦砟M一個可以接受的較好結(jié)果。接下來討論的屏幕空間環(huán)境光遮蔽(Screen space ambient occlusion, SSAO)就是這樣一種近似方法。
我們先考慮2維平面,如果某個曲面上一個片段被任意數(shù)量的點光源圍繞,這些電光源可以看成是間接光照。環(huán)境光可以被認為是照射到這個頂點上光線的總和。在一個非常平整的曲面上,任意一點對于曲面上方所有的光源而言都是可見的。然而在不平整的曲面上,并不是所有的光源都能夠照射到曲面上的每一個點,如下圖所示,對于曲面上的任意一點,曲面越不平整,能夠找到到該點到光源更少。
在上圖中圍繞曲面均勻分布著8個點光源,對于選定的曲面中心某點,只能接收到來自4個光源的光線,這個時候就需要考慮全局光照對這個點的影響。在完全全局光照模擬中,對于每個頂點,我們需要追蹤上百個,甚至上千個不同方向照射到該點的光路徑,確定哪些路徑能夠順利照射到目標點。然而這對于實時渲染程序而言計算代價過于昂貴,因此我們需要使用一種方法能夠直接在屏幕空間中計算每個頂點的環(huán)境光遮蔽情況。
在利用該技術(shù)時,需要在屏幕空間內(nèi)對每個像素選多個隨機方向延伸出直線,沿著這條線選擇多個點判斷該像素是否被遮蔽,從而計算出每個像素被遮擋的程度,最后計算每個像素的顏色。具體的方法是先準備一個幀緩存對象,首先正常將場景渲染到它的第一個顏色紋理附件和深度紋理附件中,然后將每個片段的法向量和在觀察空間中的深度值存儲在同一個幀緩存對象的第二個顏色紋理附件中。
接下來需要使用已經(jīng)獲得的數(shù)據(jù)計算每個片段的遮蔽程度。這個階段需要使用遮蔽著色器來渲染一個全屏的四邊形。該著色器讀取某個片段的深度值,選擇一個隨機方向并延伸,以一定的距離間隔選取多個點,比較每個點上的插值計算出的深度值和在深度緩存中存儲的深度值的大小,如果插值深度值大于深度緩存的值,則認為該插值點被其他幾何圖像遮擋,也意味著這個方向上的光不能夠照射到被延伸的像素。
在選擇隨機方向之前,需要先準備一個包含大量隨機單位向量的緩存對象,并將其作為一個著色器中的統(tǒng)一變量使用。隨機向量可能指向任何方向,但是我們只需要考慮和曲面法向量同側(cè)的隨機向量。通過計算曲面法向量和選取的隨機向量的點積,我們可以篩選出這種條件的隨機向量。如下圖,如果點積為負,則其指向法向量背側(cè),此時只需要取其負向量即可。
在上圖中,向量v0、v1和v4指向了法向量N同側(cè),它們的點積為正。向量v2和v3指向了法向量N背側(cè),它們的點積為負,因此我們需要取其負向量-v2和-v3作為我們判斷像素遮蔽情況的延伸方向。
當確定好隨機向量V(xv, yv, zv)后,接下來就需要沿著向量計算像素的遮蔽情況。選取屏幕空間上的某點PO(xo, yo),在之前準備好的紋理中查詢出視口空間內(nèi)深度值zo,沿著隨機向量的方向延伸一段距離得到新的點PN(xn, yn, zn)。(xn, yn)為屏幕空間內(nèi)的坐標,通過PO點和向量V2(xv, yv)和步長stepDistance計算得到,zn為視口空間內(nèi)的坐標,通過PO點和向量V1(zv)計算得到。這里點PN的三個坐標分量不在同一個坐標系中,因此并未嚴格按照隨機向量V進行插值。
通過以(xn, yn)為紋素坐標,在前一步準備好的顏色紋理中查詢該坐標對應(yīng)片段在視口空間中的深度值znv。比較znv和zn的大小,如果znv比zn更小,則這個插值得到的點被渲染的場景中某個片段阻擋,則認為被延伸的點唄遮擋。盡管這種計算方式并不準確,但是就統(tǒng)計學上而言是有效的。選擇的隨機向量個數(shù),在每個隨機向量方向上插值的次數(shù),以及插值的步長都可以控制最終得到的圖像質(zhì)量。這三個值越高,得到的圖像質(zhì)量越好。下圖展示了隨機向量數(shù)量對屏幕空間環(huán)境光遮蔽算法處理圖像質(zhì)量的影響。
上圖中,從左到右,從上到下,在計算環(huán)境光遮蔽時使用到的隨機向量數(shù)量遞增,依次為1、4、16和64。可以明顯看到,當選擇的隨機向量數(shù)量達到64個時,圖片才變得光滑。隨機向量越少,條帶越明顯。改善圖像質(zhì)量的方式有很多,但是最有效的方法之一就是為每個樣本生成一個隨機種子,用于確定環(huán)境光遮蔽計算中的步長。這種方式引入了圖像噪聲,但是卻提高了圖像質(zhì)量,下圖演示了這種技術(shù)的效果。
可以明顯看到,在確定每個樣本步長時應(yīng)用隨機種子能夠明顯的改善環(huán)境光遮蔽的效果。此時在只選取1個隨機向量的渲染結(jié)果中,盡管圖片質(zhì)量很糟糕,但是仍然能夠比固定步長的版本更好,在選取4個隨機向量的渲染結(jié)果中,圖片質(zhì)量已經(jīng)可以被接受,其對應(yīng)固定步長版本的渲染結(jié)果則會有很明顯的條帶。這種方式所引入的圖像噪聲其實也可以解決,但是已經(jīng)超出了這個例子所要討論的問題范圍。
介紹完環(huán)境光遮蔽計算方式后,需要做的就是對正常渲染的圖像應(yīng)用這種技術(shù)。環(huán)境光遮蔽是指環(huán)境光被阻擋的數(shù)量,因此每個片段的環(huán)境光計算方式是在著色器顏色計算公式中使用遮蔽系數(shù)和環(huán)境光相乘即可,這樣被渲染的場景中出現(xiàn)褶皺的地方最后其顏色值添加的環(huán)境光會更少,使得最終的渲染圖像陰影效果看上去更加真實。下圖顏色了屏幕空間環(huán)境光遮蔽技術(shù)的應(yīng)用效果。源碼傳送門
上圖中,左側(cè)的圖片僅僅計算了漫射光和鏡面高光,被渲染出來的模型看上去更像是懸掛在一個屏幕上,另外從圖片中也很難看出景物的深度。右側(cè)的圖片是應(yīng)用屏幕空間環(huán)境光遮蔽技術(shù)的渲染結(jié)果,可以看到不僅一些模型的細節(jié)更加豐滿,地面上也能看到軟陰影效果,景深的感覺也更明顯。
在第一次渲染過程和大多數(shù)例子一樣,將場景渲染到一個顏色附件中。在第二次渲染過程中需要應(yīng)用環(huán)境光遮蔽計算,示例程序ssao片段著色器代碼如下。
#version 430 core
// Samplers for pre-rendered color, normal, and depth
layout (binding = 0) uniform sampler2D sColor;
layout (binding = 1) uniform sampler2D sNormalDepth;
// Final output
layout (location = 0) out vec4 color;
// Various uniforms controlling SSAO effect
uniform float ssao_level = 1.0;
uniform float object_level = 1.0;
uniform float ssao_radius = 5.0;
uniform bool weight_by_angle = true;
uniform uint point_count = 8;
uniform bool randomize_points = true;
// Uniform block containing up to 256 random directions (x,y,z,0)
// and 256 more completely random vectors
layout (binding = 0, std140) uniform SAMPLE POINTS {
vec4 pos[256];
vec4 random_vectors[256];
} points;
void main(void) {
// Get texture position from gl_FragCoord
vec2 P = gl FragCoord.xy / textureSize(sNormalDepth, 0);
// ND = normal and depth
vec4 ND = textureLod(sNormalDepth, P, 0);
// Extract normal and depth
vec3 N = ND.xyz;
float my_depth = ND.w;
// Local temporary variables
int I;
int j;
int n;
float occ = 0.0;
float total = 0.0;
// n is a pseudo-random number generated from fragment coordinate and depth
n = (int(gl_FragCoord.x * 7123.2315 + 125.232) *
int(gl_FragCoord.y * 3137.1519 + 234.8)) ^
int(my_depth);
// Pull one of the random vectors
vec4 v = points.random vectors[n & 255];
// r is our "radius randomizer"
float r = (v.r + 3.0) * 0.1;
if (!randomize_points) {
r = 0.5;
}
// For each random point (or direction)...
for (i = 0; i < point_count; i++) {
// Get direction
vec3 dir = points.pos[i].xyz;
// Put it into the correct hemisphere
if (dot(N, dir) < 0.0) {
dir = -dir;
}
// f is the distance we’ve stepped in this direction
// z is the interpolated depth
float f = 0.0;
float z = my_depth;
// We’re going to take 4 steps - we could make this configurable
total += 4.0;
for (j = 0; j < 4; j++) {
// Step in the right direction
f += r;
// Step towards viewer reduces z
z -= dir.z * f;
// Read depth from current fragment
float their_depth = textureLod(sNormalDepth,
(P + dir.xy * f * ssao_radius), 0).w;
// Calculate a weighting (d) for this fragment’s
// contribution to occlusion
float d = abs(thei_depth - my_depth);
d *= d;
// If we’re obscured, accumulate occlusion
if ((z - their_depth) > 0.0) {
occ += 4.0 / (1.0 + d);
}
}
}
// Calculate occlusion amount
float ao_amount = vec4(1.0 - occ / total);
// Get object color from color texture
vec4 object_color = textureLod(sColor, P, 0);
// Mix in ambient color scaled by SSAO level
color = object_level * object_color +
mix(vec4(0.2), vec4(ao_amount), ssao_level);
}
4.3 無三角形渲染
前面的小節(jié)中介紹了一些在屏幕空間上使用的渲染技術(shù),這些技術(shù)都是通過渲染一個全窗口的四邊形,再對之前有幾何體組成的場景渲染結(jié)果進一步處理。在本節(jié)中,會進一步說明如和使用一個全窗口四邊形渲染整個場景。
4.3.1 渲染朱莉婭分形
這小節(jié)的示例程序渲染了一個朱莉婭集合(Julia Set),這種分形圖像只需要使用紋理坐標即可創(chuàng)建。朱莉婭集合和曼德勃羅集合(Mandelbrot Set)相關(guān),它由如下公式生成。
當Z值超過閾值時,循環(huán)結(jié)束。如果在允許的迭代次數(shù)內(nèi)Z的值不大于閾值,則認為該點位于曼德勃羅集合內(nèi)部,并使用某種默認顏色為其著色。如果Z值大于閾值,則認為這個點在集合外部,通常此時使用一個迭代函數(shù)來為該點著色。朱莉婭集合和曼德勃羅集合的區(qū)別在于Z和C的初始條件不一樣。
渲染曼德勃羅集合時,Z被定義為(0+0i),C被定義為執(zhí)行插值的點坐標。在渲染朱莉婭集合時,Z被定義為執(zhí)行插值的點坐標,C被定義為一個程序內(nèi)部指定的常量。因此曼德勃羅集合只有一個,而朱莉婭集合有無窮個。這也意味著朱莉婭集合可以通過編程控制,也可以執(zhí)行動畫。和前面的例子一樣,我們使用一個全窗口的四邊形來渲染場景,不同的是不在使用幀緩存中準備好的數(shù)據(jù),而是直接生成圖像。
在片段著色器中定義一個包含紋理坐標的輸入變量。聲明一個統(tǒng)一變了來保存C值,一個統(tǒng)一變量保存最大迭代數(shù)。為了使得生成的朱莉婭集合更好看,使用一個一維漸變顏色紋理為其著色。當確定某個點位于集合內(nèi)部時,使用迭代次數(shù)作為紋理坐標為片段著色。最大迭代數(shù)可以平衡圖像的細節(jié)程度和程序的性能。片段著色器部分代碼如下。
#version 430 core
in Fragment {
vec2 tex_coord;
} fragment;
// Here’s our value of c
uniform vec2 c;
// This is the color gradient texture
uniform sampler1D tex_gradient;
// This is the maximum iterations we’ll perform before we consider
// the point to be outside the set
uniform int max iterations;
// The output color for this fragment
out vec4 output color;
確定某個片段是否位于集合內(nèi)部的代碼如下。
int iterations = 0;
vec2 z = fragment.tex coords;
const float threshold_squared = 4.0;
// While there are iterations left and we haven’t escaped from the set yet...
while (iterations < max_iterations && dot(z, z) < threshold_squared) {
// Iterate the value of Z as Z^2 + C
vec2 z_squared;
z_squared.x = z.x * z.x - z.y * z.y;
z_squared.y = 2.0 * z.x * z.y;
z = z_squared + c;
iterations++;
}
如果循環(huán)結(jié)束后迭代的次數(shù)等于最大迭代數(shù),意味著該點位于集合內(nèi)部,將之涂為黑色,否則到漸變顏色紋理中去查詢對應(yīng)的紋素為其著色器,代碼如下。
if (iterations == max_iterations) {
output_color = vec4(0.0, 0.0, 0.0, 0.0);
} else {
output_color = texture(tex_gradient,
float(iterations) / float(max_iterations));
}
剩下的就是提供一個漸變顏色紋理,并設(shè)置一個合適的C值即可。在示例程序中,每一幀畫面都使用渲染函數(shù)中傳入的時間參數(shù)來更新C的值,從而添加動畫效果。下圖是示例程序julia中的部分幀的效果圖。Demo傳送門
4.3.2 片段著色器中的光線追蹤
OpenGL的工作原理是基于光柵化,也就是將如線、三角形和點圖元分解為片段。幾何體進入到OpenGL的圖形管線后,對于每個三角形圖元,OpenGL將會找出它所覆蓋的像素,然后運行我們編寫的著色器計算每個像素的顏色。光線追蹤的原理與之完全不同,它能夠得到更好的效果,但是其計算成本更高。
從觀察點向成像窗口上的每個點發(fā)出一條射線,直至碰到場景中最近的模型,從而計算每個像素的顏色。和傳統(tǒng)的光柵化方式相比,這種方式最大的缺點是并沒有OpenGL設(shè)計層面的直接支持,這意味著所有的工作都需要在我們自己編寫著色器完成。然而,這種方式會帶來很多好處,我們可以跳出點、線和三角形的局限,我們可以更直觀的理解射線碰到模型表面后的的行為。使用類似之前用到的判斷片段可見性的技術(shù),我們用很少的代碼就能夠模擬光的反射,陰影,甚至光的折射,并且這樣的到的效果更加真實。這種成像的方式也更接近現(xiàn)實世界中物體在人眼中成像的方式。
本小節(jié)會介紹如何使用片段著色器遞構(gòu)建簡單的遞歸光線追蹤器。該光線追蹤器能夠渲染由簡單球體和無限平面構(gòu)成的場景,可以渲染經(jīng)典的“盒子中的玻璃球“(Glossy spheres in a box)圖像。可以肯定的是,一定有更優(yōu)秀的光線追蹤算法,但是這個追蹤器已經(jīng)足夠說明光線追蹤算法的基本原理。下圖是一個簡化版本的2維簡單光線追蹤器的示意圖。
在上圖中,觀察點為O,從O點向呈像平面中像素P發(fā)出一條光線直至碰到場景中的某個模型表面上某個點Io,這個初始光線表示為R_primary,點Io處曲面的法向量為N,從點Io向光源延伸一條指向光源的射線表示為R_shadow,如果這條射線在中途碰到了其他模型,則點Io位于陰影中,否則該點被光源直接照射。另外在點Io處,根據(jù)法向量N計算入射光線的反射向量表示為R_reflected。
光線追蹤成像方式中像素的顏色計算方式和前面講到的光照顏色計算公式并不是完全不同的,我們?nèi)匀豢梢苑謩e計算漫射光和反射光,也可以使用法線紋理貼圖等提高圖像質(zhì)量。在計算像素P的顏色時,需要計算光源直接照射到點Io產(chǎn)生的顏色,以及反射光線R_reflected尋找到的另外一個模型上的點I1的對點I0的顏色貢獻。
以點O為原點,沿著光線R_primary方向向量D,并以其模長為速度,經(jīng)過時間t后到達了位于球面上某點P,假定球體的圓心為C,球體的半徑為r。兩個相同向量的點積為其模的平方,則存在如下公式。
替換其中的點P為O+tD,則存在如下公式。
展開多項式,以t作為自變量,可以將上述式子表示如下。
可以簡寫為At2+Bt+C = 0,其中
可以求得t如下
假定向量D選擇的是單位向量,則其長度為1,上式可以進一步簡寫為
如果4C的值比B2更大,則意味著t無解,表示該光線不會和這個球體相交。如果相等,表示光線和球相切,只有1個交點。如果更小,表示光線穿過球體,有兩個交點。如果存在負解,則表示交點在觀察點背面。在有兩個交點時,選擇最小的正值t作為光線和模型相交的時間,并利用公式P=O+tO計算出交點在3D空間內(nèi)的坐標。
上面尋找光線和模型交點的代碼如下。
struct ray {
vec3 origin;
vec3 direction;
};
struct sphere {
vec3 center;
float radius;
};
float intersect_ray_sphere(ray R, sphere S, out vec3 hitpos, out vec3 normal) {
vec3 v = R.origin - S.center;
float B = 2.0 * dot(R.direction, v);
float C = dot(v, v) - S.radius * S.radius;
float B2 = B * B;
float f = B2 - 4.0 * C;
if (f < 0.0) {
return 0.0;
}
float t0 = -B + sqrt(f);
float t1 = -B - sqrt(f);
float t = min(max(t0, 0.0), max(t1, 0.0)) * 0.5;
if (t == 0.0) {
return 0.0;
}
hitpos = R.origin + t * R.direction;
normal = normalize(hitpos - S.center);
return t;
}
函數(shù)intersect_ray_sphere
未找到光線和球的交點時返回0,如果找到了交點,則將交點的坐標寫入到參數(shù)hitpos
中,交點位置在球面的法向量寫入到參數(shù)normal
中。對于每個點發(fā)出的光線,我們需要其和場景中多個模型中最近的交點,可以先設(shè)置一個臨時的初始值,然后再遍歷這些球體,從而尋找到最近的交點。這部分邏輯代碼如下。
// Declare a uniform block with our spheres in it.
layout (std140, binding = 1) uniform SPHERES {
sphere S[128];
};
// Textures with the ray origin and direction in them
layout (binding = 0) uniform sampler2D tex_origin;
layout (binding = 1) uniform sampler2D tex_direction;
// Construct a ray using the two textures
ray R;
R.origin = texelFetch(tex_origin, ivec2(gl_FragCoord.xy), 0).xyz;
R.direction = normalize(texelFetch(tex_direction,
ivec2(gl_FragCoord.xy), 0).xyz);
float min_t = 1000000.0f;
float t;
// For each sphere...
for (i = 0; i < num_spheres; i++) {
// Find the intersection point
t = intersect_ray_sphere(R, S[i], hitpos, normal);
// If there is an intersection
if (t != 0.0) {
// And that intersection is less than our current best
if (t < min t) {
// Record it.
min_t = t;
hit_position = hitpos;
hit_normal = normal;
sphere_index = I;
}
}
}
假如對于每個光線追蹤尋找到的點都默認著色為白色,則對于包含單個球體的場景,這種方式的渲染結(jié)果如下。
接下來我們需要應(yīng)用光照算法為每個片段著色。在光照計算中,光線追蹤函數(shù)返回的交點曲面法向量是重要的參數(shù)。和前面例子中光照計算的方式一樣,使用曲面法向量,光線追蹤函數(shù)計算出的交點在視點空間坐標,以及材質(zhì)參數(shù)計算每個交點的顏色。應(yīng)用光照著色算法后同樣的場景渲染結(jié)果如下。
法向量不僅僅用于光照著色公式中,在接下來最終光線的下一個目標時也發(fā)揮重要作用。對于追蹤到第一個模型交點此后每個通過法向量計算出的新光線而言,每次追蹤到的交點在計算其本身的顏色后都會計算它們對于光線追蹤源像素最終顏色的貢獻分量,從而確定最終的源像素顏色。這是光線追蹤方式相對于傳統(tǒng)的光柵化方式的第一個優(yōu)點。
假設(shè)曲面上某點P,光源L和觀察點O,初始的光線從點O出發(fā)射向點P,點P到L的光線為R_Shadow,其單位向量為D。如果光線R_Shadow能夠順利到達光源,不會觸碰到場景中的其他模型,則可以直接使用光照著色公式為該點著色。否則,點P位于陰影中。這種陰影效果也是應(yīng)用光線追蹤算法的優(yōu)勢。
在交點P除了能構(gòu)建指向光源的光線外,我們還能構(gòu)建指向任意方向的光線。例如可以構(gòu)建入射光線在點P處對于法向量的折射光線,并繼續(xù)追蹤光線尋找下一個交點,將得到顏色計入光線追蹤源像素點最終顏色。
光線追蹤是一個遞歸算法,追蹤一條光線,找到一個交點為其著色,然后創(chuàng)建一條新的光線,再進行下一次迭代。而GLSL中并不支持循環(huán)語法,因此在本例中使用由多個紋理組成的棧來實現(xiàn)光線追蹤算法,這也是在上面的代碼中從紋理中讀取光線源點和方向的原因。
具體的方法是創(chuàng)建一組幀緩存對象,每個幀緩存對象添加4個顏色附件。對于幀緩存中的每個像素,這4個附件分別保存了最終合成顏色,光線的源點,當前光線的方向,以及顏色貢獻系數(shù)。在本例中允許光線最多追蹤5個模型的交點,因此創(chuàng)建了5個幀緩存對象。第一個顏色附件,即保存最終合成顏色的附件在多個幀緩存對象中是共享的,而另外3個顏色附件對于每個幀緩存對象都是獨有的。在每一次渲染行為中,我們從一組紋理中讀取數(shù)據(jù),然后寫入到另外一組紋理中,如下圖。
初始化光線追蹤器需要運行著色器將初始的原點和方向?qū)懭氲郊y理中,將最終合成顏色紋理中所有點的值設(shè)置為0,將顏色貢獻系數(shù)紋理中所有點的值設(shè)置為1。然后運行光線追蹤著色器,每次光線追蹤行為都繪制一個全窗口的四邊形。在每次繪制行為中,綁定上一次繪制準備好的原點,方向和顏色貢獻系數(shù)紋理。同時也綁定一個幀緩存對象包含需要輸出的原點,方向和顏色貢獻系數(shù)紋理,這些數(shù)據(jù)講在下一次渲染行為中使用到。對于每個像素,光線追蹤著色器通過原點和方向構(gòu)建出一條光線并向場景中追蹤,為尋找到的交點著色,再乘以顏色貢獻系數(shù)后寫入到接受數(shù)據(jù)的幀緩存對象的第一個顏色附件中。
想要計算多次光線追蹤結(jié)果累積得到的最終顏色,需要講最終合成顏色紋理作為第一個顏色附件添加到每個幀緩存對象中,并且為這個附件開啟顏色混合功能,顏色混合函數(shù)的源和目標顏色系數(shù)都需要設(shè)置為1,這表示直接講兩個顏色疊加混合。
如果在場景中添加更多的球體模型,我們可以通過這個技術(shù)更真實的模擬光線在多個模型之間折射的效果。下圖中,對于通過光線追蹤渲染的場景,隨著光線追蹤次數(shù)增加,其渲染結(jié)果的細節(jié)更加真實。
上圖中,左上角的是未進行第二次光線追蹤的渲染效果,此時能夠看出場景中的模型都比較暗淡。在右上角的圖片中,我們進行了2次光線追蹤,能夠看見一些球體表面此時已經(jīng)有反射光的效果。在左下角的圖片中,光線追蹤的次數(shù)增加到3次,此時模型的光澤可以經(jīng)過兩次反射到達另外一個模型表面。在右下角的圖片中,光線追蹤的次數(shù)繼續(xù)增加到4次,此時能夠觀察到更多的細節(jié)。
為了使得渲染的場景更加有趣,我們可以在其中加入其他類型的模型。盡管在理論上,任意類型的模型都能夠被追蹤,但是另外一個更容易查詢光線是否和其有交點的模型類型是平面。平面的一種表示方法是一個單位法向量,以及對于一個過坐標系原點的法向量,從該法向量和平面的交點沿著它的方向到原點的距離。
法向量由3個分量組成,距離是一個標量,但是其值有正負,沿著法向量方向為正,否則其值為反。可以將前者打包在1個4維向量的x、y、z分量中,將后者打包在同一個4維向量的w分量中。實際上,對于給定的法向量N,和同法向量通向過坐標系原點的線與平面交點沿著法向量方向到原點的距離d,平面可以用如下公式表示。
當上式成立時,P為平面上的任意一點。對于在光線追蹤時構(gòu)建出的線上任意一點可以表示為如下等式。其中O為光線追蹤的起點,t為經(jīng)歷的時間,D為速度向量,由于t沒有具體的時間單位,因此D為單位向量。
替換第一個公式中的P可以得到如下公式。
然后求解出t。
從上式中可以看出,如果向量D和N的點積為0,即當向量D和平面同向時,t無解。其他情況下,被追蹤的光線和參考平面一定存在交點。如果接觸的t值為負,交點位于觀察者后方,這種情況不做處理。如果t值為正,交點位于觀察者前方,此時需要進一步的著色計算。執(zhí)行光線和平面相交檢測的代碼如下。
float intersect_ray_plane(ray R, vec4 P, out vec3 hitpos, out vec3 normal) {
vec3 O = R.origin;
vec3 D = R.direction;
vec3 N = P.xyz;
float d = P.w;
float denom = dot(N, D);
if (denom == 0.0) {
return 0.0;
}
float t = -(d + dot(O, N)) / denom;
if (t < 0.0) {
return 0.0;
}
hitpos = O + t * D;
normal = N;
return t;
}
在前文渲染的場景中所有球體的背面添加一個平面后的渲染結(jié)果如下圖。在左圖中,盡管這種方式為場景添加了一些景深效果,但是仍然沒有最大發(fā)揮光線追蹤的潛力,繼續(xù)增加光線追蹤的次數(shù),此時能夠在右圖中看到平面上出現(xiàn)了更多球體的折射影像,甚至在球體表示上也折射出了平面的內(nèi)容。
添加更多的平面將整個場景包在一個盒子中,我們可以得到更有趣的圖像,其渲染結(jié)果如下圖。繼續(xù)增加光線追蹤的次數(shù),渲染得到的圖像中反射的效果就會越來越明顯,在下圖中從左至右,從上至下,光線追蹤的次數(shù)從1次,依次增加到2次、3次和4次。Demo傳送門(部分完成Demo)
光線追蹤示例程序raytracer使用了暴力計算方式,這種方式對每條構(gòu)建出的光線都需要執(zhí)行其和每一個模型是否相交的檢測邏輯。當場景中模型的數(shù)量以及類型變得越來越復(fù)雜時,你可能想要一種加速結(jié)構(gòu)(Acceleration Structure)來執(zhí)行光線追蹤運算。加速結(jié)構(gòu)作為一種在內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),它能夠是我們快速判斷某條由原點和方向定義的射線會和哪些模型發(fā)生碰撞。實際上只要找到對于選定圖元的光線碰撞檢測算法,光線追蹤的計算邏輯并不復(fù)雜。然而,光線追蹤算法的成本是巨大的,沒有強大的經(jīng)過特殊設(shè)計的硬件支持,將會為著色器帶來極大的工作量。因此在對一個包含大量球體和平面的場景中,如果需要使用實時光線追蹤來渲染場景,加速結(jié)構(gòu)非常關(guān)鍵。當前對于光線追蹤的研究幾乎都聚焦于如何創(chuàng)建、存儲和使用這種加速結(jié)構(gòu)。
關(guān)于光線追蹤技術(shù),原著寫于2013年,這部分知識已經(jīng)較舊。在2018年的全球游戲開發(fā)者大會(Game Developers Conference, GDC)上,微軟率先為DirectX 12 API增加了光線追蹤模塊,命名為DirectX Raytracing (DXR),NVIDIA則是發(fā)布了基于實時光線追蹤的RTX技術(shù),AMD也宣布是自家的ProRender渲染引擎將支持實時光線追蹤。此外諸如EA 寒霜引擎、EA Seed、Unreal 引擎、3DMark、Unity 引擎已經(jīng)宣布將會引入光線追蹤。這在軟件層面上對于游戲中使用光線追蹤技術(shù)做了鋪墊。
2018年8月21日在德國舉行的科隆游戲展上,英偉達發(fā)布了最新的游戲顯卡RTX 2080 Ti、RTX 2080、RTX 2070,在硬件層面上對光線追蹤技術(shù)做了支持,相信接下來應(yīng)用光線追蹤技術(shù)的游戲也將越來越多。
5 總結(jié)
本章節(jié)中,我們使用了在前面的內(nèi)容中學習到的基本知識和一些渲染技術(shù)制作了一些有趣的特效。首先,我們聚焦在光照模型的建立,以及如何為渲染的模型著色。這部分內(nèi)容包含了馮氏光照模型,馮氏-賓氏光照模型,以及輪廓光內(nèi)容。我們也介紹了一些比幾何圖元中的頂點信息包含更高頻的光照效果,或者能夠表達更多細節(jié)的特效技術(shù),如法線貼圖,環(huán)境貼圖以及一些其他紋理。另外我們也演示了如何添加陰影效果,以及如何實現(xiàn)簡單的氛圍特效。此外我們也討論了一些非模擬現(xiàn)實的特效技術(shù)。
在最后一部分中,我們介紹一些應(yīng)用在屏幕空間內(nèi)的渲染技術(shù)。延遲著色技術(shù)使得在幾何圖元第一次渲染時的一些復(fù)雜并且代價昂貴的計算能夠從中解耦。通過在幀緩存對象中存儲位置、法向量、顏色和一些其他曲面屬性,我們能夠在場景渲染的最終階段執(zhí)行復(fù)雜的著色計算,并且不需要擔心性能浪費。在這個過程中,實際上我們只對可見的像素執(zhí)行標準的光照計算。在屏幕空間環(huán)境遮蔽技術(shù)中,我們通過技術(shù)每個像素周圍像素對環(huán)境光的阻擋,從而確定像素最終需要添加的環(huán)境光亮度,來模擬褶皺曲面中的陰影效果。最后,我們介紹了光線追蹤技術(shù),在示例代碼中,我們在不使用任何三角形圖元的情況下,完成了整個場景的渲染。