研发实战:为VR游戏添加 LiquidVR MultiView Rendering

文章相关引用及参考:gpuopen

本文来自Croteam的引擎程序员Karlo Jez,他目前正在负责《Serious Sam VR: The Last Hope》。

映维网 2017年11月24日AMD的MultiView Rendering功能可以减少VR应用程序中的复制对象绘制调用的次数,从而允许GPU在一次绘制调用中将对象广播至左眼和右眼(原来需要渲染两次)。这有助于减少CPU负载,从而减少丢帧数和优化渲染延迟。

当我们从单通道渲染入手的时候,我们首先使用了多个GPU,因为它更容易实现,并且对性能影响最大。遗憾的是,尽管对VR渲染中的作用十分巨大,但我们大多数用户都不具备多个GPU。因此,我们计划在单个GPU上支持MultiView Rendering。MultiView Rendering不会真正影响GPU负载,但它可以大大减少CPU的渲染负载。

对我们来说重要的是,支持同一套着色器,并确保所有支持的VR渲染方法尽可能有效,从而最大限度减少只出现在其中一种渲染方法上的bug数量。

借助LiquidVR Affinity Multi-GPU,两个眼睛的图像都会渲染至相同渲染目标的相同视口,但其中一个位于主GPU上,而另一个则位于另一个GPU上。在渲染后,图像将复制到主GPU上的不同渲染目标,并传递至VR系统进行显示。在单GPU渲染的情况下,2D渲染目标的同一视口中无法同时存在两个图像,但我们可以使用渲染目标数组或包含两个并排视图的渲染目标。我们决定使用并行渲染,因为我们希望避免编译一组单独的着色器(我们在MultiView Rendering不是Texture2D,而是Texture2DArray),同时因为我们希望通过MultiView来实验MultiRes Rendering的未来实施。

对于在单个Radeon GPU上的MultiView Rendering,我们使用了AMD GPU Services (AGS) Library。除了几个初始化函数之外,我们只需要库中的一个函数:agsDriverExtensionsDX11_SetViewBroadcastMasks()。要在同一个渲染目标上使用两个视口启用MultiView Rendering,我们调用:

agsDriverExtensionsDX11_SetViewBroadcastMasks(context, 0x3, 0x1, 0);

第二个和第三个参数最为重要,因为它们将选择视口和渲染目标切片蒙版。

在渲染时,我们不设置单个视口,我们同时设置两个视口:

D3D11_VIEWPORT ad3dViewports[] = {
{fLeftX, fY, fHalfWidth, fHeight, fNear, fFar},
{fRightX, fY, fHalfWidth, fHeight, fNear, fFar}
};

pd3d11context->RSSetViewports(2, aViewports);

对基于Affinity Multi-GPU的单通道渲染而言,我们只使用一个对每个GPU都有所不同的常量缓冲区;所有其他数据都相同,即两个GPU都具有用于双眼的渲染数据,但只有一个被选择用于渲染。这使得在单个GPU上添加对MultiView Rendering的支持变得更加容易。因为所有数据都位于同一个GPU上,所以在同一个常量缓冲区中不能有不同的值。

要通过AGS获取每只眼睛的正确矩阵,所需的唯一更改是使用AmdDxExtShaderIntrinsics_GetViewportIndex()。我们不选择GPU索引,而是把命令缓冲区传递给Affinity Multi-GPU模式下的两个GPU。

uint GetVREye()
{
// c_multiview is a constant passed to all shaders
return (c_multiview>0.0f) ?
AmdDxExtShaderIntrinsics_GetViewportIndex() :
c_eye;
}

在启用MultiView时,本函数返回视口索引;在禁用MultiView时,本函数返回眼睛常量。在多通道渲染中为每一个渲染通道设置眼睛常量,或者当在Affinity Multi-GPU模式下渲染单通道时将眼睛常量上传至每一个GPU。不考虑所使用的渲染方法,本索引可以用于获取所有着色器上的每一眼睛的数据。

在MultiView模式下,我们使用的渲染目标是其平常宽度的两倍。从外部代码(即更高级别)的角度来看,渲染目标为原始尺寸(一半),例如,256×256渲染目标在内部将被创建为512×256,但在从外部代码查询纹理尺寸时,你会得到256×256。这样做的目的是为了尽量减少MultiView所需感知的代码量。只有着色器和底层渲染函数需要知道实际的纹理大小或者我们正在使用MultiView Rendering这一事实。

当绘制到渲染目标时,所有一切都像是在两个通道中渲染场景,每次都使用不同的视口。如果要对MultiView纹理进行采样,在渲染左眼视图时我们需要对纹理的左半部份进行采样;在渲染右眼视图时我们需要对右半部份进行采样。为了做到这一点,我们用一个MAD来调整UV坐标。所以它们不是从(0.0, 0.0)到(1.0, 1.0),而是左眼从(0.0, 0.0)到(0.5, 1.0);右眼从(0.5,0.0)到(1.0,1.0)。 我们如何为每只眼睛提供不同的值呢? 答案是AmdDxExtShaderIntrinsics_GetViewportIndex()。

float4 GetMultiviewMAD() {
uint eyeIndex = AmdDxExtShaderIntrinsics_GetViewportIndex();
return float4(0.5f, 1.0f, 0.5f * eyeIndex, 0.0f);
}

为了能够在MultiView和非MultiView渲染中使用相同的着色器代码,我们在对所有需要它的着色器采样之前应用了MAD,但我们为函数添加了一个条件,所以当MultiView被禁用时将会返回(1.0, 1.0, 0.0, 0.0)。

对于添加MultiView支持,最具挑战性的是通过所有的着色器,并且将MAD应用到正确的位置。这些主要是后期处理着色器,因为它们经常对先前渲染的纹理进行采样。在我们错过其中一些的情况下,视觉伪影往往很难不被注意到。由那些着色器渲染的图像的大小为双倍,而且它们的纵横比不正确(因为你将会采样整个图像,而不仅仅只是一半):

这是一个正确渲染的场景(只有左眼视图):

这里是相同的场景,但其中一个后期处理着色器(FXAA)缺少MultiView MAD:

我们可以注意到,图像是如何被挤压,而且左眼可以看到右眼视图的一部分。我们可以在采样纹理之前调整UV坐标来解决这个问题。

在修复了所有渲染错误后,我们可以在游戏中看看SteamVR的帧时序。

禁用MultiView Rendering:

启用MultiView Rendering:

你可以看到,两种情况下(大约6.5毫秒)的GPU帧时间几乎相同,但CPU从9秒降低至7毫秒,而这与我们使用LiquidVR Affinity Multi-GPU获得的结果相似。在这种情况下,性能方面的增益不到两倍,因为我们的VR渲染器已经进行了相当大的优化,但主渲染通道只能通过AGS每帧执行一次,所以我们不是用了2毫秒时间来处理渲染命令,而是4毫秒。

结论

将MultiView渲染支持添加至已经支持LiquidVR Affinity Multi-GPU的引擎并不难,因为我们提前制定了计划。在开始添加MultiView支持之前,我们已经将所有着色器设置为单通道渲染。反过来说,首先增加对MultiView的支持存在一个更大的初始障碍,因为我们需要同时处理单通道和MultiView错误。但一旦完成,我们可以非常轻松添加LiquidVR Affinity Multi-GPU支持。

补充

为清除或复制视口提供MultiView感知的底层函数非常重要。如果视口的一部分被清除,右眼也必须这样做。视口复制需要考虑是否从MultiView纹理/复制到MultiView纹理,并相应地进行操作。

另一件需要考虑的事情是,用SV_Position语义读取像素着色器输入。我们需要小心,因为它给出了渲染目标相对于渲染目标(不是相对于视口)的像素中心坐标,所以右眼视图的值将成为纹理右半部分的坐标。这不是专属于MultiView Rendering,但我们对返回值范围的错误假设导致了一些渲染错误。

原文链接https://yivian.com/news/38574.html
转载须知:转载摘编需注明来源映维网并保留本文链接
微信/QQ群加入映维网微信/QQ群
颜昳华

颜昳华

嗨!映维网是中国最大的VR行业信息数据库,有任何问题请不要犹豫在映维网上搜索,或与我交流(邮箱:po@yivian.com,微信:yivian001)。

Yivian 猜您还喜欢...

发表评论

资讯