本文同時(shí)發(fā)布在我的個(gè)人博客上:https://dragon_boy.gitee.io
??IBL,image based lighting舞骆,即基于圖片的光照,是將周圍的環(huán)境作為一個(gè)大的發(fā)光體來進(jìn)行間接的光照,通常使用立方體貼圖技術(shù)進(jìn)行計(jì)算宝泵。
??IBL算法使用周圍的環(huán)境進(jìn)行光照厨疙,它的輸入可以被考慮為一種精度較高的環(huán)境光照洲守,或者更為自然的模擬-全局光照。使用IBL的PBR流程的物體會得到更為真實(shí)的效果。
??在講解IBL前梗醇,先給出反射方程:
??在之前說過暑始,我們主要的目的是處理包含在半球內(nèi)的所有入射光,進(jìn)行積分運(yùn)算婴削。在之前的PBR例子中廊镜,我們只考慮了有限的光源,所以可以很簡單地通過遍歷所有光源進(jìn)行積分運(yùn)算唉俗,但這次針對周圍的環(huán)境進(jìn)行光照計(jì)算嗤朴,這些可能并不方便積分的計(jì)算,這也給我們帶來了兩個(gè)計(jì)算積分的要求:
- 我們需要某種方式來方便我們根據(jù)所給定的獲得場景的輻射率虫溜。
- 處理積分要快速且實(shí)時(shí)雹姊。
??現(xiàn)在第一個(gè)要求我們已經(jīng)提過了,可以使用立方體貼圖技術(shù)來完成衡楞,我們可以將立方體貼圖的每個(gè)紋素都當(dāng)成一個(gè)單獨(dú)的發(fā)光源吱雏,通過不同的我們采樣這立方體貼圖,這樣就可以得到每個(gè)不同方向的場景的輻射率瘾境。
??根據(jù)獲得場景的輻射率就像這樣:
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
??但處理積分我們需要多個(gè)方向的采樣值歧杏,不過這也意味著每個(gè)片段都需要大量的調(diào)用這些采樣值,計(jì)算的開銷是巨大的迷守。我們可以提前進(jìn)行大量的計(jì)算來讓積分的計(jì)算更有效率一些犬绒,為此我們需要深入了解一下反射方程:
??根據(jù)積分的特性,我們可以將其分成漫反射和高光兩部分:
??這一章我們只需要關(guān)注漫反射部分兑凿。lambert模型公式和折射比率作為常量凯力,我們可以將其提出來放在外面:
??我們可以假設(shè)點(diǎn)在環(huán)境貼圖的中心,那么我們這個(gè)積分的值只取決于礼华。通過這些知識點(diǎn)咐鹤,我們可以通過卷積提前計(jì)算一個(gè)新的立方體貼圖,貼圖中存儲的是每個(gè)采樣方向的漫反射積分結(jié)果圣絮。
??卷積對數(shù)據(jù)集中的每個(gè)輸入進(jìn)行一些計(jì)算祈惶,同時(shí)也考慮到了數(shù)據(jù)集中的其它輸入,數(shù)據(jù)集可以是場景的輻射率或環(huán)境貼圖晨雳。因此行瑞,當(dāng)采樣某一方向的立方體貼圖時(shí),其它的采樣值也被考慮到計(jì)算當(dāng)中餐禁。
??為了卷積計(jì)算出一張環(huán)境貼圖血久,我們對每個(gè)輸出方向采樣方向的積分進(jìn)行處理,通過半球內(nèi)的離散地進(jìn)行大量的采樣帮非,并平均所有的輻射率氧吐,而這個(gè)半球的朝向是輸出采樣方向:
??這個(gè)提前計(jì)算出的立方體貼圖讹蘑,對每個(gè)存儲一個(gè)積分結(jié)果,可以看作是提前計(jì)算了場景的所有非直接漫反射光沿著照射到一個(gè)表面筑舅。這個(gè)立方體貼圖常被稱為反照貼圖座慰。
??比如下面的立方體貼圖和根據(jù)它計(jì)算的反照貼圖:
PBR和HDR
??由于PBR是基于物理模擬真實(shí)的效果,所以說HDR是必須要考慮在內(nèi)的翠拣。如果不考慮環(huán)境貼圖版仔,HDR的效果可以很簡單的實(shí)現(xiàn),不過使用環(huán)境貼圖的話误墓,我們需要某種方式將HDR信息保存在一張環(huán)境貼圖中蛮粮。
??我們之前都是通過立方體貼圖來實(shí)現(xiàn)環(huán)境貼圖的,但它的數(shù)據(jù)存儲范圍在[0.0,1.0]之間谜慌,即LDR然想,所以這種存儲方式的值不適合作為PBR的輸入值。
hdr文件格式
??看名字就知道這是專門存儲適合HDR范圍數(shù)據(jù)的圖片格式欣范。這種格式使用的是每通道8bit存儲顏色变泄,并且使用alpha通道作為指數(shù)。下面是一張HDR格式圖片的示意:
??可以看到恼琼,這種圖片格式的圖片和立方體貼圖不一樣妨蛹,它是由一張球形圖片投影到一個(gè)平面上形成的,這樣的話驳癌,我們就可以將環(huán)境存儲在一張單獨(dú)的名為等矩形貼圖的貼圖中滑燃。
導(dǎo)入hdr
??這里可以很簡單地使用stb_image.h導(dǎo)入,像下面這樣:
#include "stb_image.h"
[...]
stbi_set_flip_vertically_on_load(true);
int width, height, nrComponents;
float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0);
unsigned int hdrTexture;
if (data)
{
glGenTextures(1, &hdrTexture);
glBindTexture(GL_TEXTURE_2D, hdrTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Failed to load HDR image." << std::endl;
}
將等矩形貼圖轉(zhuǎn)化為立方體貼圖
??雖然可以直接使用等距形貼圖進(jìn)行計(jì)算颓鲜,但相比使用立方體貼圖,開銷較大典予,因此我們將等矩形貼圖轉(zhuǎn)化為立方體貼圖進(jìn)行計(jì)算甜滨。
??為了進(jìn)行轉(zhuǎn)換,我們需要渲染一個(gè)單位立方體瘤袖,接著將等矩形貼圖從立方體內(nèi)部投影到6個(gè)面上衣摩。頂點(diǎn)著色器很簡單:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 localPos;
uniform mat4 projection;
uniform mat4 view;
void main()
{
localPos = aPos;
gl_Position = projection * view * vec4(localPos, 1.0);
}
??在片元著色器中,我們通過立方體的位置進(jìn)行插值運(yùn)算獲得片段的采樣方向捂敌,接著使用這個(gè)方向向量艾扮,在進(jìn)行一些三角變換后,用來采樣等矩形貼圖:
#version 330 core
out vec4 FragColor;
in vec3 localPos;
uniform sampler2D equirectangularMap;
const vec2 invAtan = vec2(0.1591, 0.3183);
vec2 SampleSphericalMap(vec3 v)
{
vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
uv *= invAtan;
uv += 0.5;
return uv;
}
void main()
{
vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPos
vec3 color = texture(equirectangularMap, uv).rgb;
FragColor = vec4(color, 1.0);
}
??接著我們需要渲染同一個(gè)立方體六次占婉,每次通過一個(gè)面的方向渲染泡嘴,我們將渲染結(jié)果存儲在一個(gè)幀緩沖中:
unsigned int captureFBO, captureRBO;
glGenFramebuffers(1, &captureFBO);
glGenRenderbuffers(1, &captureRBO);
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
??同樣,為每個(gè)面創(chuàng)建一個(gè)紋理來存儲渲染結(jié)果:
unsigned int envCubemap;
glGenTextures(1, &envCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
for (unsigned int i = 0; i < 6; ++i)
{
// note that we store each face with 16 bit floating point values
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F,
512, 512, 0, GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
??接著我們通過設(shè)置6個(gè)不同的視圖矩陣渲染6次立方體:
glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
glm::mat4 captureViews[] =
{
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f))
};
// convert HDR equirectangular environment map to cubemap equivalent
equirectangularToCubemapShader.use();
equirectangularToCubemapShader.setInt("equirectangularMap", 0);
equirectangularToCubemapShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrTexture);
glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int i = 0; i < 6; ++i)
{
equirectangularToCubemapShader.setMat4("view", captureViews[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderCube(); // renders a 1x1 cube
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
??現(xiàn)在encCubemap中存儲的就是轉(zhuǎn)換過后的立方體貼圖逆济。我們用一個(gè)天空盒來測試一下酌予。
??頂點(diǎn)著色器如下:
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 projection;
uniform mat4 view;
out vec3 localPos;
void main()
{
localPos = aPos;
mat4 rotView = mat4(mat3(view)); // remove translation from the view matrix
vec4 clipPos = projection * rotView * vec4(localPos, 1.0);
gl_Position = clipPos.xyww;
}
??記得修改深度測試選項(xiàng):
glDepthFunc(GL_LEQUAL);
??片元著色器如下(記得將HDR范圍色調(diào)映射到LDR磺箕,同時(shí)將結(jié)果進(jìn)行伽馬校正):
#version 330 core
out vec4 FragColor;
in vec3 localPos;
uniform samplerCube environmentMap;
void main()
{
vec3 envColor = texture(environmentMap, localPos).rgb;
envColor = envColor / (envColor + vec3(1.0));
envColor = pow(envColor, vec3(1.0/2.2));
FragColor = vec4(envColor, 1.0);
}
??渲染結(jié)果如下:
??這里我們只是測試了環(huán)境貼圖,接下來將其用來進(jìn)行光照計(jì)算
立方體貼圖卷積
??就像上面介紹過的抛虫,我們需要將環(huán)境貼圖中心半球內(nèi)的所有的輻射率卷積運(yùn)算到朝向松靡,即法線。
??接下來我們嘗試通過上面轉(zhuǎn)化好的立方體貼圖生成反照貼圖建椰。片元著色器:
#version 330 core
out vec4 FragColor;
in vec3 localPos;
uniform samplerCube environmentMap;
const float PI = 3.14159265359;
void main()
{
// 采樣方向等于半球的朝向
vec3 normal = normalize(localPos);
vec3 irradiance = vec3(0.0);
[...] // 卷積運(yùn)算
FragColor = vec4(irradiance, 1.0);
??目前存在許多方式進(jìn)行對環(huán)境貼圖進(jìn)行卷積運(yùn)算雕欺,這里我們將根據(jù)半球朝向生成一定數(shù)量的采樣方向向量,最后將結(jié)果平均棉姐。
??反射方程的積分運(yùn)算的立體角并不方便計(jì)算阅茶,這里我們使用替代的球形坐標(biāo)和。
??我們使用極坐標(biāo)方位角繞著半球豎直圓環(huán)(到)進(jìn)行采樣谅海,使用傾角繞著水平圓環(huán)采樣(到)脸哀,這樣,積分式可以改為:
??我們通過黎曼和將上述積分改為有限的連續(xù)和:
??我們離散的采樣球形值扭吁,由于球的特性撞蜂,傾角越大,采樣范圍越小侥袜,為了補(bǔ)償這一變化蝌诡,我們可以通過縮放區(qū)域。
??上述積分操作用代碼表示如下:
vec3 irradiance = vec3(0.0);
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up = cross(normal, right);
float sampleDelta = 0.025;
float nrSamples = 0.0;
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
// 將球形坐標(biāo)轉(zhuǎn)為笛卡爾坐標(biāo)(切線空間)
vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
// 切線空間轉(zhuǎn)換到世界空間
vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N;
irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
nrSamples++;
}
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));
??操作沒啥好說的枫吧,下面創(chuàng)建一個(gè)立方體貼圖來進(jìn)行紋理映射:
unsigned int irradianceMap;
glGenTextures(1, &irradianceMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap);
for (unsigned int i = 0; i < 6; ++i)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0,
GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
??注意浦旱,由于反照貼圖中存儲的都是平均過的結(jié)果,所以沒有很豐富的細(xì)節(jié)九杂,使用32*32分辨率足夠了颁湖。接著,我們修改幀緩沖的分辨率:
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
??最后例隆,設(shè)置渲染代碼:
irradianceShader.use();
irradianceShader.setInt("environmentMap", 0);
irradianceShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glViewport(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int i = 0; i < 6; ++i)
{
irradianceShader.setMat4("view", captureViews[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderCube();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
??結(jié)果如下:
??渲染結(jié)果像是模糊版的環(huán)境貼圖甥捺。
PBR和非直接輻照光照
??我們接下來使用渲染好的反照貼圖進(jìn)行非直接的環(huán)境光計(jì)算。
uniform samplerCube irradianceMap;
??由于從反照貼圖采樣的顏色同時(shí)包含漫反射和高光部分镀层,我們需要用菲涅爾等式計(jì)算的比率將其分離開:
vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0);
vec3 kD = 1.0 - kS;
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse = irradiance * albedo;
vec3 ambient = (kD * diffuse) * ao;
??由于環(huán)境光方向來自于朝向法線半球內(nèi)的所有方向镰禾,所以說沒有一個(gè)特定的中間向量來決定菲涅爾等式的結(jié)果,所以我們將法線和觀察方向點(diǎn)乘來模擬菲涅爾的效果唱逢。但之前的微表面模型構(gòu)建中吴侦,我們使用了微表面的中間向量,通過表面的粗糙度影響坞古,并將其作為輸入用在菲涅爾的計(jì)算中备韧,但這次我們不考慮粗糙度,也就意味著绸贡,表面的反射率肯定是偏高的盯蝴,不過我們所期望的是反射部分相對低一些毅哗,非直接光照的菲涅爾效果應(yīng)該是這樣的,在粗糙度較高的非金屬表面甚至?xí)ジ吖猓?/p>
??我們可以將粗糙度加入菲涅爾的運(yùn)算來改善這一問題:
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}
??最后計(jì)算環(huán)境光代碼為:
vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec3 kD = 1.0 - kS;
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse = irradiance * albedo;
vec3 ambient = (kD * diffuse) * ao;
??渲染結(jié)果應(yīng)該如下:
??很明顯缺少高光部分捧挺,這個(gè)我們在下一章節(jié)講解虑绵。
??最后,貼出原文地址供參考:https://learnopengl.com/PBR/IBL/Diffuse-irradiance