OpenGL高級(jí)渲染技術(shù)

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)景的渲染顿仇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淘正,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子夺欲,更是在濱河造成了極大的恐慌跪帝,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件些阅,死亡現(xiàn)場(chǎng)離奇詭異伞剑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)市埋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門黎泣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缤谎,你說我怎么就攤上這事抒倚。” “怎么了坷澡?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵托呕,是天一觀的道長。 經(jīng)常有香客問我频敛,道長项郊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任斟赚,我火速辦了婚禮着降,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拗军。我一直安慰自己任洞,他們只是感情好蓄喇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著交掏,像睡著了一般妆偏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耀销,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天楼眷,我揣著相機(jī)與錄音,去河邊找鬼熊尉。 笑死罐柳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的狰住。 我是一名探鬼主播张吉,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼催植!你這毒婦竟也來了肮蛹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤创南,失蹤者是張志新(化名)和其女友劉穎伦忠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稿辙,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昆码,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邻储。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赋咽。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吨娜,靈堂內(nèi)的尸體忽然破棺而出脓匿,到底是詐尸還是另有隱情,我是刑警寧澤宦赠,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布陪毡,位于F島的核電站,受9級(jí)特大地震影響勾扭,放射性物質(zhì)發(fā)生泄漏缤骨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一尺借、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧精拟,春花似錦燎斩、人聲如沸虱歪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笋鄙。三九已至,卻和暖如春怪瓶,著一層夾襖步出監(jiān)牢的瞬間萧落,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工洗贰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留找岖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓敛滋,卻偏偏與公主長得像许布,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绎晃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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