4 更多渲染技術(shù)
傳統(tǒng)的向前渲染方式需要一個(gè)完整的圖像渲染管線,以頂點(diǎn)著色器為起點(diǎn)碟摆,氣候跟隨多個(gè)后續(xù)階段加袋,通常以片段著色器為終點(diǎn)兴想。片段著色器負(fù)責(zé)計(jì)算每個(gè)片段的最終顏色,隨著每一條繪制指令的執(zhí)行,幀緩存對(duì)象的內(nèi)容逐漸變得完整礼患。然而我們并不是只有這一種渲染場(chǎng)景的方式,正如你接下來將在本小節(jié)中看到的一樣槐瑞,你可以只計(jì)算部分著色信息据过,當(dāng)所有模型完成繪制后,仍然能夠得到渲染好的場(chǎng)景屑宠。甚至你可以繞過傳統(tǒng)的基于頂點(diǎn)的幾何圖形表示厢洞,將所有的幾何處理邏輯都放在片段著色器中。
4.1 延遲著色(Demo要求OpenGL4.2)
幾乎到目前為止我們所有的示例程序中片段著色器都是用于計(jì)算當(dāng)前正在渲染的片段的顏色。現(xiàn)在考慮這樣一個(gè)場(chǎng)景躺翻,當(dāng)你渲染多個(gè)模型時(shí)丧叽,某些模型的部分區(qū)域會(huì)覆蓋其他模型,而被覆蓋的部分實(shí)際上已經(jīng)執(zhí)行過完整的渲染邏輯公你,這種現(xiàn)象稱為覆蓋渲染(overdraw)踊淳。這種情況下需要使用新的渲染結(jié)果來覆蓋之前的渲染結(jié)果,也就意味著你之前的那部分渲染工作的結(jié)果全部被拋棄陕靠。如果片段著色器運(yùn)行的成本很高迂尝,或者有大量的覆蓋渲染,這會(huì)很影響性能剪芥。為了解決這個(gè)問題垄开,你可以使用延遲著色(Deferred Shading)技術(shù),它使得片段著色器中的高成本計(jì)算邏輯可以被推遲到最后一刻執(zhí)行税肪。
在使用延遲著色技術(shù)時(shí)溉躲,首先我們需要使用一個(gè)非常簡(jiǎn)單版本的片段著色器,該著色器需要將我們真正執(zhí)行渲染任務(wù)所需要使用到的參數(shù)輸入片段著色器中益兄。在大多數(shù)場(chǎng)景中锻梳,我們都需要使用多個(gè)幀緩存附件【煌保回顧前面介紹光照效果時(shí)使用過的渲染邏輯唱蒸,在渲染單個(gè)片段的時(shí)候需要使用到的參數(shù)有片段的漫射系數(shù),所處曲面的法向量灸叼,在世界坐標(biāo)系中的位置神汹。盡管在世界坐標(biāo)系中的位置可以通過片段在屏幕空間內(nèi)的坐標(biāo)和深度緩存中的數(shù)據(jù)重建,但是我們還是將這些數(shù)據(jù)直接存儲(chǔ)到一個(gè)幀緩存附件中會(huì)更高效古今,也會(huì)更方便屁魏。用于存儲(chǔ)這部分?jǐn)?shù)據(jù)的幀緩存對(duì)象我們通常稱為G緩存(G-buffer)。在這里G表示的是幾何體(Geometry)捉腥,意為存儲(chǔ)的是幾何體的點(diǎn)位置而不是圖像數(shù)據(jù)氓拼。
當(dāng)G緩存準(zhǔn)備好后,就可以使用一個(gè)視圖窗口的四邊形來繪制整個(gè)場(chǎng)景抵碟。這一次渲染將執(zhí)行整個(gè)光照算法邏輯桃漾,但是我們并沒有對(duì)場(chǎng)景中所有模型的每個(gè)三角形圖元光柵化得到的每個(gè)片段進(jìn)行處理,對(duì)整個(gè)幀緩存中的每個(gè)像素僅僅執(zhí)行了一個(gè)高成本的光照計(jì)算拟逮。這能夠很大程度的降低片段著色的性能開銷撬统,尤其當(dāng)使用的著色算法很復(fù)雜時(shí)這種性能提升更明顯。
4.1.1 生成G緩存
延遲著色的第一步是創(chuàng)建一個(gè)G緩存敦迄,具體的方法是為一個(gè)幀緩存對(duì)象添加多個(gè)附件恋追。OpenGL最多支持為一個(gè)幀緩存對(duì)象添加8個(gè)附件凭迹,每個(gè)附件最高支持4個(gè)32位的通道,如格式GL_RGBA32F
苦囱。然而每個(gè)附件的每個(gè)通道都會(huì)消耗一定的內(nèi)存帶寬(Memory Bandwidth)嗅绸,如果我們完全不考慮寫入到幀緩存中的數(shù)據(jù)體積,那么盡管我們提示了渲染邏輯的效率撕彤,但是我們卻增加了數(shù)據(jù)存儲(chǔ)成本鱼鸠。
通常情況下,使用16位的浮點(diǎn)數(shù)據(jù)來存儲(chǔ)顏色和法向量信息已經(jīng)足夠羹铅。32位的浮點(diǎn)數(shù)據(jù)用于存儲(chǔ)對(duì)精度要求更高的每個(gè)片段在世界坐標(biāo)系中的位置蚀狰。此外又是我們還需要存儲(chǔ)一些材質(zhì)數(shù)據(jù),例如每個(gè)片段的高光指數(shù)(Specular Exponent)睦裳,也可以稱為閃光因子(Shininess Factor)。通常對(duì)于需要存儲(chǔ)的數(shù)據(jù)我們需要使用不同的格式存儲(chǔ)撼唾,考慮內(nèi)存帶寬的高效性廉邑,一個(gè)好的方式是將這些數(shù)據(jù)打包成為相同的格式,而不是在一個(gè)幀緩存對(duì)象中使用多個(gè)不同數(shù)據(jù)格式的附件倒谷。如將2個(gè)16位的數(shù)據(jù)打包成一個(gè)32位的數(shù)據(jù)蛛蒙,具體方法稍后演示。
在本節(jié)的示例程序中渤愁,將使用3個(gè)16位數(shù)據(jù)格式的分量來存儲(chǔ)每個(gè)片段的法向量牵祟,3個(gè)16位數(shù)據(jù)格式的分量來存儲(chǔ)每個(gè)片段自己的顏色,3個(gè)32位數(shù)據(jù)格式的分量來存儲(chǔ)每個(gè)片段在世界坐標(biāo)系中的位置抖格,1個(gè)32位的整形變量來存儲(chǔ)每個(gè)片段使用的材質(zhì)索引诺苹,以及1個(gè)32位的數(shù)據(jù)格式分量存儲(chǔ)每個(gè)像素的高光指數(shù)。
總的來說需要6個(gè)16位分量和5個(gè)32位的分量雹拄。我們可以將6個(gè)16位分量打包后存儲(chǔ)在格式為GL_RGBA32UI的幀緩存的前三個(gè)分量中收奔,剩余的1個(gè)分量剛好能夠存儲(chǔ)1個(gè)32位的數(shù)據(jù)。剩下的3個(gè)32位世界空間頂點(diǎn)坐標(biāo)以及1個(gè)32位數(shù)據(jù)可以打包存儲(chǔ)在一個(gè)格式為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);
當(dāng)G緩存準(zhǔn)備完畢后,接下來需要做的事情就是向其中填充數(shù)據(jù)势篡。前面已經(jīng)提過我們需要將兩個(gè)個(gè)16位的數(shù)據(jù)打包成為1個(gè)32位的數(shù)據(jù)翩肌,這可以通過在著色器語言中調(diào)用函數(shù)packHalf2x16
(該函數(shù)要求OpenGL4.2)將2個(gè)32位浮點(diǎn)型數(shù)據(jù)轉(zhuǎn)換為2個(gè)16位浮點(diǎn)型數(shù)據(jù),并按位轉(zhuǎn)換為32位整形數(shù)據(jù)實(shí)現(xiàn)禁悠。假定我們能夠在片段著色器中拿到必要的數(shù)據(jù)念祭,那么通過如下的代碼可以將這些數(shù)據(jù)通過兩個(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;
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;
}
在準(zhǔn)備好G緩存后碍侦,下一步就是計(jì)算其中所有像素的最終顏色棒卷,并將其輸出到屏幕上顾孽。
4.1.2 使用G緩存
在準(zhǔn)備好包含漫射色,法向量比规,高光指數(shù)若厚,片段世界坐標(biāo)系中頂點(diǎn)以及其他必要信息的G緩存后,需要做的事情是從這個(gè)緩存中讀取數(shù)據(jù)蜒什,并解包重建原始數(shù)據(jù)测秸。使用函數(shù)unpackHalf2x16
可以執(zhí)行和上面代碼相反的解包邏輯,該函數(shù)會(huì)將1個(gè)32位整形數(shù)據(jù)按位解包為2個(gè)16位浮點(diǎn)型數(shù)據(jù)灾常,再轉(zhuǎn)化為2個(gè)32位浮點(diǎn)型數(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è)普通的顏色幀緩存中可以直觀看到G緩存的內(nèi)容钞瀑。其渲染結(jié)果如下沈撞。源碼傳送門由于函數(shù)unpackHalf2x16
等需要OpenGL4.2接口才能正常工作,該Demo未驗(yàn)證雕什。
左上角的圖是直接使用漫射光顏色的渲染結(jié)果缠俺,右上角的圖表示了每個(gè)片段的曲面法向量,左下角的圖片表示每個(gè)片段在世界坐標(biāo)系中的位置贷岸,右下角的圖表示了每個(gè)片段的材質(zhì)索引壹士。
對(duì)于G緩存解包重建后的數(shù)據(jù),可以使用本章節(jié)前面的任何光照模型來計(jì)算每個(gè)片段的最終顏色偿警。本實(shí)例中將使用標(biāo)準(zhǔn)馮氏著色模型躏救,具體計(jì)算邏輯如下。
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;
}
使用延時(shí)著色最終得到的場(chǎng)景渲染結(jié)果如下圖螟蒸。
在上圖中盒使,通過多實(shí)例渲染的方式繪制了超過200個(gè)甲殼蟲模型,絕大部分片段都存在覆蓋渲染情況七嫌。最終片段顏色的計(jì)算考慮了64個(gè)光源忠怖,使用延遲著色后,增加或者減少光源的數(shù)量對(duì)整個(gè)程序性能影響不是很大抄瑟。實(shí)際上程序中計(jì)算成本大的部分是生成G緩存凡泣,以及從G緩存中讀取并重建原始數(shù)據(jù),單這部分計(jì)算只需要執(zhí)行一次皮假,并且和光源的數(shù)量無關(guān)鞋拟。在本實(shí)例中為了使代碼更容易理解,使用到的G緩存效率并不高惹资,它消耗的內(nèi)存帶寬仍然還有優(yōu)化的空間贺纲,程序的性能也還能進(jìn)一步優(yōu)化。
4.1.3 法向量貼圖和延遲著色
在前面的章節(jié)中介紹過法向量貼圖褪测,這種技術(shù)可以通過將片段的法向量存儲(chǔ)在一個(gè)紋理中猴誊,通過讀取紋理中的值獲得單個(gè)片段的法向量潦刃,從而更精細(xì)的控制光照效果,以獲得更多的圖像細(xì)節(jié)懈叹。大多數(shù)法向量貼圖算法使用的都是切線空間法向量(Tangent space normals)乖杠,并在切線空間中執(zhí)行所有的光照計(jì)算。其中需要計(jì)算光照向量L和視點(diǎn)向量V澄成,在頂點(diǎn)著色器中胧洒,使用TBN矩陣將它們轉(zhuǎn)換到切線空間中,然后將轉(zhuǎn)換后到向量傳遞到片段著色器中用于光照著色計(jì)算墨状。然而卫漫,在延遲渲染中,在G緩存中存儲(chǔ)的法向量總是以世界坐標(biāo)系或者視圖坐標(biāo)系為參考肾砂。
為了生成存儲(chǔ)于G緩存中列赎,用于延遲著色,以視圖空間為參考的法向量镐确,我們需要從法線貼圖中讀取切線空間法向量包吝,并將其轉(zhuǎn)換到視圖坐標(biāo)系中,然后對(duì)普通的法向量貼圖算法進(jìn)行微調(diào)即可辫塌。
首先漏策,在頂點(diǎn)著色器中計(jì)算出視圖空間法向量N和切向量T派哲,并將它們傳遞到片段著色器中臼氨。在片段著色器中對(duì)向量N和T進(jìn)行標(biāo)準(zhǔn)化處理得到單位向量,通過它們的外積計(jì)算出副切線向量B芭届。在片段著色器中通過這三個(gè)向量構(gòu)建出TBN矩陣储矩,然后從法線貼圖中讀取切線空間的片段法向量,使用TBN的逆矩陣將其轉(zhuǎn)換到視圖空間內(nèi)褂乍,由于TBN是正交矩陣持隧,因此其逆矩陣就是它的轉(zhuǎn)置矩陣。被轉(zhuǎn)換到視圖空間中的片段法向量隨后被存入到G緩存中逃片。
生產(chǎn)G緩存的頂點(diǎn)著色器不需要修改屡拨,和上面的延遲著色示例程序使用到的頂點(diǎn)著色器相同。修改后的片段著色器如下褥实。
#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è)的突破包含更多細(xì)小的細(xì)節(jié)。示例程序DeferredShading源碼傳送門由于函數(shù)unpackHalf2x16
等需要OpenGL4.2接口才能正常工作僻澎,該Demo未驗(yàn)證貌踏。
4.1.4 延遲著色的缺點(diǎn)
盡管延遲著色技術(shù)能夠減少大量復(fù)雜的光照著色技術(shù)對(duì)于程序性能的影響十饥,但是它并不能解決所有問題。除了在生成G緩存時(shí)會(huì)額外占用大量的內(nèi)存帶寬外祖乳,它還存著一些其他缺點(diǎn)逗堵。通過一些努力,也許你能解決其中部分問題凡资,但是當(dāng)你準(zhǔn)備寫一個(gè)延遲渲染器時(shí)砸捏,你都應(yīng)該考慮如下幾件事情。
首先隙赁,你應(yīng)該仔細(xì)考慮延遲渲染著色器所需要的內(nèi)存帶寬垦藏。在本小節(jié)的示例程序中,G緩存中的每個(gè)像素都消耗了256位的內(nèi)存伞访,我們并沒有特別高效的組織數(shù)據(jù)存儲(chǔ)方式掂骏。我們將世界坐標(biāo)系直接存儲(chǔ)在G緩存中,這消耗了96位內(nèi)存空間厚掷。然而我們可以在渲染階段直接獲取到屏幕坐標(biāo)系下每個(gè)像素的位置弟灼,可以從片段著色器內(nèi)建變量gl_FragCoord
獲取x,y分量冒黑,從深度緩存中獲取到z分量田绑,從而構(gòu)建片段在屏幕坐標(biāo)系(采集坐標(biāo)系)下的坐標(biāo)。通過視口變化的逆操作抡爹,即簡(jiǎn)單的縮放和平移操作掩驱,得到標(biāo)準(zhǔn)設(shè)備坐標(biāo)系中的位置,再通過應(yīng)用投影和觀察矩陣的逆矩陣將坐標(biāo)從采集坐標(biāo)系中移動(dòng)到世界坐標(biāo)系中冬竟。觀察矩陣通常只包含平移和旋轉(zhuǎn)變化欧穴,它的逆矩陣運(yùn)算較為簡(jiǎn)單。但是投影矩陣以及齊次坐標(biāo)的逆運(yùn)算會(huì)比較復(fù)雜泵殴。
另外我們使用了48位來編碼表面法向量涮帘,但是實(shí)際上只需要存儲(chǔ)xy分量即可,由于這里使用的都是單位法向量笑诅,因此可以通過公式x2 + y2 + z2 = 1來計(jì)算z軸分量调缨。當(dāng)然這里z的符號(hào)是未確定的,但是假定我們的曲面法向量z軸都不為負(fù)吆你,那么這種方式將不會(huì)有任何問題弦叶。
另外高光指數(shù)和材質(zhì)ID我們都分別使用了32位數(shù)據(jù)來保存,但是通常情況下在渲染的場(chǎng)景中材質(zhì)的數(shù)量不會(huì)大于16位能夠表示的6000個(gè)早处。高光指數(shù)也可以保存對(duì)數(shù)形式的數(shù)據(jù)湾蔓,在我們計(jì)算的時(shí)候?qū)υ偾?的指數(shù)重構(gòu)原始數(shù)據(jù)即可,這樣也能節(jié)省一部分內(nèi)存開銷砌梆。
延遲渲染了另一個(gè)問題是它在抗鋸齒的能力上較弱默责。通常情況下贬循,使用多重采樣抗鋸齒的程序會(huì)取一個(gè)像素多個(gè)樣本的平均或者加權(quán)平均值來作為這個(gè)像素的最終輸出顏色。因此對(duì)于延遲渲染的程序桃序,在啟用多重采樣特性后我們還需要為所有的數(shù)據(jù)杖虾,如深度數(shù)據(jù)、法向量以及材質(zhì)索引等媒體數(shù)據(jù)準(zhǔn)備對(duì)多重采樣紋理并綁定到G緩存上媒熊。更糟糕的是奇适,由于最后真正圖像渲染階段使用的是一個(gè)覆蓋整個(gè)屏幕的四變形,因此對(duì)于其內(nèi)部的所有像素而言芦鳍,并沒有任何邊緣像素嚷往,這破壞了傳統(tǒng)的多重采樣抗鋸齒的計(jì)算邏輯。另外在解析階段柠衅,我們也需要準(zhǔn)備一個(gè)自定義的解析著色器為每個(gè)樣本執(zhí)行光照著色計(jì)算皮仁,這會(huì)極大的增加程序的計(jì)算成本。
最后菲宴,大多數(shù)延遲渲染算法都不能很好的處理透明問題贷祈。因?yàn)樵贕緩存中每個(gè)像素點(diǎn)我們只存儲(chǔ)了一個(gè)片段的數(shù)據(jù),而在處理透明問題時(shí)喝峦,我們需要從某個(gè)像素點(diǎn)位置離觀察者最近的片段開始直至查找到不透明的片段势誊。有一些算法使用這種方式處理透明圖元,它們都是圖元的順序不回影響最終結(jié)果的場(chǎng)景中使用谣蠢。另外一種方式首先處理所有不透明的圖元粟耻,然后再渲染透明的圖元。這種方式要求渲染器維護(hù)一個(gè)列表保存所有透明曲面漩怎,在穿越場(chǎng)景逐個(gè)渲染模型時(shí)跳過這些表面勋颖,或者穿越場(chǎng)景兩次嗦嗡。無論選擇哪種方式都是一個(gè)高成本的方案勋锤。
總的來說,如果你小心使用延遲渲染技術(shù)侥祭,很好的處理算法叁执,它將會(huì)你的程序性能帶來極大的提升。
4.2 基于屏幕空間渲染技術(shù)
到目前為止矮冬,本系列文章中所使用的渲染技術(shù)都是逐圖元渲染的谈宛。然而在前一小節(jié)中講到的延遲渲染技術(shù)并不是這樣,這意味著我們可以將一些渲染程序推遲到最后胎署,通過渲染和屏幕空間等大的圖元去執(zhí)行吆录。在這個(gè)小節(jié)中我們將接受其他一些能夠?qū)?zhí)行時(shí)機(jī)推遲的算法。在有些場(chǎng)景中琼牧,這是實(shí)現(xiàn)某些技術(shù)唯一方法恢筝,在另外一些場(chǎng)景中延遲計(jì)算邏輯到所有的幾何體已經(jīng)渲染完成后會(huì)極大的提升程序性能哀卫。
4.2.1 環(huán)境光遮蔽
現(xiàn)實(shí)世界中物體鏡面反射或者漫反射出的光線回到我們眼睛中,使得物體呈現(xiàn)特定的顏色撬槽。而這種現(xiàn)象根據(jù)物體接收到的光源類型分為直接光照和間接光照此改,其中直接光照指物體接收的光線直接來自于光源的光線,間接光照指物體接收的光線來源于物體之間反復(fù)反射后光源后的剩余光侄柔,以及物體吸收光線后再次發(fā)出的光共啃。全局光照(Global Illumination)則指的是直接光照和間接光照結(jié)合的效果。
環(huán)境光(Ambient Light)是間接光照產(chǎn)生得到的光的近似值暂题,它是一個(gè)小的移剪,固定的量被添加到光照計(jì)算公式中。環(huán)境光遮蔽(Ambient Occlusion)指在很深的褶皺中或者物體之間的間隙內(nèi)薪者,附近的曲面遮蔽環(huán)境光的現(xiàn)象挂滓。實(shí)時(shí)全局光照是當(dāng)前到一個(gè)研究課題,盡管目前已經(jīng)有了相當(dāng)多的工作啸胧,但這個(gè)課題仍然是一個(gè)未解決的問題赶站。然而我們?nèi)匀荒軌蚴褂靡恍┓钦椒椒ê痛致缘慕浦祦砟M一個(gè)可以接受的較好結(jié)果。接下來討論的屏幕空間環(huán)境光遮蔽(Screen space ambient occlusion, SSAO)就是這樣一種近似方法纺念。
我們先考慮2維平面贝椿,如果某個(gè)曲面上一個(gè)片段被任意數(shù)量的點(diǎn)光源圍繞,這些電光源可以看成是間接光照陷谱。環(huán)境光可以被認(rèn)為是照射到這個(gè)頂點(diǎn)上光線的總和烙博。在一個(gè)非常平整的曲面上,任意一點(diǎn)對(duì)于曲面上方所有的光源而言都是可見的烟逊。然而在不平整的曲面上渣窜,并不是所有的光源都能夠照射到曲面上的每一個(gè)點(diǎn),如下圖所示宪躯,對(duì)于曲面上的任意一點(diǎn)乔宿,曲面越不平整,能夠找到到該點(diǎn)到光源更少访雪。
在上圖中圍繞曲面均勻分布著8個(gè)點(diǎn)光源详瑞,對(duì)于選定的曲面中心某點(diǎn),只能接收到來自4個(gè)光源的光線臣缀,這個(gè)時(shí)候就需要考慮全局光照對(duì)這個(gè)點(diǎn)的影響坝橡。在完全全局光照模擬中,對(duì)于每個(gè)頂點(diǎn)精置,我們需要追蹤上百個(gè)计寇,甚至上千個(gè)不同方向照射到該點(diǎn)的光路徑,確定哪些路徑能夠順利照射到目標(biāo)點(diǎn)。然而這對(duì)于實(shí)時(shí)渲染程序而言計(jì)算代價(jià)過于昂貴番宁,因此我們需要使用一種方法能夠直接在屏幕空間中計(jì)算每個(gè)頂點(diǎn)的環(huán)境光遮蔽情況蹲堂。
在利用該技術(shù)時(shí),需要在屏幕空間內(nèi)對(duì)每個(gè)像素選多個(gè)隨機(jī)方向延伸出直線贝淤,沿著這條線選擇多個(gè)點(diǎn)判斷該像素是否被遮蔽柒竞,從而計(jì)算出每個(gè)像素被遮擋的程度,最后計(jì)算每個(gè)像素的顏色播聪。具體的方法是先準(zhǔn)備一個(gè)幀緩存對(duì)象朽基,首先正常將場(chǎng)景渲染到它的第一個(gè)顏色紋理附件和深度紋理附件中,然后將每個(gè)片段的法向量和在觀察空間中的深度值存儲(chǔ)在同一個(gè)幀緩存對(duì)象的第二個(gè)顏色紋理附件中离陶。
接下來需要使用已經(jīng)獲得的數(shù)據(jù)計(jì)算每個(gè)片段的遮蔽程度稼虎。這個(gè)階段需要使用遮蔽著色器來渲染一個(gè)全屏的四邊形。該著色器讀取某個(gè)片段的深度值招刨,選擇一個(gè)隨機(jī)方向并延伸霎俩,以一定的距離間隔選取多個(gè)點(diǎn),比較每個(gè)點(diǎn)上的插值計(jì)算出的深度值和在深度緩存中存儲(chǔ)的深度值的大小沉眶,如果插值深度值大于深度緩存的值打却,則認(rèn)為該插值點(diǎn)被其他幾何圖像遮擋,也意味著這個(gè)方向上的光不能夠照射到被延伸的像素谎倔。
在選擇隨機(jī)方向之前柳击,需要先準(zhǔn)備一個(gè)包含大量隨機(jī)單位向量的緩存對(duì)象,并將其作為一個(gè)著色器中的統(tǒng)一變量使用片习。隨機(jī)向量可能指向任何方向捌肴,但是我們只需要考慮和曲面法向量同側(cè)的隨機(jī)向量。通過計(jì)算曲面法向量和選取的隨機(jī)向量的點(diǎn)積藕咏,我們可以篩選出這種條件的隨機(jī)向量状知。如下圖,如果點(diǎn)積為負(fù)孽查,則其指向法向量背側(cè)饥悴,此時(shí)只需要取其負(fù)向量即可。
在上圖中卦碾,向量v0铺坞、v1和v4指向了法向量N同側(cè)起宽,它們的點(diǎn)積為正洲胖。向量v2和v3指向了法向量N背側(cè),它們的點(diǎn)積為負(fù)坯沪,因此我們需要取其負(fù)向量-v2和-v3作為我們判斷像素遮蔽情況的延伸方向绿映。
當(dāng)確定好隨機(jī)向量V(xv, yv, zv)后,接下來就需要沿著向量計(jì)算像素的遮蔽情況。選取屏幕空間上的某點(diǎn)PO(xo, yo)叉弦,在之前準(zhǔn)備好的紋理中查詢出視口空間內(nèi)深度值z(mì)o丐一,沿著隨機(jī)向量的方向延伸一段距離得到新的點(diǎn)PN(xn, yn, zn)。(xn, yn)為屏幕空間內(nèi)的坐標(biāo)淹冰,通過PO點(diǎn)和向量V2(xv, yv)和步長stepDistance計(jì)算得到库车,zn為視口空間內(nèi)的坐標(biāo),通過PO點(diǎn)和向量V1(zv)計(jì)算得到樱拴。這里點(diǎn)PN的三個(gè)坐標(biāo)分量不在同一個(gè)坐標(biāo)系中柠衍,因此并未嚴(yán)格按照隨機(jī)向量V進(jìn)行插值。
通過以(xn, yn)為紋素坐標(biāo)晶乔,在前一步準(zhǔn)備好的顏色紋理中查詢?cè)撟鴺?biāo)對(duì)應(yīng)片段在視口空間中的深度值z(mì)nv珍坊。比較znv和zn的大小,如果znv比zn更小正罢,則這個(gè)插值得到的點(diǎn)被渲染的場(chǎng)景中某個(gè)片段阻擋阵漏,則認(rèn)為被延伸的點(diǎn)唄遮擋。盡管這種計(jì)算方式并不準(zhǔn)確翻具,但是就統(tǒng)計(jì)學(xué)上而言是有效的履怯。選擇的隨機(jī)向量個(gè)數(shù),在每個(gè)隨機(jī)向量方向上插值的次數(shù)裆泳,以及插值的步長都可以控制最終得到的圖像質(zhì)量虑乖。這三個(gè)值越高,得到的圖像質(zhì)量越好晾虑。下圖展示了隨機(jī)向量數(shù)量對(duì)屏幕空間環(huán)境光遮蔽算法處理圖像質(zhì)量的影響疹味。
上圖中,從左到右帜篇,從上到下糙捺,在計(jì)算環(huán)境光遮蔽時(shí)使用到的隨機(jī)向量數(shù)量遞增,依次為1笙隙、4洪灯、16和64【固担可以明顯看到签钩,當(dāng)選擇的隨機(jī)向量數(shù)量達(dá)到64個(gè)時(shí),圖片才變得光滑坏快。隨機(jī)向量越少铅檩,條帶越明顯。改善圖像質(zhì)量的方式有很多莽鸿,但是最有效的方法之一就是為每個(gè)樣本生成一個(gè)隨機(jī)種子昧旨,用于確定環(huán)境光遮蔽計(jì)算中的步長拾给。這種方式引入了圖像噪聲,但是卻提高了圖像質(zhì)量兔沃,下圖演示了這種技術(shù)的效果蒋得。
可以明顯看到,在確定每個(gè)樣本步長時(shí)應(yīng)用隨機(jī)種子能夠明顯的改善環(huán)境光遮蔽的效果乒疏。此時(shí)在只選取1個(gè)隨機(jī)向量的渲染結(jié)果中额衙,盡管圖片質(zhì)量很糟糕,但是仍然能夠比固定步長的版本更好怕吴,在選取4個(gè)隨機(jī)向量的渲染結(jié)果中入偷,圖片質(zhì)量已經(jīng)可以被接受,其對(duì)應(yīng)固定步長版本的渲染結(jié)果則會(huì)有很明顯的條帶械哟。這種方式所引入的圖像噪聲其實(shí)也可以解決疏之,但是已經(jīng)超出了這個(gè)例子所要討論的問題范圍。
介紹完環(huán)境光遮蔽計(jì)算方式后暇咆,需要做的就是對(duì)正常渲染的圖像應(yīng)用這種技術(shù)锋爪。環(huán)境光遮蔽是指環(huán)境光被阻擋的數(shù)量,因此每個(gè)片段的環(huán)境光計(jì)算方式是在著色器顏色計(jì)算公式中使用遮蔽系數(shù)和環(huán)境光相乘即可爸业,這樣被渲染的場(chǎng)景中出現(xiàn)褶皺的地方最后其顏色值添加的環(huán)境光會(huì)更少虚循,使得最終的渲染圖像陰影效果看上去更加真實(shí)添诉。下圖顏色了屏幕空間環(huán)境光遮蔽技術(shù)的應(yīng)用效果钢拧。源碼傳送門
上圖中培己,左側(cè)的圖片僅僅計(jì)算了漫射光和鏡面高光,被渲染出來的模型看上去更像是懸掛在一個(gè)屏幕上钧忽,另外從圖片中也很難看出景物的深度毯炮。右側(cè)的圖片是應(yīng)用屏幕空間環(huán)境光遮蔽技術(shù)的渲染結(jié)果,可以看到不僅一些模型的細(xì)節(jié)更加豐滿耸黑,地面上也能看到軟陰影效果桃煎,景深的感覺也更明顯。
在第一次渲染過程和大多數(shù)例子一樣大刊,將場(chǎng)景渲染到一個(gè)顏色附件中为迈。在第二次渲染過程中需要應(yīng)用環(huán)境光遮蔽計(jì)算,示例程序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ù)都是通過渲染一個(gè)全窗口的四邊形,再對(duì)之前有幾何體組成的場(chǎng)景渲染結(jié)果進(jìn)一步處理伴郁。在本節(jié)中耿战,會(huì)進(jìn)一步說明如和使用一個(gè)全窗口四邊形渲染整個(gè)場(chǎng)景。
4.3.1 渲染朱莉婭分形
這小節(jié)的示例程序渲染了一個(gè)朱莉婭集合(Julia Set)蛾绎,這種分形圖像只需要使用紋理坐標(biāo)即可創(chuàng)建昆箕。朱莉婭集合和曼德勃羅集合(Mandelbrot Set)相關(guān)鸦列,它由如下公式生成租冠。
當(dāng)Z值超過閾值時(shí)鹏倘,循環(huán)結(jié)束。如果在允許的迭代次數(shù)內(nèi)Z的值不大于閾值顽爹,則認(rèn)為該點(diǎn)位于曼德勃羅集合內(nèi)部纤泵,并使用某種默認(rèn)顏色為其著色。如果Z值大于閾值镜粤,則認(rèn)為這個(gè)點(diǎn)在集合外部捏题,通常此時(shí)使用一個(gè)迭代函數(shù)來為該點(diǎn)著色。朱莉婭集合和曼德勃羅集合的區(qū)別在于Z和C的初始條件不一樣肉渴。
渲染曼德勃羅集合時(shí)公荧,Z被定義為(0+0i),C被定義為執(zhí)行插值的點(diǎn)坐標(biāo)同规。在渲染朱莉婭集合時(shí)循狰,Z被定義為執(zhí)行插值的點(diǎn)坐標(biāo),C被定義為一個(gè)程序內(nèi)部指定的常量券勺。因此曼德勃羅集合只有一個(gè)绪钥,而朱莉婭集合有無窮個(gè)。這也意味著朱莉婭集合可以通過編程控制关炼,也可以執(zhí)行動(dòng)畫程腹。和前面的例子一樣,我們使用一個(gè)全窗口的四邊形來渲染場(chǎng)景儒拂,不同的是不在使用幀緩存中準(zhǔn)備好的數(shù)據(jù)寸潦,而是直接生成圖像。
在片段著色器中定義一個(gè)包含紋理坐標(biāo)的輸入變量社痛。聲明一個(gè)統(tǒng)一變了來保存C值甸祭,一個(gè)統(tǒng)一變量保存最大迭代數(shù)。為了使得生成的朱莉婭集合更好看褥影,使用一個(gè)一維漸變顏色紋理為其著色池户。當(dāng)確定某個(gè)點(diǎn)位于集合內(nèi)部時(shí),使用迭代次數(shù)作為紋理坐標(biāo)為片段著色凡怎。最大迭代數(shù)可以平衡圖像的細(xì)節(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;
確定某個(gè)片段是否位于集合內(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ù)寨典,意味著該點(diǎn)位于集合內(nèi)部,將之涂為黑色房匆,否則到漸變顏色紋理中去查詢對(duì)應(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));
}
剩下的就是提供一個(gè)漸變顏色紋理,并設(shè)置一個(gè)合適的C值即可井氢。在示例程序中弦追,每一幀畫面都使用渲染函數(shù)中傳入的時(shí)間參數(shù)來更新C的值,從而添加動(dòng)畫效果花竞。下圖是示例程序julia中的部分幀的效果圖劲件。Demo傳送門
4.3.2 片段著色器中的光線追蹤
OpenGL的工作原理是基于光柵化,也就是將如線约急、三角形和點(diǎn)圖元分解為片段零远。幾何體進(jìn)入到OpenGL的圖形管線后,對(duì)于每個(gè)三角形圖元厌蔽,OpenGL將會(huì)找出它所覆蓋的像素牵辣,然后運(yùn)行我們編寫的著色器計(jì)算每個(gè)像素的顏色。光線追蹤的原理與之完全不同奴饮,它能夠得到更好的效果纬向,但是其計(jì)算成本更高。
從觀察點(diǎn)向成像窗口上的每個(gè)點(diǎn)發(fā)出一條射線拐云,直至碰到場(chǎng)景中最近的模型罢猪,從而計(jì)算每個(gè)像素的顏色。和傳統(tǒng)的光柵化方式相比叉瘩,這種方式最大的缺點(diǎn)是并沒有OpenGL設(shè)計(jì)層面的直接支持膳帕,這意味著所有的工作都需要在我們自己編寫著色器完成。然而薇缅,這種方式會(huì)帶來很多好處危彩,我們可以跳出點(diǎn)、線和三角形的局限泳桦,我們可以更直觀的理解射線碰到模型表面后的的行為汤徽。使用類似之前用到的判斷片段可見性的技術(shù),我們用很少的代碼就能夠模擬光的反射灸撰,陰影谒府,甚至光的折射,并且這樣的到的效果更加真實(shí)浮毯。這種成像的方式也更接近現(xiàn)實(shí)世界中物體在人眼中成像的方式完疫。
本小節(jié)會(huì)介紹如何使用片段著色器遞構(gòu)建簡(jiǎn)單的遞歸光線追蹤器。該光線追蹤器能夠渲染由簡(jiǎn)單球體和無限平面構(gòu)成的場(chǎng)景债蓝,可以渲染經(jīng)典的“盒子中的玻璃球“(Glossy spheres in a box)圖像壳鹤。可以肯定的是饰迹,一定有更優(yōu)秀的光線追蹤算法芳誓,但是這個(gè)追蹤器已經(jīng)足夠說明光線追蹤算法的基本原理余舶。下圖是一個(gè)簡(jiǎn)化版本的2維簡(jiǎn)單光線追蹤器的示意圖。
在上圖中锹淌,觀察點(diǎn)為O匿值,從O點(diǎn)向呈像平面中像素P發(fā)出一條光線直至碰到場(chǎng)景中的某個(gè)模型表面上某個(gè)點(diǎn)Io,這個(gè)初始光線表示為R_primary葛圃,點(diǎn)Io處曲面的法向量為N千扔,從點(diǎn)Io向光源延伸一條指向光源的射線表示為R_shadow憎妙,如果這條射線在中途碰到了其他模型库正,則點(diǎn)Io位于陰影中,否則該點(diǎn)被光源直接照射厘唾。另外在點(diǎn)Io處褥符,根據(jù)法向量N計(jì)算入射光線的反射向量表示為R_reflected。
光線追蹤成像方式中像素的顏色計(jì)算方式和前面講到的光照顏色計(jì)算公式并不是完全不同的抚垃,我們?nèi)匀豢梢苑謩e計(jì)算漫射光和反射光喷楣,也可以使用法線紋理貼圖等提高圖像質(zhì)量。在計(jì)算像素P的顏色時(shí)鹤树,需要計(jì)算光源直接照射到點(diǎn)Io產(chǎn)生的顏色铣焊,以及反射光線R_reflected尋找到的另外一個(gè)模型上的點(diǎn)I1的對(duì)點(diǎn)I0的顏色貢獻(xiàn)。
以點(diǎn)O為原點(diǎn)罕伯,沿著光線R_primary方向向量D曲伊,并以其模長為速度,經(jīng)過時(shí)間t后到達(dá)了位于球面上某點(diǎn)P追他,假定球體的圓心為C坟募,球體的半徑為r。兩個(gè)相同向量的點(diǎn)積為其模的平方邑狸,則存在如下公式懈糯。
替換其中的點(diǎn)P為O+tD,則存在如下公式单雾。
展開多項(xiàng)式赚哗,以t作為自變量,可以將上述式子表示如下硅堆。
可以簡(jiǎn)寫為At2+Bt+C = 0屿储,其中
可以求得t如下
假定向量D選擇的是單位向量,則其長度為1硬萍,上式可以進(jìn)一步簡(jiǎn)寫為
如果4C的值比B2更大扩所,則意味著t無解,表示該光線不會(huì)和這個(gè)球體相交朴乖。如果相等祖屏,表示光線和球相切助赞,只有1個(gè)交點(diǎn)。如果更小袁勺,表示光線穿過球體雹食,有兩個(gè)交點(diǎn)。如果存在負(fù)解期丰,則表示交點(diǎn)在觀察點(diǎn)背面群叶。在有兩個(gè)交點(diǎn)時(shí),選擇最小的正值t作為光線和模型相交的時(shí)間钝荡,并利用公式P=O+tO計(jì)算出交點(diǎn)在3D空間內(nèi)的坐標(biāo)街立。
上面尋找光線和模型交點(diǎn)的代碼如下。
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
未找到光線和球的交點(diǎn)時(shí)返回0埠通,如果找到了交點(diǎn)赎离,則將交點(diǎn)的坐標(biāo)寫入到參數(shù)hitpos
中,交點(diǎn)位置在球面的法向量寫入到參數(shù)normal
中端辱。對(duì)于每個(gè)點(diǎn)發(fā)出的光線梁剔,我們需要其和場(chǎng)景中多個(gè)模型中最近的交點(diǎn),可以先設(shè)置一個(gè)臨時(shí)的初始值舞蔽,然后再遍歷這些球體荣病,從而尋找到最近的交點(diǎn)。這部分邏輯代碼如下渗柿。
// 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;
}
}
}
假如對(duì)于每個(gè)光線追蹤尋找到的點(diǎn)都默認(rèn)著色為白色个盆,則對(duì)于包含單個(gè)球體的場(chǎng)景,這種方式的渲染結(jié)果如下做祝。
接下來我們需要應(yīng)用光照算法為每個(gè)片段著色砾省。在光照計(jì)算中,光線追蹤函數(shù)返回的交點(diǎn)曲面法向量是重要的參數(shù)混槐。和前面例子中光照計(jì)算的方式一樣编兄,使用曲面法向量,光線追蹤函數(shù)計(jì)算出的交點(diǎn)在視點(diǎn)空間坐標(biāo)声登,以及材質(zhì)參數(shù)計(jì)算每個(gè)交點(diǎn)的顏色狠鸳。應(yīng)用光照著色算法后同樣的場(chǎng)景渲染結(jié)果如下。
法向量不僅僅用于光照著色公式中悯嗓,在接下來最終光線的下一個(gè)目標(biāo)時(shí)也發(fā)揮重要作用件舵。對(duì)于追蹤到第一個(gè)模型交點(diǎn)此后每個(gè)通過法向量計(jì)算出的新光線而言,每次追蹤到的交點(diǎn)在計(jì)算其本身的顏色后都會(huì)計(jì)算它們對(duì)于光線追蹤源像素最終顏色的貢獻(xiàn)分量脯厨,從而確定最終的源像素顏色铅祸。這是光線追蹤方式相對(duì)于傳統(tǒng)的光柵化方式的第一個(gè)優(yōu)點(diǎn)。
假設(shè)曲面上某點(diǎn)P,光源L和觀察點(diǎn)O临梗,初始的光線從點(diǎn)O出發(fā)射向點(diǎn)P涡扼,點(diǎn)P到L的光線為R_Shadow,其單位向量為D盟庞。如果光線R_Shadow能夠順利到達(dá)光源吃沪,不會(huì)觸碰到場(chǎng)景中的其他模型,則可以直接使用光照著色公式為該點(diǎn)著色什猖。否則票彪,點(diǎn)P位于陰影中。這種陰影效果也是應(yīng)用光線追蹤算法的優(yōu)勢(shì)不狮。
在交點(diǎn)P除了能構(gòu)建指向光源的光線外降铸,我們還能構(gòu)建指向任意方向的光線。例如可以構(gòu)建入射光線在點(diǎn)P處對(duì)于法向量的折射光線荤傲,并繼續(xù)追蹤光線尋找下一個(gè)交點(diǎn)垮耳,將得到顏色計(jì)入光線追蹤源像素點(diǎn)最終顏色颈渊。
光線追蹤是一個(gè)遞歸算法遂黍,追蹤一條光線,找到一個(gè)交點(diǎn)為其著色俊嗽,然后創(chuàng)建一條新的光線雾家,再進(jìn)行下一次迭代。而GLSL中并不支持循環(huán)語法绍豁,因此在本例中使用由多個(gè)紋理組成的棧來實(shí)現(xiàn)光線追蹤算法芯咧,這也是在上面的代碼中從紋理中讀取光線源點(diǎn)和方向的原因。
具體的方法是創(chuàng)建一組幀緩存對(duì)象竹揍,每個(gè)幀緩存對(duì)象添加4個(gè)顏色附件敬飒。對(duì)于幀緩存中的每個(gè)像素,這4個(gè)附件分別保存了最終合成顏色芬位,光線的源點(diǎn)无拗,當(dāng)前光線的方向,以及顏色貢獻(xiàn)系數(shù)昧碉。在本例中允許光線最多追蹤5個(gè)模型的交點(diǎn)英染,因此創(chuàng)建了5個(gè)幀緩存對(duì)象。第一個(gè)顏色附件被饿,即保存最終合成顏色的附件在多個(gè)幀緩存對(duì)象中是共享的四康,而另外3個(gè)顏色附件對(duì)于每個(gè)幀緩存對(duì)象都是獨(dú)有的。在每一次渲染行為中狭握,我們從一組紋理中讀取數(shù)據(jù)闪金,然后寫入到另外一組紋理中,如下圖论颅。
初始化光線追蹤器需要運(yùn)行著色器將初始的原點(diǎn)和方向?qū)懭氲郊y理中哎垦,將最終合成顏色紋理中所有點(diǎn)的值設(shè)置為0喝检,將顏色貢獻(xiàn)系數(shù)紋理中所有點(diǎn)的值設(shè)置為1。然后運(yùn)行光線追蹤著色器撼泛,每次光線追蹤行為都繪制一個(gè)全窗口的四邊形挠说。在每次繪制行為中,綁定上一次繪制準(zhǔn)備好的原點(diǎn)愿题,方向和顏色貢獻(xiàn)系數(shù)紋理损俭。同時(shí)也綁定一個(gè)幀緩存對(duì)象包含需要輸出的原點(diǎn),方向和顏色貢獻(xiàn)系數(shù)紋理潘酗,這些數(shù)據(jù)講在下一次渲染行為中使用到杆兵。對(duì)于每個(gè)像素,光線追蹤著色器通過原點(diǎn)和方向構(gòu)建出一條光線并向場(chǎng)景中追蹤仔夺,為尋找到的交點(diǎn)著色琐脏,再乘以顏色貢獻(xiàn)系數(shù)后寫入到接受數(shù)據(jù)的幀緩存對(duì)象的第一個(gè)顏色附件中。
想要計(jì)算多次光線追蹤結(jié)果累積得到的最終顏色缸兔,需要講最終合成顏色紋理作為第一個(gè)顏色附件添加到每個(gè)幀緩存對(duì)象中日裙,并且為這個(gè)附件開啟顏色混合功能,顏色混合函數(shù)的源和目標(biāo)顏色系數(shù)都需要設(shè)置為1惰蜜,這表示直接講兩個(gè)顏色疊加混合昂拂。
如果在場(chǎng)景中添加更多的球體模型,我們可以通過這個(gè)技術(shù)更真實(shí)的模擬光線在多個(gè)模型之間折射的效果抛猖。下圖中格侯,對(duì)于通過光線追蹤渲染的場(chǎng)景,隨著光線追蹤次數(shù)增加财著,其渲染結(jié)果的細(xì)節(jié)更加真實(shí)联四。
上圖中,左上角的是未進(jìn)行第二次光線追蹤的渲染效果撑教,此時(shí)能夠看出場(chǎng)景中的模型都比較暗淡朝墩。在右上角的圖片中,我們進(jìn)行了2次光線追蹤驮履,能夠看見一些球體表面此時(shí)已經(jīng)有反射光的效果鱼辙。在左下角的圖片中,光線追蹤的次數(shù)增加到3次玫镐,此時(shí)模型的光澤可以經(jīng)過兩次反射到達(dá)另外一個(gè)模型表面倒戏。在右下角的圖片中,光線追蹤的次數(shù)繼續(xù)增加到4次恐似,此時(shí)能夠觀察到更多的細(xì)節(jié)杜跷。
為了使得渲染的場(chǎng)景更加有趣,我們可以在其中加入其他類型的模型。盡管在理論上葛闷,任意類型的模型都能夠被追蹤憋槐,但是另外一個(gè)更容易查詢光線是否和其有交點(diǎn)的模型類型是平面。平面的一種表示方法是一個(gè)單位法向量淑趾,以及對(duì)于一個(gè)過坐標(biāo)系原點(diǎn)的法向量阳仔,從該法向量和平面的交點(diǎn)沿著它的方向到原點(diǎn)的距離。
法向量由3個(gè)分量組成扣泊,距離是一個(gè)標(biāo)量近范,但是其值有正負(fù),沿著法向量方向?yàn)檎有罚駝t其值為反评矩。可以將前者打包在1個(gè)4維向量的x阱飘、y斥杜、z分量中,將后者打包在同一個(gè)4維向量的w分量中沥匈。實(shí)際上蔗喂,對(duì)于給定的法向量N,和同法向量通向過坐標(biāo)系原點(diǎn)的線與平面交點(diǎn)沿著法向量方向到原點(diǎn)的距離d咐熙,平面可以用如下公式表示弱恒。
當(dāng)上式成立時(shí),P為平面上的任意一點(diǎn)棋恼。對(duì)于在光線追蹤時(shí)構(gòu)建出的線上任意一點(diǎn)可以表示為如下等式。其中O為光線追蹤的起點(diǎn)锈玉,t為經(jīng)歷的時(shí)間爪飘,D為速度向量,由于t沒有具體的時(shí)間單位拉背,因此D為單位向量师崎。
替換第一個(gè)公式中的P可以得到如下公式。
然后求解出t椅棺。
從上式中可以看出犁罩,如果向量D和N的點(diǎn)積為0,即當(dāng)向量D和平面同向時(shí)两疚,t無解床估。其他情況下,被追蹤的光線和參考平面一定存在交點(diǎn)诱渤。如果接觸的t值為負(fù)丐巫,交點(diǎn)位于觀察者后方,這種情況不做處理。如果t值為正递胧,交點(diǎn)位于觀察者前方碑韵,此時(shí)需要進(jìn)一步的著色計(jì)算。執(zhí)行光線和平面相交檢測(cè)的代碼如下缎脾。
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;
}
在前文渲染的場(chǎng)景中所有球體的背面添加一個(gè)平面后的渲染結(jié)果如下圖祝闻。在左圖中,盡管這種方式為場(chǎng)景添加了一些景深效果遗菠,但是仍然沒有最大發(fā)揮光線追蹤的潛力治筒,繼續(xù)增加光線追蹤的次數(shù),此時(shí)能夠在右圖中看到平面上出現(xiàn)了更多球體的折射影像舷蒲,甚至在球體表示上也折射出了平面的內(nèi)容耸袜。
添加更多的平面將整個(gè)場(chǎng)景包在一個(gè)盒子中,我們可以得到更有趣的圖像牲平,其渲染結(jié)果如下圖堤框。繼續(xù)增加光線追蹤的次數(shù),渲染得到的圖像中反射的效果就會(huì)越來越明顯纵柿,在下圖中從左至右蜈抓,從上至下,光線追蹤的次數(shù)從1次昂儒,依次增加到2次沟使、3次和4次。Demo傳送門(部分完成Demo)
光線追蹤示例程序raytracer使用了暴力計(jì)算方式渊跋,這種方式對(duì)每條構(gòu)建出的光線都需要執(zhí)行其和每一個(gè)模型是否相交的檢測(cè)邏輯腊嗡。當(dāng)場(chǎng)景中模型的數(shù)量以及類型變得越來越復(fù)雜時(shí),你可能想要一種加速結(jié)構(gòu)(Acceleration Structure)來執(zhí)行光線追蹤運(yùn)算拾酝。加速結(jié)構(gòu)作為一種在內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)燕少,它能夠是我們快速判斷某條由原點(diǎn)和方向定義的射線會(huì)和哪些模型發(fā)生碰撞。實(shí)際上只要找到對(duì)于選定圖元的光線碰撞檢測(cè)算法蒿囤,光線追蹤的計(jì)算邏輯并不復(fù)雜客们。然而,光線追蹤算法的成本是巨大的材诽,沒有強(qiáng)大的經(jīng)過特殊設(shè)計(jì)的硬件支持底挫,將會(huì)為著色器帶來極大的工作量。因此在對(duì)一個(gè)包含大量球體和平面的場(chǎng)景中脸侥,如果需要使用實(shí)時(shí)光線追蹤來渲染場(chǎng)景建邓,加速結(jié)構(gòu)非常關(guān)鍵。當(dāng)前對(duì)于光線追蹤的研究幾乎都聚焦于如何創(chuàng)建湿痢、存儲(chǔ)和使用這種加速結(jié)構(gòu)涝缝。
關(guān)于光線追蹤技術(shù)扑庞,原著寫于2013年,這部分知識(shí)已經(jīng)較舊拒逮。在2018年的全球游戲開發(fā)者大會(huì)(Game Developers Conference, GDC)上罐氨,微軟率先為DirectX 12 API增加了光線追蹤模塊,命名為DirectX Raytracing (DXR)滩援,NVIDIA則是發(fā)布了基于實(shí)時(shí)光線追蹤的RTX技術(shù)栅隐,AMD也宣布是自家的ProRender渲染引擎將支持實(shí)時(shí)光線追蹤。此外諸如EA 寒霜引擎玩徊、EA Seed租悄、Unreal 引擎、3DMark恩袱、Unity 引擎已經(jīng)宣布將會(huì)引入光線追蹤泣棋。這在軟件層面上對(duì)于游戲中使用光線追蹤技術(shù)做了鋪墊。
2018年8月21日在德國舉行的科隆游戲展上畔塔,英偉達(dá)發(fā)布了最新的游戲顯卡RTX 2080 Ti潭辈、RTX 2080、RTX 2070澈吨,在硬件層面上對(duì)光線追蹤技術(shù)做了支持把敢,相信接下來應(yīng)用光線追蹤技術(shù)的游戲也將越來越多。
5 總結(jié)
本章節(jié)中谅辣,我們使用了在前面的內(nèi)容中學(xué)習(xí)到的基本知識(shí)和一些渲染技術(shù)制作了一些有趣的特效修赞。首先,我們聚焦在光照模型的建立桑阶,以及如何為渲染的模型著色柏副。這部分內(nèi)容包含了馮氏光照模型,馮氏-賓氏光照模型联逻,以及輪廓光內(nèi)容搓扯。我們也介紹了一些比幾何圖元中的頂點(diǎn)信息包含更高頻的光照效果,或者能夠表達(dá)更多細(xì)節(jié)的特效技術(shù)包归,如法線貼圖,環(huán)境貼圖以及一些其他紋理铅歼。另外我們也演示了如何添加陰影效果公壤,以及如何實(shí)現(xiàn)簡(jiǎn)單的氛圍特效。此外我們也討論了一些非模擬現(xiàn)實(shí)的特效技術(shù)椎椰。
在最后一部分中厦幅,我們介紹一些應(yīng)用在屏幕空間內(nèi)的渲染技術(shù)。延遲著色技術(shù)使得在幾何圖元第一次渲染時(shí)的一些復(fù)雜并且代價(jià)昂貴的計(jì)算能夠從中解耦慨飘。通過在幀緩存對(duì)象中存儲(chǔ)位置确憨、法向量译荞、顏色和一些其他曲面屬性,我們能夠在場(chǎng)景渲染的最終階段執(zhí)行復(fù)雜的著色計(jì)算休弃,并且不需要擔(dān)心性能浪費(fèi)吞歼。在這個(gè)過程中,實(shí)際上我們只對(duì)可見的像素執(zhí)行標(biāo)準(zhǔn)的光照計(jì)算塔猾。在屏幕空間環(huán)境遮蔽技術(shù)中篙骡,我們通過技術(shù)每個(gè)像素周圍像素對(duì)環(huán)境光的阻擋,從而確定像素最終需要添加的環(huán)境光亮度丈甸,來模擬褶皺曲面中的陰影效果糯俗。最后,我們介紹了光線追蹤技術(shù)睦擂,在示例代碼中得湘,我們?cè)诓皇褂萌魏稳切螆D元的情況下,完成了整個(gè)場(chǎng)景的渲染顿仇。