|
14.4.3.5 特殊技术
Advanced Screenspace Antialiasing描述了抗锯齿的各类技术,包含形态抗锯齿、基于屏幕空间的抗锯齿及实现细节。该文总结了当时的主流抗锯齿技术:
文中关于删除瑕疵的描述如下:
- 已经丢失的信息无法被恢复。例如下采样瑕疵无法被删除。
- 半透明瑕疵比较棘手。深度剥离、模板路径的K-Buffer可能可以解决此问题,避免透明几何图形(如窗口和框架)的交叉,在透明发生的地方添加不透明边缘。
- Alpha Test几何体和几何边缘可以被处理。
形态抗锯齿(Morphological Antialiasing)使用颜色不连续检测器,可能会错过重要的边(其中颜色差异很小),可能过于敏感(检测纹理细节),会检测透明,不需要额外的缓冲区(如法线、纹理坐标和深度)。常用的边缘检测卷积核如下:
检测各种类型的线段,包含S形和由2个L形组成的U形:
所需的实际长度和像素位置覆盖率可以使用截距定理(intercept thorem)计算得到:
对于水平计数,2个额外的缓冲区用于边缘检测(仅显示一个):
4个额外缓冲区的计数技术(上、下、左、右):
总是拒绝所有不在垂直不连续缓冲区中的像素:
形态抗锯齿总是以明确的方式处理U形,Biri.v的实现占用了大量的内存:2个ARGB计数缓冲区、1个argb混合缓冲、1个RG间断缓冲器、1512×512像素的浮点覆盖率计算查找表。通过巧妙的封装和缓冲区共享,可以显著减少总体内存消耗(同时增加计算成本)。该算法在像素着色器中使用了大量的条件分支,使用了大量的通道,没有使用Early-Stencil。因此,它不适合当时的主机。
文中提出了改进版本,称作Advanced Screenspace Antialiasing(ASAA)。ASAA实现没有使用颜色来进行不连续检测,因为深度会更精确,深度显示并不是所有的边缘和细粒度细节丢失。添加法线和纹理坐标作为额外的边缘检测提示,这两个额外的缓冲区可以安全地与场景颜色共享,因为它们正在被使用。使用拉普拉斯卷积核进行不连续检测→检测边缘两侧的不连续:
通过改进边缘检测、不连续检测、计数、覆盖率计算等过程,ASAA获得了以下优点:
- 内存占用率适中。1个RGB缓冲区用于技术和不连续(比2xMSAA低),2个G16R16用于法线和纹理坐标,可共享,建议压缩这些值,因为边缘检测着色器是纹理绑定的。
- 没有条件分支。
- 适合当时的主机。
- 其它特点:对U形的模糊处理,在Xbox360和PS3上,计数可以转移到CPU上。
ASAA的实现步骤和流程如下:
其中混合可以优化,XBox360上可以使用线性纹理,可以在GPU上计数。ASAA的效果对比如下:
Destruction Masking in Frostbite 2 using Volume Distance Fields分享了Frostbite 2引擎使用体积距离场来渲染破坏物体的效果。
有符号体积距离场的使用是将球体放置在几何体上,标记破坏面罩的位置,从球体组计算距离场,结果存储在体积纹理中,使用低分辨率:约2米/像素,每个遮蔽的几何体一个纹理。
点采样、三线性过滤、放大+细节等技术可以获得更高细节的效果:
// 高细节的距离场计算代码
float opacityFromDistanceField(float distanceField, float detail)
{
distanceField += detail * g_detailInfluence;
return saturate(distanceField * g_distMultiplier + g_distOffset);
}
体积纹理注意事项,Xbox 360体积纹理要求32×32×4的尺寸倍数,许多小纹理会浪费内存,使用纹理图集,每个维度一个图集简化了打包,需要填充以防止边界泄漏。
绘制破坏遮罩时,与几何图形一起绘制,在像素着色器中使用 [branch] 进行优化,RSX上的动态分支效率不高,6个循环命中分支,粗分段大小(800-1600 像素),将遮罩绘制为延迟贴花。延期贴花的绘制步骤:
- 绘制场景几何。
- 在贴花区域周围绘制凸体积。
- 获取深度缓冲区并转换为体积内的局部位置。
- 使用局部位置来查找不透明度,例如体积纹理。
- 在几何图形之上混合。
对于投影细节/法线纹理,需要切线向量,G-Buffer法线不适合,包含来自法线贴图的数据。需要的切向量数量有限,使用主要几何将索引写入G-Buffer,使用索引对包含切线向量查找表的纹理进行采样。
对于Alpha混合,固定功能混合在混合到G-Buffer时会导致问题,用于混合和目标alpha的输出alpha。受限于贴花的输出alpha,也受限于G-Buffer布局中,可编程混合是个不错的用例。
对于纹理mipmap的选择,由于四边形mipmap选择会导致边界周围的伪影,可在着色器中计算mip级别,使用tex2Dlod进行采样。
$$
\text{lod} = \log_2\cfrac{\text{pixelPerMeter} \ \times\ \tan(\text{fov}) \ \times\ \text{distToNearPlane}}{\text{screenRes} \ \times\ \bold v \cdot \bold n}
$$
在四边形内的纹理坐标中存在不连续性时,该四边形的mipmap选择将是错误的。在右侧放大的图片中,四边形上部的纹理坐标来自砖墙纹理,底部来自地板纹理,这些纹理坐标位于一个纹理图集中的不同位置,GPU将为这组像素选择最低的mipmap。解决这个问题的方法是手动计算每个单独像素的正确 mip 级别,并使用tex2Dlod显式采样。可以通过使用输入v.n和distToNearPlane创建2D查找表纹理来优化计算。
处理距离场三角剔除时,使用逐三角形的分支,针对距离场测试每个三角形,输出两个索引缓冲区,发出两个绘图调用。缓存剔除结果,距离场变化时更新。
基于体积距离场渲染的破坏效果。
Advanced Material Rendering介绍了几种渲染高级材料的技术,例如皮肤、水晶、玻璃、海水、沼泽水和泥水。提出的算法针对当前一代的游戏机,同时考虑到有限的内存和计算能力。涵盖了几个重要的材质特性,例如:次表面散射、半透明、透明度、水散射、动态表面等。此外,还讨论了延迟渲染器中的功能、性能、美学和实现问题,为上述材质渲染提供了久经考验的解决方案。
文中提到抖动技巧,抖动是以某种模式进行采样以掩盖更合理噪声中的欠采样,通常使用样本偏移的“旋转盘”完成分布,包含均匀和泊松。
使用旋转盘抖动,预先计算好的偏移分布表,使用磁盘分布的标准化空间中的N个点。对于每个阴影像素,获得随机法线向量N,对于每个样本,将圆盘分布中的点旋转N,使用该点作为缩放偏移进行采样。由于非离散采样点,线性采样很重要。
抖动的使用案例:阴影。双抛物面软阴影,仅4个Tap,最小的额外开销,似是而非的噪音,更大的柔软度需要更多的图案。
未使用(上)和使用(下)抖动的阴影效果对比。
对于透明度,延迟架构的透明度很棘手,常用的情形有:简单透明度(照亮)、全透明材料、半透明材质(照亮)、半透明材质(始终照亮)。
对于简单透明度,可以使用纱窗(screen door)效果,计算/查找抖动模式,使用它们“杀死”像素,根据透明度值在图案之间交替。4级透明度在带宽受限时易于计算,记得检查编译器是否在剔除像素——应该尽快做。代码如下:
float jitteredTransparency(float alpha, float2 vP)
{
const float jitterTable[4] =
{
float( 0.0 ),
float( 0.26 ),
float( 0.51 ),
float( 0.76 ),
};
float jitNo = 0.0;
int2 vPI = 0;
vPI.x = vP.x % 2;
vPI.y = vP.y % 2;
int jitterIndex = vPI.x + 2 * vPI.y;
jitNo = jitterTable[jitterIndex];
if (jitNo > alpha)
return -1;
return 1;
}
抖动的透明度在720p中看起来很糟糕,想要模糊那些讨厌的抖动像素,但负担不起另一个会检测到它们并模糊的通道。已在边缘AA的pass中执行。
自定义边缘AA是延迟渲染器中的常用技术,是全屏通道,根据深度/法线数据查找边缘,然后模糊它们,只需提示边缘AA过滤器即可找到“介于”被剔除像素之间的边缘,可以免费获得漂亮的混合,可以通过改变边缘检测的来源(将不连续性深入)来使用标志或更hacky的方式来完成。
左:没有自定义边缘AA;右:有自定义边缘AA。
完全透明物体不需要照明,只是反射/折射光线,适用于玻璃、水、扭曲粒子,视为后效,需要后缓冲作为纹理,便于在Alpha通道中获取深度信息。
半透明材质,需要照明以保证正确,与整个场景一致,带阴影。因此希望它处于延迟模式,最好具有单一的照明和着色成本,在样本重建中使用抖动模式。可以使用2通道渲染:
- 第1个通道:使用抖动模式将半透明材料写入G-Buffer。
图案覆盖基本渲染四边形(即 2x2),图案选择取决于被覆盖的透明材料层的数量,一个2x2四边形可以覆盖,每增加一层都会导致照明质量下降。
- 第2个通道:材质在光累积后完全渲染,使用样本重建来获得正确的照明值,需要排序和alpha混合。
重叠的半透明材质从后到前排序(半透明首先被渲染),对于每种重叠材质,以正确的模式对光缓冲进行采样以获得原始光照值,使用全分辨率纹理和重建照明渲染材质,透明度是通过与后缓冲区进行alpha混合来处理的。
光照重建时,只采集一个样本会导致严重的锯齿,必须采集多个样本进行重建。检查被着色的像素是否是原始像素,如果为假,则对邻域进行采样以获取有效样本,对它们进行加权并平均以进行样本重建,如果为真,请保持不变。在运动过程中减少锯齿并提高稳定性,对超过2种材质使用2x2四边形=大量纹理缓存垃圾和锯齿。
- 类似的方法是推断渲染(Inferred Rendering)。
还存在具有单一透明度的延迟渲染器:
- 半透明几何图形被渲染到具有棋盘格图案的g-buffer。
- 延迟着色后。
- 累积缓冲区包含半透明几何照明信息和底层阴影几何的交替像素。
- Pass 2重建两者:照明数据,着色背景。
除了半透明,该文还涉及了皮肤、头发、水体等特殊材质的渲染。其中水体的光学原理包含表面法线、反射、折射、光散射、消光、焦散、固体表面贴花、镜面反射等(下图)。
针对水面的以上光学原理,文中给出了对应的渲染解决方法。
Adaptive Volumetric Shadow Maps是Intel的图形研究人员针对烟、雾、云、头发等体积性的材质能够获得可信的阴影而研发。
AVSM可以流式简化算法,使用较小的固定内存占用量生成自适应体积光衰减函数,具有固定数量的节点,可变和无限的误差,易于使用的方法,不对光线遮挡体的类型和/或其空间分布做出任何假设。
一个单独的AVSM纹素编码N个节点,每个节点由一个深度和一个透射率值表示。节点始终按排序(从前到后)的顺序存储。通过将纹素中的所有节点初始化为相同的值来清除AVSM,将深度设置为远平面,将透射率设置为 1(无遮挡)。传入的光线遮挡体由光视图矢量对齐的段表示,一段由两个点(入口点和出口点)和出口点的透射率(入口点的透射率隐式设置为1)定义。假设入口点和出口点之间的空间由均匀致密的介质填充,通常会生成形状为分段指数曲线的透射率曲线,可以使用线条来简化问题(在大多数情况下没有太大的视觉差异)。
第一个和最后一个节点永远不会被压缩/删除,因为它们提供了非常重要的视觉提示。最后一个节点非常重要,因为它对投射在位于体积块后面的任何接收器上的阴影信息进行编码。 例如,一些香烟烟雾投射在桌子上的阴影总是正确的(没有压缩伪影)。删除节点后,就不会更新剩余节点的位置以更好地拟合原始曲线(deep shadow maps)。事实上,当节点在压缩平面上执行随机游走时,在十几次插入压缩迭代中更新节点位置可能会产生一些不可预测的结果。
基于DirectX 11的实现时,为流式简化而设计的算法,但映射到同一像素的运行的片元会导致数据竞争,对像素着色器当前不可用的结构的原子RMW操作。有两个实现版本:
- 基于计算着色器,速度较慢但内存固定,粒子的软件管线原型几乎没有优化工作,比可变内存实现慢约2倍。
- 基于像素着色器,速度更快但内存可变。
AVSM的性能方面表现在:竞争性能,更高的图像质量,阴影查找占主导地位,通常<30%的AVSM相关渲染时间花费在插入代码中,DSM比AVSM慢20-40倍。
总之,AVSM的优点是通过自适应采样提高图像质量,避免基于定期采样或可见性函数系列扩展的方法的常见缺陷,固定且易于使用,不需要任何关于遮光剂类型和空间分布的先验知识,易于权衡图像质量以换取速度和存储。缺点是快速的固定内存实现需要图形硬件在帧缓冲区上添加对读-修改-写操作的支持。
Per-Pixel Linked Lists with Direct3D 11介绍了基于DX11实现的逐像素链表的特点、实现、优化和应用等内容。
链表对编程有用的数据结构,使用以前的实时图形API很难有效实现,DX11允许高效地创建和解析链表,逐像素链表,枚举属于同一屏幕位置的所有像素的链表集合。链表涉及两步处理:
“片元和链接”缓冲区包含所有片元的数据和链接以存储,必须足够大以存储所有片元,使用计数器支持创建,UAV视图中使用D3D11_BUFFER_UAV_FLAG_COUNTER标志。声明如下:
struct FragmentAndLinkBuffer_STRUCT
{
FragmentData_STRUCT FragmentData; // Fragment data
uint uNext; // Link to next fragment
};
RWStructuredBuffer <FragmentAndLinkBuffer_STRUCT> FLBuffer;
“起始偏移”缓冲区包含在每个像素位置写入的最后一个片元的偏移,屏幕尺寸:宽 * 高 * sizeof(UINT32) ,初始化为魔法值(例如 -1),魔法值表示不再存储片元(即列表末尾)。声明如下:
RWByteAddressBuffer StartOffsetBuffer;
链表创建时,没有颜色渲染目标绑定, 也还没有渲染,只是存储在LL。如果需要,绑定深度缓冲区,OIT将在后面需要它,绑定UAV作为输入/输出:StartOffsetBuffer (R/W) 、FragmentAndLinkBuffer (W)。
Per-Pixel Linked List创建示意图。图中的黄色三角形占了Start Offset Bufer的两个像素,每个像素指向了Fragment and Link Buffer的一个位置。注意计数器是一直累积的,黄色之前已经有绿色和橙色的像素被处理和存储。
链接创建代码如下:
float PS_StoreFragments(PS_INPUT input) : SV_Target
{
// 计算片段数据(颜色、深度等)。
FragmentData_STRUCT FragmentData = ComputeFragment();
// 检索当前像素数并增加计数器。
uint uPixelCount = FLBuffer.IncrementCounter();
// 在StartOffsetBuffer中交换偏移量。
uint vPos = uint(input.vPos);
uint uStartOffsetAddress= 4 * ( (SCREEN_WIDTH*vPos.y) + vPos.x );
uint uOldStartOffset;
StartOffsetBuffer.InterlockedExchange(uStartOffsetAddress, uPixelCount, uOldStartOffset);
// 在片段和链接缓冲区中添加新的片段条目。
FragmentAndLinkBuffer_STRUCT Element;
Element.FragmentData = FragmentData;
Element.uNext = uOldStartOffset;
FLBuffer[uPixelCount] = Element;
}
- 从链表渲染。链表遍历和存储片段的处理。渲染像素的步骤:
1、将“起始偏移”缓冲区和“片段和链接”缓冲区绑定为SRV:
Buffer<uint> StartOffsetBufferSRV;
StructuredBuffer<FragmentAndLinkBuffer_STRUCT>
FLBufferSRV;2、渲染全屏四边形。
3、对于每个像素,解析链表并检索此屏幕位置的片段。
上图在第2行第个位置检索到了有效数据,根据Start Offset Buffer去获取Fragment and Link Buffer的数据。
上图在Fragment and Link Buffer获取黄色像素的数据时,发现该像素还存在其它数据(橙色),于是根据索引去读取下一个像素数据。
4、根据需要处理片元列表。取决于算法,例如排序、查找最大值等。
读取像素链表的所有数据之后,就根据需要处理片元列表,图中是平均像素的颜色。
float4 PS_RenderFragments(PS_INPUT input) : SV_Target
{
// 计算UINT对齐的起始偏移缓冲区地址
uint vPos = uint(input.vPos);
uint uStartOffsetAddress = SCREEN_WIDTH*vPos.y + vPos.x;
// 获取当前像素的第一个片段的偏移量
uint uOffset = StartOffsetBufferSRV.Load(uStartOffsetAddress);
// 解析该位置所有片段的链表
float4 FinalColor=float4(0,0,0,0);
while (uOffset!=0xFFFFFFFF) // 0xFFFFFFFF是魔法数字
{
// 在当前偏移处检索像素
Element=FLBufferSRV[uOffset];
// 根据需要处理像素
ProcessPixel(Element, FinalColor);
// 检索下一个偏移量
uOffset = Element.uNext;
}
return (FinalColor);
}
通过逐像素链表可以实现OIT(与顺序无关的透明度),将透明片元存储到PPLL,渲染阶段按从后到前的顺序对像素进行排序,并在像素着色器中手动混合它们,混合模式可以是每个像素唯一的!MSAA支持的特殊情况。
链表结构通过减少向UAV写入/读取的数据量来优化性能(例如uint而不是float4的颜色),OIT的示例数据结构:
struct FragmentAndLinkBuffer_STRUCT
{
uint uPixelColor; // 打包的像素颜色
uint uDepth; // 像素深度
uint uNext; // 下一个链接地址
};
也可以将颜色和深度打包到同一个uint中(如果相同的Alpha),使用16位颜色 (565) + 16位深度,性能/内存/质量的权衡。
使用仅可见片元,在Linked List创建像素着色器前使用 [earlydepthstencil],可确保仅存储通过深度测试的透明片元(即可见片元),可以节省性能和渲染正确性!
[earlydepthstencil]
float PS_StoreFragments(PS_INPUT input) : SV_Target
{
(...)
}对像素进行排序,就地排序需要对链表的R/W访问权限,稀疏的内存访问 = 慢!更好的方法是将所有像素复制到临时寄存器数组中,然后进行排序,临时数组声明意味着对每个屏幕坐标的像素数的硬性限制,性能所需的权衡。排序的具体过程见下面一组图:
// 存储像素以进行排序
(...)
static uint2 SortedPixels[MAX_SORTED_PIXELS];
// Parse linked list for all pixels at this position
// and store them into temp array for later sorting
int nNumPixels=0;
while (uOffset!=0xFFFFFFFF)
{
// Retrieve pixel at current offset
Element=FLBufferSRV[uOffset];
// Copy pixel data into temp array
SortedPixels[nNumPixels++]=
uint2(Element.uPixelColor, Element.uDepth);
// Retrieve next offset
[flatten]uOffset = (nNumPixels>=MAX_SORTED_PIXELS) ?
0xFFFFFFFF : Element.uNext;
}
// Sort pixels in-place
SortPixelsInPlace(SortedPixels, nNumPixels);
(...)
// PS中的像素混合
(...)
// Retrieve current color from background texture
float4 vCurrentColor=BackgroundTexture.Load(int3(vPos.xy, 0));
// Rendering pixels using SRCALPHA-INVSRCALPHA blending
for (int k=0; k<nNumPixels; k++)
{
// Retrieve next unblended furthermost pixel
float4 vPixColor= UnpackFromUint(SortedPixels[k].x);
// Manual blending between current fragment and previous one
vCurrentColor.xyz= lerp(vCurrentColor.xyz, vPixColor.xyz,
vPixColor.w);
}
// Return manually-blended color
return vCurrentColor;
通过支持MSAA的逐像素链表实现OIT时,若将单个样本存储到链接列表中需要大量内存,性能会受到影响!解决方案是像以前一样将透明像素存储到PPLL中,但也包括样本覆盖数据!需要与MSAA模式一样多的位,在PS结构中声明SV_COVERAGE:
struct PS_INPUT
{
float3 vNormal : NORMAL;
float2 vTex : TEXCOORD;
float4 vPos : SV_POSITION;
// 像素覆盖数据
uint uCoverage : SV_COVERAGE;
}
链表结构与之前几乎没有变化,深度现在被打包成24位,8位用于存储覆盖范围:
struct FragmentAndLinkBuffer_STRUCT
{
uint uPixelColor; // Packed pixel color
uint uDepthAndCoverage; // Depth + coverage
uint uNext; // Address of next link
};
样本覆盖率示例如下图,第三个样本被覆盖,此时uCoverage = 0x04(二进制的 0100):
打包深度和覆盖数据,然后存储:
Element.uDepthAndCoverage = ( In.vPos.z*(2^24-1) << 8 ) | In.uCoverage;渲染阶段需要能够写入单个样本,因此PS以采样频率运行,可以通过在输入结构中声明SV_SAMPLEINDEX来完成,解析链表并将像素存储到临时数组中以供以后排序,类似于非MSAA案例,区别在于仅在覆盖数据与被光栅化的样本索引匹配时才存储样本。渲染代码如下:
static uint2 SortedPixels[MAX_SORTED_PIXELS];
// Parse linked list for all pixels at this position
// and store them into temp array for later sorting
int nNumPixels=0;
while (uOffset != 0xFFFFFFFF)
{
// Retrieve pixel at current offset
Element=FLBufferSRV[uOffset];
// Retrieve pixel coverage from linked list element
uint uCoverage=UnpackCoverage(Element.uDepthAndCoverage);
if ( uCoverage & (1<<In.uSampleIndex) )
{
// Coverage matches current sample so copy pixel
SortedPixels[nNumPixels++]=Element;
}
// Retrieve next offset
[flatten]uOffset = (nNumPixels>=MAX_SORTED_PIXELS) ?
0xFFFFFFFF : Element.uNext;
}
Texture Compression in Real-Time Using the GPU介绍如何在当前控制台和DirectX 10.1视频卡上使用GPU来执行DXT纹理压缩,提出了一种不依赖GPU计算API或存在按位数学运算的方法,涉及每个平台的实现细节和优化。
使用GPU压缩的理由是游戏使用更多运行时生成的内容(混合地图、动态立方体贴图、用户生成内容),CPU压缩速度较慢,并且需要额外的同步和延迟。下图的性能对比来自实时DXT压缩论文的CPU性能数据:
DXT1/BC1是代表4x4纹素的64位块,其中有4个颜色值、2个存储值、2个插值:
颜色索引的伪代码如下:
Index00 = color_0;
Index01 = color_1;
Index10 = 2/3 * color_0 + 1/3 * color_1;
Index11 = 1/3 * color_0 + 2/3 * color_1;
if (color_1 > color_0)
{
Index 10 = 1/2 * color_0 + 1/2 * color_1;
Index 11 = “Transparent”;
}DXT压缩的基本步骤:
float2 texel_size = (1.0f / texture_size);
texcoord -= texel_size * 2;
float4 colors[16];
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
float2 uv = texcoord + float2(j, i) * texel_size;
colors[i*4+j] = uv;
}
}
- 找到想用作存储颜色的颜色。此操作可能非常昂贵,但是后面会提到一些方法,查找端点颜色或非常便宜!建立端点值,需要小心Alpha。
- 将每个4x4纹素匹配到最合适的颜色。查找纹素指数的代码如下:
float3 color_line = endpoints[1] - endpoints[0];
float color_line_len = length(color_line);
color_line = normalize(color_line);
int2 indices = 0;
for(int i=0; i<8; i++)
{
int index = 0;
float i_val = dot(samples - endpoints[0], color_line) / color_line_len;
float3 select = i_val.xxx > float3(1.0/6.0, 1.0/2.0, 5.0/6.0);
index = dot(select, float3(2, 1, -2));
indices.x += index * pow(2, i*2);
}
重复接下来的8个像素。
dxt_block.r = max(color_0_565, color_1_565);
dxt_block.g = min(color_0_565, color_1_565);
dxt_block.b = indices.x;
dxt_block.a = indices.y;
return dxt_block;
- 将结果放入纹理中。方法因平台而异,渲染目标应该是源的1/4尺寸,1024x1024的源 = 256x256的目标,使用16:16:16:16的unsigned short格式。
漫反射贴图的运行时压缩和离线压缩的对比及它们的颜色差异如下:
可以采取一些措施调整性能,着色器编译器很聪明,但并不完美,确保测试比较过展开(unrolling)与循环(looping)的性能,为目标格式创建变体着色器,如果只使用2个组件,法线贴图会更便宜。法线贴图的运行时压缩和离线压缩的对比及它们的颜色差异如下:
An Optimized Diffusion Depth Of Field Solver (DDOF)分享了CoC景深的几种深度优化技术。DOF的解算涉及三对角系统(下图),其中橙色是源自CoC的每个像素的输入行/列,绿色是生成模糊的行/列,红色是输入图像的行/列:
文中提及的之前的解算器(Solver)有混合的GDC2010解算器、圆形缩减 (CR) 解算器。以下是Vanilla CR解算器的处理过程:
但是上图的Stop at size 1阶段会阻碍并行,引起性能下降。可以以合理尺寸停止,随后以足够大的并行工作负载解算Y:
在内存优化上,可以将rgba32f改成rgba16f(无明显瑕疵)。此外,上图的abc纹理再一次保存大量的内存,因为它是求解器使用的最大表面,可以跳过abc构造过程,并在第一个reduce通道期间动态计算abc。(下图)
第一个reduce之后的纹理又会显著保存大量内存,可以reduce的4个通道减少成成1个特殊的reduce通道,在一个特殊的替换过程中替换1到4。
在DX11中,可以将abc和X打包进一张rgba_uint纹理,可以使用SM5的数据打包:
DX11内存优化还可以对解算器进行水平和垂直通道,低分辨率RT链需要出现两次,水平减少/替代链,垂直减少/替代链。UAV允许将水平链的数据重用于垂直链,概念验证实现表明这样做很有效,但会显著影响运行时性能(帧率降低约 40%)。保留RT,因为内存已经很低了,仅在真正关心内存时使用。
经过以上方式优化之后,4-to-1 Reduction + SM5 Packing的方法获得了最好的性能,且占用内存最低:
Five Rendering Ideas from Battlefield 3 & Need For Speed介绍了来自Frostbite 2引擎的5种渲染技术:可分离的Bokeh DOF、Hi-Z//Z-Cull、色度亚采样图像处理、基于平铺的延迟着色、时间稳定的屏幕空间环境遮挡。
可分离的Bokeh DOF的模糊过程有以下几种方法:
- 高斯模糊。常见于DX9游戏中。
- 2D区域采样。纹理tap爆炸限制了内核大小。
- GS扩大的点精灵。繁重的填充率,CryEngine3和UE3采纳的方法。
图像空间中的任意模糊是O(N^2),高斯模糊可以是可分离的O(N),可以被分离的2D模糊有:高斯、方框、倾斜的方框。
六角模糊(Hexagonal Blur)可以把一个六边形分解成三个菱形,每个菱形都可以通过分离的模糊计算,总共7个pass:3个形状 × 2个模糊+ 1个组合。
使用可分离滤镜的六边形模糊,但7个通道和6次模糊并不具有竞争力,需要减少通道。
下图显示了总共只需要2个通道,但通道1必须输出两个MRT,并且通道2必须读取2个MRT。现在总共有5个模糊,也减少了1个,最后的组合通道是通道2的一部分。
下图展示的方法在通道1中,像往常一样将向上模糊输出到MRT0,但在输出到MRT1之前,还将它添加到左下模糊的结果中。在通道2 中,彻底的模糊将同时在菱形2和3上运行。只需要1次模糊!!
模糊效果如下:
高斯和六边形的对比:高斯总共有2个模糊;六边形2个通道(3个解析),总共4个模糊,但每个模糊只需要一半的tap数,因此虽然相同的tap数,但每个tap贡献一样(高斯的不一样),所以需要更少的tap,以满足一个给定的美学过滤器的内核宽度。
因为有相等权重的模糊,可以对模糊使用迭代优化,多个通道填充欠采样,双重迭代模糊需要总共5个通道、8次半模糊。
伪分散过滤器(Pseudo Scatter filter),适当的散景应该有它的模糊分散到它的邻居。然而,像素着色器是用来收集结果的,它们不能分散结果。典型的模糊默认过滤器内核为像素的CoC,而默认用大的CoC,并根据采样纹素的CoC拒绝,额外的方法可以避免溢色瑕疵,并可以锐化平滑的梯度。
文中提到了专用于X360的Hi-Z/Z的反向重新加载技巧。在现有深度缓冲区上添加渲染目标的重叠,初始重叠的RT成D3DHIZFUNC GREATER EQUAL,绘制全屏矩形(PS设为NULL、Zfun==Never),此时Hi-Z颠倒了:
反向Hi-Z用于CSM渲染:定向光的每个级联都以世界空间中的一个长方体为界,只有长方体内部的世界空间像素会投射到阴影图上,通过绘制长方体背面,只有这些像素将通过反向Z测试。非常适合CSM,CSM的设计使它们包围的体积相交并将相机包围在平面附近,虽然后面的级联不是这样,但它们会包含前面的级联。
作为单独的通行证进行评估,输入是深度缓冲区,创建一个L8掩码纹理输入到定向光通道。可以做一个之前的全屏通过标签背面像素写到在模板的光源,启发式的太阳与相机的角度。潜在的1/4分辨率双边上采样,模板被更新以表示已经处理过的像素。
基于反向Hi-Z的CSM。从上到下依次是级联0到级联3。
后续还可以用Min/Max深度来实现PCF软阴影(下图),具体过程可参阅原文。
色度亚采样(Chroma Sub-Sampling)不是一个新想法,已用于电视广播。Jpeg / Mpeg压缩将图像分解为亮度和色度,只用全分辨率存储亮度(因为人眼对亮度更敏感),用低分辨率存储色度。
最顶部是正常的图片,下面三幅分别是分解出来的Y、U、V分量图,其中Y是亮度,U和V是色度。
后处理需要大量的带宽,如果用普通的格式存储数据,对GPU产生巨大的压力和瓶颈。相反,如果使用Luma方法,可以将所需的带宽减少到原始的1/4,但需要对颜色进行额外处理,可以是1/4分辨率的2个通道(原始大小的1/8)。
采用色调亚采样之后,着色器可以有4倍的速度提升吗?答案是否定的,因为受限于ALU:纹理单元和ALU是为4组件分量(如float4)的SIMD设计的,只使用了一个组件,需要打包4个亮度值一起处理。
只需要1次纹理读取就可以获得4个亮度值,所以1280x720亮度缓冲区是320x720的ARGB缓冲区,用压缩缓冲区执行双线性过滤是不正确的,必须手动使用DOTP水平过滤。
在打包数据时使用了蝴蝶打包(Butterfly Packing)的技巧,覆盖每个象限到ARGB,镜面周围的图像中心点,现在双线性除了跨越边界外都是有效的,以额外的混合和重组重新绘制一个strip(条带),水平方向为R<-->g、B<-->A,垂直方向为R<-->B和G<-->A,径向模糊有效。蝴蝶解包的过程如下图:
文中提到的TBDR技术除了传统的渲染过程,还使用了GPGPU裁剪,过程如下:
- 屏幕被划分为920个32x32像素的块(tile)。
- 下采样并将场景从720p划分到40x23(1像素 = 1 tile)。
- 找到每个tile的最小/最大深度。
- 找到每个tile的材质排列。
- 下采样是通过多通道和MRT完成的。
下图是材质分类的示例,假设场景有3个材质:默认材质(红色)、皮肤着色(绿色)、金属(蓝色)。
当下采样(通过手动双线性tap)时,将材质组合成最终输出颜色。可以在下图右边看到头部周围的下采样是如何产生黄色的,这是红色和绿色的组合,红色和蓝色类似,呈现洋红色。
跳过几个步骤后得到下图,下图是40x23的纹理,将准确地提供每个tile中的材质组合。在并行MRT中,还下采样并存储了最小/最大深度。有了这些信息,现在可以使用GPU来剔除光源,因为已经知道相机视锥体中的所有灯光和每个tile(和天空)的最小/最大深度和排列组合(迷你的视锥体)。
- 为每个tile构建一个迷你的视锥体。
- 在着色器中剔除忽略天空的tile的光源。
- 将裁剪结果存储在纹理中(Column == Light ID、Row == Tile ID)。
- 实际上,可以一次处理4个光源(A-R-G-B)。
- 在CPU上回读贡献结果,准备计算光照。
计算光照的伪代码如下:
// 分析纹理上的裁剪结果, 在CPU上执行.
ParseCullingResultsTexture();
For each light type:
For each tile:
For each material permutation:
// 重新组合并设置PS常量的光照参数.
RegroupAndSetLightParametersForPSConstants(...);
// 设置着色器循环计数器.
SetupShaderLoopCounter(...);
// 使用单个绘制调用累加地渲染灯光(到最终的HDR照明缓冲区).
RenderLights(...);
时间稳定的SSAO使用了线段采样,线段采样基本上是占用函数的解析积分乘以圆盘上每个采样点的球体深度范围。由于所有2D样本都将投影到z缓冲区中的不同点,因此线采样也更加有效和稳定,而在3D空间中随机点采样的常用方法,两个点样本可能会投影到同一位置,导致样本数量下降。
SSAO渲染过程还涉及了快速灰度模糊,其工作原理如下:
Pre-Integrated Skin Shading阐述了皮肤渲染涉及的相关技术,包含SSS、各种近似方法,提出了运行时效率极高的预积分皮肤渲染,在工业界产生了深远的影响。文中先总结了之前研究出的几种近似方法:
基于纹理空间扩散(TSD)的几种皮肤渲染方法。
快速次表面散射方法。
屏幕空间的次表面散射。
但是,以上方法都或多或少存在一些问题或限制。文中提出了Pre-Integrated Skin Shading,它的目标是只用一个简单的像素着色器,所有输入本地存储在纹理/凹凸贴图中(无模糊通道)。关键观察:散射并非随处可见,发生在入射光照变化附近(光照梯度)。策略是查找光照梯度和预积分散射。
纹理空间扩散的几种情况,如上图数字标识。数字1的区域表示入射光是常量,不需要做任何扩散;数字2处是小表面凹凸,表面曲率产生了强烈的漫反射衰减;数字3处通过皱纹和凹凸等特征扩散;数字4处光照散射在阴影内,形成独特的皮肤外观。
以上4处可以总结成3个待处理的问题:
- 表面曲率(Surface Curvature)。预积分基于曲率的BRDF。
对于普通的漫反射、环绕光照和距离衰减函数,都无法适用于表面曲率光照计算。但是,可以在环绕光照(wrap lighting)基础上做改进,用预模糊漫射BRDF与皮肤轮廓来解决不是基于真实的皮肤扩散剖面的问题,使用曲率参数化 BRDF来解决不考虑曲面曲率的问题。
经过上述分析之后,就可以进行常规的漫反射光计算,并准确计算特定尺寸(或特定曲率)球体上的散射情况,通过收集来自球体(或环,更容易)上所有点的所有漫射光来做到这一点。可以使用任何昂贵的技术,因为是离线计算,只会记录结果。对于漫反射衰减的每个点(下图右显示法线和光线之间的一个角度),整合从整个球体散射而来的所有光,这一步非常昂贵,所以记录这个值,以便以后可以使用它。
下面分别是不同参数的漫反射预计算结果:
现在已经使用准确的皮肤轮廓来捕捉各种表面曲率上的散射外观,可以将所有这些数据存储在由N\cdot L和曲率参数化的简单2D纹理中:
事实证明,可以通过使用法线向量的变化率和在世界空间中位置的变化率之间的简单相似三角形关系来获得曲率的一阶估计,可以使用导数指令得到这些无穷小的变化(下图左)。下图右是将其应用于头部模型的结果。注意:此处为了可视化曲率,但应该使用扩散轮廓中的正确单位,然后校正查找纹理中的曲率范围。
BRDF适用于光滑的曲面,法线贴图中的小凹凸呢?光照应该散射通过几个表面凸起,使用曲率在小尺寸时会失效,现在只关注法线。
使用法线贴图,可以对邻域进行采样(可以使用许多样本,光源、权重和累积)。或者,可以预先过滤法线贴图吗?预过滤点N\cdot L是正确的,预过滤max(0, dot(N, L))并不严格正确(但很接近),LEAN/CLEAN映射对镜面反射执行此操作。
切线可以获取法线贴图,一种应用光照梯度度来捕捉法线的技术,不同的光照颜色产生不同的捕捉法线。下图使用了4个法线贴图,其中每个法线贴图分别用于计算红、绿、蓝漫射光和镜面光。
不需要捕获的法线来使用这种技术,从“真正的”法线贴图(高光)开始,使用R/G/B皮肤轮廓预过滤新的法线贴图,最佳情况下,需要4个法线:R/G/B 和高光。看起来很棒,但占用很多内存!
优化弯曲法线的方法有:
- 使用几何和高光法线(混合其余部分)。仅适用于某些艺术,法线贴图必须只包含细节(皱纹/毛孔)。
- 使用两个正常样本(混合其余样本)。适用于所有艺术,一个法线贴图,两个采样器,将一个采样器截取在模糊的mip阈值处。
有很好的解决方案:弯曲但光滑的表面(预集成的BRDF),平坦但凹凸不平的表面(预集成的法线贴图)。它们相得益彰,从主要曲率(几何)中选择的BRDF,来自主表面的广泛散射,弯曲法线填补了空白,提供局部细节的细粒度散射。
事实证明可以应用一个非常相似的技术,就是来自盒子过滤器的阴影。给定阴影值,反转半影模糊功能,找到阴影内的位置。(下图)
如果衰减函数中有位置/距离,基本上可以指定一个新的衰减函数,一个为散射留下一堆额外空间的,然后就可以使用皮肤扩散配置文件将散射效果预先积分到阴影中。
下图是预先积分到阴影得到的结果。类似于漫反射计算,此处有一个2D查找。第二个维度代表了在世界空间中的半影大小,由于表面的坡度或阴影的模糊程度,可能会有不同的尺寸。这个宽度可以通过多种方式计算,例如使用光的表面斜率,甚至可能使用阴影本身的导数。注意,如果有一些疯狂的东西(比如抖动阴影贴图),就不会有硬边,而且可能会得到不正确的散射。此外,在2D中,距离不会完全正确,但外观才是最重要的。
下俩图是纹理空间扩散和预积分的对比:
阴影还可以结合PCF、VSM进行效果改进。下图是通过2的幂来改变尺寸和强度的效果矩阵,其中横坐标是尺寸,纵坐标是强度:
Practical Occlusion Culling on PS3分享了用于PS3的遮挡剔除技术,包含SPU运行时、创建遮挡体、调试工具、性能优化等。PS3游戏杀戮地带的渲染管线见下图:
SPU、PPU和内存之间的协作和交互如下图:
在遮挡剔除方面,计划如下:
- 离线创建几何遮挡。
- SPU每一帧渲染遮挡到720p深度缓冲。
- 将缓冲区分割成16像素高的块用于光栅化。
- 下采样缓冲到80x45(16x16最大滤波器)。
- 在场景遍历期间测试这个边界框。
- 准确:光栅化+深度测试。
- 粗糙:某种恒定时间点测试。
添加遮挡剔除阶段的管线如下:
遮挡体查询作业:
- 在(截断的)视锥体中查找遮挡体。
- 遮挡体是正常的渲染图元,使用标记位标识的可绘制对象的其余部分。
遮挡体设置作业:
- 解码RSX风格的顶点和索引数组。
- 输出剪切+投影三角形到暂存区域。
- 隐藏DMA延迟的内部管道。
光栅化作业:
- 启动一个光栅每条工作。
- 使用列表DMA从暂存区加载三角形。
- 在LS中绘制三角形到一个640x16深度的浮点缓冲区。
- 压缩深度缓冲到uint16并存储。
过滤作业:
- 光栅化完成后运行。
- 生成粗糙的剔除数据。在查询期间用于剔除小对象。
- 回写拒绝缓冲区(相邻且闭塞的缓冲区)。
遮挡查询作业:
- 测试工作在两级层次结构中。对象存在于kd树中,并包含多个部件。
- 测试对象以避免提取部件。
- 测试部件,避免绘制它们。
- 最终的查询结果写入主存,并用于建立显示列表。
提高裁剪率:
- 尽量使用球体测试,以得到足够的RSX剔除。
- 要确保对所有物体都进行全面测试。这使得优化变得更加重要。
文中还涉及了裁剪各个阶段的实现细节和优化建议,可点击原文查看。
Analytic Anti-Aliasing of Linear Functions on Polytopes阐述了多面体上基于线性函数的特殊解析抗锯齿。
采样(Sampling)是计算机图形学中非常常见的核心技术,应用广泛,最主要的用例之一是将数据采样到常规网格。
普通输入网格经过某种采样方式之后,利用有限的样本值重建网格。本文只涉及从原始网格采样的领域。
已知有一张具有空间高频的图片:
利用不同的滤波将上图下采样到半分辨率:
由此可知,Box、Hat滤波都存在一些问题,而高斯滤波效果最佳。为了更好地采样,通常还需要加入随机抖动:
解析采样的主体流程和步骤如下:
其卷积公式和图例如下:
获得的结果是复杂场景的无锯齿采样:
总之,本文提出了多边形和多面体的分析抗锯齿,允许网格上的线性函数(例如颜色、密度……)、高阶径向滤波器函数、常规和非常规采样网格。
Water Technology of Uncharted分享了神秘海域的水体模拟、实现和优化。水有多种形式,从小到大的水体,与水的相互作用,可弄湿衣服,快速移动,缓慢而难以导航。水体的着色模型如下:
水流模拟图如下:
基于流量的位移,每个顶点以不同的相位φ在圆形图案上移动:
海洋是渲染的挑战,开阔的海洋,大浪(100 米以上),海浪驱动船只和驳船,动画循环被考虑但未使用,可以游泳。
波系统,包含程序化、参数、确定性、LOD等。有两种波浪:Gerstner波,简单但高频细节不够,在高开销之前只能使用几个。FFT波比较真实,更多细节,频谱让艺术家难以控制,在低分辨率网格上平铺视觉失真。
还有波粒子(Wave Particle),来自点源的波,神秘海域不使用点源,相反,在环形域中随机分布,粒子来近似开放水域的混沌运动,一定速度范围内的随机位置和速度,产生一个可平铺的矢量位移场。波粒子的特点是艺术家可以直观地控制,没有平铺失真,速度快, 适合SPU向量化。时间确定性,无需移动粒子,新位置来源于初始位置、速度和时间。
波场是4个Gerstner波+波粒子(使用4次):
流网格是在网格中编码流、泡沫、幅度乘数:
添加更简单的波之后:
对于细节层次,有多种方法创建水体网格,屏幕投影网格 → 锯齿失真,准投影网格 → 处理大位移的问题。
不规则几何剪贴图基于Geometry Clipmaps: Terrain Rendering Using Nested Regular Grids,修改成水渲染。不同的分裂来固定环水平的T形接头,关卡之间的动态混合,小块(patch)可提高SPU利用率。
水体裁剪图运行机制。(只选取了部分步骤)
不同LOD级别的水体网格边界会出现裂痕,需要修复:
需要对水体网格Patch进行剔除,使用Frustum-bbox测试剔除截锥体之外的Patch。
用天空光照亮场景时,用视锥体-包围盒测试剔除视锥体之外的patch,用平面和包围盒测试剔除天窗外的patch:
计算天空光光照时,对于渲染使用着色器丢弃操作来进行平面裁剪(下图左),用于评估大厅包围盒内的钳位点(下图右)。
对于漂浮对象,采样点和最适合定位的平面,在相交区域,将幅度相乘。附加对象,采样点和最适合定位的平面,在相交区域,将幅度相乘。
网格计算时,对于每个环,运行一个SPU作业来处理patch (i % 3):
J1: (0,3,6,9,12,15)
J2: (1,4,7,10,13,[16])
J3: (2,5,8,11,14)最小化环级计算,双缓冲网格输出。
由于每个作业都会创建一个看起来完美的网格,因此无需缝合网格。海洋的最终网格,由多个网格组成。(下图)
需要注意时间,剪辑图需要特定时间的波粒子,只要一个波粒子工作,此作业生成位移网格。为了同步作业,设置了一个屏障来等待波粒子作业完成生成其网格。(下图)
渲染效果截图:
Graphics Gems for Games - Findings from Avalanche Studios描述了游戏中的一些特殊渲染技术,如粒子修剪、合并实例、电话线抗锯齿、第二深度抗锯齿。
文中提到GPU越来越强大,ALU涨麻了!TEX相当不错的增长,BW(带宽)有点迟钝,ROP(渲染输出单元)冰川般的速度:
如果ROP受限,想方设法绘制更少的像素。对于粒子修剪,典型的ROP密集案例包含粒子、云、广告牌、图形用户界面元素。解决方案:渲染到低分辨率渲染目标、滥用MSAA。本文的解决方案:修剪粒子多边形以减少浪费。粒子使用的网格常可见大量alpha=0的区域,浪费填充率,调整粒子的网格以减少浪费,可使用自动化工具。
裁剪之后可以节省大量填充率,更多顶点更大的节省,但收益率递减(下图),Just Cause 2使用4个顶点用于云、8个顶点用于粒子效果。
粒子裁剪有两种方式:
- 手动修剪。乏味,但证明了这个概念,可用于云图集。2倍性能,数十个图集粒子纹理。
- 自动工具。输入纹理、Alpha阈值、顶点数,输出优化的封闭多边形。
粒子裁剪算法:
- 阈值化Alpha。
- 将所有实心像素添加到凸边形。通过潜在角测试进行优化。
- 减少Hull顶点数。替换最不重要的边,重复直到最大船体顶点数。
- 蛮力遍历所有有效边缘的排列,选择面积最小的多边形。
粒子裁剪算法过程。1:原始纹理;2:阈值化纹理;3:将所有实心像素添加到凸边;4:减少凸边形的面积;5:最终4个顶点的多边形(60.16%);6:最终6个顶点的多边形 (53.94%);7:最终8个顶点多边形 (51.90%)。
粒子修剪的问题:
- 多边形延伸到原始四边形之外。常规纹理没问题, 使用CLAMP。可能会切入相邻的图集tile,首先计算所有外壳,拒绝与另一个外壳相交的解决方案,如果没有有效的解决方案,则恢复为对齐的矩形。
- 性能。蛮力,保持凸包顶点数合理地低。
- 过滤。添加像素的所有四个角(更快),或插入子像素alpha值(准确)。
- 处理“奇怪”的纹理,例如在纹理边缘alpha != 0。
接下来是合并实例化(Merge-Instancing)。
实例化是一个网格多个实例,有多个绘制调用;合并是多个网格,每个网格一个实例,有顶点数据的重复;合并实例化是一次绘制调用,没有顶点重复。下面是实例化和合并实例化的对比代码:
// 实例化
for (int instance = 0; instance < instance_count; instance++)
for (int index = 0; index < index_count; index++)
VertexShader( VertexBuffer[IndexBuffer[index]], InstanceBuffer[instance] );
// 合并实例化
for (int vertex = 0; vertex < vertex_count; vertex++)
{
int instance = vertex / freq;
int instance_subindex = vertex % freq;
int indexbuffer_offset = InstanceBufferspan class="p">[instance].IndexOffset;
int index = IndexBuffer[indexbuffer_offset + instance_subindex];
VertexShader( VertexBuffer[index], InstanceBuffer[instance] );
}
合并奇数大小的网格,选择常用频率,根据需要复制实例数据,根据需要使用退化三角形填充。例子——Mesh0:39个顶点,Mesh1:90个顶点,选择频率 = 45,用2个退化三角形(6个顶点)填充Mesh0。
Instances[] = {
( Mesh0, InstanceData[0] ),
( Mesh1, InstanceData[1] ),
( Mesh1 + 45, InstanceData[1] ) }
接下来聊电话线抗锯齿。锯齿的来源:
- 几何边缘。主要由MSAA解决,后处理AA通常也有效,用细几何体分解。
- 着色。通过mipmapping解决的排序,缺乏研究/理解,一些实用的游戏技巧,如LEAN映射。
电话线是常见的游戏内容,通常是亚像素大小,MSAA有帮助但不多,在子样本大小处中断。其实可以不要亚像素大小!
电话线是长圆柱形状,由中心点、法线和半径定义。避免进入亚像素,将半径钳制为半像素大小,以半径减小比淡化。
// Compute view-space w
float w = dot(ViewProj[3], float4(In.Position.xyz, 1.0f));
// Compute what radius a pixel wide wire would have
float pixel_radius = w * PixelScale;
// Clamp radius to pixel size. Fade with reduction in radius vs original.
float radius = max(actual_radius, pixel_radius);
float fade = actual_radius / radius;
// Compute final position
float3 position = In.Position + radius * normalize(In.Normal);接下来是第二深度抗锯齿。过滤AA方法包含:
- 处理AA:MLAA、SMAA、FXAA、DLAA。
- 分析方法:GPAA、GBAA、DEAA、SDAA。
深度缓冲区和第二深度缓冲区,深度在屏幕空间中是线性的,简化边缘检测,可以预测原始几何。两种类型的边缘:折痕、剪影,剪影需要第二深度缓冲,使用正面剔除进行pre-z通道,或者输出深度以渲染背面几何图形的目标。
先尝试折痕,看深度坡度,计算交点,距离<1像素时有效,如果距离 < 半像素则使用,如果无效,尝试剪影。
尝试作为剪影,邻居深度没用,看第二深度,计算交点,如果距离 < 半像素则使用。
第二深度AA效果对比(左无右有):
Using GPUView to Understand your DirectX 11利用GPU分析工具GPUView阐述了GPU的工作机制、原理及调试过程。图形和WDDM(Windows Display Driver Model)的层级结构关系如下图所示:
发送任务给GPU的过程:
标准的DMA代表了图形系统状态对象、绘制命令、对资源分配的引用(纹理、顶点和索引缓冲区、渲染目标、常量缓冲区)。GPUView可以查看DMA的详情,还可以对上下文、队列进行监控:
CPU软件上下文队列是代表提交给GPU上下文的工作,队列在时间上表示为一个堆栈,堆栈在UMD提交工作时增长,当GPU完成工作时,堆栈收缩。
GPU硬件上下文队列在时间上表示为一个堆栈,通过KMD提交工作时堆栈增长,对象被GPU完成工作时堆栈收缩,间隙表示CPU端瓶颈。还可以选择特定的队列,显示其延迟:
分页缓冲包:作为结果提交分页操作(可能是一个大的纹理),原因通常是准备一个DMA缓冲器,查看分页操作之后的DMA数据包。对于硬件线程,颜色代表空闲,间隙代表工作:
对于线程执行,浅蓝色代表内核模式,暗蓝色代表dxgkrnl(DX内核),红色代表KMD(内核模式驱动):
还可以查看垂直同步情况:
获得正确的遮挡查询,延迟获取结果为N帧,其中N = GPU数,可能需要人为膨胀遮挡体积以避免跳变。避免分页控制显存的使用,特别是在MSAA模式下降低低端硬件的纹理分辨率,避免使用过多的动态数据纹理和顶点缓冲。总之,确保GPU保持工作状态,保持跟踪CPU/GPU交互,保持跟踪线程,监控multi-GPU交互,添加GPUView到工具箱。
Sand Rendering in Journey分享了游戏Journey的沙粒及沙漠的渲染。
沙子的渲染采用了锐化mip、各向异性遮罩、闪光镜面、海洋镜面、漫反射对比度、细节高度图等:
从左到右依次添加了:细节高度图、漫反射对比度、海洋镜面、闪光镜面、各向异性遮罩、锐化mip、最终成像。
沙粒的效果随着相机的距离改变而改变,文中对漫反射也做了修改,而不是使用Lambert。修改后的漫反射着色与Lambert相比,对比度要高得多:
左:修改的漫反射;右:Lambert漫反射。
左:高度图;右:高度细节图,右上两幅是微观细节,右下两幅是较宏观的细节。
Scalable High-Quality Motion Blur and Ambient Occlusion介绍了可扩展的高质量运动模糊和环境光遮蔽。
需要可扩展的原因是能够跨具有不同性能限制的平台共享效果,分享适合不同艺术风格的游戏。好处是视觉一致性、节省开发时间、一致的内容要求、“免费”质量收益。
潜在的可扩展元素有质量旋钮:最大半径、样本数、夹紧输入;分辨率独立性,允许标准化的屏幕单位而不是像素;模块化功能,准确性与速度;附加处理,附加通道,迭代算法。
文中给出的方案是:低端质量设计,向高端延伸,迭代质量和性能,迭代之间有充足的时间。
运动模糊的目标是不依赖资产,与所有几何类型一致,独立于场景复杂性,最小G-buffer编码。这些限制消除了大多数现有技术。近距离观察运动模糊:
核心问题:自然散射效果,需要表示为聚集,对象在其范围之外模糊,产生类似透明的效果。文中的方法:使用tile扩张速度,允许在对象边界之外进行模糊处理,沿扩张速度采样,分散 -> 聚集,根据速度和深度混合样本,背景估计。具体步骤:
- 渲染速度。计算屏幕空间中的速度,钳制到最大模糊,重新缩放到[0, 1]。
- 分块最大化速度。NxN下采样,N=最大模糊半径,按幅度记录最大值。
- 邻域最大化速度。输入分块最大结果,应用3x3盒式过滤器,通过中心和周围分块的大小找到最大值。
- 重建。中心深度Z_C,颜色Color_C,中心速度\overrightarrow{v}_C,邻域最大值\overrightarrow{v}_N。
- 采样。沿 \overrightarrow{v}_N两个方向计算样本,采样Z_S、Color_S、\overrightarrow{v}_S,确定前景或背景,与中心比较。
- 对比细节。有趣的案例:背景样本Z_C < Z_S,潜在背景估计,前景样本Z_C > Z_S,可能移过中心,两个样本都在移动(||\overrightarrow{v}_S|| \ne 0 , \ ||\overrightarrow{v}_C|| \ne 0),也就是想要一些模糊的东西。
- 最终细节:收集样本,比较权重总和,总和颜色贡献,结果归一化。
运动模糊总体流程。
接下来聊聊可扩展的AO。
AO极大地有利于阴影中的照明,接触和折痕阴影。项目的约束是快速、表面感知、减少锯齿。
核心问题是不想要两种不同的算法,想要的视觉一致性。解决方案是更少但更高质量的样本,基于两件事的贡献:点到中心的距离、样本距离到法线的投影长度,试验衰减函数,直到满意的结果。该AO的步骤如下:
- 在中心采样深度和法线,重建位置。
- 采样属性。选择采样位置,采样深度,重建位置。
ng)
- 样本贡献。计算 ,计算‖ ‖,计算 ,根据半径r应用衰减。
12.png)
- 总的遮挡。汇总贡献并归一化:
其中: = 艺术家调整的贡献量表, = 样本数。
- 采样不同的法线对AO也有明显的影响,下图分别是采用GBuffer法线和派生法线:
- 模糊结果。深度感知双边模糊,软化贡献,宽的滤镜可隐藏图案,结合阴影来摊销成本。最终AO:
可扩展和实现:
- 设计扩展性。增加半径,更改衰减函数,强调接触阴影,更广泛的影响,变更申请,调制一切,调制环境。
- 质量扩展性。增加半径,增加样本数,更好的采样模式,更广泛的模糊。
当前的实现:全分辨率4-tap,围绕螺旋采样,围绕随机向量旋转,使用镜像tap,将半径限制为约20像素,转置数学以一次计算,以Blue/Alpha编码深度以进行模糊。
DX11的实现:9 个样本,程序螺旋采样模式,引入一些噪点,无半径夹紧,未夹紧半径的Mip深度,以Blue/Alpha编码深度以进行模糊。
Separable Subsurface Scattering是时任职于暴雪的Jorge Jimenez等人呈现的皮肤渲染和眼睛渲染,详细阐述了皮肤建模、SSS、SSSS及眼球的渲染技术。对于皮肤,扩散曲线如下图:
使用若干项高斯函数拟合皮肤的次表面散射:
任何信号都可以通过若干个有理项来拟合并重建,例如傅里叶、快速傅里叶(FFT)、球谐函数(SH)及此处的扩散曲线等。 可分离参数化配置文件和优化后的公式如下:
当光线在物体的薄部分内部传播时,就会发生半透明,光线行进的距离越远,衰减就越多,意味着下图中第一点的半透明性将低于第二点:
除了距离,另一个因素是到达物体背面的光,但不幸的是,此信息不可用(下图红圈),因为使用的是屏幕空间方法。
观察:背面信息未知,人体皮肤的半透明隐藏了高频细节,在人体皮肤中,反照率不会发生显着变化。假设:可以使用反转的正面法线作为背面的法线,可以使用前面的反照率值,作为背面的反照率值(下图)。
这些假设允许将所有数学简化为下图红圈所示,可以预先计算成一个简单的纹理。
预计算纹理效果如下:
float scale = 2e4 * (1.0 - translucency) / sssWidth;
float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0);
// 通过使用阴影贴图,计算在物体内部行进的距离.
float4 shadowPosition = mul(shrinkedPos, lightViewProjection);
float d1 = shadowMap.Sample(LinearSampler, // &#39;d1&#39; has a range of 0..1 shadowPosition.xy / shadowPosition.w);
float d2 = shadowPosition.z; // &#39;d2&#39; has a range of 0..&#39;lightFarPlane&#39;
d1 *= lightFarPlane; // So we scale &#39;d1&#39; accordingly:
float d = scale * abs(d1 - d2);
// 使用前面显示的预积分方程计算这个距离对应的颜色.
float dd = -d * d;
float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) +
float3(0.1, 0.336, 0.344) * exp(dd / 0.0484) +
float3(0.118, 0.198, 0.0) * exp(dd / 0.187) +
float3(0.113, 0.007, 0.007) * exp(dd / 0.567) +
float3(0.358, 0.004, 0.0) * exp(dd / 1.99) +
float3(0.078, 0.0, 0.0) * exp(dd |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|