TGDC2022 | 如何优雅玩转虚幻5?从着色器开始!

来源:游戏智库 发布时间: 2022-09-09 21:54:40

针对开发者们对UE5新引擎的诉求,Epic Games的开发者关系技术美工专家Matt Oztalay一直在努力帮助虚幻引擎的用户提升实时渲染和交互的上限。深耕游戏行业10余年的他,曾参与《光环》《使命召唤》和《坦克世界》等经典系列游戏的制作,对着色器有着独到的使用见解。

 

由腾讯游戏学堂举办的TGDC2022腾讯游戏开发者大会邀请到了Matt Oztalay,为我们分享了五种将展示和材质之间沟通的技术流程,从利用UE4和UE5特有的系统将数据输入着色器,到处理这些数据,细致地讲述了一个由着色器描绘的精彩世界。

 

以下是演讲实录:

 

image1.jpeg


我叫Matt Oztalay,现任Epic Games的开发者关系技术美工专家。今天我和大家分享的主题是《用您的着色器描绘世界》。你的材质不必与世隔绝,事实上,一旦你向其传递了外部世界的信息,你就可以做一些很棒的事情。我将用一些示例举例将信息导入材质的方法与其具备的潜在优势。


image2.jpg


首先,请允许我介绍一下我自己。


我在游戏行业工作已有11年了,目前在Epic负责与行业内外的虚幻引擎用户协作共同解决技术及美术方面的问题。我花了九年时间开发了十几款游戏,其间用到过六种游戏引擎。


image3.jpg

 

几年前,我作为开发者关系技术美术加入了Epic Games,我现在主要是通过教育和引导与虚幻引擎的用户一起探索拓展技术的边界。


我将用我们最近发布的虚幻引擎5.0以及基于节点的脚本和材质系统,来向各位展示这些技术。每个示例将分为两部分:第一部分是利用UE4和UE5特有的系统将数据输入Shader;第二部分是如何处理这些数据。


此外,我建议我们对材质的处理,或者说我们对材质数据的处理应该广泛地适用于各类着色语言。听完这次的分享,相信你可以了解到UE5的某些功能对比UE4来说变得更好用了。


image4.jpg


今天我准备演示的五种技术将展示和材质之间沟通的不同方式。


第一种展示的是如何用动态材质实例,为汽车仪表盘设置动画。它允许我们在运行时更改材质的纹理、矢量和标量参数。


第二种是通过Custom Primitive Data,我们可以在多人射击游戏的基础上更改团队颜色,这样,我们就能在几何体上存储标量和向量值。


第三种是运用Per-Instance Custom Data来驱动一条链。


第四种方式会用到Runtime Virtual Textures将装饰融入地形。


最后一种,我将向你展示如何使用材质参数集合,将纹理投影到World Space中(材质参数集合是可以从不同材质中读取的标量和矢量参数的全局结构)。


image5.jpg

 

你可能会注意到在示例当中,有部分运算会从 CPU 转移到 GPU,因为某些情况下,这种转移会给我们带来很多性能优势。在其中的一个示例中,我们只会进行一次GPU运算,然后对运算结果反复使用。这些技术的另一个好处是能够提高创造效率,它们能够减少迭代时间,或者减少项目所需的资产,从而易于创建内容,让我们工作起来更轻松。


image6.jpg


让我们直入正题!


我们从虚幻引擎中的一些基本东西开始。你可以创建一些支持更改参数或输入的动态材质实例,当你用到的是同样的材质且需要频繁处理不同的输入信息时,动态材质实例就很有用。


image7.jpg

 

为了说明这一点,我们仅使用汽车的材质来为表盘制作动画。我在虚幻引擎附带的高级载具模板中设置了几个不同的表盘,我们将使用World Position Offset为每个刻度盘设置动画,这是Vertex Shader里一个高效的附加函数。不同表盘对应不同数值,我们有车速表、转速表、燃油表和温度表。


传统的方法或者说用CPU运行的方法,是获取所有信息并逐帧更新组件的位置。然而,这种CPU驱动的方式可能会造成巨大开销,特别是当组件数量过于庞大时。重要的是,其实我们在游戏时并不会关心汽车内的仪表盘,因此我们可以将这些旋转信息和计算转移到 GPU 上。


image8.jpg


为此,我们将首先把表盘材质实例化,并对表盘的最小值和最大值,以及指针旋转的最大限度进行一次性参数更新。然后我们将相关信息传递到每个材质中。例如:要显示120度到320度之间的值,油温指示器大约只需1/4圈,而速表从0转至 10000 rpms 则需要转3/4圈。


尽管我们可以通过CPU进行运算,然后将单个“旋转值”传递给每个刻度盘,但我更愿意选择GPU——我的目标就是尽可能多地降低CPU的使用。最后,我们可以根据所有输入参数,计算出每个表盘的旋转值,并用Vertex Shader对其旋转。


image9.jpg


下面我来演示一下。如视频所示,这里有温度与油表两个表盘。当我开车时,速度表会变大,转速表也会跟着变大。等车辆停下来时,他们也会下降到0。



这些表盘每一个都是独立的组件,负责将信息传递给其材质,在开始播放时,我将材质进行了实例化,并将它存储起来以供检索,然后再传入一些一次性参数值。


image10.jpg


为了提高效率,我们将最大值、最小值和旋转的限度打包成向量,这个向量可以在材质内进行拆分。我们只是在材质实例上调用了“Set Vector Parameter”,并将名称设置为我们将在材质中设置的名称,然后这个蓝图有一个函数,可以将“显示值”传递给材质实例。现在这些表盘会使用相同的参数名称,也就是显示值,因为它们显示的具体内容不需要被玩家关注。


在表盘的蓝图中,我们为每个不同的表盘输入适当的值,包括转速表的转速、速度表的速度等。每一帧显示的都是最新数值,这些是蓝图方面所涉及的内容。每个表盘都是它自身的蓝图,所有这些逻辑都在其中运行。


image11.jpg


在材质方面,我设置了一个矢量参数。为了便于操作,我为每个通道进行了命名,以匹配我从蓝图中获取的内容。


image12.jpg


为了用World Position Offset或Vertex Shader让指示器旋转起来,我们还需要设定一个介于 0 和 1 之间的旋转值。首先我们需要对输入的显示值进行归一化,但传入的最小值和最大值,分别对应的是 0 和“旋转限度”,因此显示值是小于1的,而不是 0 到 1。


image13.jpg


例如,油温的显示值从120 到 320华氏度,只旋转了大约 1/4圈,也就是说表盘旋转一圈是580度。因此我们需要先搞清楚表盘旋转一圈所显示的值是多少。为此我们需要对输入的显示值进行归一化,这个值介于0到最大值1/旋转限度之间,因为最大值1/旋转限度表示的是表盘旋转一圈所显示的数值。当然,为了避免“爆表”。我们会用Clamp节点把数值限制在最小值0 和旋转限度之间。


有了这样一个0到1范围内的旋转值,我们就可以用Rotate About Axis节点计算我们需要对顶点进行多少偏移。Rotate About Axis能为你提供一个delta值,有了这个值,你可以旋转任何从给定枢轴点围绕给定轴旋转给定百分比的向量。


image14.jpg


为此,我将使用Object Orientation作为旋转轴,即当前图元在World Space中所指向的向量。对于枢轴点来说,因为这个表盘是嵌入在Actor中的,我们真正需要的是图元原点而不是Actor或Object Position,所以我们需要将0,0,0从Local Space变换为World Space。然后我们要旋转的就变成了绝对世界位置,这也是顶点的位置。


为了把它呈现出来,我们需要旋转这个表盘的每个顶点,从World Space的图元原点出发,根据我们之前计算的量,绕轴Object Orientation旋转。我们只需将Rotate About Axis的节点输出,传递给主材质World Position Offset的节点输入。


image15.jpg


在虚幻引擎中,我们会为每个顶点运行Vertex Shader,把它们置于几何体中编码的位置,并且我们可以选择在World Place中对其进行偏移。


当然,如果移动了顶点,特别是如果我们旋转它,那么它的指向,乃至光照与着色都会有问题。为此我们还需要旋转顶点法线,就像我们旋转顶点本身一样。


image16.jpg


幸运的是,我们现在有这么一个节点,我们只需把输入连接起来并将新的输出传递给材质节点的法线当中。现在,表盘的法线将根据它们的位置变化,指向正确的方向。然后,你可以将其作为World Place基础法线值,并混合你所需要的任何其他法线贴图。


image17.jpg


为了向大家证明这个方法的优点,我在这里给出了一个表盘蓝图,它可以把材质和表盘组件的更新情况分别呈现出来。用这种蓝图法可以使Post Tick Component Update增加0.5毫秒。


你可以看到,我们现在是在旋转材质,而如果我们旋转的是组件,那么我们需要对Post Tick Component Update,以及Transform或者Render Data进项额外的绘制调用。如果我把它切换回来,这些都没有了。所以,要旋转这些,CPU需要进行大量不必要的运算,如果只是一两个组件更新 问题倒不是很大,但如果是大量的组件更新开销就会增大。


image18.jpg


在我们深入之前,还可以做很多事情。例如我们可以修改动态材质实例上的信息,虽然很酷,但我并不想搞出新的材质实例,这样我们就需要在运行时管理它们。因为每个新的动态实例都需要单独进行绘制调用(即使你可以依赖虚幻引擎内置的动态合批把相同模型和材质的物体合并成一次绘制)。


image19.jpg


我可以使用相同的材质,但修改的是几何体上的值。如果我可以在某个材质中查找该值而不以Per-Instance的方式进行,那么我可以批处理所有相同材质,对它们进行一次绘制调用。这样我就不需要查看大量资产。


image20.jpg


在虚幻引擎中,这个技术叫做Custom Primitive Data。


当你的材质相同,却要对不同输入信息进行处理时,Custom Primitive Data就很有用。并且它可以进行变频更新,让我用一张图向大家展示这个优势!


image21.jpg


这张图的一侧是蓝队,另一侧是红队。我们可以用图元(或几何体上)上的数值来设置基础色。


例如,我不想对这些材质设置红色和蓝色版本的基础色,我希望将所有内容批量进行绘制调用;我还希望美术师们在创建地图时能自由一点,在不需要生成新资产的前提下,让美术师也能设置这些参数,给了他们很大的自由度,也减少了迭代时间。


image22.jpg


首先,我们需要找到图元上的参数,然后将数值从图元传到材质,再用这些参数来自定义资产的颜色。


在材质上,我有一个叫Team Color的向量参数,我选中了“Use Custom Primitive Data”复选框,它会生成一个覆盖材质的遮罩。


image23.jpg


你会发现向量参数会变成“Custom Primitive Data Index 0, 1, 2, 3”。那是因为遮罩下的每个Custom Primitive Data都是被打包到几何体中的单个浮点。遮罩下的四个连续浮点我们可以理解为一个向量参数。然后,在每个图元上,我们会对Custom Primitive Data默认值进行元素添加。


在UE5中,向量预先填充了材质中的参数名(这是UE5版本的新功能)。在UE4中 你需要在此处添加三到四个浮点数,并且需要为红、绿、蓝三种颜色及Alpha通道分别进行添加。而在UE5中,我们可以把它们显示为命名参数,并用颜色选择器编辑他们,现在 我可以在关卡中复制这些网格体,并且随心所欲地更改团队颜色。


image24.jpg


为了进行演示,我创建了7个barrier材质实例,它们使用的是参数而非Custom Primitive Data。在下面,我又创建了7个使用Custom Primitive Data的barrier,我们来看看它们有什么区别!


image25.jpg


除了为团队颜色设置基础色,Custom Primtive Datab还有多种用途。


我想向你展示这些年来出现的一些用法——你可以用它来驱动物体磨损度这样的参数,或者用它来选择贴画用哪个Subuv应用于物体表面。最近 我就用它把“弯曲”参数传递给spline mesh跑道,当该剖面图切线变化时,我会在这个梯级图案中加入lerp插值节点,这样玩家就会认为跑道是弯曲的。


image26.jpg


还有另一种方法可以通过实例化几何体将信息传递给材质,实际上是同样的概念,也是Per-Instance而不是Per-Primitive。这个图像看起来和ustom Primitive Data图像差不多,但眼尖的你会发现这些是实例化静态网格体,而不是静态网格体Actors。


image27.jpg


Per-instance Custom Data非常适用于当你有大量图元,且这些图元需要对你编程的不同输入信息进行处理,同时这些输入信息又不会经常更新时。当然,因为我们处理的是实例化图元,所以它们使用的材质都一样。


image20.jpg


为了证明这一点 我将在微风中来回摆动链条,而不设置任何动画。事实上,这个示例正如表盘一样不需要对它做任何更新。


我们将在Vertex Shader中进行操作,但与表盘不同的是,我们不会为链中的每一节都创建动态材质实例。相反,要使链节摆动起来,我们会充分利用分层实例化静态网格体。这些是可以生成LOD的实例化几何体,以及Per-instance Custom Data。


便于理解,你们大可以认为我有这么一个蓝图:它沿样条线以编程的方式加入链节实例,并且我们可以控制这些链节。在虚幻引擎中,我们有分层实例化静态网格体组件,可以在运行时处理大量相同的静态网格体,就是我现在用的这个。



在使用虚幻引擎时,这种技术的效率远高于许多替代方案——


因为我们只是做一些简单的摇摆,所以GPU计算起来会比完全模拟更快;又因为它是一个静态网格体,所以它会比人工进行动画的蒙皮网格体更快;因为它用到了分层实例化静态网格体,所以它在GPU上的计算效率也会比我们使用单个大型静态网格体的效率更高;因为引擎会自动剔除,我们看不到的链节,它的开销也会更少;因为我们只关注样条线上的单个链节,不需要链的每个部分都是不同的几何体,而且因为它是基于一些用户参数动态构建的,所以它们也更容易放置和更改……


image28.jpg


任何情况下,如果你只需要一条链,你就不需要ChainA、B、C、D、E……不过我们需要知道很多内容:链的旋转轴(从链的起始点到末端的向量)、链的旋转角度(链沿着样条旋转了多少)、旋转的原点等。


image29.jpg


好消息是从蓝图中获取所有这些信息相当简单,并且因为有虚幻引擎的Per-Instance Custom Data,我们可以通过脚本在链节的每个实例上设置这些值。就像表盘一样,我们会对动态材质进行实例化,并导入链的起点和终点作为向量参数。


对于每个链节,我们都要弄清楚两个重要的事情——


第一,根据不同链节的相对位置,我们需要知道它们摆动了多少,这就是链节的摆动百分比。


然后我们可以构建旋转轴。我们会用到一个标量值,即起点和终点之间的百分比距离。我们可以在线上找到最近点,这就是Line Percent。我们会用Per-instance Custom Data将这两个浮点值传递给每个实例。就像表盘一样,我们需要通过World Position Offset为这些链节设置动画,我在这里标注了颜色。例如要想找到这个紫色点,我们需要知道沿着绿线的Per-Instance,然后这个蓝线正在摆动。


image30.jpg


但不是所有内容都是Per-Instance,因为其中的一些值对整个链是全局的,例如链的起点和终点。为此,和之前一样,我创建了一个动态材质实例,并将链的起始点和终点设置为材质上的向量。为了简化GPU计算,我所有这些我都是在World Space里做的。你可以看到,我把局部的终点放进了World Space。


image31.jpg


我需要让引擎知道,我准备在Instanced Static Mesh Component上用Number Custom Data Floats传入一些Per-Instance数据。这些就是“Sway Amount”和“Line Percent”。你也可以在Instanced Static Meshes上设置Custom Primitive Data,但它们会应用在组件中的所有实例上。


image32.jpg


Line Percent是链到向量的距离,Sway Amount则是我们根据每个给定链节到链的距离计算出来的值。因此要得到链,我们需要用Chain Percent来计算Sway Amount,我可以用CPU来计算而不是GPU,因为它只需计算一次。所以 在Construction Script中,我们将遍历所有实例来设置这些值,Chain Percent是当前链数除以链节总数,这就是我们的Chain Percent。


第二,我们需要要弄清楚链节应该摆动多少。我将在 -1 和 1 之间重新映射 0 到 1 的Chain Percent,然后将其输入到这个函数中,即π的余弦乘以重新映射的Chain Percent,再除以2,最后得到一个幂。


image33.jpg


这样我们就可以得到大概这样子的曲线图。它比你只做一个简单的线性插值所得到的曲线要好很多。


image34.jpg


然后呢,我们将该介于0到1的值作为Per-Instance Custom Data的一个浮点。这会是我的“Sway Amount”,也是我的第一个Custom Data浮点,也就是这个Custom Data Index 0。它还是我们这条链的实例数据。


要得到Line Percent有些困难,但幸运的是,虚幻引擎有一个内置的“获取沿线最近点”的节点,为此 我只是从实例化组件这里获取了我们这个实例的World Space位置。


线的原点是Actor的原点,方向是从起点到终点的向量,这样 我们就可以获得沿线最近点。有了它,我们只需要知道这个点到链的原点的距离,再除以链的总长就可以算出最近点到向量的距离。然后将其设置为组件上的第二个Custom Data值。我将在材质中用它来重新创建完整的向量,因为相较于用三个Per-Instance Data标量,这样开销会小一些。


image35.jpg


在材质中,我首先会创建从起点到终点的向量并将其归一化,因为我们会再次使用 Custom Primitive Data 来传递 Sway Speed和Sway Amounts。然后我会用我们在Construction Script中计算出的Line Percent,乘以那个还没有进行归一化的向量并将其添加到起始点来创建我的原点。


image36.jpg


对于旋转角度,我会用Sway Percent 乘以时间正弦,再乘以能够限制旋转范围的总的Sway Amount。这是我们用Per-Instance Custom Data 0传递的信息。我会再乘以总的Sway Amount来限制旋转的范围,最后将所有这些导入到Rotate About Axis节点中,并进行World Position Offset。


image37.jpg


这样就实现了链条在风中摇摆的效果,你甚至可以做许多链条,每一根的动画效果都不一样,摆动速度也不一样,但它却是只有一个链节的几何体。



这种特定技术的一个局限在于每个分层实例化网格组件都会单独进行绘制调用,这些组件不会进行动态地批处理。它的优点更多是关于内存占用。


我这里用1个链节与N个链节乘以你需要的所有不同链。例如,我为我的链节用了一个有20面的环面。假如你是想有一套独立的链条,你可能不会愿意这样做,比较一下我的单个链节之有120kb,而你这个组合起来的链有4.8mb。


另一个优点是现在的美术师都能够更加自由地做出链来,而无需每次都生成新资产,或者受到预制作链的限制,但这不仅仅在于我们可以设定团队基础色,或者为链节的Custom Primtive Data设置动画。


image38.jpg


当你通过编程一次性构建大量对象时,它会大放异彩!你可以用它来处理植被或者碎石的变化,当你按照程序构建建筑物时也可以用它来处理窗户等的信息。我现在也在尝试用Per-Instance Custom Data来使游戏性能可视化。


image39.jpg


image40.jpg


第四个技术用到的是Runtime Virtual Textures,它在运行时利用GPU按需生成纹理数据。


作为一种针对大场景高效渲染的方式,它将从上到下进行高效渲染,这就很适合地形这样的对象。随着层数的增加,地形材质的开销一般都会增大,但是在美术师绘制完地形之后,这些基本上都是静态的了。


image41.jpg


你可以将这些材质的大部分计算转移到不常更新的Runtime Virtual Texture上并从中采样,以绘制到地形本身。这样地形材质中需要更多开销的非动态部分就不用每帧都计算在内,最终当你在屏幕上绘制像素时,只需要对像素世界位置上的RVT进行采样即可。


image42.jpg


关于如何使用RVT 有两个方面。但为了保持读取方面的一致性,当你有许多需要对同一组输入数据做出反应的图元时,而这些数据又随着像素在变化,那么这组数据就不必更新每一帧。


为了演示这个技术,我想向大家展示,如何将几何体混合到地形中 而不需要额外的几何体,或者不必再重新计算材质的参数。


image43.jpg


这些年来我们面临的众多挑战之一,就是确保我们用来突出地形的静态几何体能与地形很好的地衔接。早在2013年或14年的时候,我就与另一位前同事讨论过这个问题,他在17年向我展示了如何用一种名为Dirt Skirts的几何体来做到这一点——这个技巧深刻影响了不止一款游戏。


image44.jpg


曾经,这个技术需要将部分地形和整块岩石导出到Houdini,现在在虚幻引擎中,你也可以直接使用Houdini引擎执行此操作。然后你可以找到两个几何体的交线并沿着岩石和地形的副法线把它移出来,这样我们就生成了几何体的Skirt。它的边缘有一些alpha通道,可以应用于地形材质,或者类似效果的专用材质上,这样就可以把一切混合在一起。


image45.jpg


我们需要把地形材质及其高度,输出到整个纹理中。然后我们需要弄清楚像素到地形有多远,并用它来混合地形纹理集和基础材质。


因此,我们不需要再为地形和几何体的每一个像素重新计算整个地形材质,也就是我们需要混合的东西。我们可以只绘制地形一次,然后在多处进行采样,这样可以快速、简洁地读取纹理信息。


image46.jpg


这种方法之所以行得通,是因为RVT不会局限于特定材质中,它也可以从其他材质中采样你只需要一个世界坐标,然后RVT采样器就能取得该位置的纹理信息。


image47.jpg


所以这项技术一个很棒的地方在于——当我在关卡中移动网格体时,它们会自动抓取附近的地形。不必担心每次移动它时都会重新生成多余的几何体地形材质。它也可以为RVT输出提供基础色、高光、粗糙度和法线。



可以这么说,我已经在幕后处理了该输出的所有细节。这一点很关键!这意味着每当材质在RVT周围进行绘制时,都会为它输出材质信息,我们需要做的就是这些。


image48.jpg


现在我有了地形材质,就不必在混合的几何体中运行整个地形材质。但是如果我不知道地形相对于几何体的位置,那么从给定的世界位置对RVT进行采样有什么好处呢?——我可以知道在哪里停止混合,然后直接对地形本身进行采样。


顺带一提,在虚幻引擎中有这么一个“材质属性”的概念,它是一个庞大的体系,包含表中所有可能的材质信息。它对于材质信息的合并或拆解都很有用,这就是上一张PPT中的Get Material Attributes部分的内容。


image49.jpg


我们可以为RVT的通道导入高度。这意味着我们纹理的X/Y坐标可以和Z轴的高度值结合起来,如果我把地形的高度写入到RVT中,那么我可以知道材质上任何一点回到地形材质的高度。


image50.jpg


我在这里所做的是获取地形的绝对世界位置,用遮罩把它加入到这里的B组件中(或 Z 轴),并将其输入到运行时虚拟纹理输出节点的世界高度中。任何时候只要这个RVT绘制图元上的材质被设置了高度,该值就会被写入RVT,是不是非常简单?


我们需要对RVT的高度信息进行采集,它位于对象的材质中,我们需要从当前像素位置采集RVT的高度信息,这是RVT的默认值,或者说它是RVT采样的默认值。我需要一个单一的标量值,它可以告诉我正在被绘制的像素距离地形的远近程度。所以我们在这里做一个简单的归一化处理,并从当前像素的Z坐标中减去世界高度,然后除以标量参数就可以对每个材质实例进行修改。使之饱和,保持在Clamp(0,1)之间,但它的计算开销却更小。


image51.jpg


现在我们就得到了这样一张图——越靠近地形 颜色就会越深,反之,离地形越远,颜色就越浅。当我们在做混合时,当数值为1时,它离地形就越近,当数值越接近0时,离地形就越远。


image52.jpg


现在我们知道岩石与地形的距离,我们只需要在岩石材质中重新创建地形材质。和之前一样,我们需要对RVT进行采样,现在我们要用到一些其他引脚。但是我不会对基础材质中的每个值都进行Lerp,而是向大家介绍一个比较有趣的方法——你可以借助材质属性来做。


image53.jpg


image54.jpg


首先,如果我将材质属性直接输入到材质当中,你可以看到岩石只对材质进行了采样。所以我们需要用Blend Material Attributes节点对地形材质和岩石的基础材质进行混合,并用我们先前生成的距离混合值来驱动此混合。


image55.jpg


这个技术的另一个好处是可以让我混合更多相对平缓的网格体,比如这个岩石集群。


image56.jpg


而这个效果,凭借Dirt Skirt技术却不一定能办得到。为了测试这种技术与其他替代技术之间的性能差异,我创建了一个全屏四边形,并在两种材质之间进行切换,其中一个是基础 RVT 混合材质,另一种能够混合所有材质层,在这两者之间来回切换。


在GPU上 它们的bass pass成本差大约为0.1毫秒,这样看来似乎没有优势。但当你需要同时对一堆不同对象进行绘制调用时,优势就会显现。


image57.jpg


这些年来,RVT混合和RVT高度我都经常用到。它还有一些其他用途,只要你知道地形的高度和结构就能用到。


举个例子:你可以用它来为雨雪加上遮罩,因为你知道最上面的高度,而RVT又是从上往下进行绘制的;有了它,你还可以在水下做一些伪焦散的阴影投射,而不必担心使用光照函数或贴花;你也可以用它来查找水岸线,而不是依赖像深度消退这样的昂贵的东西;你还可以用地形的颜色RVT来更改植被以及植被映射到地形上的地面颜色;有了景观的高度,你还可以将树根之类的顶点移到地面,而不是用预先生成的资产来尝试完美地适应地形。


image58.jpg


image59.jpg


image60.jpg


最后,我想向你展示用Material Parameter Collections可以做什么。


它们具有全局性,任何材质都可以对它们做出反应。这也意味着每当你更新参数时,只需执行一次操作,而不需要处理多次。因此,当你有多个图元且这些图元需要一次或多次处理同样的输入信息时,Material Parameter Collections就能很好地发挥作用。


image61.jpg


我来展示一下如何利用“投影仪”对象的变换。将纹理投影到World Space中。例如它能做到的一件事,就是可以将电影投影到粒子当中。


image62.jpg


这项技术能以它极大的魅力真正打动并震慑住那些绞尽脑汁想吸引更多观众的电影商,我们来看看它是怎么工作的。



假设我有一个屏幕和一个投影仪,我现在用投影仪把从0到1的UV坐标投影到屏幕上,但是如果我想在屏幕材质中查找这些UV坐标怎么办?我们需要知道投影仪在哪里,以及它所指的方向,有了这些我们就可以将向量从“投影仪到像素”变换为“投影仪空间”。


这样我们就知道该向量相较于投影仪的前向向量偏离了多少,然后我们可以计算出投影纹理的尺寸。因为我们也不可能拿着卷尺去量有了该纹理的尺寸我们就可以对变换向量进行归一化,这样就能得出最终的UV坐标了。


它的一大好处在于任何材质都可以使用这些全局参数,我们只用设置一次参数就能够在多处对其进行采样,我可以把它用于多个粒子特效中却只用到了一个材质函数。


image63.jpg


回到问题上来,我们现在知道投影机的FOV(也就是紫色部分)、投影仪的位置(前上右四个向量),以及我们准备绘制的像素。在World Space中的位置,我们可以设置一个叫MPC_Projector的Material Parameter Collection,再设置位置前、上和右四个向量参数,还有一个叫做 FOV 的标量参数。我根据一个名为BP_Projector的摄像机,创建了一个蓝图,每一帧它都会用Set Collection Parameter指向我们创建的MPC,来传送它的位置、前、上和右向量,以及它的FOV。


image64.jpg


我们现在正在收集所有已知信息,并将其发送到材质中。在蓝图方面,我们要做的就是这样;在材质方面,我会为这个材质函数设定所有这些UV逻辑,然后你可以在很多不同材质中用到这个函数,它会输出UV值,我可以在其他材质中随意地使用。


image65.jpg


所以我需要做的第一件事是确保我一直在“投影仪空间”中操作。这样当摄像机移动时,投影也会随着它变换位置。我们仍然是想弄清楚“投影仪到像素”这个向量相较于与投影仪空间中投影仪的前向向量偏离了多少。


尽管虚幻引擎无法构建向量,并将其变换到任意空间中,但它却可以使用Inverse Transform Material材质函数。但你传入定义空间的向量时,它们的性质基本相同。


image66.jpg


我们再来快速了解一下矩阵乘法。我们的输入向量是一个1x3矩阵,我们的三个基础向量,组成了一个 3x3 矩阵,得到的是一个 1x3 矩阵。它主要是每一列基础矩阵与向量的点积。所以这个函数是让输入向量与基础向量相乘并将其作为输出的组件,它通过InVec与BasisX相乘来获取X组件。我们跳过使用内置节点创建矩阵这一步骤,只处理矩阵乘法的组件,因为这才是我们需要的。


我们来快速了解下点积。点积可以返回一个标量值,我们可以通过它得出两个输入向量的对齐情况,如果向量相互垂直,则值为 0,如果它们完全相反,则结果为负,如果它们完全对齐,则结果为正。


我们只需要转变向量,也就是世界位置到投影仪位置这个向量。当你从Material Parameter Collection中采样时,它们会自动以Float4的形式出现,因此在坐标轴上为它加上遮罩。


image67.jpg


我们正在构建的这个空间可能看起来有点奇怪,为什么我选择把投影仪的右轴作为BasisX?


还记得我们是如何通过点积得出向量的对齐情况的吗?这个节点的所得到的向量是Pixel to Projector Dot Basis,我希望这个向量的 X 值能告诉我像素投影仪向量与右向向量的对齐情况。


因此,当像素投影仪移动到投影仪前向向量的右侧时,该值会增加。同样,我希望所得向量的Y 值,能告诉我像素投影仪向量,与投影仪的向上向量的对齐情况。


image68.jpg


但我希望该值会随着对齐程度的增大而减小,随着对齐程度的减小而增大,所以我将其反转了一下,最后为了使空间显得圆润一点,我们需要前向对齐变成Z轴向对齐。


现在就比较有趣了,如果我为Z组件加上遮罩,使向量归一化,再对其进行转换,就会得到一个X组件。当像素投影仪向量移到前向向量的右侧时,它的值会在0到1之间递增,我也得到了一个Y组件,当像素投影仪向量移到前向向量的下方时,它的值会在0到1之间递增。这一点和UV坐标非常相似。


image69.jpg


但是因为我希望我的投影在前向向量上以.5 .5进行采样,我应该可以从变换后的向量中减去.5 .5以获得我向量的东西对么?但并不是这样。因为我们无法对变换后的向量进行归一化处理,像素投影仪向量不是单位向量,它的大小等于投影仪和像素之间的距离,并且我们没有用到投影机的FOV,如果没有完全在下或完全在右,这个向量不会给我们一个 0 到 1 的空间。这对我们没有任何帮助。


image70.jpg


为了适当地将该向量偏移并归一化,到 0 到 1 空间,我们必须知道纹理有尺寸,就好像它是从我们的投影仪投影过来的一样。我们需要用什么东西把它除开,我们已经有了像素投影仪向量,也有它的长度,我们也知道投影仪的FOV,让我来把它简化一下。


image71.jpg


有时,去掉一个维度会让问题简单一些。那么我现在有A,也就是像素投影仪向量,还有θ,也就是投影仪的FOV,我需要求出B。当我把投影仪的线条延伸到屏幕两边,整个图形看起来很像等腰三角形,等腰三角形可以看成是由两个直角三角形构成。那么我们可以只需要知道一条边的边长和一个角度就可以求出很多东西,我们现在有FOV,如果这个直角三角形的面积是等腰三角形面积的一半,那么这个角度就是FOV角度的一半,我知道这条边的边长,也就是投影仪到屏幕的距离,所以如果我能算出,三角形另一边的边长。是的,这条边就是这个角的邻边!


image72.jpg


这里我们要用到正切,tanθ x邻边 = 对边。我们有了相机的FOV,接下来只需要将其减半,然后可以把邻边作为像素到投影仪的距离。为了讲得更清楚一些,我使用了新的Named Reroute节点,它有助于我跟踪材质中的信息。


image73.jpg


现在,简要介绍一下虚幻引擎中的三角运算。我们的材质的三角函数处于周期1,没有单位,这与使用2π周期的图形计算器有很大不同。我在周期1中绘制了一个正弦波,也就是红线所示,周期2是这里的绿线。


image74.jpg


因此,如果我想将图形计算器的度数转换为弧度,那么这个2π弧度是360°除以2π,或大约 57°。对于周期1的弧度,一个弧度只有 1 °,这意味着为了将我们的FOV转换为弧度,我们只需将其除以360°,然后用tanθ乘以对边,就能获得对边纹理尺寸的一半。


image75.jpg


现在我们有了直角三角形对边的边长,以及½个FOV角度,那么投影纹理的尺寸就是它们的两倍。因为现在我们的UV坐标不是以(0.5, 0.5)为中心,我们需要将其偏置为纹理宽度的一半,然后除以纹理的宽度,以获得 0 到 1 的值。看起来,我们的UV坐标已经发生了变化,因此(0.5, 0.5)正好在正向轴的上方。


为了看得清楚一点,我屏蔽掉了不属于0到1这个区间的值,然后你所要做的就是用该材质函数,将它的UV坐标输出到纹理采样器中。


image76.jpg


比如,我现在需要做的就是为幕后工作者进行视频的采样。这里我可以插入剩余的自发光堆栈,根据需要对其混合。


image77.jpg


现在所有这些粒子特效都用到了之前生成的材质函数来对视频进行采样,它们都发生在粒子上,并且这里没有采用像光照函数这样会造成更大开销的替代物 。


image78.jpg


说到光照函数,在虚幻引擎中你可以将材质应用于聚光灯。那么为什么不用它呢?这似乎是一项非常艰巨的工作,光照函数会有一些局限,它们并不能完全适合这种情况,比如:光照函数是灰度的,它们只改变投射光的强度,而不是它的颜色。所以对于全彩色投影,你需要为颜色系统提供同样的材质,这个相当复杂。因为为光源设置光照函数意味着这个光源将被看成是动态的阴影投射器,对于你的性能预算而言,这可能会开销极大。


image79.jpg


相反,我们可以借助一些来自外部世界的信息,通过数学运算,在接收的材质中查看纹理,而无需使用阴影投射灯。


为了测试性能差异,我再次设置了一个具有两种不同材质的全屏四边形。一种展示的是纹理的投影,就和之前的粒子一样,另一种是默认的被点亮的灰色材质。


image80.jpg


当我切换到灰色材质时,我切换到三个聚光灯那组,它们分别发出红光、绿光和蓝光,并且正在通过光照函数播放视频。它们在我这台设备上的base pass成本差可以忽略不计。但光照函数在Lights、Shadow Depths和Shadow Projection之间增加了大约1毫秒的计算时间。


image81.jpg


在过去的几年里,我多次用到它。它现在对我来说是一个非常有用的工具,所以我很愿意向大家分享!

 

这些是我使用它的一些方法,如果这个演讲能激发你的灵感的话,我很想知道你是如何使用它的!

扫描左侧二维码,关注微信公众号

即可获得游戏智库每日精彩内容推送,并且在第一时间获取游戏行业新鲜资讯。

APP 下载

扫描二维码
下载iOS或安卓APP
返回顶部