1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-22 20:25:28 +08:00

7-2.PBR光照章节校对

This commit is contained in:
yuyn
2023-01-25 14:46:06 +08:00
committed by Gary Wang
parent e390dd448a
commit 6a3c793ccf

View File

@@ -4,25 +4,10 @@
---|---
作者 | JoeyDeVries
翻译 | [KenLee](https://hellokenlee.github.io/)
校对 | 暂无
校对 | JeremyYvv
!!! note
本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。
!!! Important
译者注:
<br>
阅读本节请熟悉上一节提到的几个名词:
<br>
- 辐射通量(Radiant flux)<br>
- 辐射率(Radiance)<br>
- 辐照度(Irradiance)<br>
- 辐射强度(Radiant Intensity)
在[上一个教程](https://learnopengl-cn.github.io/07%20PBR/01%20Theory/)中我们讨论了一些PBR渲染的基础知识。 在本章节中,我们将重点放在把以前讨论过的理论转化为实际的渲染器,这个渲染器将使用直接的(或解析的)光源:比如点光源,定向灯或聚光灯。
在[上一个教程](https://learnopengl-cn.github.io/07%20PBR/01%20Theory/)中我们讨论了一些PBR的基础知识。在本章节中我们把重点放在将之前讨论的理论转化为实际的渲染器这个渲染器将使用直接的或解析的光源比如点光源定向灯或聚光灯。
我们先来看看上一个章提到的反射方程的最终版:
@@ -32,13 +17,13 @@ L_o(p,\omega_o) = \int\limits_{\Omega}
L_i(p,\omega_i) n \cdot \omega_i d\omega_i
$$
我们大致上清楚这个反射方程在干什么,但我们仍然留有一些迷雾尚未揭开。比如说我们究竟将怎样表示场景上的辐照度(Irradiance), 辐射率(Radiance) \(L\)? 我们知道辐射率\(L\)(在计算机图形领域中)表示在给定立体角\(\omega\)的情况下光源的辐射通量(Radiant flux)\(\phi\)或光源在角\(\omega\)下发送出来的光能。 在我们的情况下,不妨假设立体角\(\omega\)无限小,这样辐射度就表示光源在一条光线或单个方向向量上的辐射通量。
我们大致上清楚这个反射方程在干什么,但我们仍然留有一些迷雾尚未揭开。比如说我们究竟将怎样表示场景上的辐照度(Irradiance), 辐射率(Radiance) \(L\)我们知道辐射率\(L\)(在计算机图形领域中)表示光源的辐射通量(Radiant flux)\(\phi\)或光源在给定立体角\(\omega\)下发的光能。在我们的情况下,不妨假设立体角\(\omega\)无限小,这样辐射度就表示光源在一条光线或单个方向向量上的辐射通量。
基于以上的知识,我们如何将其转化为前的教程中积累的一些光照知识呢? 那么想象一下,我们有一个点光源(一个光源在所有方向具有相同亮度它的辐射通量为用RBG表示为**23.47,21.31,20.79**。该光源的辐射强度(Radiant Intensity)等于其在所有出射光线的辐射通量。 然而,当我们为一个表面上的特定的点\(p\)着色时,在其半球领域\(\Omega\)的所有可能的入射方向上,只有一个入射方向向量\(\omega_i\)直接来自于该点光源。 假设我们在场景中只有一个光源,位于空间中的某一个点,因而对于\(p\)点的其他可能的入射光线方向上的辐射率为0
基于以上的知识,我们如何将其转化为前的教程中积累的一些光照知识呢? 那么想象一下,我们有一个点光源(一个在所有方向具有相同亮度的光源它的辐射通量为用RGB表示为**23.47, 21.31, 20.79**。该光源的辐射强度(Radiant Intensity)等于其在所有出射光线的辐射通量。 然而,当我们为一个表面上的特定的点\(p\)着色时,在其半球领域\(\Omega\)的所有可能的入射方向上,只有一个入射方向向量\(\omega_i\)直接来自于该点光源。 假设我们在场景中只有一个光源,位于空间中的某一个点,因而对于\(p\)点的其他可能的入射光线方向上的辐射率为0
![](../img/07/02/lighting_radiance_direct.png)
如果从一开始,我们就假设点光源不受光线衰减(光照强度会随着距离变暗)的影响,那么无论我们把光源放在哪,入射光线的辐射率总是一样的(除去入射角\(cos\theta\)对辐射率的影响之外)。 正正是因为无论我们从哪个角度观察它,点光源总具有相同的辐射强度,我们可以有效地将其辐射强度建模为其辐射通量: 一个常量向量**23.47,21.31,20.79**。
如果从一开始,我们就假设点光源不受光线衰减(光照强度会随着距离变暗)的影响,那么无论我们把光源放在哪,入射光线的辐射率总是一样的(除去入射角\(cos\theta\)对辐射率的影响之外)。 是因为无论我们从哪个角度观察它,点光源总具有相同的辐射强度,我们可以有效地将其辐射强度建模为其辐射通量: 一个常量向量**23.47, 21.31, 20.79**。
然而,辐射率也需要将位置\(p\)作为输入,正如所有现实的点光源都会受光线衰减影响一样,点光源的辐射强度应该根据点\(p\)所在的位置和光源的位置以及他们之间的距离而做一些缩放。 因此,根据原始的辐射方程,我们会根据表面法向量\(n\)和入射角度\(w_i\)来缩放光源的辐射强度。
@@ -51,15 +36,15 @@ float attenuation = calculateAttenuation(fragPos, lightPos);
float radiance = lightColor * attenuation * cosTheta;
```
除了一些叫法上的差异外,这段代码对你们来说应该很TM熟悉:这正是我们一直以来怎么计算(漫反射(diffuse))光照的!当涉及到直接照(direct lighting)时,辐射率的计算方式和我们之前计算只有一个光源照射在物体表面的时候非常相似。
除了一些术语上的差异外,这段代码对你们来说应该很熟悉:这正是我们一直以来怎么计算漫反射光照的!当涉及到直接照(direct lighting)时,辐射率的计算方式和我们之前计算只有一个光源照射在物体表面的时候非常相似。
!!! Important
请注意,这个假设成立的条件是点光源体积无限小,相当于在空间中的一个点。如果我们认为该光源是具有体积的,它的辐射会在一个以上的入射光方向不等于零。
请注意,这个假设成立的条件是点光源体积无限小,相当于在空间中的一个点。如果我们认为该光源是具有体积的,它的辐射会在不只一个入射光方向上非零。
对于其它类型的从单点发出来的光源我们类似地计算出辐射率。比如,定向光(directional light)拥有恒定的\(w_i\)而不会有衰减因子;而一个聚光灯光源则没有恒定的辐射强度,其辐射强度是根据聚光灯的方向向量来缩放的。
这也让我们回到了对于表面的半球领域(hemisphere)\(\Omega\)的积分\(\int\)上。由于我们事先知道的所有贡献光源的位置因此对物体表面上的一个点着色并不需要我们尝试去求解积分。我们可以直接拿光源的已知的数目去计算它们的总辐照度因为每个光源仅仅只有一个方向上的光线会影响物体表面的辐射率。这使得PBR对直接光源的计算相对简单因为我们只需要有效地遍历所有有贡献的光源。而当我们后把环境照明也考虑在内的[IBL]()教程中,我们就必须采取积分去计算了,这是因为光线可能会在任何一个方向入射。
这也让我们回到了对于表面的半球领域(hemisphere)\(\Omega\)的积分\(\int\)上。由于我们事先知道的所有贡献光源的位置因此对物体表面上的一个点着色并不需要我们尝试去求解积分。我们可以直接拿光源的已知的数目去计算它们的总辐照度因为每个光源仅仅只有一个方向上的光线会影响物体表面的辐射率。这使得PBR对直接光源的计算相对简单因为我们只需要有效地遍历所有有贡献的光源。而当我们后把环境照明也考虑在内的[IBL](./03%20IBL/01%20Diffuse%20irradiance.md)教程中,我们就必须采取积分去计算了,这是因为光线可能会在任何一个方向入射。
# 一个PBR表面模型
@@ -82,7 +67,7 @@ uniform float ao;
我们把通用的顶点着色器的输出作为输入的一部分。另一部分输入则是物体表面模型的一些材质参数。
然后片段着色器的开始部分我们做一下任何光照算法都需要做的计算:
然后片段着色器的开始部分我们做一下任何光照算法都需要做的计算:
```glsl
void main()
@@ -93,9 +78,9 @@ void main()
}
```
## 直接光照
## 直接光照
在本教程的例子中我们会采用总共4个点光源来直接表示场景的辐照度。为了满足反射率方程我们循环遍历每一个光源计算他们独立的辐射率然后求和接着根据BRDF和光源的入射角来缩放该辐射率。我们可以把循环当作在物体的半球领域对所直接光源求积分。首先我们来计算一些可以预计算的光照变量:
在本教程的例子中我们会采用总共4个点光源来直接表示场景的辐照度。为了满足反射率方程我们循环遍历每一个光源计算他们各自的辐射率然后求和接着根据BRDF和光源的入射角来缩放该辐射率。我们可以把循环当作在物体的半球领域对所直接光源求积分。首先我们来计算一些可以预计算的光照变量:
```glsl
vec3 Lo = vec3(0.0);
@@ -110,7 +95,7 @@ for(int i = 0; i < 4; ++i)
[...]
```
由于我们线性空间内计算光照(我们会在着色器的尾部进行Gamma校正我们使用在物理上更为准确的平方倒数作为衰减。
由于我们线性空间内计算光照(我们会在着色器的最后进行Gamma校正我们使用在物理上更为准确的平方倒数作为衰减。
!!! Important
@@ -122,16 +107,16 @@ $$
\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}
$$
首先我们想计算的是镜面反射和漫反射的系数, 或者说发生表面反射和折射的光线的比值。 我们从[上一个教程]()知道可以使用菲涅尔方程计算
首先我们想计算的是镜面反射和漫反射之间的比值,或者说与表面折射的光线相比,它反射了多少光线。 我们从[上一个教程](./01%20Theory.md)知道可以使用菲涅尔方程计算(注意这里用的`clamp`是为了避免黑点)
```glsl
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
```
菲涅尔方程返回的是一个物体表面光线被反射的百分比, 也就是我们反射方程中的参数\(k_s\)。Fresnel-Schlick近似接受一个参数`F0`被称为0°入射角的反射(surface reflection at zero incidence)表示如果直接(垂直)观察表面的时候有多少光线会被反射。 这个参数`F0`会因为材料不同而不同,而且会因为材质是金属而发生变色。在PBR金属流中我们简单地认为大多数的绝缘体在`F0`为0.04的时候看起来视觉上是正确的,我们同时会特别指定`F0`当我们遇到金属表面并且给定反射率的时候。 因此代码上看起来会像是这样:
Fresnel-Schlick近似法接收一个参数`F0`被称为0°入射角的反射率,或者说是直接(垂直)观察表面有多少光线会被反射。 这个参数`F0`会因为材料不同而不同,而且对于金属材质会带有颜色。在PBR金属流中我们简单地认为大多数的绝缘体在`F0`为0.04的时候看起来视觉上是正确的,对于金属表面我们根据反射率特别指定`F0`。 因此代码上看起来会像是这样:
```glsl
vec3 F0 = vec3(0.04);
@@ -139,11 +124,11 @@ F0 = mix(F0, albedo, metallic);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
```
可以看到,对于非金属材质来说`F0`永远保持0.04这个值,我们根据表面的金属性来改变`F0`这个值, 并且在原来的`F0`和反射率中插值计算`F0`
可以看到,对于非金属表面`F0`始终为0.04。对于金属表面,我们根据初始的`F0`和表现金属属性的反射率进行线性插值
我们已经算出\(F\) 剩下的项就是计算法线分布函数\(D\)和几何遮蔽函数\(G\)了。
因此一个直接PBR光照着色器中\(D\)和\(G\)的计算代码类似于:
直接PBR光照着色器中\(D\)和\(G\)的计算代码类似于:
```glsl
float DistributionGGX(vec3 N, vec3 H, float roughness)
@@ -153,11 +138,11 @@ float DistributionGGX(vec3 N, vec3 H, float roughness)
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float num = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
return num / denom;
}
float GeometrySchlickGGX(float NdotV, float roughness)
@@ -165,10 +150,10 @@ float GeometrySchlickGGX(float NdotV, float roughness)
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float num = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
return num / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
@@ -181,9 +166,9 @@ float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
}
```
这里比较重要的是和[上一个教程]()不同的是,我们直接传了粗糙度(roughness)参数给上述函数通过这种方式我们可以针对每一个不同的项对粗糙度做一些修改。根据迪士尼公司给出的观察以及后来被Epic Games公司采用的光照模型光照在几何遮蔽函数和正太分布函数中采用粗糙度的平方会让光照看起来更加自然。
这里比较重要的是和[理论章节](./01%20Theory.md)相比,我们直接粗糙度(roughness)作为参数传给了上述函数通过这种方式我们可以针对每一个不同的项对粗糙度做一些修改。根据迪士尼公司给出的观察以及后来被Epic Games公司采用的光照模型在几何遮蔽函数和法线分布函数中采用粗糙度的平方会让光照看起来更加自然。
现在两个函数都给出了定义在计算反射的循环中计算NDF和G项变得非常自然
现在两个函数都给出了定义在计算反射的循环中计算NDF和G项变得很简单
```glsl
float NDF = DistributionGGX(N, H, roughness);
@@ -200,7 +185,7 @@ vec3 specular = nominator / denominator;
注意我们在分母项中加了一个0.001为了避免出现除零错误。
现在我们终于可以计算每个光源在反射率方程中的贡献值了!因为菲涅尔方程直接给出了\(k_S\) 我们可以使用`F`表示镜面反射在所有打在物体表面上的光线的贡献。 从\(k_S\)我们很容易计算折射的比值\(k_D\)
现在我们终于可以计算每个光源在反射率方程中的贡献值了!因为菲涅尔方程直接给出了\(k_S\) 我们可以使用`F`表示所有打在物体表面上的镜面反射光的贡献。 从\(k_S\)我们很容易计算折射的比值\(k_D\)
```glsl
vec3 kS = F;
@@ -219,9 +204,7 @@ kD *= 1.0 - metallic;
}
```
最终的结果`Lo`,或者说是出射光线的辐射率,实际上是反射率方程的在半球领域\(\Omega\)的积分的结果。但是我们实际上不需要去求积因为对于所有可能的入射光线方向我们知道只有4个方向的入射光线会影响片段(像素)的着色。因为这样我们可以直接循环N次计算这些入射光线的方向(N也就是场景中光源的数目)。
比较重要的是我们没有把`kS`乘进去我们的反射率方程中这是因为我们已经在specualr BRDF中乘了菲涅尔系数`F`了,因为`kS`等于`F`,因此我们不需要再乘一次。
最终的结果`Lo`,或者说是出射光线的辐射率,实际上是反射率方程的在半球领域\(\Omega\)的积分的结果。但是我们实际上不需要去求积因为对于所有可能的入射光线方向我们知道只有4个方向的入射光线会影响片段的着色。因为这样我们可以直接循环N次计算这些入射光线的方向(N也就是场景中光源的数目)。
剩下的工作就是加一个环境光照项给`Lo`,然后我们就拥有了片段的最后颜色:
@@ -232,7 +215,7 @@ vec3 color = ambient + Lo;
## 线性空间和HDR渲染
直到现在,我们假设的所有计算都在线性的颜色空间中进行的,因此我们需要在着色器最后做[伽马矫正]()。 在线性空间中计算光照是非常重要的因为PBR要求所有输入都是线性的如果不是这样我们就会得到不正常的光照。另外我们希望所有光照的输入都尽可能的接近他们在物理上的取值这样他们的反射率或者说颜色值就会在色谱上有比较大的变化空间。`Lo`作为结果可能会变大得很快(超过1)但是因为默认的LDR输入而取值被截断。所以在伽马矫正之前我们采用色调映射使`Lo`从LDR的值映射为HDR的值。
直到现在,我们假设的所有计算都在线性的颜色空间中进行的,因此我们需要在着色器最后做[伽马矫正]()。 在线性空间中计算光照是非常重要的因为PBR要求所有输入都是线性的如果不是这样我们就会得到不正常的光照。另外我们希望所有光照的输入都尽可能的接近他们在物理上的取值这样他们的反射率或者说颜色值就会在色谱上有比较大的变化空间。`Lo`作为结果可能会变大得很快(超过1),但是因为默认的低动态范围(LDR而取值被截断。所以在伽马矫正之前我们采用色调映射使`Lo`从LDR的值映射为HDR的值。
```glsl
color = color / (color + vec3(1.0));
@@ -322,7 +305,7 @@ void main()
}
```
希望经过上一个教程的[理论知识]()以及学习过关于渲染方程的一些知识后这个着色器看起来不会太可怕。如果我们采用这个着色器加上4个点光源和一些球体同时我们令这些球体的金属性(metallic)和粗糙度(roughness)沿垂直方向和水平方向分别变化,我们会得到这样的结果:
希望经过上一个教程的[理论知识](./01%20Theory.md)以及学习过关于渲染方程的一些知识后这个着色器看起来不会太可怕。如果我们采用这个着色器加上4个点光源和一些球体同时令这些球体的金属性(metallic)和粗糙度(roughness)沿垂直和水平方向分别变化,我们会得到这样的结果:
![](../img/07/02/lighting_result.png)
@@ -359,16 +342,6 @@ void main()
![](../img/07/02/lighting_textured.png)
你可以在这里找到纹理贴图过的全部[代码](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/lighting_textured.cpp) 以及我用的[纹理](http://freepbr.com/materials/rusted-iron-pbr-metal-material-alt/)(记得加上一张全白色的ao Map)。注意金属表面会在场景中看起来有点黑,因为他们没有漫反射。它们会在考虑环境镜面光照的时候看起来更加自然,不过这是我们下一个教程的事情了。
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/lighting_textured.cpp)找到纹理贴图示例的全部代码,以及我用的[纹理](http://freepbr.com/materials/rusted-iron-pbr-metal-material-alt/)(记得加上一张全白色的环境光遮蔽贴图)。注意金属表面会在场景中看起来有点黑,因为他们没有漫反射。它们会在考虑环境镜面光照的时候看起来更加自然,不过这是我们下一个教程的事情了。
相比起在网上找到的其他PBR渲染结果来说尽管在视觉上不算是非常震撼因为我们还没考虑到[基于图片的关照,IBL]()。我们现在也算是有了一个基于物理的渲染器了(虽然还没考虑IBL)你会发现你的光照看起来更加真实了。
!!! Important
译者注:
<br>
本章教程有几个小坑原作者没有说清楚,可能是希望读者自己思考,在这译者稍稍提醒一下:
<br>
- 首先是球体的生成主流的球体顶点生成有两种方法作者源码采用的是UVSphere方法 IcoSpher方法可以参考[这里](http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html)
<br>
- 对于贴图的PBR来说我们需要TBN矩阵做坐标转换(切线空间-> 世界空间 或者 世界空间 -> 切线空间,参考 法线贴图 章节。)。这有两种方法一种是在片段着色器中使用叉乘计算TBN矩阵(作者采用的方法)另外一种是在根据顶点预计算TBN然后VAO中传入TBN矩阵理论上来说后者会比较快(但是比较麻烦),不过在译者的实际测试中两者速度差距不大。
相比起在网上找到的其他PBR渲染结果来说尽管在视觉上不算是非常震撼因为我们还没考虑到[基于图片的光照(IBL)](./03%20IBL/01%20Diffuse%20irradiance.md),但我们现在也算是有了一个基于物理的渲染器了虽然还没考虑IBL,但你会发现你的光照看起来更加真实了。