本文重点:
采样环境光 使用反射探针 创建粗糙和光滑的镜子 执行盒子投影立方体贴图采样 混合反射探针
(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起,链接在文章底部)
这是有关渲染的系列教程的第八部分。在上一部分中,我们增加了对阴影的支持。本部分介绍间接反射。
本教程使用Unity 5.4.0f3制作。
(有时候它们会反射自己)
1 环境贴图
当前,我们的着色器通过组合表面上的环境反射,漫反射和镜面反射为片段着色。至少在表面比较粗糙的情况下,会产生看似逼真的图像。但是,有光泽的表面看起来就不太正确。
闪亮的表面就像镜子一样,尤其是金属的时候。完美的镜子可以反射所有光线。这意味着根本没有漫反射。只有镜面反射。因此,通过将“Metallic ”设置为1,将“Smoothness”设置为0.95,将我们的材质变成一面镜子。使其成为为纯白色。
(一个闪亮的白色金属球)
但结果表面几乎是全黑的,即使它自己的颜色设置是白色。我们只看到一个小的亮点,把光源直接反射给了我们。所有其他光都沿不同方向反射回去。如果将平滑度增加到1,则高光也会消失。
这看起来根本不像是真正的镜子。镜子不是黑色的,它们可以反射事物!在这种情况下,它应该反映出天空盒,显示蓝天和灰色地面才对。
1.1 间接镜面光照
我们的球体变了黑色,因为我们只包含了方向光。为了反映环境,我们还必须包括间接光。具体而言,间接光用于镜面反射。在CreateIndirectLight函数中,我们配置了Unity的UnityIndirect结构。而之前,我们把它的镜面反射分量设置为零。这就是为什么球体变成黑色的原因!
将场景的环境强度设置为零,以便我们专注于反射。再次将我们的材质变成无光泽的非金属,平滑度为0.5。然后将间接镜面反射颜色更改为明显的颜色,例如红色。
(黑色和红色间接镜面颜色,平滑度0.5)
球体呈现红色。这时,红色表示反射率。因此,我们的球体从其中心向我们反射了一些环境光。显然,它的边反射的更多。那是因为随着视角变得越来越浅,每个表面都变得更具反射性。在掠射角处,大多数光被反射,所有物体都变成一面镜子。这称为菲涅耳反射。我们正在使用的UNITY_BRDF_PBS版本会为我们计算出来。
表面越光滑,菲涅耳反射越强。使用高平滑度值时,红色环变得非常明显。
(平滑度为0.15 和 0.95)
因为反射来自于间接光,所以它与直接光源无关。结果,反射也独立计算该光源的阴影。因此,菲涅耳反射在球的其他阴影边缘变得非常明显。
就金属而言,间接反射在任何地方都占主导地位。现在,我们得到一个红色的球,而不是黑色的球。
(金属,平滑度为0.15,0.5和0.95)
1.2 采样环境
为了反映实际环境,我们必须对天空盒立方体贴图进行采样。它在UnityShaderVariables中定义为unity_SpecCube0。此变量的类型取决于目标平台,该目标平台在HSLSupport中确定。
使用3D向量对立方体贴图进行采样,该向量指定了采样方向。我们可以为此使用UNITY_SAMPLE_TEXCUBE宏,它会为我们处理类型差异。让我们从仅使用法线向量作为采样方向开始。
(环境采样)
天空盒出现了,但是太亮了。这是因为立方体贴图包含HDR(高动态范围)颜色,这使其可以包含大于1的亮度值。我们必须将样本从HDR格式转换为RGB。
UnityCG包含我们可以使用的DecodeHDR函数。HDR数据使用RGBM格式存储在四个通道中。因此,我们必须采样一个float4值,然后进行转换。
(HDR解码)
DecodeHDR是什么样的?
RGBM包含三个RGB通道,以及一个包含幅度因子的M通道。通过将它们乘以
来计算最终的RGB值。这里,x 是标量,y 是指数,存储在解码指令的前两个部分中。
M通道的转换是必需的,因为当存储在纹理中时,它被限制为0到1范围内的8位值。所以 X 指令将其放大,并且 y指令使它成为非线性的,就像伽玛空间一样。
1.3 追踪反射
我们得到了正确的颜色,但是还没有看到实际的反射。因为我们使用球体的法线来采样环境,所以投影不取决于视图方向。这就像在一个球体画了环境一样。
为了产生实际的反射,我们必须采取从照相机到表面的方向,并使用表面法线对其进行反射。可以为此使用反射功能,就像我们在第4部分中所做的那样。这时,需要查看方向,因此需要将其作为参数添加到CreateIndirectLight。
(使用法线 VS 反射方向)
1.4 使用反射探针
反射天空盒是不错的选择,但反射实际场景的几何形状会更好。因此,我们创建一个简单的建筑物。使用旋转的四边形作为地板,并在其顶部放置了一些立方体柱,在其顶部放置了一些立方体梁。球体悬停在建筑物的中心。
(一些用来反射的物件)
要查看建筑物的反射,必须首先捕获它。这是通过反射探针完成的,可以通过GameObject/ Light / Reflection Probe添加。创建一个并将其放置在与我们的球体相同的位置。
(默认的反射探针)
场景视图指示存在圆形Gizmo的反射探针。其外观取决于场景视图的配置。由于Gizmo阻碍了我们球体的视野,我们将其关闭。你可以通过打开场景视图工具栏中的Gizmo下拉菜单,向下滚动到ReflectionProbe,然后单击其图标来做到这一点。
(关闭反射探针Gizmo)
反射探针通过渲染立方体贴图来捕获环境。这意味着它将渲染场景六次,每个立方体的面一次。默认情况下,其类型设置为烘焙。在这种模式下,立方体贴图由编辑器生成并包含在构建中。这些贴图仅包含静态几何体。因此,我们的建筑物在呈现到立方体贴图之前必须是静态的。
或者,我们可以将反射探针的类型更改为实时。此类探针在运行时呈现,你可以选择多长时间一次。还有一个自定义模式,可以让你完全控制。
尽管实时探针最灵活,但是如果频繁更新,它们也是最昂贵的。同样,实时探针不会在编辑模式下更新,而烘焙的探针或静态几何图形在编辑时会更新。这里,我们使用烘焙好的探针并使我们的建筑物保持静态。
对象实际上不需要完全是静态的。你可以将它们标记为静态,以用于各种子系统。在这种情况下,相关设置为“Reflection Probe Static”。启用后,将对象渲染到烘焙的探针。你可以在运行时移动它们,但是它们的反射会保持冻结。
(反射探针静止)
将建筑物标记为静态后,会更新反射探针。它会先显示为黑色,然后出现反射。反射球不是反射本身的一部分,因此请保持动态。
(反射的几何物体)
2 不完美的反射
只有完全光滑的表面才能产生完全清晰的反射。表面变得越粗糙,其反射越扩散。钝镜会产生模糊的反射。我们如何让反射模糊呢?
纹理可以具有mipmap,它是原始图像的降采样版本。以全尺寸查看时,较高的Mipmap会产生模糊的图像。这些将是块状图像,但是Unity使用不同的算法来生成环境图的mipmap。这些代理体积的贴图代表了从清晰到模糊的良好发展。
(Mipmap 级别从0到5)
2.1 粗糙的镜子
使用UNITY_SAMPLE_TEXCUBE_LOD宏在特定的mipmap级别对立方体贴图进行采样。环境立方体贴图使用三线性过滤,因此我们可以在相邻层之间进行混合。这使我们可以根据材质的平滑度选择mipmap。材质越粗糙,我们应该使用的mipmap级别越高。
当粗糙度从0变为1时,我们必须按使用的mipmap范围对其进行缩放。Unity使用UNITY_SPECCUBE_LOD_STEPS宏来确定此范围,因此我们也要使用它。
UNITY_SPECCUBE_LOD_STEPS在哪里定义?
除非先前在其他地方定义,否则UnityShaderVariables会将其定义为6。因此,你可以在包含其他文件之前,在自己的着色器中自行定义它。Unity的着色器没有在其他任何地方定义它,因此它们始终使用6。环境映射的实际大小未考虑在内。
(平滑度为0.5)
实际上,粗糙度与mipmap级别之间的关系不是线性的。Unity使用转换公式
其中 r是原始粗糙度。
VS 线性)
(平滑度0.5,0.75,0.95)
UnityStandardBRDF包含文件包含Unity_GlossyEnvironment函数。它包含所有用于转换粗糙度,对立方体贴图采样以及从HDR转换的代码。因此,让我们使用该函数代替我们自己的代码。
要将立方体贴图作为参数传递,我们必须使用UNITY_PASS_TEXCUBE macrp。这照顾了类型差异。同样,粗糙度和反射方向也必须打包在Unity_GlossyEnvironmentData结构中。
Unity_GlossyEnvironment有什么不同吗?
它执行与我们相同的操作,但是根据目标平台和其他设置有一些变化。另外,它包含一些注释和禁用的代码,这些代码涉及如何创建mipmap的详细信息。
最后的优化部分是针对PVR GPU的,以避免依赖的纹理读取。为了使其工作,需要将反射向量作为插值器传递。
DecodeHDR_NoLinearSupportInSM2函数仅转发给DecodeHDR,但对Shader Model 2.0目标进行了优化。
2.2 凹凸镜
除了使用平滑度表示较粗糙的镜像外,你当然还可以使用法线贴图添加较大的变形。当我们使用失真的法线来确定反射方向时,这是可行的。
(凹凸镜 平滑度分别为0.5,0.75,0.95)
2.3 金属与非金属
金属和非金属表面都可以产生清晰的反射,只是看起来有所不同。镜面反射在发亮的介电材质上看起来可能很好,但是它们并不能控制外观。仍然有大量的漫反射可见。
(非金属,平滑度分别为0.5,0.75,0.95)
回想一下,金属会使其镜面反射着色,而非金属则不会。对于镜面高光和镜面环境反射都是如此。
(红色的金属与非金属)
2.4 镜子和阴影
正如我们前面所看到的,间接反射与表面的直接照明无关。这对于其他阴影区域最为明显。在非金属的情况下,这只会导致视觉上更亮的表面。你仍然可以看到直接光线投射的阴影。
(非金属 平滑度分别为0.5,0.75,1)
相同的规则适用于金属,但间接反射占主导地位。因此,随着光亮度的增加,直接光的阴影消失。完美的镜子上没有阴影。
(金属 平滑度分别为0.5,0.75,1)
尽管从物理上讲这是正确的,但现实生活很少是完美的。例如,你可能会看到粘在原本完美的镜子上的污垢和灰尘上的直接光线和阴影。并且有许多材质是金属和非金属成分的混合。你可以通过将Metallic滑块设置在0到1之间的某个位置来模拟这一点。
(金属度0.75,一个有灰尘的镜子)
3 盒投影
我们目前有一个反射球和一个反射探针。两者都悬停在我们建筑物的中心。让我们添加更多球体,将其放置在内部正方形区域的边缘附近。但是,仅在中心保留一个探针。
(所有的反射都长一样)
这些反射出了点问题。它们看起来都一样。它们视角略有不同,但是所有球体都将环境反射为仿佛它们位于建筑物的中心一样。虽然它们不是,但是反射探头是!
如果我们想要更真实的反射,则必须为每个球创建一个探针,并将其放置在适当的位置。这样,每个球体都会从自己的角度获取环境图。
(一个球一个探针,不同的反射效果)
虽然这效果更好,但仍然不是完全真实的。为此,我们必须为渲染的每个片段使用一个反射探针。这将需要将许多探针放置在球体的表面上。幸运的是,对于球体而言,近似值并不算太差。但如果是平面镜呢?
首先,卸下除中央反射探头以外的所有探头。然后创建一个四边形并对其进行定位,使其覆盖建筑物的内部并接触支柱的中点。将其变成镜子并观察反射。
(不正确的地面反射)
反射根本不匹配!方向看起来正确,但是比例和位置错误。如果我们对每个片段使用一个探针,反射会很好。但是现在只有一个探针。这种近似值足以有效地无限远地飞行,例如天窗。但这不适用于附近事物的反射。
当一片环境无限远时,确定反射率,我们无需考虑视角位置。但是,当大多数环境都在附近时,我们就需要注意。假设我们在一个空的房间中间有一个反射探针。它的环境图包含此房间的墙壁,地板和天花板。如果立方体贴图和房间对齐,则立方体贴图的每个面都与墙壁,地板或天花板之一精确对应。
下一步,假设我们在这个房间的任何地方都有一个表面位置和一个反射方向。向量最终将在某处与立方体的边缘相交。我们只需一点数学就可以计算出这个交点。然后,我们可以构造一个从房间中心到此点的向量。使用此向量,可以对立方体贴图进行采样并最终得到正确的反射。
(投影以找到采样方向)
这个房间不一定要是一个立方体。就像我们建筑物的内部一样,任何矩形都可以。但是,房间和立方体贴图必须对齐。
3.1 反射探针盒
反射探针的大小和原点确定了相对于其位置的世界空间中的立方区域。它始终与轴对齐,这意味着它将忽略所有旋转。它也忽略缩放。
该区域用于两个目的。首先,Unity使用这些区域来决定在渲染对象时使用哪个探针。其次,该区域用于盒投影,这就是我们要做的。
选择探针后,可以在场景视图中显示该框。反射探针检查器的顶部是“Probe Scene Editing Mode”切换按钮。左按钮打开盒投影边界的gizmos。
(盒投影边界)
你可以使用边界中心的黄点进行调整。还可以通过在检查器中编辑“Size”和“Probe Origin”矢量来调整它们。通过调整原点,可以相对于采样点移动框。你也可以使用其他编辑模式在场景中对其进行调整,但是它有点笨拙,并且当前无法与撤消一起很好地工作。
调整盒子,使其覆盖建筑物的内部,覆盖支柱并一直到达最高点。我将其设置得比它大一点,以防止由于场景视图中的gizmos的Z角冲突而导致闪烁。
(调整边界)
3.2 调整采样方向
要计算盒投影,需要初始反射方向,来从中采样的位置,立方体贴图位置以及盒边界。为此,在CreateIndirectLight上方的着色器中添加一个函数。
首先,调整边界,使其相对于表面位置。
接下来,我们必须缩放方向矢量,使其从该位置到达所需的交点。让我们首先考虑X维度。如果方向的X分量为正,则指向最大边界。否则,它指向最小范围。将适当的边界除以该方向的X分量即可得到所需的标量。当方向为负时,这也适用,因为最小边界也为负,因此除法后会产生正结果。
Y和Z尺寸也是如此。
现在,我们有三个标量,但是哪个是正确的?这取决于哪个标量最小。它表明面的边界最接近。
(选择最小的因子)
当其中一个除数为零时会发生什么?
方向矢量的一个或两个分量可能为零。这将产生无效的结果,不会传递选择的最小值。
现在,我们可以通过将缩放方向添加到位置来找到交点。然后从中减去立方体贴图的位置,得到了新的投影样本方向。
(找到新的投影方向)
新方向不是必须归一化吗?
可以使用任何非零向量对立方体贴图进行采样。硬件立方体贴图采样基本上完成了我们刚才所做的事情。它找出向量指向的面,然后进行除法以找到与立方体贴图面的交点。使用此点的适当坐标来采样脸部纹理。
通过在单个float3表达式中组合三个候选因子,将减法和除法运算推迟到选择了适当的界限之后,来稍微简化此代码。
现在,在CreateIndirectLight中使用我们的新函数来修改环境样本矢量。
(投影反射)
对于我们的平面镜,调整后的反射看起来更好。但是重要的是,反射面不能超出探头范围。缩小镜子,使其与边界完全匹配,并接触支柱。
(调整地板的镜子)
现在,支柱的反射与真实的反射完全匹配。至少恰好在镜和探测器边界的边缘。距离较近的所有内容均未对齐,因为这些点的投影是错误的。但这是我们使用单个反射探针可以做到的最好的事情。
显然有问题的另一件事是,我们看到地板反射镜反射了一部分地板。发生这种情况是因为从地面镜上方的角度渲染了环境贴图。可以通过以下方式解决此问题:将探头原点降低到镜面略上方,同时保持边界不变。
(降低探针中心)
尽管如此低的采样点对于地板反射镜更好,但对于浮动球体却不是那么好。因此,让我们再次将其向上移动,看看这些球体。
(1个投影探针与9个非投影探针)
事实证明,单个盒投影探头产生的反射与九个独立探头的反射非常相似!因此,盒投影是一个非常方便的技巧,尽管它并不完美。
3.3 可选投影
是否使用盒式投影因探针而异,这由其“Box Projection”切换按钮控制。Unity将这些信息存储在立方体贴图位置的第四分量中。如果该分量大于零,则探针应使用盒投影。让我们使用if语句来解决这个问题。
即使我们使用了if语句,也不意味着编译后的代码也包含if。例如,OpenGL Core以条件分配结束,这不是分支。
Direct3D 11也是如此。
我们可以通过在自己的分支之前插入UNITY_BRANCH宏来请求实际分支。虽然在着色器中应避免分支,但在这种情况下还不错,因为条件是统一的。对象的所有片段都使用相同的探针设置,因此最终采用相同的分支。
OpenGL Core现在包含一个明显的分支。
Direct3D 11也是如此。
盒投影没有Unity功能吗?
有。UnityStandardUtils包含BoxProjectedCubemapDirection函数。它所做的与我们相同,包括分支。但它也归一化反射方向参数,这不是必需的。这就是为什么我们不使用它。
4 混合反射探针
我们的建筑物内部发生了很好的反射,但是外面如何?一旦你将一个球体移出探测器的边界,它将切换到天空盒。
(探针盒内外的球体)
探针和天空盒之间的切换是突然的。我们可以增加探针盒,使其也覆盖建筑物外部的空间。然后,我们可以将一个球体移入和移出建筑物,其反射将逐渐改变。但是,探针的点位于建筑物内部。在建筑物外使用它会产生非常奇怪的反射。
(很大的盒子)
为了获得建筑物内部和外部的良好反射,我们必须使用多个反射探针。
(第二个反射探针)
这些反射是有道理的,但是在两个不同的探测区域之间仍然存在突然变清晰的过渡。
4.1 插值探针
Unity为着色器提供了两个反射探针的数据,因此我们可以在它们之间进行混合。第二个探针是unity_SpecCube1。我们可以对两个环境图都进行采样并根据哪个更占优势进行插值。Unity为我们计算此值,并将插值器存储在unity_SpecCube0_BoxMin的第四个坐标中。如果仅使用第一个探针,则将其设置为1;如果存在混合,则将其设置为较低的值。
上面的代码很可能会产生编译错误。显然,samplerunity_SpecCube1变量不存在。这是因为访问纹理需要纹理资源和采样器,而第二个探针没有任何资源。相反,它依赖于第一个探针的采样器。
使用UNITY_PASS_TEXCUBE_SAMPLER宏将第二个探针的纹理与唯一的采样器结合在一起。这样就摆脱了错误。
(仍然没有混合)
4.2 重叠探针盒
为了使混合有效,多个探针的边界必须重叠。因此,调整第二个盒,使其延伸到建筑物中。重叠区域中的球应获得混合反射。网格渲染器组件的检查器还显示了正在使用的探针及其权重。
(重叠的探针盒可实现混合)
如果过渡不够顺畅,你可以在其他两个之间添加第三个探针。该探针的框与其他两个框重叠。因此,在向外移动时,首先要在内部和中间探针之间以及在中间和外部探针之间进行混合。
(三个探针)
还可以在探针和天空盒之间进行混合。你必须将对象的“Reflection Probes”模式从“Blend Probes”更改为“Blend Probes and Skybox”。当对象的边界框部分超出探针边界时,就会发生混合。
(融合一个探针和天空盒)
其他反射探针模式又如何呢?
“off”表示该对象根本不使用探针。它始终使用天空盒。
"Simple"禁用混合。它始终使用最重要的探测器或天空盒。
4.3 优化
对两个探针进行采样需要大量工作。我们只有在需要混合时才这样做。因此,添加一个基于插值器的分支。Unity也在标准着色器中执行此操作。声明一下,这是一个通用分支。
当目标平台无法处理时,Unity的着色器也会禁用混合。这由UNITY_SPECCUBE_BLENDING控制,在可能进行混合时将其定义为1,否则定义为0。我们可以使用预处理器条件块仅在需要时包括代码。
我们不是应该使用#if defined(UNITY_SPECCUBE_BLENDING)吗?
不应该,因为UNITY_SPECCUBE_BLENDING是常驻定义的。在这种情况下,实际定义很重要。1代表真,而0代表假。
基于UNITY_SPECCUBE_BOX_PROJECTION的定义,盒投影存在类似的优化。
这两个值在哪里定义?
它们是由编辑器根据目标平台定义的。除此之外,当针对低于3.0的着色器模型时,UnityStandardConfig会将它们设置为0。
5 嵌套反射
当两个镜子彼此面对时,最终会出现看似无止尽的嵌套反射级联。可以在Unity中看到类似的情况吗?
(没有嵌套反射)
我们的镜子不包含在反射本身中,因为它们不是静态的。因此,让我们将地板镜子设为静态。球体应该保持动态,因为否则探针将无法再看穿它们,从而产生怪异的反射。
(静态地面镜子,黑色反射)
反射镜现在显示在我们的单反射探头中,但显示为纯黑色。那是因为渲染探针时,它的环境图还不存在。它试图反射自己,但失败了!
默认情况下,Unity在环境贴图中不包含反射。但这可以通过照明设置进行更改。“Environment Settings ”部分包含“Reflection Bounces ”滑块,默认情况下设置为1。让我们将其设置为2。
(bounces设置为2)
置为两次反弹时,Unity首先以正常渲染每个反射探针开始。然后,使用现在可用的反射数据再次渲染它们。结果,来自地板反射镜的初始反射现在包含在环境贴图中。
Unity最多支持五次弹跳。这需要大量渲染,因此你绝对不想在运行时使用它!要查看实际效果,请复制地板镜并将其变成天花板镜。
(镜像的地板和天花板,有五次反弹)
因此可以在Unity中获得嵌套反射,但是它们是有限的。而且,投影是错误的,因为探针的边界不会延伸到镜子之外的虚拟空间中。
既然有这些限制,那反射有实际作用吗?
在本教程中,我们将重点放在它们上,因此我们看到了带有所有缺陷的裸露的反射。完美的镜子是不切实际的,但是微妙的反射是可行的。了解了它们的局限性,你可以确定何时何地可以有效地使用它们。
反射探针是向场景添加反射的默认方法,也是最方便的方法,但这不是唯一的方法。如果不透明的平面镜,则另一种方法是从虚拟观察者的角度渲染场景,并将其用作镜子的纹理。还有一种方法是镜像场景里的几何体。用这种方法可以获得很好的结果,但是这些方法同样有很多局限性,还不如反射探针普遍。然后是屏幕空间的反射,这将在后面的延迟渲染里介绍。
下一章,介绍复合材质。