深入理解UE中纹理在实时流程中的生命周期
24487 0
实名

通过了实名认证的内容创造者

发布于 2024-5-24 16:27:06

您需要 登录 才可以下载或查看,没有账号?注册

x
点击上方蓝字CG世界关注我们“ 感知技术 · 感触CG · 感受艺术 · 感悟心灵 ”中国很有影响力影视特效CG动画领域自媒体
这篇Dev Hub文章将带大家深入探究虚幻引擎中的纹理,了解导入、压缩、Mip、流送以及纹理的存储位置,并学习如何加载它们来进行渲染。请继续阅读并欣赏一位上了年纪的VFX美术师如何“对着天空挥舞拳头”。

1be4cc7244175e8bbd6cb90a1f7ce6cd.png

观看视频:
《虚幻引擎虚拟制片中的纹理、MIP、压缩、流送以及内存》讲座https://youtu.be/ibmSyKY6pmQ?si=DUU7V5MWOGlm9spf

559f5896893e8f7814149c9323bbf7cd.png

“我们不在乎什么实时。”
当我和有意使用虚幻引擎打造作品的动画或视效工作室第一次交流时,几乎每次都会听到他们这么说。这种想法本身是完全没问题的。如果你之前都是30小时才能完成1帧的渲染,现在看到虚幻引擎的影片渲染队列可以1分钟就完成1帧的渲染,肯定会欣喜若狂。但如果比起每秒24帧的渲染结果,你更希望有更高的采样率、更清晰的画面和更多的毛发数量,那也完全可以理解。
我认为这里最关键的就是要因地制宜。我一般讲完优化方面的话题之后就会说出这句话。传统的CG美术师在开始使用虚幻引擎时最大的误解就是:如果他们可以不在乎每秒24帧的渲染,那么他们就不需要担心优化问题,这就大错特错了。我试过提醒他们,我也列出了所有的原因:这会影响你在引擎中工作的速度、会有漫长的Perforce同步时间、冗长的导入时间、还有GPU内存问题等等……他们只是点头微笑,礼貌地听着我对CG的理解,就像看一个疯老头对着天空挥舞拳头。视频会议结束以后,他们的脑子又瞬间空空如也,再也不考虑这个问题了。毕竟多年以来,昂贵的硬件和软件已经让我们可以生成比之前多出几千万个多边形的角色,可谓前所未见;细致到一砖一瓦的纹理分辨率,还用极高的密度绘制出一片城市街区的景象;枪口冒出一道几乎看不见的火光,VDB可能就要占用多少G的容量。我们打着“质量”的旗号,赞美着这些自我催眠式的懒惰行为。或者就像我之前听某家大型游戏公司的首席技术官说的那样,“视效美术师正在经历一场缓慢的脑叶切除手术”。事后我肯定会接到同一家工作室打来的电话,又是某位压力山大的主管或制作人打来求救。
“我们的渲染崩了!救命!”
你要明白,即便你不在乎什么速度和性能,但你的GPU可不这么想。如果你要渲染的内容无法全部容纳在GPU的内存里,它就无法渲染,就这么简单。如果所有这些多边形和纹理像素加起来的内存超过了GPU的可用内存,那你的渲染就会崩。你可以购买更昂贵的GPU,但即使是最贵的GPU也有必须遵守的限制。在我看到的因GPU内存问题而导致的渲染崩溃中,每10次就有9次是由于纹理完全失控造成的。在这篇Dev Hub帖子中,我会带领大家深入了解一下虚幻引擎中的纹理。
UASSET
和将几何体导入虚幻引擎不同,将纹理拖入虚幻引擎后,uasset会直接显示在内容浏览器中,并不会弹出什么对话框。我觉得这有时会给人一种错觉,以为一切工作都自动完成了。引擎确实会尽最大努力将这些纹理导入并转换为uasset。很多时候它也确实能做到非常完美。但任何事物都有要遵循的规则,而如果你不了解这些规则,那么很快就会出错。
当纹理导入虚幻引擎之后,引擎会将该纹理转换为.uasset文件。因此你的exr或png文件在虚幻引擎中已经不再是exr或png,它们现在是uasset文件。这个uasset包含源像素以及生成的任何MIP。源像素仍然存在于资产中,因此如果你想从引擎中导出纹理,还可以得到你之前输入的像素。即使它保留了源纹理导入到引擎中的PC文件路径位置,你也不需要保留它来修改或将源像素再导出到引擎之外。虚幻引擎不再与该文件有实时的连接,除非你想从源文件重新导入。它们会将自己的数据保存在你的DDC或“Derived Data Cache”文件夹中。



纹理资产编辑器
我们来看一下这个所谓的纹理uasset。这里我不会介绍每一项属性,还请在官方文档中查看。但我想强调一下“离线渲染(Offline Rendering)”这个重要属性。你真正需要了解的纹理信息都在“细节(Details)”输出里。我们会先介绍一些简单的内容,然后再讨论流送和内存池等更为重要的主题。
这里有几个重要的因素需要考量。
分辨率“导入(Imported)”指的是源像素的分辨率。“显示(Displayed)”指的是资产编辑器中显示的内容,你可以在上方的工具栏里更改。“游戏内最大(Max In-Game)”指的是用于渲染的最大分辨率。你可以更改“LOD偏移(LOD Bias)”属性来查看效果。如果源像素导入的是8k,但你知道自己的项目只需要2k,那就可以使用Mip偏移来让游戏内最大分辨率为2k。源文件大小(Resource Size)- 这是将会被载入GPU内存的大小。方法(Method)- 纹理是否会流送。格式(Format)- 压缩格式。Mip - 已生成的mip等级数量。sRGB - 像素是用sRGB还是用线性色彩空间表示。
虽然Oodle非常棒,但我们暂时不考虑使用Oodle选项卡,因为它更适用于打包项目,例如要运行执行文件的游戏项目。压缩一栏下的“高级(Advanced)”下拉菜单也是如此。如需了解更多关于Oodle的内容,请点击下方链接来查看Stephen Phillips的文章。
https://dev.epicgames.com/community/learning/tutorials/ry2D/reducing-package-sizes-with-oodle-compression

sRGB
这一点我就简单说明一下。引擎会尽力为你做好设置,虽然很多时候它能设置正确,但我还是看到过很多色彩空间设置错误的纹理。我一般拿到关卡后做的第一件事就是开始检查数据贴图。很多数据贴图都会被设置为sRGB。有一套很简单的经验办法:如果你希望颜色在渲染中显示,比如反射率或放射贴图,那么可能就应该设置为sRGB;如果要使用像素值来驱动粗糙度或遮罩贴图等材质属性的数据贴图,则应禁用线性处理贴图;如果你发现关卡的光照有点不对劲,好像没有得到你期望的着色器响应,那么就应该开始检查这个问题。我不会再详细解释什么是色彩空间,因为这对任何传统视效美术师来说都不是什么新东西了,我这里只想简单说明一下,大家应该了解虚幻引擎中的这一设置。
MIP
什么是Mip?基本上在导入虚幻引擎时,系统会以逐渐降低的分辨率来创建纹理实例。每个连续的mip都是前一个mip沿各个轴减半(四舍五入)。比如2048x1024、1024x512、512x256、256x128......以此类推。我们需要mip,不然在较远距离进行渲染时纹理就会闪烁。引擎会努力将渲染像素与纹素的比例保持在1:1。
2的幂。这也是我们一直在强调的,你的纹理分辨率必须是2的幂,虚幻引擎才会生成mip。X和Y不用一样,只需要是2的幂,比如2048x2048和2048x1024都可以。
在5.1版本中,这就不是硬性要求了,但是你最好还是用2的幂。因为虽然虚幻引擎会为你生成mip,但如果不是2的幂,就会浪费大量的内存,因为它会在内存中自动填充到最接近的2的幂。也就是说513x513和1024x1024占用的内存其实是一样的,那你还不如直接削减像素,将其保持在512x512,或者直接用1024x1024来创建纹理,从而获得更高的质量。说实话,谁还不会用2的幂来制作纹理呢?毕竟大家都是技术高超的数字绘景师了!
因此,尽管现在引擎会为你制作mip,但你最好还是用2的幂次来制作纹理。可别像我之前提到的那样彻底放弃思考了。
压缩
关于虚幻引擎中可用的不同压缩类型,相关的资料已经有很多了,所以我这里就不浪费时间一一赘述了。有大量的文档和视频可供你参考。
https://docs.unrealengine.com/5.0/zh-CN/texture-format-support-and-settings-in-unreal-engine/
https://www.youtube.com/watch?v=h95X255NhOo
我想说明的是你对于压缩所做的决定会造成的影响,因为GPU纹理格式会对VRAM的使用产生重大影响。BC1可能是项目中最常用的压缩格式。它是一种非常全面的格式,比未压缩的BGRA8格式(基本就是未压缩的纹理)小8倍。如果你想要更高的质量,可以选择BC3,但也要做出相应的取舍,因为它只比未压缩的格式小4倍。虚幻引擎会尽力为你选择最合适的压缩格式,它通常能够很好地判断出纹理是颜色贴图还是法线贴图。大多数情况下我们会希望法线贴图和粗糙度贴图更加清晰,因此会选择不同的压缩类型,例如法线贴图使用BC5,粗糙度贴图使用BC3或RGB8/灰度图。但如果我们将灰度图压缩设置与默认的BC1压缩进行比较,就会发现资源大小会增加一倍,所以你需要考虑资源保真度相关的取舍,这么做到底值不值得。无论如何,能够慎重考量这一点都是非常棒的习惯,因为它可能为你的项目节省大量的VRAM资源。



还需要注意的是,如果纹理的分辨率不是4的倍数,那就不会被压缩,就是这么简单。几乎所有(几乎就是全部)游戏引擎的压缩格式(DXT、ETC、ASTC等)都是基于4x4模块的概念,或者是6x6或8x8。如果输入的图像尺寸是4的倍数,比如304x304,那就会采用DXT压缩格式,而305x305则会采用B8G8R8A8(未压缩)格式。因此,如果你需要一些Po2以外的纹理,建议长宽都限制为4的倍数(为了安全起见,也可以是8或12)。我知道这些数字在视效/动画行业可能并不常见,但还请牢记在心,尤其是对于数字绘景师而言。
此外,可能有些人还不知道:Alpha通道并不会被压缩。在使用打包贴图时,这个问题可能会引起一些麻烦。打包贴图的效率非常高,一定要好好利用。有人以为把贴图放到Alpha通道里是超级高效的做法,但其实如果你出于质量考量而不想压缩的贴图,完全可以将其放入打包贴图的Alpha通道中,因为无论你在虚幻引擎里选择哪种压缩设置,它都不会被压缩。这里我也只是提示一下。你应该先做好测试,到底是将信息放到Alpha通道里,还是做新的贴图然后放到颜色通道里,看看哪种更合适。



还有一点需要考虑的是,大多数视效/动画工作室都将exr作为默认文件格式。exr非常棒,它本身并不是问题,而且虚幻引擎导入exr的速度和效率与导入其它文件格式的一样高,所以你也不用担心这个问题。潜在的问题是虚幻引擎可能会认为exr是浮点高动态范围。举个例子,让我们来看一个最近导入的exr文件,它是一个粗糙度的贴图,并且大小达到了174MB。如果你的显卡只有8GB的显存,174MB的纹理可是一笔不小的开支。即便我们把压缩方式切换为HDRCompressed,也只会降低到21MB。



接下来的话你应该会听我说很多遍……你要在画面帧中渲染的所有纹理都必须适合你的显卡。如果不能,那么根据流送与否,虚幻引擎要么会直接让你的渲染崩溃,要么会以比你预期更低的分辨率来流送纹理。场景的总资源大小非常重要。如果想全面了解场景占用的纹理资源,可以在“工具(Tools)”-> “审计(Audit)”下的统计窗口中查看,或者使用“stat streaming”命令来列出引擎中流送的大小。与其在这里赘述,我不如直接给一个视频链接,里面会解释得更加清楚。这段视频主要就是讲解流送,也是我们的下一个主题。
https://www.youtube.com/watch?v=uk3W8Zhahdg



流送
什么是纹理流送?其实就是你可以告诉虚幻引擎:在渲染时从DDC将适当的纹理mip分辨率加载到VRAM中,或者将所有mip加载到GPU上进行渲染。因此,假如你的纹理Uasset有13个mip等级,从4096x4096的mip0到1x1的mip12,虚幻引擎都会尝试将它们载入VRAM,而且它会认为画面帧中需要所有这些纹理。当你试过在GPU上加载数百个纹理以后,就会明白为什么要做压缩了。不过这也引出了一个问题,为什么不直接别用流送了呢?毕竟要加载画面帧中所有纹理的mip可吃不消啊!
比方说,你正在场景中四处走动,而渲染器发现有新的纹理出现在你的视野中,或者需要比已有纹理更高的mip等级,又或者需要一个全新的纹理,那么如果启用了纹理流送,它可能就需要从DDC获取纹理,而如果禁用了流送,那么纹理就已经在内存中了,所以速度会更快。
又或者,你的镜头在场景中四处移动,虚幻引擎注意到你正在放大某个特定的纹理,而它目前只将纹理拉伸了5%左右。就像我之前提到的,虚幻引擎会努力使纹理与渲染之间像素和纹素的比例达到1:1,但也允许有一定的回旋余地,所以使用当前的mip还是没问题的。不过在某些时候,引擎会开始排队请求细节更多的mip,幸运的话在几帧之后就可以使用了。只要你的移动还算流畅,引擎就能很好地预测接下来3帧的需求,但有时你还是能很清楚地看到画面有问题。比如你快速跳转到城市的另一端可能就会发生问题。在这种情况下,如果所有mip都已经在内存中,你可能就不会明显看到这种变化,而从磁盘中调取这些mip则可能要多花一两帧的时间。如果内存空闲,那为什么不用呢?这就是不对纹理进行流送处理的好处。
你可以将特定纹理设置为永不流送,也可以将整个项目设置为永不流送纹理。实际上在默认设置中,MRQ会将渲染设置为永不流送纹理。你可以在MRQ的“游戏覆写(Game Overrides)”部分找到相关设置。很多人都不知道,即使这一部分在UI中不可见,它也能控制你的渲染。这也是我遇到的大多数MRQ渲染崩溃的罪魁祸首。很多人会从Substance中导出项目的所有8k或4k纹理,然后试图将它们全部加载到VRAM中,然后GPU就不堪重负崩溃了。



那接下来自然地引出了一个问题,为什么虚幻引擎不出手相救,而是眼睁睁看着你的GPU不堪重负呢?好消息是它其实可以!但是需要启用流送才行。这就又引出了下一个主题:内存池。
内存池
虚幻引擎中有各种各样的内存池,你引擎用得越多,接触到的内存池也就越多。不过我们这里只讨论纹理的流送内存池。到这一步你可能已经看到了“纹理流送池超出预算X”的红色警告了。


所谓“纹理池”(Texture pool)基本上是内存中预定的空间,用于放置加载到GPU上的纹理。如前所述,你可以在控制台中使用stat streaming命令来查看不同内存池的大小和当前场景的预算。你也可以使用cvar r.Streaming.Poolsize来设置内存池的大小,不过内存池的大小也受引擎扩展性的限制,你可以在DefaultScalability.ini中进行相关设置。如果你和参与项目的所有成员都拥有非常高端的显卡,那可以考虑将这些数字调大。



如前所述,内存池可以阻止虚幻引擎占用内存到崩溃,但只有启用流送时才会生效。如果整个项目都禁用了纹理流送,系统就会忽略内存池大小,不惜一切代价去尝试加载它认为需要的所有纹理和所有MIP。
启用纹理流送之后,如果达到了内存池的大小限制,但虚幻引擎认为还需要加载新的纹理,它就会开始四处寻找最近并未使用过的纹理,并开始把它们从内存中赶出去。如果它已经去掉了当前画面帧中不需要的所有纹理,但纹理池中仍然没有足够的空间,那么它就会开始使用更小的mip,直到最终可以加载和渲染纹理。
虚幻引擎并不会在你转过镜头,看不到某些纹理的那一瞬间就立刻将纹理从VRAM中移除。但一旦你看不到这些纹理了,它们就肯定就被划到了“危险区域”,下次需要空间时它们就会被移除。基本上已经加载的纹理是被新的纹理加载“挤出去”的,并不是每一帧都会被直接清空。
从这一部分我们可以看出来,如果你出现了内存崩溃的问题,那么你可能需要开启纹理流送。如果你在渲染中看到的MIP分辨率低于预期,那就应该查看日志或使用查看数据的命令,看看是否出现了过度分配的问题。如果是这种情况,而你又有足够的空间,就应该考虑增加内存池的大小了。实际上对于视效/动画工作室来说,应该根据制作内容和基础架构来管理内存池的大小。虚幻引擎的默认设置可能太小,并不能满足你的需求。我通常会建议工作室始终使用纹理流送。如果你发现有一两个纹理引擎中加载的速度不够快,那么你可以把这些纹理单独设置为永不流送,而不是整个项目。



无论如何,当你尝试在虚幻引擎中渲染项目时,都需要注意内存池,因为内存池可能就是导致崩溃的原因,也可能导致你的纹理表现不佳。
流送虚拟纹理
与传统mip不同的是,虚拟纹理系统只会流送虚幻引擎需要的部分纹理,并使其可见。具体做法是将所有mip等级分割成固定大小的小块。我记得是128x128的图块。GPU会决定屏幕上所有可见的像素需要哪些可见的图块。这也就意味着,当虚幻引擎认为某个对象可见时,就会传达给GPU,由GPU将所需的纹理加载到GPU的内存缓存中。无论纹理的大小如何,SVT的固定图块大小只会考虑可见的纹理。因此它会更加精细,而且只会请求对最终图像产生影响的纹素。也就是说,在传统的mip中,只要对象的一小部分可见,那么整个对象都会被视为可见,然后虚幻引擎会确保加载该对象的全部纹理,而SVT有点像是给纹理用的Nanite,如果一个对象的90%都隐藏在一棵大树的后面,那它就只会加载该对象可见的10%所需的纹理图块。
这样做的另一个好处就是,你可以不局限于为一个对象选择单一的mip等级。比如说你有一个纹理,然后几乎都是用边缘视角去看的,比如地上的路面纹理或远离镜头的墙面纹理。而在高倍率下(靠近你的部分)看到的纹理就要完整的分辨率,而当你不断接近纹理消失点的视角位置时,需要的纹理mip就会越来越小。VT可以为靠近你的部分请求高分辨率的图块,而为远处的部分请求低分辨率的图块。这样你就可以节省大量的内存!
那为什么引擎没有把它作为默认设置呢?也许不久的将来就会实现吧。不过SVT也有几个潜在的隐患需要解决。首先,使用SVT会产生性能成本。当我们使用虚拟纹理时,会增加GPU和CPU处理图块流送请求的时间。虽然程度不大,但也不是一点没有。而且每渲染一个VT,这个时间都会增加。此外在DDC和编辑器之间,它们还有自己的内存池和VT专用缓存,这两者可能都需要去管理,因此VT其实是要“收税”的。尽管如此,我并不认为有什么理由要完全不用VT。事实上,如果你想在项目中使用udim,就需使用VT。还要注意的是,VT是有自己的内存池的,可能需要你去调整。你可以在这里找到更多相关信息。
https://docs.unrealengine.com/5.0/zh-CN/virtual-texture-memory-pools-in-unreal-engine/
之前SVT在影片渲染队列是可能出现问题的。有时候如果虚幻引擎计算出所需的分辨率以后,却发现内存池已满的情况下,它就又会在内存池中创建必要的空间,而你就会看到镜头里渲染帧中所有的小图块都在更新。5.1版本引入了“Cinematic Prestreaming(影片预流送)”插件,从而解决了这一问题。这样能够帮助虚幻引擎在开始渲染MRQ中的镜头之前,先计算出所需的图块,这样就不会出现之前那种“临时抱佛脚”的情况。
分享几个很好用的cvar:
r.VT.Residency.Show 1,可以在屏幕上显示VT内存池大小的叠加图。r.VT.DumpPoolUsage,可以列出因Mip偏移或其它问题而导致在内存池中占用空间超出预期的纹理
虚幻引擎中纹理的生命周期
好了,我们已经知道了上面的那些知识,现在我想稍微概括一下虚幻引擎中纹理的生命周期,这样我们就可以把上面学到的知识结合起来了。而当我们了解到纹理的使用方法之后,这些知识的重要性也会变得越发明显。
纹理先是导入到虚幻引擎,然后创建了uasset。接着创建Mip,再根据上面的方法进行压缩。相关信息会连同源像素(也就是uasset)一起存储在DDC。当编辑器加载纹理uasset时,会立即从DDC请求部分编码数据,而这些是非流送的mip,流送的mip会保留在DDC。然后纹理才会在视口中变为可见,并且只要有任何内容引用这个纹理uasset就会如此。流送mip也会根据在镜头中的可见性而被调入内存。而在进入GPU以后,经过编码的mip会驻留在内存中,并在渲染纹理采样过程中按需进行解码,解码精度也会非常细。不过这也意味着系统可以很智能地只解码所需的像素。由于压缩格式是有损压缩,因此解码后的像素仍然需要进行压缩,不过这样就无法获得原始的输入值,只能获得近似值。纹理被使用之后,通常会保留在内存中。而且如果不使用纹理流送,它们就永远不会被移除。如果你使用纹理流送,那这些纹理会存在一段时间,这段时间后没有被使用过才会被移除,以便为需要的新纹理腾出空间。最后渲染结束,或者纹理流送已经被移除,那纹理就会留在DDC中,等待新一天的战斗。
至此你应该已经明白了:流送、压缩、mip和内存池对于从引擎中获得高效、稳定和高质量的渲染效果而言有多么重要。如果你不去好好处理纹理,那么你就不得不付出更多努力来追求稳定性和效率。
https://docs.unrealengine.com/5.0/zh-CN/texture-streaming-configuration-in-unreal-engine/
特别鸣谢:Devin Doucette、Fabian Giesen、Matt Hoffman、Jonathan Litt以及Dan Thompson
全文完

我把已卸载的UE5,又装上了!


细思极恐!这仿生机械做的太炸了


脑洞这么大,是被怪物咬过么?!

评分

参与人数 3元素币 +11 活跃度 +15 展开 理由
拷给你 + 2 + 5 我和我的小伙伴们都惊呆了
雪无梦白... + 2 + 5 感谢
zhaodezhu + 7 + 5 楼主有心了

查看全部评分

内容主要涵盖影视特效,CG动国,前沿CG技术,作品欣賞
使用道具 <
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表