Impostors详解——纸片构筑的美丽幻觉

Impostors详解——纸片构筑的美丽幻觉

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


写在前面

早前,我截帧分析了《Call of Dragons》,具体内容可以看《〈Call of Dragons〉渲染截帧分析与迷思》,在其中提到了《Call of Dragons》中使用了3D场景与部分2D渲染结合的方式来优化性能,而这种方式的一个核心思路就是“三转二”,关于“三转二”的具体内容也可以参考我更早早前写的一篇《三渲二(三转二)2D动画方案调研》。而这种将3D物体转换成2D的思想,同样应用于一项渲染技术——Impostors,中译为冒名顶替者(感觉怪怪的,所以后文还是说Impostors吧)。

Impostors简单来说就是使用Billboard的2D片来表示复杂3D模型,可以说《Call of Dragons》中使用的三转二算是Impostors中的一种简化版。而《Call of Dragons》中摄像机不会旋转,因此它处理的情况更简单,甚至都用不到Billboard渲染,只需要将2D片平行于摄像机近平面摆放就行了。而广泛意义上的Impostors会在各类游戏中广泛应用(通常用于表达远景),它需要考虑摄像机观察角度的变化、透视关系、渲染效果还原、过渡等众多因素,因此Impostors的实践绝不是一件简单的事情。

在Unity Asset Store中存在着一个名为Amplify Impostors的工具,它提供了非常好的Impostors技术的实践,因此本文的主要目的是从Amplify Impostors中详细分析Impostors的实践原理与使用到的技术,由此对Impostors渲染技术进行学习与分析。

在本文中,你可以了解到:

  1. Impostors概念。
  2. Amplify Impostors的简单使用方法,包含界面参数介绍、配合LOD Group等。
  3. 官方手册的要点提炼,包括支持、不支持的功能、不同Impostors类型、适合的使用场景等。
  4. Amplify Impostors中烘培Impostors的过程详解,包括Billboard Mesh的创建、GBufferTextures的生成等。
  5. 对Amplify Impostors提供的Demo场景的简要分析。

测试环境:Unity 2021.3.12f1、Amplify Impostors 0.9.9.3、URP 12.1.7

一、Impostors概念

参考《Real-Time Rendering, Fourth Edition》(感谢Morakito翻译的中文版)中的13.6章节,介绍了广告牌技术Billboarding,其原理是基于摄像机观察方向来修改纹理矩形朝向,通常会让矩形总是与屏幕平行。Billboard技术可以用于表示草、烟、能量盾、云等(比如《原神》中的远景云使用了Billboard实现),原书中也举例了一个用Billboard模拟的云层。

而Impostors是Billboard技术的一种应用,通过将当前观察点中的复杂物体渲染成纹理,并将改纹理映射到一个Billboard Mesh上,从而创建一个Impostors。它可以用于同一个物体的若干实例,或者在几帧中重复使用,从而分摊它的创建成本。

在渲染物体存在的地方,Impostors纹理是不透明的,而在其他任何地方都是完全透明的,它可以将复杂的模型简化成单个的纹理,因此Impostors对于快速渲染远处的物体十分有用。说到渲染远处的物体,当然还有一项耳熟能详的技术——LOD,即对于远处的物体使用更简化的Mesh。然而这种简化的Mesh往往会丢失物体的形状信息和颜色信息。而Impostors则没有这个缺点,因为可以使生成的纹理分辨率与显示器的分辨率近似匹配。另一种使用Impostors的情况是,当观察者只能看到这个物体的同一个侧面的时候,可以使用Impostors(在《Call of Dragons》中就是这种使用场景)。

在渲染物体创建Impostors纹理的过程中,我们需要将Camera朝向物体包围盒的中心,并将视锥体设定为物体投影包围盒的最小矩形。Impostors纹理的Alpha值一开始会被清除为0,而在物体渲染的地方,Alpha值则会设置为1.0。然后将生成的纹理用作面向视点的Billboard Mesh的纹理贴图。

参考UE4的Document——Render 3D Imposter SpritesImpostors是使用翻页书风格的Textures的Sprites,用于存储一个静态Mesh的每一个可能的观察角度下的渲染结果,尽可能让Sprite的渲染效果在材质和光照方面与原始Mesh匹配。但是,它也可能带来帧之间的Popping,意味着它不一定适用于所有场景,当渲染大量不会靠近相机且移动缓慢的物体时,它非常有用。

翻页书风格的Textures如下图所示,通过将不同角度下材质属性(Albedo、Normal等)存储到Textures中,用于渲染Sprite时尽可能模拟原始Mesh,这些就是Impostors所用的GBufferTextures。

二、Amplify Impostors简介

Amplify Impostors是一个强大的工具,通过使用经典的Billboard技术的现代版本来构建几何简化单体积准确的复杂模型表示。

拿官方的一张图片来举例,我们就可以知道Impostors做的事情,使用2D片代替3D Mesh来营造视觉欺骗,以此来优化渲染上的性能与表现。通常,Impostors可以应用于远景的表达。

在官方提供的Demo中,有一个场景对比了Impostors和3D Mesh的渲染效果,如下图所示(左下是3D Mesh,右上是Impostors)。从中可以看到,Amplify Impostors的效果可以做到几乎与原始Mesh无法区分,在距离渲染物体较远的情况下,这个效果完全可以以假乱真。

但在距离渲染物体近的情况下,可以看到Impostors会有比较明显的穿帮,并且在视角转动时会有Popping与不自然的过渡鬼影。

三、Amplify Impostors的Start Screen

导入Amplify Impostors.unitypackage后,会自动弹出一个Amlify Impostors Start Screen,如下图所示。

通过Manual按钮,可以快速跳转到官方提供的使用手册。

同时,Start Screen中提供了3种渲染管线HDRP、URP和Built-In下的Demo用例,点击对应的按钮即可导入对应的Demo用例。原文中提到了不要手动解压用例,否则不能保证导入正确,请使用Start Screen导入Demo用例

官方提供了对Unity 2019.4 LTS以上、URP 10或HDRP 10(SRP 10)以上以及Built-in管线的一键导入支持。注意,对于URP和SRP,官方只确保了对(10以上)原生URP和HDRP的支持,而在实际项目中,我们很可能会对URP或HDRP进行修改,可能会导致导入用例失败。在我一开始使用实际项目的URP管线时,导入就失败了,原因是丢失了Decal Feature和SSAO Feature,因为Decal被我们从URP中删除了。

所以对于初步上手Amplify Impostors,建议先使用原生URP管线(HDRP也行,但我用的是URP,所以后文均会在URP的情况下实践)。在了解Amplify Impostors后,可以按照需要移植到实际项目的URP管线中。

四、Manual提炼

1. Impostors类型
Amplify Impostors中提供了三种预烘培Impostors类型

  • Spherical:以经典的经纬度划分烘培角度进行快照。这种情况的Impostors Shader非常简单,因此性能很好,并且Impostors近距离看下来表现基本没问题,但是在视角发生变化的多帧之间,抖动很明显。可以通过牺牲渲染分辨率以提高帧率来减少该问题(虽然我想应该不会有人这么做)。

Spherical的图示如下,左边的猴子是真实3D模型,右边的猴子是Impostors效果,灰色几何体代表预烘培的所有角度(后两种类型的图示也是这种排布,后续不再赘述)。

  • Octahedron:以Icosphere(短程线性多面体Geodesic Polyhedron)划分烘培角度进行快照。这种方式烘培的好处是,给出摄像机的任意坐标,都可以在多面体上找到最接近该位置的3个Frame,然后对3个Frame进行混合。这种方法很好地解决了视角移动时多个Frame之间的抖动。但是,Shader复杂度会更高,当近距离观察Impostors时,混合会产生明显的鬼影Artifacts

  • HemiOctahedron:Octahedron的变体,只从“Icosphere的上半球”进行相同数量的快照,从而有效地将混合精度提高了一倍。其缺点在于,如果不烘培下半球,从下面看时会产生不正确的结果。只有当我们已知永远只会从上方观察该物体时,该方法才适用。

2. 实时渲染Feature

  • SRP(HD and LW)(v4.9.0+)
  • 标准/Legacy的前向渲染和延迟渲染
  • 动态光照和阴影
  • 物体相交时的深度写入
  • 全局光照
  • 烘培的Lightmaps(通过自定义烘培)
  • GPU Instancing
  • 抖动穿插过渡

3. 其他支持的功能与不支持的功能

  • 支持自定义打包贴图:渲染最多4个贴图用于Standard材质和自定义材质,也可以使用自定义烘培最多8个不同的贴图。
    支持自定义形状编辑器:自动生成Impostors的自定义形状,减少Impostors的透明区域以减少Overdraw,同时也支持手动编辑
  • 支持烘培预设:定义了烘培Impostors的预设,其中还包括各种导入和导出选项。
  • 不支持Skinned Mesh的烘培,目前不支持Animated Skinned Meshes,比如角色。作者计划提供一个实时烘培变体,允许动画物体以指定速率渲染为Impostors。
  • 如果想在近距离下改善质量,只能提高纹理分辨率,因为近距离下渲染Impostors会产生Artifacts。注意,Impostors不适用于近距离渲染,它不能代替真实的Mesh,只能作为远距离下渲染的性能优化。2K尺寸的纹理通常效果OK,对于移动端,甚至可以使用1K(感觉1K也很大啊)。
  • 仅当Shader包含Deferred Pass时才支持标准烘培,比如Unity Standard Shaders。但是创建出的Impostors可以同时用于前向和延迟渲染两种模式。如果原始的Mesh使用了自定义的前向Shader(比如卡通渲染),我们需要使用Custom Baking Shader来烘培它。

五、Impostors烘培界面

Impostors的烘培操作是通过在Inspector视图中Amplify Impostors脚本提供的界面下进行的,如下图所示。

参考Manual,对几个重要参数进行解释:

  1. Impostors Asset:对烘培Impostors生成的资源文件的引用,其中包含了该对象大部分的Impostors信息。使用资源文件形式的好处是如果我们有多个相同对象的Impostors,资源文件可以共享。
  1. LOD Group:引用对象身上的LOD Group组件,作者似乎更建议Impostors要和LOD Group搭配使用。
  1. References:要烘培成Impostors的Renderer的引用。
  1. BakeType:烘培和渲染Impostors使用的技术,即上文提到的三种Impostors类型,Spherical、Octahedron和HemiOctahedron。
  1. Texture Size:最终烘培出的Image尺寸。更高的尺寸意味着在近距离下有更好的表现,但会带来更高的内存占用和运行时渲染消耗。
  1. Axis Frames:每个轴上的Frames数量。例如,16代表会对单个Impostors执行256(16x16)次快照。(这个轴指最终Impostors对应的2DTextures的横轴纵轴)对于Spherical Impostors,每个轴上可以指定不同的Frame数量。
  1. Pixel Padding:对于每个快照的边缘像素的填充Padding,以避免由Mipmap引起的渲染伪影
  1. Billboard Mesh:可用于编辑烘培的Billboard Mesh形状,使用了Unity内置的Shape Editor来自动估计形状,同时提供了手动编辑功能。
  1. Bake Preset:提供了Standard烘培预设以及可自定义的预设,在预设中,可以配置烘培用的Shader以及运行时渲染Impostors用的Shader(可自定义),也可以配置烘培输出的Impostors对象的材质Textures(_AlbedoAlpha、_SpecularSmoothness等等)以及Textures的一些属性。

六、Impostors的烘培过程

讲完烘培Impostors的操作界面,接下来看下Impostors的烘培过程。首先,在前面提到过,仅当Shader包含Deferred Pass时才支持标准烘培,同时在Manual中提到过,默认烘培会使用原始物体的Shader中的Deferred Pass

整个烘培过程可以简单描述成3个步骤:

  1. 生成Billboard Mesh。
  2. 烘培GBufferTextures。
  3. 创建Impo Billboard。

1. 生成Billboard Mesh
因为最终Impostors是渲染在Billboard方式的Mesh上的,所以得先思考Mesh长什么样。可能我们会首先想到,直接用一个矩形Quad不就行了吗?没错,矩形片完全OK可用,但实际渲染对象可能是不规则的形状,矩形片上会有许多完全透明的区域,这意味着我们需要在Billboard Mesh使用的纹理上也空出这些透明的区域,因为UV坐标是在整个Mesh的所有顶点之间插值的,由此造成了纹理空间的浪费以及纹理的实际精度下降。因此,对于Billboard Mesh的构建,使用最贴近原渲染对象边缘的简单不规则几何片是最合理的,这样能充分利用纹理,也能减少Overdraw

接下来就是考虑如何生成最贴近原渲染对象边缘的简单不规则几何片,主要思路就是找到所有Frame下的渲染对象边界,然后使用其中的最大(小)值作为最终Billboard Mesh的边界。在Amplify Impostors中,使用的方法可以简单描述成以下两个步骤(为了易于理解,我描述的步骤和代码实现会有略微不同,但原理完全是一致的):

  1. 创建一张默认大小为256x256的AlphaTexture对于每个Frame,将渲染对象使用正交投影(并确定合适的VP矩阵,确保渲染对象尽量保持在画面的正中心)渲染到AlphaTexture,渲染的Pass是按DEFERRED、Deferred、GBuffer的顺序寻找到一个可用的延迟渲染Pass。简单理解就是将所有Frame下渲染对象的Alpha值(相当于Mask)叠加渲染到一个Mask上,该Mask可以覆盖到所有Frame下渲染对象的不透明区域。可以在代码中配置AlphaTexture的尺寸,但是太大没有意义,这里只是用来生成简单多边形。同时这里我们也看到,在Amplify Impostors中烘培用的Shader必须包含延迟渲染Pass
  1. 将上一步生成的AlphaTexture的Alpha通道值渲染(使用Blit)到一张同尺寸256x256CombinedAlphaTexture上(Combined顾名思义)。使用的Pass代码如下,非常简单。重新Blit的主要目的是过滤出AlphaTexture的Alpha通道值(因为上一步走的延迟渲染Pass还会让AlphaTexture包含RGB值)。
  1. 根据CombinedAlphaTexture(相当于一个最终的Mask)使用一定算法(只要能找到近似几何边缘的算法就行)求出原渲染对象的近似几何边缘,由这个近似几何边缘的顶点生成最终的Billboard Mesh。在Amplify Impostors中使用的求边缘方法为Unity Editor下的SpriteUtilityGenerate Outline方法,Sprite本身提供了符合这个功能的方法,省的自己实现了。

接下来详细介绍这三个步骤的具体实现,核心逻辑在于叠加渲染所有Frame下的3D物体来生成AlphaTexture该逻辑和最终生成Impostors的GBufferTextures的逻辑大部分相同,很多逻辑相通(比如确定当前Frame下的CameraVP矩阵),因此了解了生成AlphaTexture的逻辑就必然能理解最终烘培GBufferTextures的逻辑。

1.1 生成AlphaTexure
生成AlphaTexure详细过程如下:

  1. 首先通过AmlifyImpostor.cs引用的AmplifyImpostorAsset获取烘培用到的所有信息,包括Impostors类型、烘培用的shader、预设等等。
  1. 计算3D Mesh在所有快照角度下的Bound的最大值(对应CalculatePixelBounds函数),生成各个快照角度下的CameraView矩阵的旋转部分,应用于mesh.bounds,得到每个角度旋转后的meshBounds信息,然后在所有角度中对xy方向Bound和Depth方向Bound找到最大值。
  1. 初始化创建要烘培的AlphaTexture(如果是烘培GBufferTextures创建的就是_AlbedoAlpha、_SpecularSmoothness这些GBufferTextures),以及一张trueDepth Texture(用于在烘培时执行深度测试),Texture的尺寸均为minAlphaResolution默认256值(如果是烘培GBufferTextures则创建的尺寸为配置的Texel Size,比如2048)。
  1. 开启sRGB写入
  1. 记录原Renderer的Transform信息(Position、Rotation、LocalScale这些)并存储到成员变量,并将其重置为默认值(位置归零、缩放为1)。
  1. 开始烘培Impostors(对应RenderImpostors函数)。首先判断Bake Shader是否为空:是,则启用标准烘培;不是,则启用自定义烘培。
  1. 创建一个CommandBuffer,SetRenderTarget为所有要烘培的AlphaTexture(如果是烘培GBufferTextures则设置为GBufferTextures数组,利用MRT一次烘培多张Textures)和trueDepth(DepthRT,临时资源,用于深度测试),进行一次ClearRenderTarget对所有Textures初始化清理。
  1. 筛选下要烘培的Mesh,去除所有为空的、Disable的、ShadowsOnly的Renderer。
  1. 遍历所有快照角度,在每个Frame角度下进行快照,每次快照具体步骤如下。
  1. 对于每个Frame角度,首先计算CameraView矩阵的旋转部分,计算旋转后mesh.bounds作为FrameBounds(当前Frame的边界信息)。然后根据FrameBounds计算烘培需要使用的Camera的V矩阵和正交P矩阵。其中V矩阵包含了一些信息,摄像机从哪看——从Bounds中心退后一定距离,以及要看向哪——FrameBounds的中心。P矩阵包含了一些信息,投影模式为正交(必须是正交),根据Bounds以及第2步计算出的最大值计算视锥体各个平面(即near、far、left、right等),主要目的就是找到一个合适大小的视锥体。
  1. 得到当前Frame角度的V矩阵和P矩阵后,通过CommandBuffer设置对应Pipeline状态(VP矩阵、视口),注意对于AlphaTexture的渲染,视口统一为(0, 0, 256, 256)。然后,选择烘培用的材质:如果启用了标准烘培,则在当前Renderer使用的材质中按DEFERRED、Deferred、GBuffer的顺序寻找到一个可用的Pass,如果全部都没找到,则Fallback到LightMode为Deferred的Pass,并且如果存在DepthOnly的PrePass,则记录下该PrePass;如果启用了自定义烘培,则将<当前Renderer的材质,自定义烘培使用的材质>的键值对关系保存到bakeMats字典中,在自定义烘培的情况下,默认会使用Pass 0
  1. 根据Renderer的属性,设置Lightmap、全局光照相关的关键字和值。关闭LightProbe关键字,因为LightProbe原本作用是影响动态物体,即使Renderer本身是动态的,但在烘培时也不需要让Renderer包含LightProbe提供的的光照信息,否则物体所处的位置、周围环境的变化会影响烘培结果。虽然对于烘培AlphaTexture这一步是多余的,因为我们只需要Alpha值,但是对于烘培GBufferTextures是必要的。
  1. 对Renderer进行烘培:在标准烘培的情况下,调用CommandBuffer.DrawRenderer,如果包含DepthOnly的PrePass则先渲染PrePass,再渲染GBUFFER,将GBUFFER渲染到AlphaTexture(如果是烘培GBufferTextures,则渲染到多张GBufferTextures)上;在自定义烘培的情况下,调用CommandBuffer.DrawRenderer,使用自定义的烘培材质的Pass 0将物体烘培到AlphaTexture(如果是烘培GBufferTextures,则渲染到多张GBufferTextures)上。
  1. 重复10~13步,烘培得到所有Frame下渲染对象Alpha值合并后的AlphaTexture
  1. 清理所有临时资源,包括CommandBuffer、烘培材质。

1.2 生成CombinedAlphaTexture
有了所有Frame下的AlphaTextures,生成CombinedAlphaTexture就很简单了,即过滤出AlphaTexture的Alpha通道值

  1. 开启sRGB写入。
  2. 加载采样Alpha值用的Shader及材质(Shader名为ShaderPacker)。
  3. 创建一张RenderTexture,名为CombinedAlphaTexture,尺寸为minAlphaResolution默认256值。
  4. 将AlphaTexture使用上述步骤2的材质Blit到CombinedAlphaTexture上。
  5. 将类型为RenderTexture的CombinedAlphaTexture存储到Texture2D类型的CombinedAlphaTexture中,方法很简单,使用Texuture2D.ReadPixels函数,这里不过多描述。

1.3 生成Billboard Mesh
传入参数CombinedAlphaTexture以及一些必要的参数,直接调用SpriteUtility.GenerateOutline生成BillboardMesh。

如下图所示,木桶(3D Mesh)最后生成的CombinedAlphaTexture(256x256)和Billboard Mesh长这样。

2. 烘培GBufferTextures
烘培GBufferTextures可以简单分为以下几个步骤:

  1. 生成GBufferTextures。
  2. 对GBufferTextures执行一些可选操作。

2.1 生成GBufferTextures
接下来就是生成GBufferTextures实际运行时渲染Billboard Mesh要采样的GBufferTextures了,在了解了AlphaTexture如何生成后,生成GBufferTextures的原理我们就几乎完全知道了。

生成GBufferTextures的步骤和7.1.1节生成AlphaTexture几乎完全相同,除了视口Viewport设置会有所区别。在渲染GBufferTextures时,每个Frame的渲染结果不会叠加在RenderTarget上,而是会在GBufferTextures上划分出快照数量(通常是Frame x Frame)个Grid区域,渲染每个Frame时将视口指定为对应Frame的区域(有点像阴影级联的逻辑)。逻辑在这里就不赘述了,因为只有视口的区别以及一些细枝末节的区别。

如下图即为一个木桶生成的16x16的GBufferTextures中的AlbedoAlpha纹理。

2.2 可选操作
根据烘培的预设、提供的Feature以及单次烘培的配置,可以有一些额外的步骤,不是很重要的点,因此略过详细内容。

  1. (可选操作)在标准烘培下,对GBufferTextures的值做一定修正(Depth、Albedo等)。
  2. (可选操作)如果输出GBufferTextures的图片格式为TGA,则将通道布局改为BGRA。
  3. (可选操作)若烘培预设中PixelPadding值大于0,则使用ImpostorDilate Shader对GBufferTextures进行边缘扩张。
  4. (可选操作)若在烘培预设的基础上Override了当前烘培的GBufferTextures尺寸,则对GBufferTextures进行Resize,(也就是说可以单独修改单次烘培中个别GBufferTexture的尺寸)。

3. 生成Impostors Billboard
目前我们生成了Billboard Mesh和GBufferTextures,接下来就是组合成最后的Impostors Billboard(GameObject)

  1. 配置Runtime下渲染用的Shader并创建对应材质保存到AmlifyImpostorAsset中:如果在烘培预设中指定了Runtime Shader,则使用指定的Runtime Shader;如果没指定,则根据Impostors Type使用Amplify Impostors提供的对应Runtime Shader。
  1. 将(RAM中的)GBufferTextures创建并保存到Assets中对应文件夹(磁盘空间)中,生成贴图资源文件
  1. 创建ImpostorsGameObject,为其创建并配置SharedMesh(Billboard Mesh)、LOD Group。
  1. 为Runtime下的材质设置正确的GBufferTextures Properties,根据Impostors类型开启关闭对应关键字,将一些必要的Properties设置为正确值。

由此,最终的Impostors Billboard就生成完毕了。

如下图所示,最后的AmplifyImpostorAsset的资源包括以下内容:Runtime材质、Billboard Mesh、GBufferTextures。这些资源也就是渲染一个Impostors需要的所有东西。

七、Runtime渲染

上一节完整介绍了Impostors Billboard的离线烘培生成,接下来我们来看向运行时如何渲染Impostors

在这一节中,我们会重点关注Impostors的Frames选择与插值,而不会关注Impostors Shader的前向渲染Pass和延迟渲染Pass,因为其不是本文的主要内容。接下来,会以效果最好的OctachedronImpostorsURP.shader为例,对Shader进行分析。

首先分析在Vertex Shader中,Billboard片的实现和如何确定使用哪些Frame以及对应顶点UV

  1. 首先是Billboard片的实现,将CameraPosition从世界空间变换到模型空间objectCameraPosition,求出模型空间物体指向摄像机的单位向量objectCameraDirection
  1. 使用简单的叉乘计算,求出定义Billboard Mesh的(单位)正交向量,即计算Billboard Mesh朝向objectCameraDirection时Local的上向量(Billboard Local Y)与右向量(Billboard Local X)。如下图所示,只考虑相机在Object Space的XY平面内的情况下,Billboard正交向量示意图。

  1. 直接对Billboard Mesh的Vertex的X分量乘以右向量(Billboard Local X),Y分量同理。这样做是正确的,因为Vertex的X分量确定了顶点水平方向到空间中心的距离,因此该距离等于Billboard显示时顶点水平方向到Billboard空间中心的距离。因此该操作保证了Billboard显示时从Camera看到的Mesh和模型空间下从Z轴负方向看向Mesh是一致的。以上三步即为Amplify Impostors中Billboard的实现,该方法利用了Billboard Mesh只关注顶点的XY分量(Z分量始终为0)的特性,从而避免了矩阵与向量的运算,而是直接使用标量乘以向量,简化了计算。
  1. 接下来确定使用哪些Frame以及对应顶点UV,首先计算从Camera指向变换后的Billboard顶点的向量localDir,对于localDir使用八面体映射投影到二维平面上。八面体映射的逻辑可以参考《八面体参数化球面映射方式及实现》,该技术主要用于将球面参数化映射到平面,通常会用于CubeMap的纹理映射,这篇文章中对该方法介绍的很详细,在此不做过多赘述。
  1. 将localDir映射到二维平面之后,就相当于将localDir映射到GBufferTextures上的UV坐标OctaUV(范围0到1),此时OctaUV必定落在某一个Frame的Grid内部,那么容易得到floor(OctaUV x framesHorizontal)就是当前落到的Frame索引baseOctaFrame
  1. 通过baseOctaFrame,很容易计算出该Frame对应的Grid在GBufferTextures上的UV范围,比如对于2x2的Grid,左下角的Grid对应的UV范围为[0, 0.5]。同时,我们也可以得到该Frame的起始UV点(即Grid的左下角),将起始UV点使用八面体映射的逆运算映射回三维空间下的向量,该向量即对应该Frame下烘培相机的近平面Plane的法线Normal
  1. 接下来就是求当前顶点对应的UV坐标。已知该Frame下烘培相机近平面Plane的法线Normal、objectCameraPosition、localDir,求出以objectCameraPosition为起点、localDir为方向的射线与法线为Normal的平面的交点p
  1. 创建一个与Plane正交的本地切线空间坐标系TBN,向量Tangent与Bitangent与Normal正交。
  1. 已知交点p、以Plane为XZ平面的切线空间的X、Z方向单位向量Tangent、Bitangent,求出p在X、Z方向上的分量frameX、frameZ,由此求得当前顶点对应的UV坐标uvs=-(frameX, frameZ)(为什么是负的?)。同时,我们计算局部法线localNormal
  1. 对UVS应用Scale和Offset。
  1. 由于OctachedronImpostors中,会对3个Frame进行采样插值,所以接下来会找到与当前落到的Frame索引baseOctaFrame相邻的2个Frame进行6~10步的计算,得到另2个UV坐标,关于相邻Frame的选择在此不做赘述。
  1. 最终,得到Octachedron多面体上找到最接近该相机位置的3个Frame对应的3组顶点UV

Fragment Shader就没什么好说的了,主要就是采样三个Frame然后根据权重插值,其余就是正常的延迟渲染Pass了。

八、【番外篇】Amplify Impostors Demo分析

终于,我们分析完了Amplify Impostors中所有核心逻辑。接下来是一些我在分析Amplify Impostors提供的Demo时的一些分析过程,就当作番外篇了,有兴趣的同学可以阅读,跳过也完全OK。

因为之前提到了我在使用自己的URP管线时导入失败,那么就首先看下官方Demo用例中的URP Asset和Renderer都有些什么特点吧。

Demo中提供了三种质量的URP Asset,分为高保真度High Fidelity、均衡模式Balanced和性能至上Performant。三种Asset使用的Renderer均为同一个AI Universal Render Pipeline Asset Renderer。

在这里就拿Balanced分析吧(鲁迅先生说过,中国人的性情总是喜欢调和和折中的)。

首先看AI Universal Render Pipeline Asset Balanced(AI显然是Amplify Impostors的缩写)。

虽然是个毫无特点而言的URP Asset(主要也是因为URP Asset本身玩不出什么花样了),但要注意到一点是,当前使用的Renderer并不支持OpenGLES3,这个对于手游项目而言还是挺致命的,很多手机设备都可能用的是OpenGLES3(快都给我去用Vulkan啦!),之后查一下Renderer不支持OpenGLES3的原因,埋坑ing~

在该URP Asset中,启用了Depth Texture和Opaque Texture特性。Depth Texture特性会在渲染完Opaque Pass(取决于Renderer上的Depth Texture Mode,在这里是After Opaques)之后Copy Camera深度图到一张_CameraDepthTexture中,而Opaque Texture特性会在渲染完Opaque Pass之后使用Opaque Downsampling(可配置的降采样模式)将ColorAttachment降采样到一张_CameraOpaqueTexture中。

对于URP Asset,基本上没别的好说的了。接下来看AI Universal Render Pipeline Renderer

这里看到Rendering Path为Deferred,即使用了延迟渲染Depth Texture Mode为After Opaques,上文中提到过,会在Opaque Pass之后Copy Depth到一张Texture。

分析完了URP管线,接下来看一个Demo场景Barriels & Boulders(木桶和巨石)。

这个场景非常简洁,放了5个木桶和3块石头。就拿一个木桶来分析吧。

对于一个木桶Game Object,其使用了LOD Group组件:LOD0是个正常的3D模型,有2106个三角面;LOD1是Impostors,只有6个三角面。也就是说,当摄像机距离木桶很远时,木桶会切到Impostors显示。

这样是Impostors的一个很恰当的实践方法——即近景使用3D模型,远景使用Impostors。正如官方手册中提到的,"The common purpose of using such technique is to be able to represent far distant objects with a very low polycount, for instance, trees, bushes, rocks, ruins, buildings, props, etc." Imposter适合用于代替3D模型表现远景。虽然Impostors在近景表现也还算可以,但在一些情况(如视角转动)下还是会穿帮,以及一些过渡的ArtiFacts,是否在近景使用Impostors取决于项目需求

九、小结

好啦,目前我已经介绍了我对Impostors的全部认识,包括Impostors的概念、以及Amplify Impostors中对该技术的实现细节。相信在足够了解之后,无论是自己去实践Impostors,还是基于Amplify Impostors进行改造,都会变得不那么遥不可及。至于Impostors的实际性能怎么样,这里我尚且没有关注,一是因为我目前也仍然在Impostors学习过程中,二是对于不同的场景、不同的Impostors使用,性能都是未知的,不能一概而言。本文也告一段落了,未来如果有对Impostors新的认知,后面会及时更新到本文中。

参考

  1. https://assetstore.unity.com/packages/tools/utilities/amplify-impostors-119877#description
  2. https://wiki.amplify.pt/index.php?title=Unity_Products:Amplify_Impostors/Manual
  3. https://en.wikipedia.org/wiki/Geodesic_polyhedron
  4. https://zhuanlan.zhihu.com/p/408898601
  5. https://zhuanlan.zhihu.com/p/667993188
  6. https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/RenderToTextureTools/3/
  7. 题图来自画师wlop

这是侑虎科技第1561篇文章,感谢作者欧几里得范数供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:https://www.zhihu.com/people/ou-ji-li-de-fan-shu-34

再次感谢欧几里得范数的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)