目录 |
1 高亮的高光 1.1 光滑度 1.2 表面数据 1.3 漫反射光 1.4 镜面选项 1.5 镜面高光 1.6 逐物体平滑度 2 反射环境 2.1 采样环境 2.2 调制反射 2.3 菲涅尔 2.4 金属表面 3 非同一的缩放值 3.1 移除假设条件 4 反射探针 4.1 盒投影 4.2 探针混合 4.3 HDR解码 5 透明表面 5.1 预乘Alpha 5.2 预设 |
本文重点:
1、添加镜面高光
2、反射环境
3、支持非同一的缩放
4、和反射探针一起工作
5、结合反射和透明度
这是涵盖Unity的可脚本化渲染管道的教程系列的第七部分。它涵盖了反射,镜面高光和采样反射探针的添加。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。
本教程使用Unity 2018.3.0f2制作。
(倒影生动起来)
1 高亮的高光
到目前为止,我们仅使用了简单的漫射照明,没有任何照明。我们现在将改变它,增加对镜面照明的支持。我们将使用与Unity的Lightweight渲染管道中使用的模型相同的模型。“渲染4,拳头照明”教程介绍了镜面照明的原理,但使用了不同的模型。
1.1 光滑度
完美的漫反射表面(即完美的漫射器)根本没有镜面反射。表面是如此粗糙,以至于没有聚焦反射发生。表面越光滑,反射的光线就越多,而不是扩散。我们将通过从0到1的平滑度材料属性(默认值为?)进行控制。
(光滑度 滑动条)
添加相应的着色器变量。
1.2 表面数据
囊括镜面反射会使我们的照明模型更加复杂。让我们将其所有代码放在一个单独的文件Lighting.hlsl中。在里面定义一个LitSurface结构,该结构包含执行照明计算所需的所有表面数据。那就是表面的法线,位置,漫反射和镜面反射的颜色及其粗糙度。还会包含视图方向,它实际上不是表面的属性,但是相对于我们正在使用的表面点而言是恒定的。
添加一个GetLitSurface函数,该函数返回Lit的表面数据。在此HLSL文件中,我们将不依赖于着色器特定的属性,纹理或定义,因此必须将所有相关数据定义为参数。尽管我们把平滑度作为输入,但是在内部,我们使用粗糙度,只是反过来而已。此外,我们现在使用金属工作流程并将自己局限于电介质材质。介电材质是非金属的,仅反射少量的光,相当于平均0.04的灰度镜面反射。就像Unity的光照代码一样,我们将对其进行硬编码。
根据迪斯尼模型(Disney model),此时我们拥有的粗糙度实际上是一个称为感知粗糙度的值。物理粗糙度是其平方。我们都需要。
在Lit.hlsl中包含Lighting.hlsl。
现在,在执行任何光照计算之前,我们可以在LitPassFragment中获取表面。视线方向就是相机位置减去片段位置(归一化)。
1.3 漫反射光
我们将把照明计算移到Lighting.hlsl。为此,为其提供一个LightSurface函数,以表面和光的方向为参数。它执行漫反射点乘积,并将其乘以表面的漫反射色。我们假设这里是理想的光线,纯白色而没有衰减。
调整MainLight,使其仅将表面作为参数,而不是位置和法向矢量。让它调用LightSurface而不是执行点乘积本身。和以前一样,在此处应用衰减和光色。
以相同的方式调整DiffuseLight,将其重命名为GenericLight,从现在开始可以更好地描述其目的。
现在,LitPassFragment必须将表面作为参数传递。而且,漫反射现在也沿途(而不是在末尾)计入颜色。在开始时,我们仍然必须将其纳入顶点照明。
我们还必须对顶点灯光使用GenericLight。顶点照明是纯漫反射的,因此仅需要位置和法线。颜色为白色,其他值无关紧要。让我们为Lighting.hlsl添加一个方便的包装函数。
然后在LitPassVertex中使用它。
1.4 镜面选项
在添加镜面高光之前,让我们可以跳过它,以便仍然支持完美的漫反射材质。我们将通过添加到表面并作为GetLitSurface的参数的布尔字段来指示默认情况下将其设置为false。这个想法是,你可以对参数进行硬编码,或者使其依赖于着色器关键字。
如果是完美的散光器(diffuser),则平滑度和镜面反射度应始终为零。
同样,我们可以使所有镜面反射计算都以该布尔为条件。始终只需要最终的点积。
这正是我们顶点照明所需要的。
1.5 镜面高光
镜面反射高光背后的想法是,它代表纯粹的反射。光线照射到表面,并且其中一些反射。如果相机对准的话,使其与反射完美匹配,那么它将看到光线,否则看不到。但是,如果表面不是十分光滑,则反射会散布一些,因此即使未完全对准,照相机仍会看到一些反射。表面越粗糙,过渡区域越大。这样就创建了镜面高光。
Unity的轻量级渲染管线使用修改后的简约CookTorrance BRDF计算高光。我们将执行相同的计算。有关详细信息和链接,请参见Lightweight RP软件包中的Lighting.hlsl。
被反射的光也不会散射,因此,除非表面是完美的散射体,否则在GetLitSurface中相应地减少散射体的颜色。
(有和没有镜面高光)
1.6 逐物体平滑度
我们制作了InstancedColor组件,因此我们可以为每个对象提供自己的颜色,而无需使用单独的材质。为了实现平滑,我们也可以这样做。可以使用相同的组件来做这两个事情,但是我们应该重命名它以更好地描述其更通用的功能。
首先,在“Project”窗口中将资产文件从InstancedColor重命名为InstancedMaterialProperties,然后在代码中重命名类定义并添加平滑度。按此顺序进行操作可使Unity编辑器保持所有组件引用完整。否则,你必须修复丢失的脚本引用。
(逐物体变化的光滑度)
为了保持GPU实例化,我们必须将smoothness变量移至实例化缓冲区。
并使用UNITY_ACCESS_INSTANCED_PROP宏检索它。
2 反射环境
镜面高光表示光源的直接反射。但是,光(直接和间接)都可以来自各个方向。我们无法实时跟踪所有这些反射,因此我们将对包含环境照明的立方体贴图进行采样。
2.1 采样环境
默认环境是skybox。Unity通过unity_SpecCube0在着色器中将其变为可用。这是一个立方体贴图纹理资源,我们可以通过TEXTURECUBE宏以及samplerunity_SpecCube0采样器状态进行定义。在Lighting.hlsl中定义这些参数以及其他变量和资源,因为我们如何获取环境数据的细节与照明计算无关。
再次在Lit.hlsl中,以表面为参数定义SampleEnvironment函数。它将按照“渲染8,反射”中所述的相同方式对立方体贴图进行采样。首先,使用具有负视图方向和表面法线的反射函数来获取反射向量。然后调用PerceptualRoughnessToMipmapLevel来基于感知粗糙度找到正确的Mip级别。之后,我们可以使用SAMPLE_TEXTURECUBE_LOD宏对立方体贴图进行采样,并将结果用作最终颜色。
PerceptualRoughnessToMipmapLevel在ImageBasedLighting.hlsl核心RP库文件中定义。粗糙表面会产生模糊反射,我们可以通过选择适当的Mip级别来获得模糊反射。Unity生成特殊的Mipmap,以表示散射的反射。
要检查是否可行,请在LitPassFragment的末尾对环境进行采样,并将其用作最终颜色,以覆盖照明。
结果是统一的灰色,这意味着没有可用的立方体贴图。我们必须指示Unity绑定环境地图,这是通过在MyPipeline.Render的渲染器配置中设置RendererConfiguration.PerObjectReflectionProbes标志来完成的。因为此时我们还没有使用反射探针,所以所有对象最终都带有天空盒立方体贴图。
(采样天空盒)
2.2 调制反射
反射多少环境取决于表面的粗糙度。为此,以表面和环境颜色为参数,向Lighting.hlsl添加ReflectEnvironment函数。请注意,此函数并不关心环境颜色是从立方体贴图样本,或者统一值还是其他值衍生而来。
理想的漫反射器不会反射任何东西,因此结果始终为零。否则,将镜面反射颜色计入结果中。然后将其除以平方粗糙度+1。
通过此功能过滤采样的环境。
(调制反射)
2.3 菲涅尔
在掠射角处,大多数光被反射,并且所有表面都像镜子一样起作用。这称为菲涅耳反射。这实际上是根据表面法线和视图方向之间的角度增强镜面反射颜色。我们将使用一个减去法线和视图方向的饱和点积,并提高到四次方。核心RP库为此定义了一个方便的Pow4函数。使用结果将高光颜色插值为白色,然后将其分解为ReflectEnvironment中的环境颜色。
(菲涅尔反射)
但是粗糙度也会影响菲涅耳反射。表面越粗糙,效果越弱。将fresnelStrength字段添加到表面以跟踪此情况。我们将简单地使其等于平滑度。
现在提高菲涅耳强度,而不是始终保持最大强度。
(调制菲涅尔反射)
最后,将环境反射添加到直接照明中,而不是替换它。
(反射添加到直接照明)
2.4 金属表面
固定的单色镜面反射镜0.04适用于介电材质,但不适用于金属。纯金属仅反射光,但会改变其颜色。要指示某物是否为金属,请添加另一个着色器属性。再次使用范围为0-1的滑块,这样就可以表示由电介质和金属混合而成的材质。
(金属度滑块)
我们也将立即将METALAL属性添加到InstancedMaterialProperties中。
(逐物体的金属度)
添加着色器实例属性。
并在平滑之前将其传递给LitPassFragment中的LitSurface。
将相应的参数添加到GetLitSurface,并为LitSurface提供一个字段以存储其反射率。如果是完美的扩散器,则始终为零。
对于金属,提供的颜色控制镜面反射而不是漫反射。由于金属不是二进制的,因此可以使用它在非金属的0.04和金属的颜色之间插入镜面反射颜色。在那之后,反射率将简单地等于金属,除了我们使用0.04作为最小值。因此,根据金属从0.04插值到1。最后,反射率增加了菲涅耳强度,最大为1。
(一半 VS 全部 金属度)
3 非同一的缩放值
在继续之前,让我们重新考虑所有对象具有统一比例的假设。该限制适用于简单的场景,但是要构建具有简单形状的更复杂的场景,将它们拉伸会很有用,这会导致缩放比例不一致。如果现在这样做,最终将导致着色不正确,如渲染4,第一束光中所述。
(拉伸的球体下 不正确的着色)
3.1 移除假设条件
为了使阴影正确,无论使用何种缩放比例,我们都必须在着色器中摆脱假设一致的缩放编译指示。
如果比例尺不均匀,则必须使用对象的wold-to-object矩阵正确转换法线向量。因此,除了unity_ObjectToWorld外,我们还需要UnityPerDraw缓冲区中的unity_WorldToObject。
除此之外,Unity的实例化代码希望通过UNITY_MATRIX_I_M宏将其定义为UNITY_MATRIX_M的反函数。
在LitPassVertex中转换法线向量时,我们必须交换矩阵,逆转乘法顺序,并对结果进行归一化。但是,仅在不采用统一缩放比例的情况下才需要这样做。如果确实启用该选项,则将定义UNITY_ASSUME_UNIFORM_SCALING,并且仍然可以使用更便宜的方法。
(拉伸的球体 正确的着色)
4 反射探针
仅当没有其他反射探头可用时,才将skybox用于环境反射。你可以通过GameObject/ Light / Reflection Probe将一个添加到场景中。默认情况下,反射探针的“Type”设置为“Baked”,这意味着其立方体贴图由Unity编辑器渲染一次,并且仅显示标记为反射探针为静态的对象。如果更改了静态内容,它将更新。设置为“Realtime”时,它将显示整个场景。何时渲染立方体贴图取决于“Refresh Mode”和“Time Slicing ”设置。在编辑模式下,场景变化时,实时反射探针并不总是刷新。有关更多详细信息,请参见渲染8,反射。
(反射探针设置为实时渲染)
反射探针仅在有东西要反射时才有用,因此将它们放在其他物体附近的场景中。你可以将它们放置在一个对象中,只要该对象使用剔除背面的材质即可。这样,你可以获得该对象的最准确的反射。
(场景中间有一个反射探针)
Unity在渲染立方体贴图面时会使用我们的管道,尽管这不会在帧调试器中显示。但可以通过记录“Render”中使用的摄像机类型,在MyPipeline中对此进行验证。默认情况下,立方体贴图在需要时渲染一次。这意味着反射面最终会比环境图中的预期要暗,因为它们自己依赖于环境图。你可以通过照明设置增加反射量。例如,2的反射值意味着所有立方体贴图都将正常渲染,但随后会使用先前用于环境映射的结果再次渲染它们。
(2次反射值)
4.1 盒投影
默认情况下,由立方体贴图表示的光被视为来自无限远的地方。这不适用于附近的表面。盒映射可用于使小区域的反射更准确。Unity的轻量级渲染管道不执行此操作,但我们将使用与Unity的Legacy渲染管道相匹配的“渲染8,反射”中描述的方法来支持盒映射。
将探针盒的最小和最大边界及其位置的变量添加到UnityPerDraw缓冲区。
直接从其他教程中复制BoxProjection函数。
使用该函数在SampleEnvironment中查找调整后的样本坐标。
为反射探针启用“盒投影”,并调整其盒边界,使其最匹配其反射的内容。
(盒投影)
4.2 探针混合
反射探针经过一些调整后可以产生不错的结果,但仅适用于合理接近探针位置的物体。否则,除了反射未对齐之外,对象最终有可能反射自己。如果错误太明显,则必须使用更多探针。
(用两个探针替代1个)
每个对象使用哪种反射探针由其盒控制。选择覆盖对象的最接近或最重要的探针。如果该对象未包含在任何探针盒中,它将使用天空盒。但这会导致从一个探针到下一个探针的艰难过渡。可以通过在探针之间进行混合来缓解这种情况,可以针对每个对象进行配置。最多可以将两种探针以这种方式混合。
为了支持混合,我们必须为第二个探针添加另一组变量。
除了第二个立方体贴图纹理外,我们还可以为两者使用相同的采样器状态。
再次,我们从其他教程中复制该方法。
(混合探针)
4.3 HDR解码
现在,我们支持反射探针,但前提是它们的纹理包含未编码的光数据。通常是这种情况,但是当烘焙的贴图经过HDR编码或探针的强度发生变化时,情况并非如此。为了支持这一点,我们可以依赖在ImageBasedLighting.hlsl核心RP库文件中定义的DecodeHDREnvironment。
每个探测器都有HDR解码指令,为此我们必须添加变量。
然后,我们可以解码立方体贴图样本。
(双倍强度)
请注意,当使用多次反射时,增加探头的强度会产生巨大的变化,因为每次反射都会使光线增强。
5 透明表面
我们通过回顾半透明的表面来总结。在上一教程中,我们增加了对淡入淡出表面的支持。淡出表面的整个颜色,也包括反射。如果你想使所有物体淡化,那效果还不错,但是它不适用于水晶或玻璃等材质。
(淡入淡出反射)
5.1 预乘Alpha
玻璃通常几乎是完全透明的,但仍会反射光。为了表示这一点,我们只需要将不透明度应用于漫反射光照。我们通过将Alpha用作漫反射颜色来实现。因此,我们预乘alpha而不是在完成所有光照后应用它,而不是使用常规片段混合。之后,我们根据反射率提高Alpha值,以使反射替换表面后面的任何东西。向Lighting.hlsl添加一个函数来相应地调整表面和Alpha。
如果定义了_PREMULTIPLY_ALPHA着色器关键字,则在LitPassFragment中获取表面后调用它。
添加切换着色器属性以控制关键字。
为了使这项工作有效,我们必须将源混合模式设置为一种,而不是source-alpha。
(开启预乘alpha)
5.2 预设
让我们向LitShaderGUI添加用于透明材质的预设。为它提供一个属性来设置_PREMULTIPLY_ALPHA关键字以及随附的着色器属性。
到目前为止,在所有预设方法中将其设置为false。
复制两个淡入淡出预设方法并进行调整,以使它们配置透明材质。因此PremultiplyAlpha变为true,SrcBlend变为BlendMode.One。
最后一步是调用OnGUI中的方法以及其他方法。
(新的透明预设)
下一章,全局光照。
往期推荐:
Unity可编程渲染管线系列(六)透明度(裁剪与淡化 Clipping and Fading)
Unity可编程渲染管线系列(五)方向阴影(级联贴图 Cascaded Maps)
Unity可编程渲染管线系列(四)聚光灯阴影(阴影贴图)
Unity可编程渲染管线系列(三)光照(单通道 正向渲染)
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials