1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

Move all the images to this repo. Close #53

This commit is contained in:
Meow J
2016-09-17 02:11:48 +08:00
parent b384a9f176
commit f0dd66f4b9
246 changed files with 221 additions and 221 deletions

View File

@@ -12,19 +12,19 @@
Phong光照很棒而且性能较高但是它的镜面反射在某些条件下会失效特别是当发光值属性低的时候对应一个非常大的粗糙的镜面区域。下面的图片展示了当我们使用镜面的发光值为1.0时,一个带纹理地板的效果:
![](http://learnopengl.com/img/advanced-lighting/advanced_lighting_phong_limit.png)
![](../img/05/01/advanced_lighting_phong_limit.png)
你可以看到镜面区域边缘迅速减弱并截止。出现这个问题的原因是在视线向量和反射向量的角度不允许大于90度。如果大于90度的话点乘的结果就会是负数镜面的贡献成分就会变成0。你可能会想这不是一个问题因为大于90度时我们不应看到任何光对吧
错了这只适用于漫散射部分当法线和光源之间的角度大于90度时意味着光源在被照亮表面的下方这样光的散射成分就会是0.0。然而,对于镜面光照,我们不会测量光源和法线之间的角度,而是测量视线和反射方向向量之间的。看看下面的两幅图:
![](http://learnopengl.com/img/advanced-lighting/advanced_lighting_over_90.png)
![](../img/05/01/advanced_lighting_over_90.png)
现在看来问题就很明显了。左侧图片显示Phong反射的θ小于90度的情况。我们可以看到右侧图片视线和反射之间的角θ大于90度这样镜面反射成分将会被消除。通常这也不是问题因为视线方向距离反射方向很远但如果我们使用一个数值较低的发光值参数的话镜面半径就会足够大以至于能够贡献一些镜面反射的成份了。在例子中我们在角度大于90度时消除了这个贡献如第一个图片所示
1977年James F. Blinn引入了Blinn-Phong着色它扩展了我们目前所使用的Phong着色。Blinn-Phong模型很大程度上和Phong是相似的不过它稍微改进了Phong模型使之能够克服我们所讨论到的问题。它放弃使用反射向量而是基于我们现在所说的一个叫做半程向量halfway vector的向量这是个单位向量它在视线方向和光线方向的中间。半程向量和表面法线向量越接近镜面反射成份就越大。
![](http://learnopengl.com/img/advanced-lighting/advanced_lighting_halfway_vector.png)
![](../img/05/01/advanced_lighting_halfway_vector.png)
当视线方向恰好与反射向量对称时,半程向量就与法线向量重合。这样观察者距离原来的反射方向越近,镜面反射的高光就会越强。
@@ -59,13 +59,13 @@ vec3 specular = lightColor * spec;
引入了半程向量来计算镜面反射后我们再也不会遇到Phong着色的骤然截止问题了。下图展示了两种不同方式下发光值指数为0.5时镜面区域的不同效果:
![](http://learnopengl.com/img/advanced-lighting/advanced_lighting_comparrison.png)
![](../img/05/01/advanced_lighting_comparrison.png)
Phong和Blinn-Phong着色之间另一个细微差别是半程向量和表面法线之间的角度经常会比视线和反射向量之间的夹角更小。结果就是为了获得和Phong着色相似的效果必须把发光值参数设置的大一点。通常的经验是将其设置为Phong着色的发光值参数的2至4倍。
下图是Phong指数为8.0和Blinn-Phong指数为32的时候两种specular反射模型的对比
![](http://learnopengl.com/img/advanced-lighting/advanced_lighting_comparrison2.png)
![](../img/05/01/advanced_lighting_comparrison2.png)
你可以看到Blinn-Phong的镜面反射成分要比Phong锐利一些。这通常需要使用一点小技巧才能获得之前你所看到的Phong着色的效果但Blinn-Phong着色的效果比默认的Phong着色通常更加真实一些。

View File

@@ -14,7 +14,7 @@
人类所感知的亮度恰好和CRT所显示出来相似的指数关系非常匹配。为了更好的理解所有含义请看下面的图片
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_brightness.png)
![](../img/05/02/gamma_correction_brightness.png)
第一行是人眼所感知到的正常的灰阶亮度要增加一倍比如从0.1到0.2你才会感觉比原来变亮了一倍译注这里的意思是说比如一个东西的亮度0.3让人感觉它比原来变亮一倍那么现在这个亮度应该成为0.6而不是0.4也就是说人眼感知到的亮度的变化并非线性均匀分布的。问题的关键在于这样的一倍相当于一个亮度级例如假设0.1、0.2、0.4、0.8是我们定义的四个亮度级别在0.1和0.2之间人眼只能识别出0.15这个中间级而虽然0.4到0.8之间的差距更大这个区间人眼也只能识别出一个颜色。然而当我们谈论光的物理亮度比如光源发射光子的数量的时候底部第二行的灰阶显示出的才是物理世界真实的亮度。如底部的灰阶显示亮度加倍时返回的也是真实的物理亮度译注这里亮度是指光子数量和正相关的亮度即物理亮度前面讨论的是人的感知亮度物理亮度和感知亮度的区别在于物理亮度基于光子数量感知亮度基于人的感觉比如第二个灰阶里亮度0.1的光子数量是0.2的二分之一),但是由于这与我们的眼睛感知亮度不完全一致(对比较暗的颜色变化更敏感),所以它看起来有差异。
@@ -22,7 +22,7 @@
监视器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于监视器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_gamma_curves.png)
![](../img/05/02/gamma_correction_gamma_curves.png)
点线代表线性颜色/亮度值译注这表示的是理想状态Gamma为1实线代表监视器显示的颜色。如果我们把一个点线线性的颜色翻一倍结果就是这个值的两倍。比如光的颜色向量\(\bar{L} = (0.5, 0.0, 0.0)\)代表的是暗红色。如果我们在线性空间中把它翻倍,就会变成\((1.0, 0.0, 0.0)\),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的监视器上,监视器上显示的实际颜色就会是\((0.218, 0.0, 0.0)\)。在这儿问题就出现了当我们将理想中直线上的那个暗红色翻一倍时在监视器上实际上亮度翻了4.5倍以上!
@@ -30,7 +30,7 @@
因为所有中间亮度都是线性空间计算出来的译注计算的时候假设Gamma为1监视器显以后实际上都会不正确。当使用更高级的光照算法时这个问题会变得越来越明显你可以看看下图
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_example.png)
![](../img/05/02/gamma_correction_example.png)
## Gamma校正
@@ -81,7 +81,7 @@ void main()
结果就是纹理编辑者所创建的所有纹理都是在sRGB空间中的纹理所以如果我们在渲染应用中使用这些纹理我们必须考虑到这点。在我们应用gamma校正之前这不是个问题因为纹理在sRGB空间创建和展示同样我们还是在sRGB空间中使用从而不必gamma校正纹理显示也没问题。然而现在我们是把所有东西都放在线性空间中展示的纹理颜色就会变坏如下图展示的那样
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_srgbtextures.png)
![](../img/05/02/gamma_correction_srgbtextures.png)
纹理图像实在太亮了发生这种情况是因为它们实际上进行了两次gamma校正想一想当我们基于监视器上看到的情况创建一个图像我们就已经对颜色值进行了gamma校正所以再次显示在监视器上就没错。由于我们在渲染中又进行了一次gamma校正图片就实在太亮了。
@@ -125,7 +125,7 @@ float attenuation = 1.0 / distance;
双曲线比使用二次函数变体在不用gamma校正的时候看起来更真实不过但我们开启gamma校正以后线性衰减看起来太弱了符合物理的二次函数突然出现了更好的效果。下图显示了其中的不同
![](http://learnopengl.com/img/advanced-lighting/gamma_correction_attenuation.png)
![](../img/05/02/gamma_correction_attenuation.png)
这种差异产生的原因是光的衰减方程改变了亮度值而且屏幕上显示出来的也不是线性空间在监视器上效果最好的衰减方程并不是符合物理的。想想平方衰减方程如果我们使用这个方程而且不进行gamma校正显示在监视器上的衰减方程实际上将变成\((1.0 / distance^2)^{2.2}\)。若不进行gamma校正将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实因为它实际变成了\((1.0 / distance)^{2.2} = 1.0 / distance^{2.2}\)。这和物理公式是很相似的。

View File

@@ -10,7 +10,7 @@
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。场景和物体的深度感因此能够得到极大提升,下图展示了有阴影和没有阴影的情况下的不同:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_with_without.png)
![](../../img/05/03/01/shadow_mapping_with_without.png)
你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,当使用阴影的时候浮在地板上的立方体的事实更加清晰。
@@ -22,7 +22,7 @@
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory.png)
![](../../img/05/03/01/shadow_mapping_theory.png)
这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment它们应该渲染为带阴影的。如果我们绘制一条从光源出发到达最右边盒子上的一个片元上的线段或射线那么射线将先击中悬浮的盒子随后才会到达最右侧的盒子。结果就是悬浮的盒子被照亮而最右侧的盒子将处于阴影之中。
@@ -30,7 +30,7 @@
你可能记得在[深度测试](http://learnopengl.com/#!Advanced-OpenGL/Depth-testing)教程中在深度缓冲里的一个值是摄像机视角下对应于一个片元的一个0到1之间的深度值。如果我们从光源的透视图来渲染场景并把深度值的结果储存到纹理中会怎样通过这种方式我们就能对光源的透视图所见的最近的深度值进行采样。最终深度值就会显示从光源的透视图下见到的第一个片元了。我们管储存在纹理中的所有这些深度值叫做深度贴图depth map或阴影贴图。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory_spaces.png)
![](../../img/05/03/01/shadow_mapping_theory_spaces.png)
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个\(T\)变换,它可以将任何三维位置转变到光源的可见坐标空间。
@@ -179,7 +179,7 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
最后在光的透视图视角下很完美地用每个可见片元的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上和我们在帧缓冲一节做的后处理过程类似就能在屏幕上显示出来我们会获得这样的东西
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_depth_map.png)
![](../../img/05/03/01/shadow_mapping_depth_map.png)
将深度贴图渲染到四边形上的像素着色器:
@@ -353,7 +353,7 @@ float ShadowCalculation(vec4 fragPosLightSpace)
激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_shadows.png)
![](../../img/05/03/01/shadow_mapping_shadows.png)
如果你做对了你会看到地板和上有立方体的阴影。你可以从这里找到demo程序的[源码](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping_shadows)。
@@ -365,11 +365,11 @@ float ShadowCalculation(vec4 fragPosLightSpace)
前面的图片中明显有不对的地方。放大看会发现明显的线条样式:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne.png)
![](../../img/05/03/01/shadow_mapping_acne.png)
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做**阴影失真(Shadow Acne)**,下图解释了成因:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne_diagram.png)
![](../../img/05/03/01/shadow_mapping_acne_diagram.png)
因为阴影贴图受限于解析度,在距离光源比较远的情况下,多个片元可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片元从同一个深度值进行采样。
@@ -377,7 +377,7 @@ float ShadowCalculation(vec4 fragPosLightSpace)
我们可以用一个叫做**阴影偏移**shadow bias的技巧来解决这个问题我们简单的对表面的深度或深度贴图应用一个偏移量这样片元就不会被错误地认为在表面之下了。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne_bias.png)
![](../../img/05/03/01/shadow_mapping_acne_bias.png)
使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以这样实现这个偏移:
@@ -394,7 +394,7 @@ float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
这里我们有一个偏移量的最大值0.05和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_with_bias.png)
![](../../img/05/03/01/shadow_mapping_with_bias.png)
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
@@ -402,13 +402,13 @@ float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_peter_panning.png)
![](../../img/05/03/01/shadow_mapping_peter_panning.png)
这个阴影失真叫做悬浮(Peter Panning)因为物体看起来轻轻悬浮在表面之上译注Peter Pan就是童话彼得潘而panning有平移、悬浮之意而且彼得潘是个会飞的男孩…。我们可以使用一个叫技巧解决大部分的Peter panning问题当渲染深度贴图时候使用正面剔除front face culling你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。
因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_culling.png)
![](../../img/05/03/01/shadow_mapping_culling.png)
为了修复peter游移我们要进行正面剔除先必须开启GL_CULL_FACE
@@ -426,7 +426,7 @@ glCullFace(GL_BACK); // 不要忘记设回原先的culling face
无论你喜不喜欢还有一个视觉差异就是光的视锥不可见的区域一律被认为是处于阴影中不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式我们将会得到不正确的深度结果它不是基于真实的来自光源的深度值。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_outside_frustum.png)
![](../../img/05/03/01/shadow_mapping_outside_frustum.png)
你可以在图中看到光照有一个区域超出该区域就成为了阴影这个区域实际上代表着深度贴图的大小这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。
@@ -441,7 +441,7 @@ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
现在如果我们采样深度贴图0到1坐标范围以外的区域纹理函数总会返回一个1.0的深度值阴影值为0.0。结果看起来会更真实:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_clamp_edge.png)
![](../../img/05/03/01/shadow_mapping_clamp_edge.png)
仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。
@@ -462,7 +462,7 @@ float ShadowCalculation(vec4 fragPosLightSpace)
检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终会得到下面我们所追求的效果:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_over_sampling_fixed.png)
![](../../img/05/03/01/shadow_mapping_over_sampling_fixed.png)
这些结果意味着只有在深度贴图范围以内的被投影的fragment坐标才有阴影所以任何超出范围的都将会没有阴影。由于在游戏中通常这只发生在远处就会比我们之前的那个明显的黑色区域效果更真实。
@@ -470,7 +470,7 @@ float ShadowCalculation(vec4 fragPosLightSpace)
阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对解析度的依赖很快变得很明显。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_zoom.png)
![](../../img/05/03/01/shadow_mapping_zoom.png)
因为深度贴图有一个固定的解析度,多个片元对应于一个纹理像素。结果就是多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边。
@@ -498,7 +498,7 @@ shadow /= 9.0;
使用更多的样本更改texelSize变量你就可以增加阴影的柔和程度。下面你可以看到应用了PCF的阴影
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_soft_shadows.png)
![](../../img/05/03/01/shadow_mapping_soft_shadows.png)
从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影贴图解析度的不真实感,但通常对于大多数应用来说效果已经很好了。
@@ -511,7 +511,7 @@ shadow /= 9.0;
在渲染深度贴图的时候,正交(Orthographic)和投影(Projection)矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_projection.png)
![](../../img/05/03/01/shadow_mapping_projection.png)
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。

View File

@@ -19,7 +19,7 @@
对于深度贴图我们需要从一个点光源的所有渲染场景普通2D深度贴图不能工作如果我们使用立方体贴图会怎样因为立方体贴图可以储存6个面的环境数据它可以将整个场景渲染到立方体贴图的每个面上把它们当作点光源四周的深度值来采样。
![](http://learnopengl.com/img/advanced-lighting/point_shadows_diagram.png)
![](../../img/05/03/02/point_shadows_diagram.png)
生成后的深度立方体贴图被传递到光照像素着色器它会用一个方向向量来采样立方体贴图从而得到当前的fragment的深度从光的透视图。大部分复杂的事情已经在阴影映射教程中讨论过了。算法只是在深度立方体贴图生成上稍微复杂一点。
@@ -369,7 +369,7 @@ float ShadowCalculation(vec3 fragPos)
有了这些着色器,我们已经能得到非常好的阴影效果了,这次从一个点光源所有周围方向上都有阴影。有一个位于场景中心的点光源,看起来会像这样:
![](http://learnopengl.com/img/advanced-lighting/point_shadows.png)
![](../../img/05/03/02/point_shadows.png)
你可以从这里找到这个[demo的源码](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows)、[顶点](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows&type=vertex)和[片段](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows&type=fragment)着色器。
@@ -385,7 +385,7 @@ FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
结果是一个灰度场景,每个颜色代表着场景的线性深度值:
![](http://learnopengl.com/img/advanced-lighting/point_shadows_depth_cubemap.png)
![](../../img/05/03/02/point_shadows_depth_cubemap.png)
你可能也注意到了带阴影部分在墙外。如果看起来和这个差不多你就知道深度立方体贴图生成的没错。否则你可能做错了什么也许是closestDepth仍然还在0到far_plane的范围。
@@ -420,7 +420,7 @@ shadow /= (samples * samples * samples);
现在阴影看起来更加柔和平滑了,由此得到更加真实的效果:
![](http://learnopengl.com/img/advanced-lighting/point_shadows_soft.png)
![](../../img/05/03/02/point_shadows_soft.png)
然而samples设置为4.0每个fragment我们会得到总共64个样本这太多了
@@ -465,7 +465,7 @@ float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
PCF算法的结果如果没有变得更好也是非常不错的这是柔和的阴影效果
![](http://learnopengl.com/img/advanced-lighting/point_shadows_soft_better.png)
![](../../img/05/03/02/point_shadows_soft_better.png)
当然了我们添加到每个样本的bias偏移高度依赖于上下文总是要根据场景进行微调的。试试这些值看看怎样影响了场景。
这里是最终版本的顶点和像素着色器。

View File

@@ -10,29 +10,29 @@
视差贴图属于位移贴图(Displacement Mapping)技术的一种它对根据储存在纹理中的几何信息对顶点进行位移或偏移。一种实现的方式是比如有1000个顶点更具纹理中的数据对平面特定区域的顶点的高度进行位移。这样的每个纹理像素包含了高度值纹理叫做高度贴图。一张简单的砖块表面的告诉贴图如下所示
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_height_map.png)
![](../img/05/05/parallax_mapping_height_map.png)
整个平面上的每个顶点都根据从高度贴图采样出来的高度值进行位移,根据材质的几何属性平坦的平面变换成凹凸不平的表面。例如一个平坦的平面利用上面的高度贴图进行置换能得到以下结果:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_plane_heightmap.png)
![](../img/05/05/parallax_mapping_plane_heightmap.png)
置换顶点有一个问题就是平面必须由很多顶点组成才能获得具有真实感的效果否则看起来效果并不会很好。一个平坦的表面上有1000个顶点计算量太大了。我们能否不用这么多的顶点就能取得相似的效果呢事实上上面的表面就是用6个顶点渲染出来的两个三角形。上面的那个表面使用视差贴图技术渲染位移贴图技术不需要额外的顶点数据来表达深度它像法线贴图一样采用一种聪明的手段欺骗用户的眼睛。
视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低所有这些都根据观察方向和高度贴图。为了理解它如何工作看看下面砖块表面的图片
[](http://learnopengl.com/img/advanced-lighting/parallax_mapping_plane_height.png)
[](../img/05/05/parallax_mapping_plane_height.png)
这里粗糙的红线代表高度贴图中的数值的立体表达,向量\(\color{orange}{\bar{V}}\)代表观察方向。如果平面进行实际位移,观察者会在点\(\color{blue}B\)看到表面。然而我们的平面没有实际上进行位移,观察方向将在点\(\color{green}A\)与平面接触。视差贴图的目的是,在\(\color{green}A\)位置上的fragment不再使用点\(\color{green}A\)的纹理坐标而是使用点\(\color{blue}B\)的。随后我们用点\(\color{blue}B\)的纹理坐标采样,观察者就像看到了点\(\color{blue}B\)一样。
这个技巧就是描述如何从点\(\color{green}A\)得到点\(\color{blue}B\)的纹理坐标。视差贴图尝试通过对从fragment到观察者的方向向量\(\color{orange}{\bar{V}}\)进行缩放的方式解决这个问题,缩放的大小是\(\color{green}A\)处fragment的高度。所以我们将\(\color{orange}{\bar{V}}\)的长度缩放为高度贴图在点\(\color{green}A\)处\(\color{green}{H(A)}\)采样得来的值。下图展示了经缩放得到的向量\(\color{brown}{\bar{P}}\)
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_scaled_height.png)
![](../img/05/05/parallax_mapping_scaled_height.png)
我们随后选出\(\color{brown}{\bar{P}}\)以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这能工作是因为向量\(\color{brown}{\bar{P}}\)是使用从高度贴图得到的高度值计算出来的所以一个fragment的高度越高位移的量越大。
这个技巧在大多数时候都没问题,但点\(\color{blue}B\)是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量\(\color{brown}{\bar{P}}\)最终不会和\(\color{blue}B\)接近,就像下图这样:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_incorrect_p.png)
![](../img/05/05/parallax_mapping_incorrect_p.png)
视差贴图的另一个问题是,当表面被任意旋转以后很难指出从\(\color{brown}{\bar{P}}\)获取哪一个坐标。我们在视差贴图中使用了另一个坐标空间,这个空间\(\color{brown}{\bar{P}}\)向量的x和y元素总是与纹理表面对齐。如果你看了法线贴图教程你也许猜到了我们实现它的方法是的我们还是在切线空间中实现视差贴图。
@@ -46,7 +46,7 @@
你可能已经注意到,上面链接上的那个位移贴图和教程一开始的那个高度贴图相比是颜色是相反的。这是因为使用反色高度贴图(也叫深度贴图)去模拟深度比模拟高度更容易。下图反映了这个轻微的改变:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_depth.png)
![](../img/05/05/parallax_mapping_depth.png)
我们再次获得\(\color{green}A\)和\(\color{blue}B\),但是这次我们用向量\(\color{orange}{\bar{V}}\)减去点\(\color{green}A\)的纹理坐标得到\(\color{brown}{\bar{P}}\)。我们通过在着色器中用1.0减去采样得到的高度贴图中的值来取得深度值,而不再是高度值,或者简单地在图片编辑软件中把这个纹理进行反色操作,就像我们对连接中的那个深度贴图所做的一样。
@@ -152,7 +152,7 @@ vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
最后的纹理坐标随后被用来进行采样diffuse和法线贴图下图所展示的位移效果中height_scale等于1
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping.png)
![](../img/05/05/parallax_mapping.png)
这里你会看到只用法线贴图和与视差贴图相结合的法线贴图的不同之处。因为视差贴图尝试模拟深度,它实际上能够根据你观察它们的方向使砖块叠加到其他砖块上。
@@ -166,13 +166,13 @@ if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y <
丢弃了超出默认范围的纹理坐标的所有fragment视差贴图的表面边缘给出了正确的结果。注意这个技巧不能在所有类型的表面上都能工作但是应用于平面上它还是能够是平面看起来真的进行位移了
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_edge_fix.png)
![](../img/05/05/parallax_mapping_edge_fix.png)
你可以在这里找到[源代码](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping),以及[顶点](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping&type=vertex)和[像素](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping&type=fragment)着色器。
看起来不错,运行起来也很快,因为我们只要给视差贴图提供一个额外的纹理样本就能工作。当从一个角度看过去的时候,会有一些问题产生(和法线贴图相似),陡峭的地方会产生不正确的结果,从下图你可以看到:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_issues.png)
![](../img/05/05/parallax_mapping_issues.png)
问题的原因是这只是一个大致近似的视差映射。还有一些技巧让我们在陡峭的高度上能够获得几乎完美的结果,即使当以一定角度观看的时候。例如,我们不再使用单一样本,取而代之使用多样本来找到最近点\(\color{blue}B\)会得到怎样的结果?
@@ -180,7 +180,7 @@ if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y <
陡峭视差映射(Steep Parallax Mapping)是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量\(\color{brown}{\bar{P}}\)到\(\color{blue}B\)。它能得到更好的结果,它将总深度范围分布到同一个深度/高度的多个层中。从每个层中我们沿着\(\color{brown}{\bar{P}}\)方向移动采样纹理坐标,直到我们找到了一个采样得到的低于当前层的深度值的深度值。看看下面的图片:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_parallax_mapping_diagram.png)
![](../img/05/05/parallax_mapping_steep_parallax_mapping_diagram.png)
我们从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的\(\color{brown}{\bar{P}}\)向量部分在表面之下。我们继续这个处理过程直到有一层的深度高于储存在深度贴图中的值:这个点就在(经过位移的)表面下方。
@@ -232,7 +232,7 @@ return texCoords - currentTexCoords;
有10个样本砖墙从一个角度看上去就已经很好了但是当有一个强前面展示的木制表面一样陡峭的表面时陡峭的视差映射的威力就显示出来了
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_parallax_mapping.png)
![](../img/05/05/parallax_mapping_steep_parallax_mapping.png)
我们可以通过对视差贴图的一个属性的利用,对算法进行一点提升。当垂直看一个表面的时候纹理时位移比以一定角度看时的小。我们可以在垂直看时使用更少的样本,以一定角度看时增加样本数量:
@@ -248,7 +248,7 @@ float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir
陡峭视差贴图同样有自己的问题。因为这个技术是基于有限的样本数量的,我们会遇到锯齿效果以及图层之间有明显的断层:
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_steep_artifact.png)
![](../img/05/05/parallax_mapping_steep_artifact.png)
我们可以通过增加样本的方式减少这个问题,但是很快就会花费很多性能。有些旨在修复这个问题的方法:不适用低于表面的第一个位置,而是在两个接近的深度层进行插值找出更匹配\(\color{blue}B\)的。
@@ -260,7 +260,7 @@ float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir
视差遮蔽映射(Parallax Occlusion Mapping)和陡峭视差映射的原则相同,但不是用触碰的第一个深度层的纹理坐标,而是在触碰之前和之后,在深度层之间进行线性插值。我们根据表面的高度距离啷个深度层的深度层值的距离来确定线性插值的大小。看看下面的图片就能了解它是如何工作的:
[](http://learnopengl.com/img/advanced-lighting/parallax_mapping_parallax_occlusion_mapping_diagram.png)
[](../img/05/05/parallax_mapping_parallax_occlusion_mapping_diagram.png)
你可以看到大部分和陡峭视差映射一样,不一样的地方是有个额外的步骤,两个深度层的纹理坐标围绕着交叉点的线性插值。这也是近似的,但是比陡峭视差映射更精确。
@@ -287,7 +287,7 @@ return finalTexCoords;
视差遮蔽映射的效果非常好,尽管有一些可以看到的轻微的不真实和锯齿的问题,这仍是一个好交易,因为除非是放得非常大或者观察角度特别陡,否则也看不到。
![](http://learnopengl.com/img/advanced-lighting/parallax_mapping_parallax_occlusion_mapping.png)
![](../img/05/05/parallax_mapping_parallax_occlusion_mapping.png)
你可以在这里找到[源代码](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping),及其[顶点](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping&type=vertex)和[像素](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/parallax_mapping_occlusion&type=fragment)着色器。

View File

@@ -8,7 +8,7 @@
一般来说,当存储在帧缓冲(Framebuffer)中时亮度和颜色的值是默认被限制在0.0到1.0之间的。这个看起来无辜的语句使我们一直将亮度与颜色的值设置在这个范围内尝试着与场景契合。这样是能够运行的也能给出还不错的效果。但是如果我们遇上了一个特定的区域其中有多个亮光源使这些数值总和超过了1.0又会发生什么呢答案是这些片段中超过1.0的亮度或者颜色值会被约束在1.0,从而导致场景混成一片,难以分辨:
![](http://learnopengl.com/img/advanced-lighting/hdr_clamped.png)
![](../img/05/06/hdr_clamped.png)
这是由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。这损失了很多的细节,使场景看起来非常假。
@@ -18,7 +18,7 @@
HDR原本只是被运用在摄影上摄影师对同一个场景采取不同曝光拍多张照片捕捉大范围的色彩值。这些图片被合成为HDR图片从而综合不同的曝光等级使得大范围的细节可见。看下面这个例子左边这张图片在被光照亮的区域充满细节但是在黑暗的区域就什么都看不见了但是右边这张图的高曝光却可以让之前看不出来的黑暗区域显现出来。
![](http://learnopengl.com/img/advanced-lighting/hdr_image.png)
![](../img/05/06/hdr_image.png)
这与我们眼睛工作的原理非常相似也是HDR渲染的基础。当光线很弱的啥时候人眼会自动调整从而使过暗和过亮的部分变得更清晰就像人眼有一个能自动根据场景亮度调整的自动曝光滑块。
@@ -86,7 +86,7 @@ void main()
这里我们直接采样了浮点颜色缓冲并将其作为片段着色器的输出。然而这个2D四边形的输出是被直接渲染到默认的帧缓冲中导致所有片段着色器的输出值被约束在0.0到1.0间尽管我们已经有了一些存在浮点颜色纹理的值超过了1.0。
![](http://learnopengl.com/img/advanced-lighting/hdr_direct.png)
![](../img/05/06/hdr_direct.png)
很明显在隧道尽头的强光的值被约束在1.0因为一大块区域都是白色的过程中超过1.0的地方损失了所有细节。因为我们直接转换HDR值到LDR值这就像我们根本就没有应用HDR一样。为了修复这个问题我们需要做的是无损转化所有浮点颜色值回0.0-1.0范围中。我们需要应用到色调映射。
@@ -113,7 +113,7 @@ void main()
有了Reinhard色调映射的应用我们不再会在场景明亮的地方损失细节。当然这个算法是倾向明亮的区域的暗的区域会不那么精细也不那么有区分度。
![](http://learnopengl.com/img/advanced-lighting/hdr_reinhard.png)
![](../img/05/06/hdr_reinhard.png)
现在你可以看到在隧道的尽头木头纹理变得可见了。用了这个非常简单地色调映射算法我们可以合适的看到存在浮点帧缓冲中整个范围的HDR值给我们对于无损场景光照精确的控制。
@@ -140,7 +140,7 @@ void main()
在这里我们将`exposure`定义为默认为1.0的`uniform`从而允许我们更加精确设定我们是要注重黑暗还是明亮的区域的HDR颜色值。举例来说高曝光值会使隧道的黑暗部分显示更多的细节然而低曝光值会显著减少黑暗区域的细节但允许我们看到更多明亮区域的细节。下面这组图片展示了在不同曝光值下的通道
![](http://learnopengl.com/img/advanced-lighting/hdr_exposure.png)
![](../img/05/06/hdr_exposure.png)
这个图片清晰地展示了HDR渲染的优点。通过改变曝光等级我们可以看见场景的很多细节而这些细节可能在LDR渲染中都被丢失了。比如说隧道尽头在正常曝光下木头结构隐约可见但用低曝光木头的花纹就可以清晰看见了。对于近处的木头花纹来说在高曝光下会能更好的看见。

View File

@@ -10,7 +10,7 @@
光晕效果可以使用一个后处理特效泛光来实现。泛光使所有明亮区域产生光晕效果。下面是一个使用了和没有使用光晕的对比(图片生成自虚幻引擎):
![](http://learnopengl.com/img/advanced-lighting/bloom_example.png)
![](../img/05/07/bloom_example.png)
Bloom是我们能够注意到一个明亮的物体真的有种明亮的感觉。泛光可以极大提升场景中的光照效果并提供了极大的效果提升尽管做到这一切只需一点改变。
@@ -20,25 +20,25 @@ Bloom和HDR结合使用效果很好。常见的一个误解是HDR和泛光是一
我们来一步一步解释这个处理过程。我们在场景中渲染一个带有4个立方体形式不同颜色的明亮的光源。带有颜色的发光立方体的亮度在1.5到15.0之间。如果我们将其渲染至HDR颜色缓冲场景看起来会是这样的
![](http://learnopengl.com/img/advanced-lighting/bloom_scene.png)
![](../img/05/07/bloom_scene.png)
我们得到这个HDR颜色缓冲纹理提取所有超出一定亮度的fragment。这样我们就会获得一个只有fragment超过了一定阈限的颜色区域
![](http://learnopengl.com/img/advanced-lighting/bloom_extracted.png)
![](../img/05/07/bloom_extracted.png)
我们将这个超过一定亮度阈限的纹理进行模糊。泛光效果的强度很大程度上被模糊过滤器的范围和强度所决定。
![](http://learnopengl.com/img/advanced-lighting/bloom_blurred.png)
![](../img/05/07/bloom_blurred.png)
最终的被模糊化的纹理就是我们用来获得发出光晕效果的东西。这个已模糊的纹理要添加到原来的HDR场景纹理的上部。因为模糊过滤器的应用明亮区域发出光晕所以明亮区域在长和宽上都有所扩展。
![](http://learnopengl.com/img/advanced-lighting/bloom_small.png)
![](../img/05/07/bloom_small.png)
泛光本身并不是个复杂的技术,但很难获得正确的效果。它的品质很大程度上取决于所用的模糊过滤器的质量和类型。简单的改改模糊过滤器就会极大的改变泛光效果的品质。
下面这几步就是泛光后处理特效的过程,它总结了实现泛光所需的步骤。
![](http://learnopengl.com/img/advanced-lighting/bloom_steps.png)
![](../img/05/07/bloom_steps.png)
首先我们需要根据一定的阈限提取所有明亮的颜色。我们先来做这件事。
@@ -110,7 +110,7 @@ void main()
有了两个颜色缓冲,我们就有了一个正常场景的图像和一个提取出的亮区的图像;这些都在一个渲染步骤中完成。
![](http://learnopengl.com/img/advanced-lighting/bloom_attachments.png)
![](../img/05/07/bloom_attachments.png)
有了一个提取出的亮区图像,我们现在就要把这个图像进行模糊处理。我们可以使用帧缓冲教程后处理部分的那个简单的盒子过滤器,但不过我们最好还是使用一个更高级的更漂亮的模糊过滤器:**高斯模糊(Gaussian blur)**。
@@ -118,7 +118,7 @@ void main()
在后处理教程那里,我们采用的模糊是一个图像中所有周围像素的均值,它的确为我们提供了一个简易实现的模糊,但是效果并不好。高斯模糊基于高斯曲线,高斯曲线通常被描述为一个钟形曲线,中间的值达到最大化,随着距离的增加,两边的值不断减少。高斯曲线在数学上有不同的形式,但是通常是这样的形状:
![](http://learnopengl.com/img/advanced-lighting/bloom_gaussian.png)
![](../img/05/07/bloom_gaussian.png)
高斯曲线在它的中间处的面积最大使用它的值作为权重使得近处的样本拥有最大的优先权。比如如果我们从fragment的32×32的四方形区域采样这个权重随着和fragment的距离变大逐渐减小通常这会得到更好更真实的模糊效果这种模糊叫做高斯模糊。
@@ -126,7 +126,7 @@ void main()
幸运的是高斯方程有个非常巧妙的特性它允许我们把二维方程分解为两个更小的方程一个描述水平权重另一个描述垂直权重。我们首先用水平权重在整个纹理上进行水平模糊然后在经改变的纹理上进行垂直模糊。利用这个特性结果是一样的但是可以节省难以置信的性能因为我们现在只需做32+32次采样不再是1024了这叫做两步高斯模糊。
![](http://learnopengl.com/img/advanced-lighting/bloom_gaussian_two_pass.png)
![](../img/05/07/bloom_gaussian_two_pass.png)
这意味着我们如果对一个图像进行模糊处理,至少需要两步,最好使用帧缓冲对象做这件事。具体来说,我们将实现像乒乓球一样的帧缓冲来实现高斯模糊。它的意思是,有一对儿帧缓冲,我们把另一个帧缓冲的颜色缓冲放进当前的帧缓冲的颜色缓冲中,使用不同的着色效果渲染指定的次数。基本上就是不断地切换帧缓冲和纹理去绘制。这样我们先在场景纹理的第一个缓冲中进行模糊,然后在把第一个帧缓冲的颜色缓冲放进第二个帧缓冲进行模糊,接着,将第二个帧缓冲的颜色缓冲放进第一个,循环往复。
@@ -218,7 +218,7 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
通过对提取亮区纹理进行5次模糊我们就得到了一个正确的模糊的场景亮区图像。
![](http://learnopengl.com/img/advanced-lighting/bloom_blurred_large.png)
![](../img/05/07/bloom_blurred_large.png)
泛光的最后一步是把模糊处理的图像和场景原来的HDR纹理进行结合。
@@ -255,7 +255,7 @@ void main()
把两个纹理结合以后,场景亮区便有了合适的光晕特效:
![](http://learnopengl.com/img/advanced-lighting/bloom.png)
![](../img/05/07/bloom.png)
有颜色的立方体看起来仿佛更亮,它向外发射光芒,的确是一个更好的视觉效果。这个场景比较简单,所以泛光效果不算十分令人瞩目,但在更好的场景中合理配置之后效果会有巨大的不同。你可以在这里找到这个简单的例子的源码,以及模糊的顶点和像素着色器、立方体的像素着色器、后处理的顶点和像素着色器。

View File

@@ -10,17 +10,17 @@
**延迟着色法(Deferred Shading)****或者说是延迟渲染(Deferred Rendering)**为了解决上述问题而诞生了它大幅度地改变了我们渲染物体的方式。这给我们优化拥有大量光源的场景提供了很多的选择因为它能够在渲染上百甚至上千光源的同时还能够保持能让人接受的帧率。下面这张图片包含了一共1874个点光源它是使用延迟着色法来完成的而这对于正向渲染几乎是不可能的(图片来源Hannes Nevalainen)。
![](http://learnopengl.com/img/advanced-lighting/deferred_example.png)
![](../img/05/08/deferred_example.png)
延迟着色法基于我们**延迟(Defer)**或**推迟(Postpone)**大部分计算量非常大的渲染(像是光照)到后期进行处理的想法。它包含两个处理阶段(Pass):在第一个几何处理阶段(Geometry Pass)中我们先渲染场景一次之后获取对象的各种几何信息并储存在一系列叫做G缓冲(G-buffer)的纹理中;想想位置向量(Position Vector)、颜色向量(Color Vector)、法向量(Normal Vector)和/或镜面值(Specular Value)。场景中这些储存在G缓冲中的几何信息将会在之后用来做(更复杂的)光照计算。下面是一帧中G缓冲的内容
![](http://learnopengl.com/img/advanced-lighting/deferred_g_buffer.png)
![](../img/05/08/deferred_g_buffer.png)
我们会在第二个光照处理阶段(Lighting Pass)中使用G缓冲内的纹理数据。在光照处理阶段中我们渲染一个屏幕大小的方形并使用G缓冲中的几何数据对每一个片段计算场景的光照在每个像素中我们都会对G缓冲进行迭代。我们对于渲染过程进行解耦将它高级的片段处理挪到后期进行而不是直接将每个对象从顶点着色器带到片段着色器。光照计算过程还是和我们以前一样但是现在我们需要从对应的G缓冲而不是顶点着色器(和一些uniform变量)那里获取输入变量了。
下面这幅图片很好地展示了延迟着色法的整个过程:
![](http://learnopengl.com/img/advanced-lighting/deferred_overview.png)
![](../img/05/08/deferred_overview.png)
这种渲染方法一个很大的好处就是能保证在G缓冲中的片段和在屏幕上呈现的像素所包含的片段信息是一样的因为深度测试已经最终将这里的片段信息作为最顶层的片段。这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次所以我们能够省下很多无用的渲染调用。除此之外延迟渲染还允许我们做更多的优化从而渲染更多的光源。
@@ -145,7 +145,7 @@ void main()
如果我们现在想要渲染一大堆纳米装战士对象到`gBuffer`帧缓冲中,并通过一个一个分别投影它的颜色缓冲到铺屏四边形中尝试将他们显示出来,我们会看到向下面这样的东西:
![](http://learnopengl.com/img/advanced-lighting/deferred_g_buffer.png)
![](../img/05/08/deferred_g_buffer.png)
尝试想象世界空间位置和法向量都是正确的。比如说指向右侧的法向量将会被更多地对齐到红色上从场景原点指向右侧的位置矢量也同样是这样。一旦你对G缓冲中的内容满意了我们就该进入到下一步光照处理阶段了。
@@ -220,7 +220,7 @@ void main()
运行一个包含32个小光源的简单Demo会是像这样子的
![](http://learnopengl.com/img/advanced-lighting/deferred_shading.png)
![](../img/05/08/deferred_shading.png)
你可以在以下位置找到Demo的完整[源代码](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred),和几何渲染阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_geometry&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_geometry&type=fragment)着色器,还有光照渲染阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred&type=vertex)着色器。
@@ -254,7 +254,7 @@ for (GLuint i = 0; i < lightPositions.size(); i++)
然而,这些渲染出来的立方体并没有考虑到我们储存的延迟渲染器的几何深度(Depth)信息,并且结果是它被渲染在之前渲染过的物体之上,这并不是我们想要的结果。
![](http://learnopengl.com/img/advanced-lighting/deferred_lights_no_depth.png)
![](../img/05/08/deferred_lights_no_depth.png)
我们需要做的就是首先复制出在几何渲染阶段中储存的深度信息,并输出到默认的帧缓冲的深度缓冲,然后我们才渲染光立方体。这样之后只有当它在之前渲染过的几何体上方的时候,光立方体的片段才会被渲染出来。我们可以使用`glBlitFramebuffer`复制一个帧缓冲的内容到另一个帧缓冲中,这个函数我们也在[抗锯齿](http://learnopengl-cn.readthedocs.org/zh/latest/04%20Advanced%20OpenGL/11%20Anti%20Aliasing/)的教程中使用过,用来还原多重采样的帧缓冲。`glBlitFramebuffer`这个函数允许我们复制一个用户定义的帧缓冲区域到另一个用户定义的帧缓冲区域。
@@ -273,7 +273,7 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
在这里我们复制整个读帧缓冲的深度缓冲信息到默认帧缓冲的深度缓冲对于颜色缓冲和模板缓冲我们也可以这样处理。现在如果我们接下来再渲染光立方体场景里的几何体将会看起来很真实了而不只是简单地粘贴立方体到2D方形之上
![](http://learnopengl.com/img/advanced-lighting/deferred_lights_depth.png)
![](../img/05/08/deferred_lights_depth.png)
你可以在[这里](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_light_cube)找到Demo的源代码还有光立方体的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_light_cube&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/deferred_light_cube&type=fragment)着色器。
@@ -387,7 +387,7 @@ void main()
使用光体积更好的方法是渲染一个实际的球体,并根据光体积的半径缩放。这些球的中心放置在光源的位置,由于它是根据光体积半径缩放的,这个球体正好覆盖了光的可视体积。这就是我们的技巧:我们使用大体相同的延迟片段着色器来渲染球体。因为球体产生了完全匹配于受影响像素的着色器调用,我们只渲染了受影响的像素而跳过其它的像素。下面这幅图展示了这一技巧:
![](http://learnopengl.com/img/advanced-lighting/deferred_light_volume_rendered.png)
![](../img/05/08/deferred_light_volume_rendered.png)
它被应用在场景中每个光源上,并且所得的片段相加混合在一起。这个结果和之前场景是一样的,但这一次只渲染对于光源相关的片段。它有效地减少了从`nr_objects * nr_lights`到`nr_objects + nr_lights`的计算量,这使得多光源场景的渲染变得无比高效。这正是为什么延迟渲染非常适合渲染很大数量光源。

View File

@@ -10,7 +10,7 @@
下面这幅图展示了在使用和不使用SSAO时场景的不同。特别注意对比褶皱部分你会发现(环境)光被遮蔽了许多:
![](http://learnopengl.com/img/advanced-lighting/ssao_example.png)
![](../img/05/09/ssao_example.png)
尽管这不是一个非常明显的效果启用SSAO的图像确实给我们更真实的感觉这些小的遮蔽细节给整个场景带来了更强的深度感。
@@ -18,23 +18,23 @@
SSAO背后的原理很简单对于铺屏四边形(Screen-filled Quad)上的每一个片段,我们都会根据周边深度值计算一个**遮蔽因子(Occlusion Factor)**。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。遮蔽因子是通过采集片段周围球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。
![](http://learnopengl.com/img/advanced-lighting/ssao_crysis_circle.png)
![](../img/05/09/ssao_crysis_circle.png)
上图中在几何体内灰色的深度样本都是高于片段深度值的,他们会增加遮蔽因子;几何体内样本个数越多,片段获得的环境光照也就越少。
很明显,渲染效果的质量和精度与我们采样的样本数量有直接关系。如果样本数量太低,渲染的精度会急剧减少,我们会得到一种叫做**波纹(Banding)**的效果;如果它太高了,反而会影响性能。我们可以通过引入随机性到采样核心(Sample Kernel)的采样中从而减少样本的数目。通过随机旋转采样核心,我们能在有限样本数量中得到高质量的结果。然而这仍然会有一定的麻烦,因为随机性引入了一个很明显的噪声图案,我们将需要通过模糊结果来修复这一问题。下面这幅图片([John Chapman](http://john-chapman-graphics.blogspot.com/)的佛像)展示了波纹效果还有随机性造成的效果:
![](http://learnopengl.com/img/advanced-lighting/ssao_banding_noise.jpg)
![](../img/05/09/ssao_banding_noise.jpg)
你可以看到,尽管我们在低样本数的情况下得到了很明显的波纹效果,引入随机性之后这些波纹效果就完全消失了。
Crytek公司开发的SSAO技术会产生一种特殊的视觉风格。因为使用的采样核心是一个球体它导致平整的墙面也会显得灰蒙蒙的因为核心中一半的样本都会在墙这个几何体上。下面这幅图展示了孤岛危机的SSAO它清晰地展示了这种灰蒙蒙的感觉
![](http://learnopengl.com/img/advanced-lighting/ssao_crysis.jpg)
![](../img/05/09/ssao_crysis.jpg)
由于这个原因,我们将不会使用球体的采样核心,而使用一个沿着表面法向量的半球体采样核心。
![](http://learnopengl.com/img/advanced-lighting/ssao_hemisphere.png)
![](../img/05/09/ssao_hemisphere.png)
通过在**法向半球体(Normal-oriented Hemisphere)**周围采样,我们将不会考虑到片段底部的几何体.它消除了环境光遮蔽灰蒙蒙的感觉从而产生更真实的结果。这个SSAO教程将会基于法向半球法和John Chapman出色的[SSAO教程](http://john-chapman-graphics.blogspot.com/2013/01/ssao-tutorial.html)。
@@ -49,7 +49,7 @@ SSAO需要获取几何体的信息因为我们需要一些方式来确定一
通过使用一个逐片段观察空间位置,我们可以将一个采样半球核心对准片段的观察空间表面法线。对于每一个核心样本我们会采样线性深度纹理来比较结果。采样核心会根据旋转矢量稍微偏转一点;我们所获得的遮蔽因子将会之后用来限制最终的环境光照分量。
![](http://learnopengl.com/img/advanced-lighting/ssao_overview.png)
![](../img/05/09/ssao_overview.png)
由于SSAO是一种屏幕空间技巧我们对铺屏2D四边形上每一个片段计算这一效果也就是说我们没有场景中几何体的信息。我们能做的只是渲染几何体数据到屏幕空间纹理中我们之后再会将此数据发送到SSAO着色器中之后我们就能访问到这些几何体数据了。如果你看了前面一篇教程你会发现这和延迟渲染很相似。这也就是说SSAO和延迟渲染能完美地兼容因为我们已经存位置和法线向量到G缓冲中了。
@@ -116,7 +116,7 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
我们需要沿着表面法线方向生成大量的样本。就像我们在这个教程的开始介绍的那样,我们想要生成形成半球形的样本。由于对每个表面法线方向生成采样核心非常困难,也不合实际,我们将在[切线空间](04 Normal Mapping.md)(Tangent Space)内生成采样核心法向量将指向正z方向。
![](http://learnopengl.com/img/advanced-lighting/ssao_hemisphere.png)
![](../img/05/09/ssao_hemisphere.png)
假设我们有一个单位半球我们可以获得一个拥有最大64样本值的采样核心
@@ -161,7 +161,7 @@ GLfloat lerp(GLfloat a, GLfloat b, GLfloat f)
这就给了我们一个大部分样本靠近原点的核心分布。
![](http://learnopengl.com/img/advanced-lighting/ssao_kernel_weight.png)
![](../img/05/09/ssao_kernel_weight.png)
每个核心样本将会被用来偏移观察空间片段位置从而采样周围的几何体。我们在教程开始的时候看到,如果没有变化采样核心,我们将需要大量的样本来获得真实的结果。通过引入一个随机的转动到采样核心中,我们可以很大程度上减少这一数量。
@@ -333,7 +333,7 @@ occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0);
这并没有完全结束,因为仍然还有一个小问题需要考虑。当检测一个靠近表面边缘的片段时,它将会考虑测试表面之下的表面的深度值;这些值将会(不正确地)音响遮蔽因子。我们可以通过引入一个范围检测从而解决这个问题,正如下图所示([John Chapman](http://john-chapman-graphics.blogspot.com/)的佛像)
![](http://learnopengl.com/img/advanced-lighting/ssao_range_check.png)
![](../img/05/09/ssao_range_check.png)
我们引入一个范围测试从而保证我们只当被测深度值在取样半径内时影响遮蔽因子。将代码最后一行换成:
@@ -344,7 +344,7 @@ occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0) * rangeCheck;
这里我们使用了GLSL的`smoothstep`函数,它非常光滑地在第一和第二个参数范围内插值了第三个参数。如果深度差因此最终取值在`radius`之间它们的值将会光滑地根据下面这个曲线插值在0.0和1.0之间:
![](http://learnopengl.com/img/advanced-lighting/ssao_smoothstep.png)
![](../img/05/09/ssao_smoothstep.png)
如果我们使用一个在深度值在`radius`之外就突然移除遮蔽贡献的硬界限范围检测(Hard Cut-off Range Check),我们将会在范围检测应用的地方看见一个明显的(很难看的)边缘。
@@ -358,7 +358,7 @@ FragColor = occlusion;
下面这幅图展示了我们最喜欢的纳米装模型正在打盹的场景,环境遮蔽着色器产生了以下的纹理:
![](http://learnopengl.com/img/advanced-lighting/ssao_without_blur.png)
![](../img/05/09/ssao_without_blur.png)
可见,环境遮蔽产生了非常强烈的深度感。仅仅通过环境遮蔽纹理我们就已经能清晰地看见模型一定躺在地板上而不是浮在空中。
@@ -406,7 +406,7 @@ void main() {
这里我们遍历了周围在-2.0和2.0之间的SSAO纹理单元(Texel)采样与噪声纹理维度相同数量的SSAO纹理。我们通过使用返回`vec2`纹理维度的`textureSize`,根据纹理单元的真实大小偏移了每一个纹理坐标。我们平均所得的结果,获得一个简单但是有效的模糊效果:
![](http://learnopengl.com/img/advanced-lighting/ssao.png)
![](../img/05/09/ssao.png)
这就完成了,一个包含逐片段环境遮蔽数据的纹理;在光照处理阶段中可以直接使用。
@@ -466,7 +466,7 @@ void main()
(除了将其改到观察空间)对比于之前的光照实现,唯一的真正改动就是场景环境分量与`AmbientOcclusion`值的乘法。通过在场景中加入一个淡蓝色的点光源,我们将会得到下面这个结果:
![](http://learnopengl.com/img/advanced-lighting/ssao_final.png)
![](../img/05/09/ssao_final.png)
你可以在[这里](http://learnopengl.com/code_viewer.php?code=advanced-lighting/ssao)找到完整的源代码,和以下着色器: