LearnOpenGL-笔记-其十三

article/2025/7/1 3:15:56

PBR(Physically Based Rendering)

什么是基于物理的渲染?简单地说,还记得我们之前学习的法线贴图的内容吗?我们希望不修改物体实际几何形状的前提下去修改表面的法线方向来实现不同的光照效果,实现这个内容的基础就是我们的光照效果是基于某个光照模型——比如布林冯模型来做的,但是这种渲染模式某种意义上不会让你觉得很繁琐吗?因为他太不真实了——物体的光照模型是模拟出来的,然后光照效果也是模拟出来的,我们能否换一种更真实的做法来渲染呢?基于物理的渲染就是这样的思路:

基于微平面(Microfacet)的表面模型

所有的PBR技术都基于微平面理论。这项理论认为,达到微观尺度之后任何平面都可以用被称为微平面(Microfacets)的细小镜面来进行描绘。根据平面粗糙程度的不同,这些细小镜面的取向排列可以相当不一致。

产生的效果就是:一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散(Scatter)开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射。

在微观尺度下,没有任何平面是完全光滑的。然而由于这些微平面已经微小到无法逐像素地继续对其进行区分,因此我们假设一个粗糙度(Roughness)参数,然后用统计学的方法来估计微平面的粗糙程度。我们可以基于一个平面的粗糙度来计算出众多微平面中,朝向方向沿着某个向量ℎ方向的比例。这个向量ℎ便是位于光线向量𝑙和视线向量𝑣之间的半程向量(Halfway Vector)。

总结来说就是,取代之前用一个反射系数来概括一个物体表面的反射能力,我们PBR中的物体都是基于微平面假设的——微平面其实就是指一个平面表面由多个不规则排列的微小表面组成。我们会统计所有组成这个平面的微小平面的朝向方向与根据光线和视线得到的半程向量的一致程度的数目比例来得到这个平面整体的粗糙度。

能量守恒

能量守恒作为我们物理学中不可逾越的定律,我们渲染过程中的光线传播当然也不能忽视这个过程。一般来说当光线照射到表面上后,首先入射光线会变成反射光线和折射光线,其中反射光线就是没有被物体吸收的光线能量而折射光线就是被吸收的部分,显然根据能量守恒定律,这二者的能量就不能大于入射光线的能量。在基于物理的渲染之中我们进行了简化,假设对平面上的每一点所有的折射光都会被完全吸收而不会散开。而有一些被称为次表面散射(Subsurface Scattering)技术的着色器技术将这个问题考虑了进去,它们显著地提升了一些诸如皮肤,大理石或者蜡质这样材质的视觉效果,不过伴随而来的代价是性能的下降。

对于金属(Metallic)表面,当讨论到反射与折射的时候还有一个细节需要注意。金属表面对光的反应与非金属(也被称为介电质(Dielectrics))表面相比是不同的。它们遵从的反射与折射原理是相同的,但是所有的折射光都会被直接吸收而不会散开,只留下反射光或者说镜面反射光。亦即是说,金属表面只会显示镜面反射颜色,而不会显示出漫反射颜色。由于金属与电介质之间存在这样明显的区别,因此它们两者在PBR渲染管线中被区别处理,而我们将在文章的后面进一步详细探讨这个问题。

反射光与折射光之间的这个区别使我们得到了另一条关于能量守恒的经验结论:反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。

于是我们得到二者的计算方式:

反射率方程

没想到在GAMES101里学习的内容有一天终究还是派上了用场:是的,传说中的渲染方程还是登场了!(不过其实我不知道这是PBR的内容,GAMES101里只说这个是最接近现实情况的渲染方法)

让我们复习一下这个方程中的内容:
首先方程本身的物理意义就是光入射到点之后如何反射,L0代表的就是p点\omega0方向(立体角)的反射光。 其实更准确的描述是辐射亮度——即特定大小范围内的单位时间光照能量。

然后是fr包括括号内这一串的内容,这是一个——BRDF函数,或者叫双边反射分布函数,他描述了表面如何反射光,它接受入射(光)方向𝜔𝑖,出射(观察)方向𝜔𝑜,平面法线𝑛以及一个用来表示微平面粗糙程度的参数𝑎作为函数的输入参数。BRDF可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的贡献程度。

Li就是指输入的光线(下标为i就是input,为o就是output吧,我猜的),我们利用光线和平面间的入射角的余弦值cos⁡𝜃来计算能量,亦即从辐射率公式𝐿转化至反射率公式时的𝑛⋅𝜔𝑖,最后是这个积分,这是一个立体角

其实本身的方程内容也没有多复杂,说白了就是对立体角积分然后把入射的辐射通量乘以一个角度和BRDF函数而已,不过这个BRDF函数——作为描述不同材质如何反射光线的方程其实比较重要。

目前最主流的BRDF函数是:

这里就不展开说了,展开说就是长篇大论了,我们先了解到基本的概念。

在实际的开发中,有这么几个属性是不可或缺的:

光说不练假把式,我们来具体实现PBR的光照。

要完成PBR光照模型,就得做出来渲染方程;要做出来渲染方程,就得做出来BRDF函数;要做出来BRDF函数,就得先实现菲捏耳方程,法线分布函数和几何函数。其中:

法线分布函数(Normal Distribution Function, NDF)

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 nom   = a2;float denom = (NdotH2 * (a2 - 1.0) + 1.0);denom = PI * denom * denom;return nom / denom;
}

NDF用来描述表面微观几何的分布,控制镜面反射的分布范围以及修改粗糙度参数影响高光的扩散程度,一句话,我们用NDF来描述物体表面粗糙度。

几何函数(Geometry Function)

float GeometrySchlickGGX(float NdotV, float roughness)
{float r = (roughness + 1.0);float k = (r*r) / 8.0;float nom   = NdotV;float denom = NdotV * (1.0 - k) + k;return nom / 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;
}

几何函数用于模拟微表面间的遮蔽(Shadowing)​​ 和 ​阴影(Masking)​​ 效应。

菲涅尔方程(Fresnel Equation)

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

菲涅尔方程用来计算光线在交界处的反射比例(与入射角相关),决定材质反射特性。

然后是我们的主渲染循环:

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 BRDFfloat NDF = DistributionGGX(N, H, roughness);   float G   = GeometrySmith(N, V, L, roughness);      vec3 F    = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);vec3 numerator    = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;vec3 specular = numerator / denominator;// 能量守恒vec3 kS = F;vec3 kD = vec3(1.0) - kS;kD *= 1.0 - metallic;	  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;// HDR色调映射color = color / (color + vec3(1.0));// gamma校正color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0);
}

效果如图:

可以看到从左到右分别是不同材质的PBR光照效果,从下往上球体的金属性从0.0变到1.0, 从左到右球体的粗糙度从0.0变到1.0。

我们也可以去添加纹理来实现PBR光照效果,但是有个小小的问题是,纹理在被创造时往往就被美术工作者们给定好了诸如反射率等属性以符合真实的视觉效果,而这些属性一般是在sRGB空间里给定的,与渲染方程中需求的线性空间不同:

所以可能会涉及一个空间的转换。

效果如下:

IBL(Image based lighting)——漫反射辐照

基于图像的光照(Image based lighting, IBL)是一类光照技术的集合。其光源不是如前一节教程中描述的可分解的直接光源,而是将周围环境整体视为一个大光源。IBL 通常使用(取自现实世界或从3D场景生成的)环境立方体贴图 (Cubemap) ,我们可以将立方体贴图的每个像素视为光源,在渲染方程中直接使用它。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。

由于基于图像的光照算法会捕捉部分甚至全部的环境光照,通常认为它是一种更精确的环境光照输入格式,甚至也可以说是一种全局光照的粗略近似。基于此特性,IBL 对 PBR 很有意义,因为当我们将环境光纳入计算之后,物体在物理方面看起来会更加准确。

说白了,我们会把所有的周围环境都视作光源(在计算机中其实周围环境就是由环境立方体贴图,也就是大家熟知的天空盒实现),通过渲染方程计算发射光,这就是我们的IBL的核心。接下来我来展示如何实现IBL光照效果:

辐照度图

首先是一个新的概念:辐照度图,辐照度图(Irradiance Map)是计算机图形学中用于高效模拟环境光照(尤其是间接漫反射)的预计算纹理,其核心原理是将环境光能积分转化为可快速采样的数据。

可以这样理解辐射度图的作用:因为IBL的环境光范围比较大,一个个立体角去调用渲染方程开销非常大,于是我们就用一个容器——也就是辐射度图去预计算各个方向的环境贴图(天空盒是六个环境贴图组成的立方体贴图)的辐射强度,也就是我们最后渲染方程的结果的一部分,然后把这些数字存储在贴图里,我们渲染时只需要去采样贴图就能获得需要的数值。

// 1. 创建辐照度图帧缓冲
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);// 2. 创建帧缓冲对象
unsigned int captureFBO;
unsigned int captureRBO;
glGenFramebuffers(1, &captureFBO);
glGenRenderbuffers(1, &captureRBO);
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);

辐照度卷积着色器

辐照度计算的过程具体来说是通过卷积计算得到结果的——这就像深度学习的卷积一样,我们针对图计算辐照度时采取卷积计算以得到更贴切的结果。

#version 330 core
out vec4 FragColor;
in vec3 WorldPos;uniform samplerCube environmentMap;const float PI = 3.14159265359;void main()
{		vec3 N = normalize(WorldPos);vec3 irradiance = vec3(0.0);   // 计算切线空间vec3 up    = vec3(0.0, 1.0, 0.0);vec3 right = normalize(cross(up, N));up         = normalize(cross(N, 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){// 球面坐标转笛卡尔坐标vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));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));FragColor = vec4(irradiance, 1.0);
}

HDR环境贴图

一般来说,我们的环境贴图会采用HDR的环境贴图:这是因为环境贴图中往往包含较亮或者较暗(超过显示器亮度阈值)的部分,我们需要用HDR环境贴图以实现更好的视觉效果。除此之外,HDR环境贴图中的亮度也是线性空间的而不是像纹理一样是sRGB空间的,我们可以直接丢入算式中使用。

以下是完整的实现流程:

// 1. 加载HDR环境贴图
unsigned int hdrTexture = loadHDRTexture("environment.hdr");// 2. 创建环境立方体贴图
unsigned int envCubemap;
glGenTextures(1, &envCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
for (unsigned int i = 0; i < 6; ++i)
{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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// 3. 将HDR环境贴图转换为立方体贴图
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))
};// 4. 生成辐照度图
irradianceShader.use();
irradianceShader.setInt("environmentMap", 0);
irradianceShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);glViewport(0, 0, 32, 32);
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();
}// 5. 生成预过滤环境贴图
unsigned int prefilterMap;
glGenTextures(1, &prefilterMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
for (unsigned int i = 0; i < 6; ++i)
{glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);// 6. 生成BRDF查找表
unsigned int brdfLUTTexture;
glGenTextures(1, &brdfLUTTexture);
glBindTexture(GL_TEXTURE_2D, brdfLUTTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0);
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);

流程如上,最后生成的BRDF表用于高效计算Cook-Torrance BRDF模型中镜面反射项的菲涅尔与几何项组合值的核心组件。

效果如下:

IBL(Image based lighting)——镜面反射

我们完成漫反射部分之后就来到了镜面反射部分。

既然漫反射都涉及到这么多的不同,显然当光源不是一个个独立的部分而是一张张完整的贴图时我们的求解方法也完全不同,而解决这个差异的关键就是蒙特卡洛积分

 

法线分布函数与几何函数本质上是没有太大改变的,因为这两个函数是用来描述物体表面的粗糙情况和遮蔽情况,那我们修改光源又不修改物体本身,所以不会有改变:最大的变化来自于菲涅尔方程的变化:

也就是说,菲涅尔方程从基础的PBR模型里的单一光源方向输入切换成了整个半球的积分,且我们在半球积分时还需要应付不同入射方向的不同视线方向,所以这演变成了一个高维积分问题,而目前最好的高维积分求解的方法就是蒙特卡洛积分。

为什么其他积分不行?

归根到底,是时间复杂度问题。

镜面反射的流程本质上和漫反射大差不差:我们的漫反射提前通过卷积计算光照结果后存储在纹理中供采样读取,而镜面反射则是提前进行蒙特卡洛积分来计算结果后也是存储在纹理中进行读取。

// prefilter.fs - 预过滤环境贴图的生成
#version 330 core
out vec4 FragColor;
in vec3 WorldPos;uniform samplerCube environmentMap;
uniform float roughness;const float PI = 3.14159265359;// 1. 低差异序列生成
float RadicalInverse_VdC(uint bits) 
{bits = (bits << 16u) | (bits >> 16u);bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);return float(bits) * 2.3283064365386963e-10;
}vec2 Hammersley(uint i, uint N)
{return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}// 2. 重要性采样
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{float a = roughness*roughness;// 将均匀分布的随机数转换为GGX分布float phi = 2.0 * PI * Xi.x;float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));float sinTheta = sqrt(1.0 - cosTheta*cosTheta);// 球面坐标转笛卡尔坐标vec3 H;H.x = cos(phi) * sinTheta;H.y = sin(phi) * sinTheta;H.z = cosTheta;// 从切线空间转换到世界空间vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);vec3 tangent = normalize(cross(up, N));vec3 bitangent = cross(N, tangent);return normalize(tangent * H.x + bitangent * H.y + N * H.z);
}void main()
{		vec3 N = normalize(WorldPos);vec3 R = N;vec3 V = R;// 3. 蒙特卡洛积分const uint SAMPLE_COUNT = 1024u;vec3 prefilteredColor = vec3(0.0);float totalWeight = 0.0;for(uint i = 0u; i < SAMPLE_COUNT; ++i){// 生成采样点vec2 Xi = Hammersley(i, SAMPLE_COUNT);vec3 H = ImportanceSampleGGX(Xi, N, roughness);vec3 L = normalize(2.0 * dot(V, H) * H - V);float NdotL = max(dot(N, L), 0.0);if(NdotL > 0.0){// 计算权重并累加prefilteredColor += texture(environmentMap, L).rgb * NdotL;totalWeight += NdotL;}}prefilteredColor = prefilteredColor / totalWeight;FragColor = vec4(prefilteredColor, 1.0);
}

这里是蒙特卡洛积分的实现。

// brdf.fs - BRDF查找表的生成
#version 330 core
out vec2 FragColor;
in vec2 TexCoords;const float PI = 3.14159265359;// 1. 几何函数
float GeometrySchlickGGX(float NdotV, float roughness)
{float r = (roughness + 1.0);float k = (r*r) / 8.0;float nom = NdotV;float denom = NdotV * (1.0 - k) + k;return nom / 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;
}// 2. BRDF积分
vec2 IntegrateBRDF(float NdotV, float roughness)
{vec3 V;V.x = sqrt(1.0 - NdotV*NdotV);V.y = 0.0;V.z = NdotV;float A = 0.0;float B = 0.0;vec3 N = vec3(0.0, 0.0, 1.0);const uint SAMPLE_COUNT = 1024u;for(uint i = 0u; i < SAMPLE_COUNT; ++i){vec2 Xi = Hammersley(i, SAMPLE_COUNT);vec3 H = ImportanceSampleGGX(Xi, N, roughness);vec3 L = normalize(2.0 * dot(V, H) * H - V);float NdotL = max(L.z, 0.0);float NdotH = max(H.z, 0.0);float VdotH = max(dot(V, H), 0.0);if(NdotL > 0.0){float G = GeometrySmith(N, V, L, roughness);float G_Vis = (G * VdotH) / (NdotH * NdotV);float Fc = pow(1.0 - VdotH, 5.0);A += (1.0 - Fc) * G_Vis;B += Fc * G_Vis;}}A /= float(SAMPLE_COUNT);B /= float(SAMPLE_COUNT);return vec2(A, B);
}void main() 
{vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);FragColor = integratedBRDF;
}

积分后我们正常进行渲染方程求解。

// pbr.fs - 使用预计算的镜面反射IBL
void main()
{		// ... 其他PBR计算 ...// 1. 计算菲涅尔项vec3 F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);// 2. 分离漫反射和镜面反射vec3 kS = F;vec3 kD = 1.0 - kS;kD *= 1.0 - metallic;	  // 3. 镜面反射IBLconst float MAX_REFLECTION_LOD = 4.0;vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;    vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);// 4. 最终环境光照vec3 ambient = (kD * diffuse + specular) * ao;
}

最后在着色器中使用。

效果如下:


http://www.hkcw.cn/article/xwdYYMBOQK.shtml

相关文章

微软PowerBI考试 PL-300学习指南

微软PowerBI考试 PL-300学习指南 Microsoft Power BI 数据分析师学习指南 昨天的投票情况&#xff1a; 技能概览 准备数据 (25-30%) 数据建模 (25-30%) 可视化和分析数据 (25-30%) 管理和保护 Power BI (15–20%) 准备数据 (25-30%) 获取或连接到数据 确定并连接到数据源…

机器学习——集成学习

一、集成学习概念 集成学习: (Ensemble Learning)是一种机器学习范式&#xff0c;它通过构建并结合多个模型来完成学习任务,获得更好的泛化性能。 核心思想&#xff1a;通过组合多个弱学习器来构建一个强学习器。 bagging思想&#xff1a;有放回的抽样&#xff1b;平权投票…

ResNet改进(46):Ghost-ResNet优化卷积神经网络

1.创新点分析 引言 在计算机视觉领域,ResNet是里程碑式的架构,但其计算量较大限制了在资源受限环境的应用。 华为诺亚方舟实验室提出的Ghost模块通过"廉价操作"生成冗余特征图,显著降低了计算成本。 本文将深入解析基于Ghost模块的ResNet实现,展示如何在不显著…

光伏功率预测 | LSTM多变量单步光伏功率预测(Matlab完整源码和数据)

光伏功率预测 | MATLAB实现基于LSTM长短期记忆神经网络的光伏功率预测 目录 光伏功率预测 | MATLAB实现基于LSTM长短期记忆神经网络的光伏功率预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 光伏功率预测 | LSTM多变量单步光伏功率预测&#xff08;Matlab完整源码和…

langGraph多Agent

目录 子图&#xff08;Subgraphs&#xff09;使用子图共享状态模式&#xff08;Shared state schemas&#xff09;不同状态模式&#xff08;Different state schemas&#xff09;添加持久化查看子图状态流式获取子图输出 多智能体系统&#xff08;Multi-agent systems&#xff…

OVD开放词汇检测中COCO数据集的属性

前面的文章介绍了在Detic中基于COCO数据集实现OVD检测的操作方法&#xff0c;但是要在其他数据集上迁移&#xff0c;还是要了解COCO数据集是如何被利用的&#xff0c;这里针对数据集的属性进行说明。 COCO数据集的标签形式做过目标检测的应该都很熟悉&#xff0c;图像名称、宽…

构建高性能风控指标系统

一、引言 在金融风控领域&#xff0c;指标是风险识别的核心依据。风控平台核心系统之一--规则引擎的运行依赖规则、变量和指标&#xff0c;一个高性能的指标系统非常重要&#xff0c;本文将深入探讨风控平台指标系统的全链路技术实现&#xff0c;涵盖从指标配置到查询优化的完…

【LLM】Agent综述《Advances And Challenges In Foundation Agents》

note 拥有完善的认知架构仅仅只是第一步。Foundation Agent 的核心特征之一在于其自进化 (Self-Evolution) 的能力&#xff0c;即 Agent 能够通过与环境的交互和自我反思&#xff0c;不断学习、适应和提升自身能力&#xff0c;而无需持续的人工干预。自进化机制&#xff1a;优…

《Pytorch深度学习实践》ch3-反向传播

------B站《刘二大人》 1.Introduction 在神经网络中&#xff0c;可以看到权重非常多&#xff0c;计算 loss 对 w 的偏导非常困难&#xff0c;于是引入了反向传播方法&#xff1b; 2.Backward 这里模型为 y x * w&#xff0c;所以要计算的偏导数为 loss 对 w&#xff1b; …

房产销售系统 Java+Vue.js+SpringBoot,包括房源信息、房屋户型、房源类型、预约看房、房屋评价、房屋收藏模块

房产销售系统 JavaVue.jsSpringBoot&#xff0c;包括房源信息、房屋户型、房源类型、预约看房、房屋评价、房屋收藏模块 百度云盘链接&#xff1a;https://pan.baidu.com/s/1Ku27fPWwc47z2aSO_dow6w 密码&#xff1a;da1g 房产销售系统 摘 要 随着科学技术的飞速发展&#xf…

从0开始学vue:vue3和vue2的关系

一、版本演进关系1. 继承关系2. 版本生命周期 二、核心差异对比三、关键演进方向1. Composition API2. 性能优化 四、迁移策略1. 兼容构建模式2. 关键破坏性变更 五、生态演进1. 官方库升级2. 构建工具链 六、选型建议1. 新项目2. 现有项目 七、未来展望 一、版本演进关系 1. …

python 如何写4或5的表达式

python写4或5的表达式的方法&#xff1a; python中和是用“and”语句&#xff0c;或是用“or”语句。那么4或5的表达式为“4 or 5” 具体示例如下&#xff1a; 执行结果&#xff1a;

电子电气架构 --- 如何应对未来区域式电子电气(E/E)架构的挑战?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

绿盟 IPS 设备分析操作手册

一、操作手册说明 本手册面向安全监控分析人员&#xff0c;聚焦绿盟 IPS 设备的基础功能操作与典型攻击场景分析&#xff0c;提供安全事件监控、告警详情查看、白名单配置等功能指引&#xff0c;以及 Shiro 反序列化漏洞的检测与应急方法&#xff0c;助力及时发现并处置安全威…

Arch安装megaton

安装devkitPro https://blog.csdn.net/qq_39942341/article/details/148387077?spm1001.2014.3001.5501 安装cargo https://blog.csdn.net/qq_39942341/article/details/148387783?spm1001.2014.3001.5501 确认一下bashrc sudo pacman -S git cmake ninjagit clone https:/…

【Qt开发】对话框

目录 1&#xff0c;对话框的介绍 2&#xff0c;Qt内置对话框 2-1&#xff0c;消息对话框QMessageBox 2-2&#xff0c;颜色对话框QColorDialog 2-3&#xff0c;文件对话框QFileDialog 2-4&#xff0c;字体对话框QFontDialog 2-5&#xff0c;输入对话框QInputDialog 1&…

7.4-Creating data loaders for an instruction dataset

Chapter 7-Fine-tuning to follow instructions 7.4-Creating data loaders for an instruction dataset 我们只需将InstructionDataset对象和custom_collate_fn函数接入 PyTorch 数据加载器 ​ 使用以下代码来初始化设备信息 device torch.device("cuda" if tor…

android 上位机调试软件-安卓串口 com ttl 调试——仙盟创梦IDE

在 Android 开发中&#xff0c;基于com.ttl库的串口调试 Web 编写意义非凡。它打破了硬件与软件之间的壁垒&#xff0c;让 Android 设备能够与外部串口设备通信。对于智能家居、工业控制等领域&#xff0c;这一功能使得手机或平板能成为控制终端&#xff0c;实现远程监控与操作…

【笔记】解决虚拟环境中找不到 chromedriver 的问题

✅解决虚拟环境中找不到 chromedriver 的问题 &#x1f4cc; 问题描述&#xff1a; 在 Windows 中已将 D:\chromedriver\ 添加到系统环境变量 PATH&#xff0c;在系统终端中运行 chromedriver 没有问题。 但在项目虚拟环境&#xff08;如 .venv&#xff09;中运行项目时&…

Linux 基础指令入门指南:解锁命令行的实用密码

文章目录 引言&#xff1a;Linux 下基本指令常用选项ls 指令pwd 命令cd 指令touch 指令mkdir 指令rmdir 指令 && rm 指令man 指令cp 指令mv 指令cat 指令more 指令less 指令head 指令tail 指令date 指令cal 指令find 指令按文件名搜索按文件大小搜索按修改时间搜索按文…