进阶渲染系列(七)——三向贴图(任意表面纹理化)【进阶篇完结】

2020-07-13 15:02:51 浏览数 (1)

本文重点:

1、消除对UV和切线的依赖

2、支持通用的表面方法

3、使用平面投影在三个贴图之间融合

本教程是关于支持三向纹理映射的。它使用FXAA教程项目作为基础。

本教程使用Unity 2017.4.1f1制作

(不需要顶点UV坐标或切向量)

1 没有UV坐标的纹理

执行纹理映射的通常方法是使用网格中每个顶点存储的UV坐标。但这不是唯一的方法。有时,没有可用的UV坐标。例如,当使用任意形状的过程几何时。在运行时创建地形或洞穴系统时,通常无法为适当的纹理展开生成UV坐标。在这些情况下,我们必须使用另一种方式将纹理映射到我们的表面上。其中一种方法是三向贴图。

到目前为止,我们一直假设UV坐标可用。我们的“My Lighting Input”和“My Lighting”着色器包含依赖于它们的文件。虽然我们可以创建不依赖于顶点UV的替代方法,但如果可以使当前文件在使用UV和不使用UV的情况下都正常工作,更加方便。

我们将当前方法保留为默认方法,但是在定义NO_DEFAULT_UV时将切换为不使用UV的方法。

1.1 不使用默认UV

当网格数据不包含UV时,则没有任何UV从顶点传递到片段程序。因此,请根据NO_DEFAULT_UV宏使“My Lighting Input”中存在UV插值器。

有多个函数假定插值器始终包含UV,因此我们必须确保它们继续工作并进行编译。我们将通过在插值器声明下面引入一个新的GetDefaultUV函数来实现此目的。如果没有可用的UV,它将仅返回零,否则返回常规UV。

在可能有用的情况下,我们还可以通过定义UV_FUNCTION提供另一种方法。这与ALBEDO_FUNCTION相似,但必须在包含“My Lighting Input”之前定义覆盖。

现在,我们可以使用UV_FUNCTION(i)更改i.uv的所有用法。我只显示了GetDetailMask的更改,但是它适用于所有getter函数。

转到“My Lighting”,我们必须确保在没有UV可用时,跳过顶点程序中所有与UV相关的工作。这适用于纹理坐标转换,也适用于默认的顶点位移方法。

视差效果也依赖于默认UV,因此在UV不可用时跳过它。

1.2 收集表面属性

没有UV,就必须有另一种方法来确定用于照明的表面特性。为了使它尽可能通用,我们的包含文件不应关心如何获取这些属性,而是需要一种提供表面特性的通用方法。可以使用类似于Unity表面着色器的方法,依靠函数来设置所有表面属性。

创建一个新的MySurface.cginc包含文件。在其中定义一个SurfaceData结构,其中包含照明所需的所有表面属性。那就是反照率,发射率,法线,alpha,金属,遮挡和平滑度。

将其放在单独的文件中,因此其他代码可以在包含任何其他文件之前使用它。但是我们的文件也将依赖于此,因此请将其包括在“My Lighting Input”中。

在“My Lighting”中,在MyFragmentProgram的开头,ApplyParallax之后和使用alpha之前,使用默认功能设置一个新的SurfaceData表面变量。然后将alpha代码更改为依靠surface.alpha而不是调用GetAlpha。还要移动InitializeFragmentNormal,以便在配置表面之前处理法线向量。

在确定片段的颜色时,现在依靠表面而不是再次调用getter函数。

并且在填充G缓冲区以进行延迟渲染时。

CreateIndirectLight函数还使用了getter函数,因此向其中添加了SurfaceData参数,并改用它。

然后将surface作为参数添加到MyFragmentProgram中对其进行调用。

1.3 定制表面

为了能够更改获取表面数据的方式,我们将再次允许定义自定义函数。此功能需要输入才能使用。默认情况下,是UV坐标,主要和详细UV都打包在单个float4中。替代输入可以是位置和法线向量。将SurfaceParameters结构添加到包含所有这些输入的Surface文件中。

返回“My Lighting”,调整MyFragmentProgram,以便在定义SURFACE_FUNCTION时使用不同的方式设置表面数据。在这种情况下,请使用法线向量填充表面并将所有其他值设置为其默认值。然后创建表面参数并调用自定义表面函数。它的参数是表面(作为inout参数)和参数struct。

由于SURFACE_FUNCTION可能会更改表面法线,因此,之后将其分配回i.normal。这样,我们不需要更改所有使用i.normal的代码。

1.4 没有切线空间

请注意,与Unity的表面着色器方法不同,我们在世界空间而不是切线空间中使用法线向量。如果要在SURFACE_FUNCTION中使用切线空间法线贴图,则必须自己明确地执行此操作。我们还可以支持更多配置选项,涉及在调用SURFACE_FUNCTION之前和之后应该如何对待法线,但是在本教程中我们不会这样做。

我们将要做的是关闭默认的切线空间法线贴图方法。不使用切线时可以节省工作。仅在默认法线贴图或视差贴图处于活动状态时打开切线空间来实现此目的。使用方便的REQUIRES_TANGENT_SPACE宏在“My Lighting Input”中进行指示。

现在我们只需要在需要时包括切线和双法线矢量插值器。

在“My Lighting”中,我们可以跳过在MyVertexProgram中设置这些矢量的步骤。

并且由于没有切线空间,因此减少了InitializeFragmentNormal,因此只需对插值法线进行归一化即可。

1.5 三向着色器

我们所有的着色器仍然可以使用,但是现在可以使用没有切线空间和其他表面数据的包含文件。让我们创建一个新的着色器以利用此优势。首先,创建一个新的MyTriplanarMapping.cginc包含文件。让它定义NO_DEFAULT_UV,然后包含Surface.cginc。实际上,由于我们将使用“My Lighting Input”中已定义的_MainTex属性,因此请包含该文件。然后,使用一个inout SurfaceData参数和一个常规SurfaceParameters参数创建MyTriplanarSurfaceFunction。现在,只需使用法线设置反照率即可。将此函数定义为SURFACE_FUNCTION。

创建一个使用此包含文件而不是“My Lighting Input”的新着色器。我们将制作一个没有透明度的最小着色器,仅支持通常的渲染管道,再加上雾化和实例化。下面是具有forward base和additive 通道的着色器。

下面是延迟和阴影通道。请注意,阴影通道不需要特殊处理,因为它并不关心不透明几何图形的表面属性。我们还没有添加对光照贴图的支持,因此目前没有meta通道。

使用我们的新着色器创建材质并进行尝试。我已将旧的测试纹理用作材质的主要纹理,虽然目前尚未开始使用。

(Triplanar贴图材质,使用法线作为反照率)

2 三面纹理化

当顶点UV坐标不可用时,我们如何执行纹理映射?唯一可行的方法是将世界位置(或许是对象空间位置)用作纹理映射的UV坐标的替代来源。

2.1 基于位置的纹理映射

片段的世界位置是3D向量,但是规则的纹理映射是在2D中完成的。因此,我们必须选择两个维度以用作UV坐标,这意味着我们将纹理映射到3D空间中的平面上。最明显的选择是使用XY坐标。

(使用XY位置当做UV坐标)

使用3D纹理怎么办?

也是可以的,但是3D纹理需要更多的存储空间,并且很难使其看起来更好。

结果是我们看到纹理沿Z轴投影。但这不是唯一可能的方向。我们也可以使用XZ坐标沿Y轴投影。这对应于通常用于对地形进行纹理化的平面纹理映射。

(使用XZ位置当做UV坐标)

第三种选择是使用YZ坐标沿X投影。

(将位置YZ用于UV坐标)

但是,当我们使用YZ时,最终纹理旋转了90°。为了保持预期的方向,我们必须改用ZY。

(将位置ZY用于UV坐标)

2.2 组合所有的三个映射

当表面与投影轴基本对齐时,单平面映射效果很好,否则的话,看起来很糟糕。如果沿一个轴的结果不好,但沿另一个轴的结果可能不错。因此,支持所有三个映射非常有用,这需要我们提供三个不同的UV坐标对。

让我们保持逻辑以确定这些UV坐标是分开的。创建一个TriplanarUV结构,其中包含所有三个轴的坐标对。然后创建一个GetTriplanarUV函数,该函数根据表面参数设置UV。

在MyTriPlanarSurfaceFunction中使用此函数,并使用所有三个投影进行采样。然后,最终的反照率变为其平均值。

(平均三个映射)

2.3 基于法线的混合

现在,我们始终可以得到最佳的投影效果了,其他两个也可以。但我们不能只使用最好的那个,因为它们会在最好的地方突然变化,产生接缝。不过,可以将它们平滑的混合。

首选的贴图自然是最能与表面方向对齐的贴图,该贴图由表面法线表示。因此,我们可以使用法线来定义所有三个投影的权重。使用法线向量的绝对值,因为表面可以面向负方向。同样,权重的总和必须为1,因此我们必须通过除以权重对其进行归一化。创建一个新函数来计算这些权重。

现在,我们可以通过其权重来调整每个映射的贡献。

(混合3个贴图)

2.4 镜像了的贴图

最好的投影现在是最强的。在轴对齐的表面上,我们最终只看到一个贴图。轴对齐的立方体在所有方面都看起来不错,但其中一半以镜像映射结尾。

(纹理在另一侧镜像)

当纹理被镜像时,这并不总是一个问题,但是当使用带有数字的测试纹理时,这很明显。因此,请确保纹理不要被镜像。通过在适当的时候反向U坐标来实现。如果是X映射,那就是normal.x为负数。同样,对于Y投影,当normal.y为负时。Z则相反。

(不再镜像)

请注意,这会在每个贴图的尺寸为零的贴图中产生一个接缝,但这很没问题,因为在那里的权重也为零。

2.5 偏移贴图

因为我们要在表面上投影相同的纹理三次,所以最终可能会突然重复。在一个球体上很明显。你可以四处移动它,直到最终得到纹理对齐,如下面的屏幕截图所示。从左到右,你可以看到序列44、45、40、44、45、40,完整序列为40–45。在它下面,你可以看到34、35、30、34、35、30。在垂直方向上,你可以看到重复的44和45。

(对齐贴图)

可以通过抵消投影来消除这种重复。如果我们将X映射垂直移动½,则在X和Z之间消除它们。对于Y和Z,如果我们将X水平移动½,同样。X和Y映射不对齐,因此我们不必担心这些。

(偏移贴图)

我们使用½作为偏移量,因为那是最大值。在我们的测试纹理的情况下,它破坏了数字序列,但保持了块对齐。如果我们使用具有三个而不是六个明显边界的纹理,则用offset抵消会更好。通常,三向形贴图是使用地形(Terrain)纹理完成的,因此你不必担心精确对齐。

3 其他表面属性

除反照率外,还有更多可以存储在贴图中的表面属性。例如,对于我们的电路材质,还具有金属贴图,遮挡贴图,平滑度和法线贴图。让我们也支持这些。

(仅使用电路反照率图)

3.1 MOS 贴图

使用三向贴图时,我们使用三个不同的投影对贴图进行采样。这会使着色器中的纹理采样量增加两倍。为了让该问题易于管理,我们应力争将每个投影的样本量减至最少。可以通过在单个贴图中存储多个表面特性来做到这一点。我们已经为电路材质创建了这样的贴图,在R通道中存储金属,在G中存储遮挡,在A中存储平滑度。因此,这就是“金属-遮挡-平滑度”贴图或MOS贴图。我们将在三通道着色器中依赖于此类MOS映射,因此将其添加为属性。

(带有电路MOS贴图的材质)

为该贴图添加一个变量(因为在“My Lighting Input”中未定义),然后像反照率贴图一样对它进行三次采样。

使用triplanar 权重混合MOS数据,然后使用结果设置表面。

(使用电路MOS贴图)

3.2 法线贴图

也增加对法线贴图的支持。我们无法将其打包在另一个贴图中,因此它需要自己的属性。

(具有电路法线贴图的材质)

对贴图采样三次,然后解开每个轴的法线。

我们可以使用与其他数据相同的方式混合法线,同时也必须对其进行归一化。但是,这仅适用于世界空间法线,而我们采样的是切线空间法线。首先,假设我们可以将它们直接用作世界空间法线,然后看看会发生什么。为了使其更明显,再次将法线用于反照率。

(切线空间中的投影法线)

最终法向向量不正确。切线法线以其局部向上的方向(远离表面)存储在Z通道中,因此结果大部分为蓝色。这与Z投影的XYZ方向匹配,但与其他两个不匹配。

在Y投影的情况下,向上方向对应于Y,而不是Z。因此,我们必须交换Y和Z才能从切线空间转换为世界空间。同样,我们必须将X和Z交换为X投影。

(世界空间中的投影法线)

因为我们否定了X坐标以防止镜像,所以我们也必须对切线法线向量进行此操作。否则,这些仍将被镜像。

现在,我们还必须翻转法线的向上方向,因为它们指向内部。

(未镜像和翻转的法线)

3.3 和表面法线混合

尽管法线向量现在已经正确地与其投影对齐,但它们与实际的表面法线无关。例如,一个球体使用立方体法线。这还不是特别明显的,因为我们正在根据实际的表面法线平滑地在这些法线之间进行混合,但是当我们调整混合时,情况会变得更糟。

通常,我们将依赖于切线到世界的转换矩阵来使法线适合几何图形的表面。但是我们没有这三个预测的矩阵。相反,我们可以做的是使用泛白(whiteout)混合在每个投影法线和表面法线之间进行混合。我们可以为此使用BlendNormals函数,但也可以对结果进行归一化。考虑到我们需要混合三个结果,然后再次将其归一化,这会有点多。因此,做一个变体,该变体不对每个投影进行归一化。

whiteout如何工作?

在“渲染6”中进行了描述。

泛白混合假定Z朝上。因此,将表面法线转换为投影空间,在此切线空间中进行混合,然后将结果转换为世界空间。

(不正确的法线混合)

对于面向负方向的表面,这是错误的,因为之后我们会将两个负Z值相乘,从而翻转最终Z的符号。可以使用Z值之一的绝对值来解决此问题。但是,这等效于从一开始就不对采样的Z组件进行求反,因此我们只需删除该代码即可。

(正确的法线融合)

现在,所得的法向矢量偏向原始表面法线。尽管这并不完美,但通常就足够了。你可以更进一步,仅使用原始Z分量就可以完全删除采样的Z分量。这称为UDN混合,使用DXT5nm压缩时更便宜,因为不需要重建Z分量,但是会降低未对齐表面的法线强度。

有了法线贴图,恢复原始的反照率,这样我们就可以看到完整的电路材质了。

(使用全部的电路贴图)

3.4 缩放贴图

最后,让我们可以缩放贴图。通常,这是通过单个纹理的平铺和偏移值完成的,但这对于三向贴图并没有多大意义。偏移量不是很有用,比例尺也不是均匀的。因此,让我们改用单个比例属性。

(具有地图比例尺的材质)

添加贴图比例尺所需的变量,并在确定UV坐标时使用它缩放位置。

(使用2倍贴图缩放)

4 调整混合权重

通过使用原始表面法线在三个贴图之间进行混合,可以找到最终的表面数据。到目前为止,我们直接使用法线,仅取其绝对值并对结果进行归一化,以使权重之和为1。这是最直接的方法,但是也可以通过各种方式调整权重。

4.1 混合偏移

更改权重计算方式的第一种方法是引入偏移量。如果我们从所有权重中减去相同的数量,那么较小的权重将比较大的权重受到更大的影响,这将改变其相对重要性。他们甚至可能变为负。添加混合偏移属性以使其成为可能。

必须确保不是所有权重都为负,因此最大偏移量应小于最大可能的最小权重,即法向矢量的所有三个分量都相等时。那是√⅓,大约是0.577,但是我们只使用0.5作为最大值,默认使用0.25。

(具有混合偏移的材质)

在权重归一化之前,先从权重中减去偏移量,然后看会是什么样子。

(不正确的偏移)

当混合权重保持为正时看起来不错,但是负权重会从最终数据中消除。为防止这种情况,请在归一化之前进行钳位。

结果是偏移量越大,混合区域变得越小。要更清楚地看到混合如何变化,请使用权重作为反照率。

(调整偏移)

4.2 混合指数

减小混合区域的另一种方法是通过取幂,在标准化之前将权重提高到高于1的幂。这就像一个偏移量,但是是非线性的。为其添加一个着色器属性,使用任意的,最大值8和默认值2之间的数。

(混合指数材质)

偏移后,使用pow函数应用指数。

(调整指数)

你可能最终会同时使用这两种方法来调整混合权重。如果你将最终指数定为2、4或8,则可以通过几次乘法对其进行硬编码,而不是依靠pow。

4.3 基于高度的混合

除了依靠原始的表面法线,我们还可以使表面数据影响混合。如果表面数据包括高度,则可以将其计入权重。我们的MOS贴图仍具有未使用的通道,因此可以将它们转换为MOHS贴图,其中包含金属,遮挡,高度和平滑度数据。这是我们电路材质的相关贴图。它与MOS贴图相同,但蓝色通道中具有高度数据。

(电路MOHS贴图)

将我们的MOS属性重命名为MOHS并分配新纹理。确保禁用了其sRGB导入复选框。

(带有MOHS地图的材质)

同样重命名变量。

将三个高度值的参数添加到GetTriplanarWeights。让我们从求幂前直接使用高度开始,替换法线向量。

然后在调用函数时将高度作为参数添加。

(仅基于高度的混合)

仅使用高度不会给我们带来有用的结果,但是很清楚看到金色电路板条是最高的,因此在混合中起主导作用。现在,将高度乘以它们各自的权重。

(乘以高度)

看起来好多了,但是高处的影响仍然非常强烈。进行调整非常有必要,因此可以向我们的着色器添加“Blend Height Strength”属性。当它完全发挥作用时会完全消除一些权重,这是不应该发生的。因此,将强度的范围限制为0-0.99,默认值为0.5。

(混合高度强度的材质)

通过使用强度作为内插器,在1和高度之间进行插值来应用强度。然后乘以权重。

将高度与偏移量结合使用以限制其影响范围最有效。除此之外,指数越高,效果越明显。

(调整高度强度)

最后,恢复反照率以查看混合设置对完整材质的影响。

(所有混合设置,最小值VS最大值 情况)

5 自定义着色器GUI

我们没有使用为其他着色器创建的着色器GUI的类,因为它不适用于三向着色器。它依赖于我们的三向着色器不具备的属性。我们可以使MyLightingShaderGUI也支持此着色器,但最好使其保持简单并创建一个新类。

5.1 基础类

与其复制我们可以重用的MyLightingShaderGUI的基本功能,倒不如直接创建两个GUI都可以扩展的通用基类。我们将其命名为MyBaseShaderGUI。将MyLightingShaderGUI中的所有通用代码放入其中,其余部分省略。使应直接用于其子类的所有内容都受到保护。这允许类本身及其子类进行访问,但外部不可访问。

让MyLightingShaderGUI继承于MyBaseShaderGUI而不是直接继承ShaderGUI。然后从中删除所有已属于其基类的代码。与其在OnGUI中自行设置变量,不如通过调用base.OnGUI将其委托给其基类的OnGUI方法。

5.2 三向着色器GUI

添加一个新的MyTriplanarShaderGUI类,以为我们的三向着色器创建GUI。继承MyBaseShaderGUI。给它一个OnGUI方法,在该方法中它调用base.OnGUI,然后显示贴图比例尺属性。对贴图,混合和其他设置使用单独的方法。

声明该类为我们的三向着色器的自定义编辑器。

(只有贴图缩放)

5.3 贴图

为贴图部分创建一个标签,然后显示三个纹理属性,每个属性都在一行上。给MOHS映射一个工具提示,以解释每个通道应包含的内容。

(贴图GUI)

5.4 混合

混合部分很简单,只是一个标签和三个属性。

(混合GUI)

5.5 其他设置

对于其他设置,通过调用MaterialEditor.RenderQueueField允许自定义渲染队列。还可以切换GPU实例化。

(其他设置GUI)

6 分离顶部贴图

大部分时候,你不希望外观完全统一。一个很明显的情况是地形,其中水平表面(指的是向上而不是向下)可以是草,而其他所有表面都可以是岩石。你甚至可能希望将三向贴图与纹理(喷洒)Splat结合起来,但这很昂贵,因为它会使用更多的纹理采样。替代方法是依靠贴花,其他细节对象或顶点颜色来增加变化。

6.1 更多的贴图

为了支持单独的顶部地图,我们需要添加三个替代地图属性。

并非总是需要单独的顶部贴图,因此让我们使用_SEPARATE_TOP_MAP关键字使它成为着色器功能。将其支持添加到除阴影通道之外的所有通道中。

将这些额外的映射添加到我们的着色器GUI。使用顶部反照率贴图需要确定是否设置了关键字。

6.2 使用大理石

要查看单独的顶部地图,我们需要另一组纹理。我们可以使用大理石反照率和法线贴图。这是匹配的MOHS贴图。

(大理石 MOHS贴图)

顶部使用电路(绿色,有点像草),其余部分则使用大理石。

(顶部为电路 其他为大理石)

由于着色器尚不了解顶层贴图,因此我们目前只能看到大理石。

(只有大理石)

6.3 启用顶部贴图

将所需的采样器变量添加到MyTriplanarMapping。在对所有纹理进行采样之后,检查是否在MyTriPlanarSurfaceFunction中定义了关键字。如果是这样,请添加代码以使用顶部贴图中的样本覆盖Y投影的数据。但这仅适用于指向上方的表面,因此当表面法线具有正Y分量时。

如果所有表面都朝上怎么办?

如果是典型的基于Heightfield的地形网格,则可以确保所有表面法线都指向上方。因此,不需要检查法线的Y分量是否为正,可以省略。

这将生成一个着色器,对Y投影的常规贴图或顶部贴图进行采样。在我们的案例中,我们在大理石上获得了一个电路层。可以是草,沙或雪。

(电路在上面)

默认的混合设置会在投影之间产生相当平滑的混合,在电路和大理石相遇的地方效果不佳。指数为8会导致突然的过渡。也可以为顶部贴图支持不同的混合设置,但是高度混合已经可以通过MOHS地图进行很多控制。

(指数设置为8)

6.4 稍后展开

尽管着色器编译器使用if-else方法对顶部或常规贴图进行智能采样,但对法线进行拆包并不明智。它不能假定UnpackNormal的两种用法可以组合使用。为了帮助编译器,我们可以推迟对原始法线的展开,直到选择了贴图之后。

7 光照贴图

我们的三向着色器尚未完成,因为它尚不支持光照贴图。它可以接收烘焙光,但不起作用。通过让所有对象静止并将定向光切换为烘焙模式,很容易看到这种情况。等到烘焙完成,然后通过将场景视图模式从“Shaded ”切换为“Baked Global Illumination / Albedo”来检查烘焙的反照率。使用三向贴图的所有对象都变成黑色。

(光照贴图使用黑色反照率)

为了支持光照贴图,我们必须向着色器添加一个元通道,它需要依赖“My Lightmapping ”而不是“My Lighting”。

7.1 使用表面数据

为了使My Lightmapping与我们的三向方法一起使用,它还必须支持新的表面方法。为了简化此操作,使其包含“My Lighting Input”,并删除现在重复的所有变量,插值器和getter函数。

与“My Lighting”一样,它必须定义默认的反照率函数。并且它应该在MyLightmappingFragmentProgram中使用相同的表面方法,但它只关心反照率,发射,金属和平滑度。

用新的表面数据替换getter函数。

7.2 包含相关输入

现在,插值器还包括法线和世界位置矢量,因此应在MyLightMappingVertexProgram中设置它们。

通常不需要这些向量,因此我们可以在不需要时跳过对它们的计算,而只需使用伪常量即可。我们可以定义两个宏META_PASS_NEEDS_NORMALS和META_PASS_NEEDS_POSITION,以指示是否需要它们。

另外,仅在需要时才包括UV坐标。

7.3 三向光贴图

剩下要做的就是声明我们的三向着色器在其元通道中需要的法线和位置数据。完成后,照明再次恢复,反照率将正确显示在场景视图中。

(正确的光照贴图反照率)

现在,我们的三向着色器可以正常使用了。你可以将其用作自己工作的基础,可以根据需要扩展,调整和调整它。

光照贴图的数据似乎不依赖于世界空间?是的,当进行光照贴图时,我们最终使用对象空间而不是世界空间。发生这种情况是因为Unity没有为meta pass设置对象到世界的转换矩阵。这样的结果是,元通道仅适用于原点定位的对象,无需旋转或缩放调整。因此,它适用于典型地形,但不适用于其他事物。只要使用了单独的贴图,只要材质基本上是统一的并且顶部已正确对齐,它对于其他对象仍然可以使用。

0 人点赞