作为参考,我指的是idTech 5的MegaTexture技术首次(我相信)引入的技术的“通用名称” 。请观看此处的视频,以快速了解其工作原理。
最近,我一直在浏览与之相关的一些论文和出版物,而我不明白的是它可能如何有效。它是否需要不断将“全局纹理页面”空间中的UV坐标重新计算为虚拟纹理坐标?那怎么不抑制大多数对几何图形进行批处理的尝试?如何允许任意放大?在某个时候它是否不需要细分多边形?
有太多我不了解的东西,而且我无法找到关于该主题的任何实际上容易接近的资源。
作为参考,我指的是idTech 5的MegaTexture技术首次(我相信)引入的技术的“通用名称” 。请观看此处的视频,以快速了解其工作原理。
最近,我一直在浏览与之相关的一些论文和出版物,而我不明白的是它可能如何有效。它是否需要不断将“全局纹理页面”空间中的UV坐标重新计算为虚拟纹理坐标?那怎么不抑制大多数对几何图形进行批处理的尝试?如何允许任意放大?在某个时候它是否不需要细分多边形?
有太多我不了解的东西,而且我无法找到关于该主题的任何实际上容易接近的资源。
Answers:
有时称为虚拟纹理(VT)或稀疏虚拟纹理的主要原因是内存优化。要点是仅将渲染帧可能需要的实际纹理像素(一般表示为页面/图块)移入视频内存。因此,它可以使脱机或慢速存储(HDD,光盘,云)中的纹理数据比视频存储器甚至主存储器中容纳的纹理数据多得多。如果您了解现代操作系统使用的虚拟内存的概念,则其本质上是同一件事(名称并非偶然给出)。
VT不需要重新计算UV,因为您需要在渲染网格之前在每一帧之前进行重新计算,然后重新提交顶点数据,但是VT确实需要在Vertex和Fragment着色器中进行大量工作才能从传入的UV执行间接查找。但是,在良好的实现中,如果应用程序使用的是虚拟纹理或传统纹理,则它应该对应用程序完全透明。实际上,大多数情况下,应用程序会将虚拟和传统两种纹理混合在一起。
理论上,批处理可以很好地工作,尽管我从未研究过它的细节。由于对几何体进行分组的通常标准是纹理,并且使用VT,场景中的每个多边形都可以共享相同的“无限大”纹理,从理论上讲,您可以通过1次绘制调用来实现完整的场景绘制。但实际上,其他因素也起作用,因此不切实际。
在VT设置中,放大/缩小和相机突然移动是最难处理的事情。对于静态场景,它看起来非常吸引人,但是一旦事物开始移动,将请求比用于外部存储流更多的纹理页面/平铺。异步文件IO和线程可以提供帮助,但是如果它是实时系统(例如在游戏中),则只需渲染具有较低分辨率图块的几帧,直到不时有高分辨率的图块出现。 ,导致纹理模糊。这里没有灵丹妙药,这是该技术最大的问题,即IMO。
虚拟纹理也不能轻松地处理透明性,因此透明多边形需要为其使用单独的传统渲染路径。
总而言之,VT很有趣,但是我不建议所有人使用它。它可以很好地工作,但是很难实现和优化,此外,我的品味还需要太多特殊情况和针对特定情况的调整。但是对于大型开放世界游戏或数据可视化应用程序来说,这可能是将所有内容放入可用硬件的唯一可行方法。通过大量工作,即使在有限的硬件上也可以使其高效运行,就像我们在id的Rage的PS3和XBOX360版本中看到的那样。
在某种程度上,我设法使VT在带有OpenGL-ES的iOS上运行。我的实现不是“可运输的”,但是可以想象,如果我想要并拥有资源,可以这样做。您可以在此处查看源代码,这可能有助于更好地了解各个部分如何组合在一起。这是在iOS Sim上运行的演示的视频。它看起来很滞后,因为模拟器在模拟着色器方面很糟糕,但是它可以在设备上流畅运行。
下图概述了实现中系统的主要组件。它与Sean的SVT演示(在下面链接)有很大的不同,但其体系结构与第一本GPU Pro书中的“ 使用CUDA加速虚拟纹理化 ”一文(在下面的链接)中介绍的结构更加接近。
Page Files
是虚拟纹理,作为预处理步骤已经切成小块(AKA页面),因此可以随时将它们从磁盘移动到视频内存中。页面文件还包含整个mipmap集合,也称为虚拟mipmaps。
Page Cache Manager
保留Page Table
和Page Indirection
纹理的应用程序端表示。由于将页面从脱机存储移动到内存非常昂贵,因此我们需要一个缓存来避免重新加载现有资源。此缓存是非常简单的最近最少使用(LRU)缓存。高速缓存也是负责使用其自己的数据本地表示来使物理纹理保持最新的组件。
该Page Provider
是一个异步作业队列,将获取所需的场景给定的视图页面,并将它们发送到缓存。
该Page Indirection
纹理是虚拟纹理中每页/图块具有一个像素的纹理,该纹理会将传入的UV映射到Page Table
具有实际纹理像素数据的缓存纹理。该纹理可能会变得很大,因此必须使用一些紧凑的格式,例如RGBA 8:8:8:8或RGB 5:6:5。
但是我们仍然缺少一个关键要素,那就是如何确定哪些页面必须从存储加载到缓存中,然后再加载到中Page Table
。这就是“ 反馈通过”和“ Page Resolver
输入”的地方。
Feedback Pass是视图的预渲染,带有自定义着色器,且分辨率低得多,它将所需页面的ID写入彩色帧缓冲区。上面的多维数据集和球体的彩色拼凑是编码为RGBA颜色的实际页面索引。然后,此预传递呈现被读入主存储器并由Page Resolver
进行处理,以解码页面索引并使用触发新请求Page Provider
。
反馈预通过之后,可以使用VT查找着色器正常渲染场景。但是请注意,我们不等待新页面请求完成,这将是可怕的,因为我们只是阻塞了同步文件IO。这些请求是异步的,并且在呈现最终视图时可能尚未就绪。如果它们准备好了,那么很好,但是如果没有,我们总是将低分辨率mipmap的锁定页面保留在缓存中作为后备,因此我们可以使用一些纹理数据,但是它将变得模糊。
GPU Pro系列的第一本书。它有两篇关于VT的非常好的文章。
MrElusive的强制性文件:Software Virtual Textures。
Martin Mittring撰写的Crytek论文:高级虚拟纹理主题。
还有肖恩在youtube上的演示文稿,我发现您已经找到了。
VT在计算机图形学上仍然是一个热门话题,因此有大量可用的好材料,您应该能够找到更多。如果还有其他需要补充的内容,请随时提出。我对这个话题有些生疏,在过去的一年里没有读到太多关于它的内容,但是它对于回忆一下这些东西总是很有益的:)
虚拟纹理化是纹理地图集的逻辑极限。
纹理图集是一个巨大的纹理,其中包含内部各个网格的纹理:
由于纹理变化会导致GPU上的整个管线冲洗,因此纹理图册变得很流行。创建网格时,将压缩/移动UV,以便它们代表整个纹理图集的正确“部分”。
正如@ nathan-reed在评论中提到的那样,纹理地图集的主要缺点之一是丢失了诸如重复,钳位,边框等环绕模式。此外,如果纹理周围没有足够的边框,您可能会意外进行过滤时从相邻纹理采样。这可能导致出血伪影。
纹理图集确实有一个主要限制:大小。图形API对纹理的大小有一个软限制。也就是说,图形内存只有这么大。因此,纹理大小也受到硬性限制,具体取决于v-ram的大小。虚拟纹理通过从虚拟内存中借用概念来解决此问题。
虚拟纹理利用以下事实:在大多数场景中,您只能看到所有纹理中的一小部分。因此,仅该纹理子集需要处于vram中。其余的可以在主RAM或磁盘中。
有几种实现方法,但是我将在Sean Barrett在GDC演讲中解释该实现。(我强烈建议您观看)
我们有三个主要元素:虚拟纹理,物理纹理和查找表。
虚拟纹理代表了理论上的大型地图集,如果我们有足够的vram来适合所有内容。它实际上并不存在于任何地方的内存中。物理纹理表示我们在vram中实际拥有的像素数据。查找表是两者之间的映射。为了方便起见,我们将所有三个元素都分成相等大小的图块或页面。
查找表存储了物理纹理中图块左上角的位置。因此,给定整个虚拟纹理的UV,我们如何获得物理纹理的相应UV?
首先,我们需要在物理纹理中找到页面的位置。然后,我们需要计算页面中UV的位置。最后,我们可以将这两个偏移量相加,以获取UV在物理纹理内的位置
float2 pageLocInPhysicalTex = ...
float2 inPageLocation = ...
float2 physicalTexUV = pageLocationInPhysicalTex + inPageLocation;
计算pageLocInPhysicalTex
如果我们使查找表的大小与虚拟纹理中的图块数量相同,则可以使用最近的邻居采样对查找表进行采样,然后获得页面在物理纹理内左上角的位置。
float2 pageLocInPhysicalTex = lookupTable.Sample(virtTexUV, nearestNeighborSampler);
计算inPageLocation
inPageLocation是一个UV坐标,相对于页面的左上角,而不是整个纹理的左上角。
一种计算方法是减去页面左上角的UV,然后缩放为页面大小。但是,这是相当多的数学运算。相反,我们可以利用IEEE浮点表示形式。IEEE浮点以一系列以2为基的小数存储数字的小数部分。
在此示例中,数字为:
number = 0 + (1/2) + (1/8) + (1/16) = 0.6875
现在,让我们看一下虚拟纹理的简化版本:
1/2位告诉我们是位于纹理的左半部分还是右侧。1/4位告诉我们我们所处的一半的四分之一。在此示例中,由于纹理被分成16或4边,所以前两位告诉我们所处的页面。位告诉我们页面内的位置。
我们可以通过使用exp2()移动float并使用fract()去除它们来获得剩余的位
float2 inPageLocation = virtTexUV * exp2(sqrt(numTiles));
inPageLocation = fract(inPageLocation);
其中numTiles是一个int2,给出了纹理每侧的平铺数量。在我们的示例中,这将是(4,4)
因此,我们为绿点计算inPageLocation,(x,y)=(0.6875,0.375)
inPageLocation = float2(0.6875, 0.375) * exp2(sqrt(int2(4, 4));
= float2(0.6875, 0.375) * int2(2, 2);
= float2(1.375, 0.75);
inPageLocation = fract(float2(1.375, 0.75));
= float2(0.375, 0.75);
我们要做的最后一件事。当前,inPageLocation是虚拟纹理“空间”中的UV坐标。但是,我们需要物理纹理“空间”中的UV坐标。为此,我们只需要按虚拟纹理大小与物理纹理大小之比来缩放inPageLocation
inPageLocation *= physicalTextureSize / virtualTextureSize;
这样完成的功能是:
float2 CalculatePhysicalTexUV(float2 virtTexUV, Texture2D<float2> lookupTable, uint2 physicalTexSize, uint2 virtualTexSize, uint2 numTiles) {
float2 pageLocInPhysicalTex = lookupTable.Sample(virtTexUV, nearestNeighborSampler);
float2 inPageLocation = virtTexUV * exp2(sqrt(numTiles));
inPageLocation = fract(inPageLocation);
inPageLocation *= physicalTexSize / virtualTexSize;
return pageLocInPhysicalTex + inPageLocation;
}