学习OpenGL。 第6.2课-基于物理的渲染。 分析光源

OGL3 上一课概述了实现物理上可行的渲染模型的基础知识。 这次,我们将在直接(分析)光源(点光源,定向光源或探照灯)的参与下,从理论计算转向特定的渲染实现。


首先,让我们刷新上一课中用于计算反射率的表达式:

Lop omegao= int\极 Omegakd fracc pi+ fracDFG4 omegao cdotn omegai cdotnLip omegain cdot omegaid omegai


在大多数情况下,我们已经处理了该公式的组成部分,但问题仍然是如何具体表示辐照度 ,即总能量亮度( radianceL整个场景。 我们同意能量的亮度 L(根据计算机图形学术语)被认为是辐射通量的比值( radiant flux phi(光源的辐射能量)到立体角的值  omega。 在我们的情况下,立体角  omega我们将其视为无穷小,因此能量亮度给出了每条单独光线(其方向)的辐射通量的概念。

如何将这些计算与上一课中了解的照明模型联系起来? 首先,想象一下,给您一个单点光源(在所有方向均匀发光),其辐射通量定义为RGB三合一(23.47,21.31,20.79)。 这种源的辐射强度等于其所有方向的辐射通量。 但是,考虑到确定特定点的颜色的问题 p在表面上,您可以看到半球中所有可能的光入射方向 \欧仅矢量 wi显然将来自光源。 由于只表示一个光源,用空间中的一个点表示,因此所有其他可能的光入射方向都指向一个点 p能量亮度将等于零:

现在,如果我们不暂时考虑给定光源的光衰减定律,那么事实证明,无论我们将光源放置在何处,该光源的入射光束的能量亮度都保持不变(基于入射角的余弦的光度缩放)  phi也不算数)。 总的来说,点光源不管视角如何都保持辐射力恒定,这相当于使辐射力等于三元组常数形式的初始辐射通量(23.47、21.31、20.79)。

但是,能量亮度的计算也基于该点的坐标 p,至少任何物理上可靠的光源都显示出随着从点到光源的距离增加,辐射力的衰减。 从光度的原始表达式可以看出,还应考虑表面的方向:辐射力的计算结果必须乘以表面法线向量的标量值 n和辐射入射矢量 wi

重写以上内容:对于直接点光源,辐射功能 L考虑到距该点给定距离的衰减,确定入射光的颜色 p并考虑到按比例缩放 n cdotwi但仅适用于一束光 wi达到重点 p-本质上是连接源和点的唯一向量。 以源代码的形式,其解释如下:

vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta; 

如果您对稍微修改的术语视而不见,那么这段代码应该使您想起某些东西。 是的,是的,这些都是用于计算我们已知的照明模型中的散射分量的相同代码。 对于直接照明,能量亮度由光源的单个矢量确定,因为计算的方式与我们仍然知道的非常相似。

我注意到,只有在点光源是无穷小并由空间中的点表示的假设下,这种说法才是正确的。 对体积源建模时,其光度在许多方向上将不同于零,而不仅仅是在一个光束上。

对于从单个点发出辐射的其他光源,以相同的方式计算能量亮度。 例如,定向光源的方向恒定 wi且不使用衰减,并且投影源会根据源的方向显示变化的辐射功率。

这里我们返回积分的值  int在半球表面 \欧。 由于我们预先知道参与特定点阴影的所有光源的位置,因此我们无需尝试求解积分。 我们可以直接计算此数量的光源提供的总照射,因为每个光源的表面能量亮度受单个方向的影响。

结果,直接光源的PBR计算是一件相当简单的事情,因为所有这些都归结为对照明中涉及的光源的顺序搜索。 稍后,来自环境的组件将出现在照明模型中,我们将在基于图像的照明( 基于图像的照明IBL )教程中进行研究。 积分的估计无可避免,因为这种模型中的光从许多方向掉落。

PBR表面模型


让我们从实现上述PBR模型的片段着色器开始。 首先,我们设置表面着色所需的输入数据:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; 

在这里,您可以看到使用最简单的顶点着色器计算的常规输入,以及描述对象表面特征的一组制服。

此外,在着色器代码的最开始,我们执行从Blinn-Fong照明模型的实现中非常熟悉的计算:

 void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] } 

直接照明


本课程的示例仅包含四个点光源,这些光源清楚地指定了场景的照射。 为了满足反射率的表示,我们反复遍历每个光源,计算各个能量的亮度并总结出这一贡献,同时调制BRDF值和光束的入射角。 您可以将此迭代想象为表面上积分的一种解决方案 \欧仅用于分析光源。

因此,我们首先计算每个来源的计算值:

 vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...] 

由于计算是在线性空间进行的(在着色器的末端执行了伽马校正 ),因此根据距离的平方反比使用了更物理上正确的衰减定律:

让反平方定律在物理上更正确,以便更好地控制阻尼的性质,很有可能使用已经熟悉的包含常数,线性和二次项的公式。

此外,对于每个来源,我们还计算镜像Cook-Torrance BRDF的值:

 fracDFG4 omegao cdotn omegai cdotn


第一步是计算镜面反射与漫反射之间的比率,换句话说,计算反射光量与表面折射的光量之间的比率。 从上一课中,我们知道菲涅耳系数的计算是什么样的:

 vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } 

菲涅尔-施里克(Fresnel-Schlick)近似值在输入处期望参数F0 ,该参数显示了零入射光角(入射角为0)时的表面反射程度。 如果您沿法线从上到下看表面,则反射度为0。 F0的值因材料而异,并获得金属的偏色,这可以通过查看PBR材料的目录来看到。 对于金属工作流程过程(PBR材料的创作过程,将所有材料分为电介质和导体类别),假定所有电介质在F0 = 0.04的恒定值下看起来都相当可靠,而对于金属表面, F0是根据表面反照率设置的。 以代码形式:

 vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); 

如您所见,对于严格非金属的表面, F0设置为等于0.04。 但是同时,它可以根据表面的“金属性”从此值平滑地改变为反照率值。 该指示器通常以单独的纹理显示(实际上,从此处开始采用金属工作流程, 大约为Trans。 )。

已收到 F我们需要计算正态分布函数的值 D和几何函数 G

具有分析照明的情况下的功能代码:

 float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / denom; } float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } 

理论部分所述的一个重要区别 :这里我们直接将粗糙度参数传递给所有提到的函数。 这样做是为了使每个功能都能以自己的方式修改原始粗糙度值。 例如,迪士尼的研究反映在Epic Games的引擎中,该研究表明,如果我们在几何函数和正态分布函数中使用粗糙度的平方,则照明模型将在视觉上提供更正确的结果。

设置完所有功能后,就可以直接获得NDF和G的值:

 float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); 

总计,我们拥有用于计算整个Cook-Torrance BRDF的所有值:

 vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator; 

请注意,在将标量乘积归零的情况下,我们将分母的最小值限制为0.001,以防止被零除。

现在我们开始计算每个光源对反射率方程的贡献。 由于菲涅耳系数直接是一个变量 Ks,则可以使用F的值指示光源对表面镜面反射的贡献。 从数量 Ks可以得到折射率 Kd

 vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; 

由于我们将代表光能数量的量kS视为反射表面,因此将其减去1,就可以得到被表面折射的光kD的剩余能量。 另外,由于金属不使光折射并且不具有再发射的光的扩散分量,因此对于全金属材料,将分量kD调制为零。 经过这些计算之后,我们将拥有所有可用数据,以计算每个光源提供的反射率:

 const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } 

最终值Lo或输出能量亮度本质上是反射率表示的一种解决方案,即 表面整合结果 \欧。 在这种情况下,我们无需尝试针对所有可能的方向以一般形式求解积分,因为在此示例中,只有四个光源会影响正在处理的片段。 这就是为什么所有“集成”都限于现有光源的简单循环。

仅需将背景照明组件的相似性添加到直接光源的计算结果中,并且片段的最终颜色已准备就绪:

 vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; 

线性渲染和HDR


到目前为止,我们假设所有计算都是在线性色彩空间中进行的,因此将伽玛校正用作着色器中的最终和弦。 在线性空间中进行计算对于正确模拟PBR极为重要,因为该模型要求所有输入数据都具有线性。 尽量不要确保任何参数的线性度,否则遮光效果将不正确。 另外,最好将光源的特性设置为接近真实光源:例如,其辐射的颜色和能量亮度可以在很大的范围内自由变化。 结果, Lo可以很容易地接受较大的值,但是由于默认帧缓冲区的低动态范围( LDR ),不可避免地落在间隔[0.,1.]的截止范围内。

为了避免HDR值丢失,在进行伽玛校正之前,必须进行色调压缩:

 color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); 

这里使用了熟悉的Reinhardt算子,它使我们能够在大大改变图像不同部分的辐射的条件下保持宽广的动态范围。 由于这里我们不使用单独的着色器进行后期处理,因此可以将所描述的操作简单地添加到着色器代码的末尾。


我重复一遍,对于正确的PBR建模,记住并考虑使用线性色彩空间和HDR渲染的功能非常重要。 忽略这些方面将导致错误的计算和视觉上不美观的结果。

用于分析照明的PBR着色器


因此,连同色调压缩和伽马校正形式的最终接触,仅保留将片段的最终颜色传输到片段着色器的输出,并且可以认为用于直接照明的PBR着色器代码已完成。 最后,让我们看一下此着色器的main()函数的整个代码:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; //   uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; //   uniform vec3 lightPositions[4]; uniform vec3 lightColors[4]; uniform vec3 camPos; const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness); float GeometrySchlickGGX(float NdotV, float roughness); float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness); vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness); void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); //    vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { //        vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; // Cook-Torrance BRDF float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); vec3 specular = numerator / max(denominator, 0.001); //       Lo float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0); } 

我希望在阅读理论部分并通过今天对反射能力表达的分析后,该列表不再显得令人生畏。

我们在包含四个点光源的场景中使用此着色器,一定数量的球的表面特性将分别沿水平轴和垂直轴更改其粗糙度和金属度。 在输出中,我们得到以下图片:


金属性从零到顶部从零到一变化,粗糙度相似,但从左到右。 显而易见的是,仅改变这两个表面特性就已经可以设置各种各样的材料。

完整的源代码在这里

PBR和纹理


我们将通过传递纹理形式的特征来扩展表面模型。 通过这种方式,我们可以提供表面材料参数的每片段控制:

 [...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] } 

请注意,表面反照率纹理通常是由艺术家在sRGB颜色空间中创建的,因此在上面的代码中,我们将texel颜色返回到线性空间,以便可以将其用于进一步的计算中。 根据美术师如何创建包含环境光遮挡贴图数据的纹理,可能还必须将其带入线性空间。 金属性和粗糙度图几乎总是在线性空间中创建的。

与先前使用的照明算法相比,结合使用纹理而不是固定的表面参数与PBR算法可以显着提高视觉可靠性:


完整的纹理示例代码在此处 ,使用的纹理在此处 (以及背景阴影纹理)。 我提请您注意以下事实:强金属表面在直接照明条件下会变暗,因为漫反射的贡献很小(在极限范围内根本没有)。 仅当考虑到环境光的镜面反射时,它们的阴影才会变得更正确,这将在下一课中进行。

目前,结果可能不像某些PBR演示那样令人印象深刻-但我们尚未实现基于图像的照明系统( IBL )。 尽管如此,现在我们的渲染还是基于物理原理来考虑的,即使没有IBL,它的图像也比以前更可靠。

Source: https://habr.com/ru/post/zh-CN424453/


All Articles