【转载】UE4 豆知识 - 从 ImposterUV 中学习 Billboard 实现

628 阅读6分钟

原文链接:UE4 豆知识 - 从 ImposterUV 中学习 Billboard 实现 | 鲨莉叶

Preface

有 Billboard 的项目需要,简单分析了一下,用顶点偏移的方式做是最自然的,但是自己从头写的话感觉有重复造轮子之嫌,虽然对个人成长有利但是老板不建议这样做(。)

联系到 UE 本身有类似的效果—— Imposter,而且实现也是完全开放的,所以决定从这里入手,分析一下做法再应用到需求里,于是有了这篇笔记。

本文包括原理分析和具体实现,原理分析包含一些实验过程,只需要看节点的同学可以直接空降。

原理分析

(浅灰部分是保姆级思考过程,直接看原理可以有选择性地阅读)

首先去 ImposterUV 函数里选中我们需要的顶点偏移输出节点,右键选择上游节点,快速提取出我们需要的相关内容,粘贴到自己新建的空白材质蓝图中,进行节点整理并且补全缺失的参数。(这一步可能有些麻烦,但是为了分析起来方便最好还是做好前期的准备,尤其在分析更复杂的节点的时候,整理干净节点更容易理清思路,比较重要的是通过节点的位置判断功能区域和上下游关系)
在分析实现方式的时候从末端或始端出发是比较容易的,因为我们很清楚世界场景位置偏移的结果是什么样子的,所以从末端出发会比较容易捕捉意图。
(插一句题外话,直接用节点提炼公式也不是没想过,但是这样做出来就是一个很庞大的式子,而且很可能是被优化过或是顺序颠倒过的,有点难处理)

世界场景位置偏移的最终结果从概念上是这样的:

再看最后一部分结构就很好理解了:

  • 图1 - 右侧 Add 的 B 连接的是 ObjectPosition

世界场景位置偏移 的输入是一个向量,表示顶点相对于自己位置的偏移量,也就是这个向量:

  • 图2

这个 Result-ObjectPosition+绝对世界位置=?

  • 图3 - 倒推出红色的向量

VertexOffsetLocal:从物体原点指向旋转后该顶点的向量,也就是在本地坐标下顶点的偏移(旋转后)

这样就反拆思路就明确了:通过计算摄像机到该物体的方向,求出与该方向垂直时面片顶点的相对位置。

摄像机到物体的方向易得:

  • 图4

那么接下来就是求顶点的位置了,ImposterUV 考虑的比较全面,可以选择固定 Z 轴轴向为世界坐标的 z 轴方向或是通过视角方向计算出的相对 z 轴方向

世界坐标 z 轴比较好理解,那么相对 z 轴方向是什么概念又是如何计算的的呢?
主要通过 CreateThirdOrthogonalVector 这个函数:

  • 图5 - CreateThirdOrthogonalVector 函数内部

image.png

可以看到它的计算方式是以 Vector1 为基础,Vector2 为辅,构造出一个包含 Vector1 方向上单位向量的正交基,简单来说就是以 vector1 为坐标轴,构建出一个各轴相互垂直的新坐标系。(由于输入的两个向量不一定是垂直的,因此没有使用 vector2 作为直接输出的第二个轴向)

  • 图6 - CreateThirdOrthogonalVector 函数图解 image.png

通过这个函数,我们可以得到旋转后的面片的本地坐标系在世界坐标下三个轴向的表示。借用图 6 来说明,output123 组成的这个新坐标系就是这个面片的本地(Local)坐标系,其中 output3 对应 z 轴,也就是面片向上的方向向量。

到这里,我们相当于求得了面片的朝向,只要将顶点在本地坐标系的相对偏移乘以本地坐标系的对应轴向就可以了。

这里 ImposterUV 使用了一个很巧妙的方法:

  • 图7 - UV 映射偏移

image.png

利用 UV 来确定顶点的相对方向和距离。

UV 的好处在于可以准确且唯一地对应每一个顶点的相对位置,即使这个平面有内部细分,也能够正确地平移每一个顶点。

先说 Switch True 那个分支的计算,1-x 的作用是切换坐标轴手性,UV 本身如果补全成三维坐标,以指向屏幕外的方向为 z 轴,是右手坐标系,而引擎中的坐标系是左手坐标系,因此需要将坐标校正过来,否则最终结果会颠倒。x 轴使用 ConstantBiasScale 将原点校正到物体中心,并且缩放使顶点位置标准化(这里解释一下,因为涉及到长度问题,如果不缩放那么 uv 边界的顶点最大值是 0.5,偏移长度会受影响),y 轴通过缩放使顶点位置标准化,但不校正原点(结果相当于固定面片底边,将顶边拉伸,类似图像编辑软件里自由变换时选中顶边拉长的效果)。
Switch False 的计算中 1-xConstanrBiasScale 的作用和 True 分支的相同,多出的这个 Rotator 可以以面片的 UV 中心为原点、指向屏幕外屏幕外的方向为轴,旋转该面片。
补充说明:
这里就是一些朋友想直接用 ImpostorUV 来做 billboard,但面片出现奇异的旋转的原因,图7 中常数 1 的位置原本是连接图片左侧的那个节点的,那个节点引入了一串庞大的节点来计算平面旋转:

  • 图8 - 旋转部分逻辑

image.png

如果想要直接应用 ImposterUV 节点来做 Billboard 效果也可以直接在材质实例里将 ZRot 参数调整成 0 ,就不会在平面上旋转了。

最后的部分就是去的面片的长度和宽度,分别以 UV 相对位置为基础进行变换,输入给 VertexOffsetLocal,计算过程就成立了。

  • 图9 - 计算 VertexOffsetLocal

image.png

计算原理汇总:(伪代码!不是真正的 HLSL !)

//
float3 FrontVector = CameraPosition - ObjectPosition;
float Rotate = (atan2(FrontVector.x, FrontVector.y).x) / (2 * PI);
float RadialAngle = frac(Rotation - Rotate + 0.25);
float BlendPhase = frac(FramesX * RadialAngle - ImposterRoundingOffsetX);
float RperFrame = (-2 * PI) / FramesX;
float UpAngle = dot(normalize(FrontVector), float3(0, 0, -1));
float RotationAroundZ = ZRot * ((UpAngle * RperFrame * -0.5) + (BlendPhase * UpAngle * RperFrame));
//
float3 LocalAxisZ = CreateThirdOrOrthogonalVector(FrontVector, float3(0, 0, 1)).vector2;
float3 LocalAxisX = CreateThirdOrOrthogonalVector(FrontVector, float3(0, 0, 1)).vector3;
float2 RotatedTexCoord = rotator(texcoord_UV0, RotationAroundZ);
float2 UVProjectionBasis = (1 - RotatedTexCoord - 0.5) * 2;
float3 VertexOffsetLocal = SpriteWidth * (-1) * LocalAxisX * UVProjectionBasis.x;
VertexOffsetLocal += SpriteHeight * LocalAxisZ * UVProjectionBasis.y;
//
float3 WorldPositionOffset = VertexOffsetLocal - WorldPosition + ObjectPosition;
return WorldPositionOffset;

蓝图实现:

image.png