|
Achieving the Best Performance with Intel Graphics Tips, Tricks, and Clever Bits阐述了在Intel的GPU芯片上进行性能优化的技巧。
高效的GPU编程需要充分利用管线,如IA软件堆栈中的优化、特定于应用程序和通用的,应用程序优化的最大影响。
GPU编程优化包含应用程序、驱动和GPU三层。
绘图调度和资源更新:注意已调度操作的内存访问模式,如三维/二维作业调度、状态/着色器更改、资源位置。
排序绘制调用时,影响因素最大的且具有相同资源的Draw Call排在一起。例如,RT影响最大,故而图下将0和1安排在一起;然后是引用了相同资源的(图中)。
图形只是谜题的一部分,独特的架构特征:动力与性能、内存层次结构,成对平台:中央处理器、系统存储器,其它制约因素:热量、功率等。
CPU/GPU之间的关系,CPU或GPU瓶颈,CPU可以限制GPU......
横坐标是总功率,竖坐标是各个硬件单元的功率。左:随着总功率的提升,GPU的功率先下降后提升;CPU相反,而未被使用的功率先略微提升,后下降。右:随着总功率提升,GPU频率先降低接着持平后提升,CPU则相反。
以上图表只针对2014年前后的Intel硬件架构,目前及其他GPU厂商是否依然如此有待查证。 缓存位置是王道:优化CPU和GPU的内存访问,内存带宽限制,层次结构因平台而异,可选的CPU+GPU缓存:末级缓存(LLC)、嵌入式DRAM(eDRAM)。
架构组件:
- 切片。
- 普通切片:光栅化、着色器调度、颜色后端
- 子切片:着色器执行
架构扩展/扩展组件:
- 切片:并行图元处理。
- 子切片:并行宽度(span)处理。
采样器:每个子切片1个采样器本地纹理缓存,由L3缓存作为后盾支持。
采样器性能:还记得缓存位置吗?吞吐量:格式、采样模式,糟糕的访问模式会增加内存带宽和延迟。
填充率:逐切片通用,如像素后端、颜色缓存(RCC$)。
填充率性能,输出颜色:
- 其它因素。
- 光栅化。
- 提前Z/S。
- 像素着色器执行。
- 后期Z/S。
- 混合函数+模式。
表面格式:为颜色范围选择适当的格式,中间/最终渲染目标,不必要选择更高精度的格式,否则会降低填充率,增加内存带宽。
算术逻辑:逐子切片的块,包含执行单位(EU)、指令缓存(IC$)。
算术逻辑性能:算法复杂性,如控制流、数学、扩展数学、最大并发寄存器数。
着色器优化:基于意图的最优代码,着色器伸缩,通用着色器的情况,产生未使用的产出。
几何着色器:单个非切片,固定函数:VS、HS、TE、DS、GS、SOL,剪切,设置前端。
优化几何以提高算法复杂度,单个几何体的最佳定义,基于平台的质量扩展,意图:灯光,深度,动画…
几何体的软边、硬边的做法和效果对比。
内存带宽:都是关于内存,因平台而异,关注的原因是从内存中读出,写入内存。
采样器吞吐量:不同的架构和平台,测量所有用例,包含维度数、格式、过滤模式。
填充率:多种表面类型,包含渲染目标:格式、维度、混合/非混合,深度:读/写,模板:读/写。
几何吞吐量:固定功能带宽与算术逻辑,固定功能(剪辑/剔除、光栅化),几何变换:ALU。
时间来到了2015年,这一年,以DirectX 12、Vulkan为代表的新一代图形API正式发布,紧接着,有不少文献阐述了它们的特点和应用。D3D12 A new meaning for efficiency and performance就是其中之一。文中对Command List、Root Signature、资源同步、屏障、并发、多线程、多队列、渲染应用和性能分析等等方面阐述得由浅入深、鞭辟入里,适合入手和进阶。想了解更多的可查阅原文或剖析虚幻渲染体系(13)- RHI补充篇:现代图形API之奥义与指南。
Visual Effects in Star Citizen分享了游戏星际公民的视觉效果,该游戏由CryEngine研发。
该游戏具有非常高端的视觉效果,长期关注质量,高系统规格,当时仅限DX11。舰体拥有复杂的网格数量、材质和贴花效果。
其中航舰的破坏效果的渲染流程如下:
添加破坏效果的具体步骤如下:
由于游戏中的MMO部分,需要很多环境,因此选择了模块化方法。模块化的“套件”易于组装,简化艺术管道(例如外包),对于关卡设计师来说非常灵活。
期间遇到了许多性能问题,由于期望的保真度、多边形数量、纹理密度太高,无法烘焙纹理,平铺纹理意味着每个网格有许多绘制调用,大量的网格来建造一个房间,空间站需要更多的网格和绘制调用。
纹理数组是一种潜在的解决方案,分辨率限制意味着流数据很困难,相反,只对LOD使用低分辨率纹理数组,无需流式传输单个纹理–256x256的整个纹理数组的级别小于15Mb,只需一次绘制调用即可渲染LOD!顶点缓冲区按材质ID排序,因此如果需要,仍然可以使用高分辨率纹理。
类似KillZone的网格合并解决方案,为每个单独的模块化资产构建LOD,迭代启发式算法,结合LOD,构建具有最小绘制调用和内存的层次结构。依赖于积极的LOD,无需艺术家手动工作即可大幅减少绘制调用。
Rendering the World of Far Cry 4分享了Fay Cry 4的综合渲染技术,包含材质、光照、植被、抗锯齿、地形等。
在光照方面,FC4支持天空遮挡、环境图、间接光照等特性。天空光使用Bruneton天空模型和Preetham太阳模型,生成三阶SH照明。对于天空遮挡,将直接天空照明与间接照明分离高分辨率“自上而下”天空遮挡,从高度场创建可见性二阶SH,使用了类似SSAO的方法来计算遮挡。
FC4为了让单个cubemap在一天中的每个时间都有合适的强度,对cubemap每帧进行重照明,主流程如下:
在间接光方面,使用延迟辐射传输体积[Stefanov2012],存储辐射传输信息的光照探针。目标是上一代和当前一代使用相同的光照探针,扩大间接照明的范围,通过将CPU工作转移到GPU来加快更新速度。主流程如下:
- 离线:烘焙探针,二阶SH中的辐射传输信息。
- CPU:流化探针数据,上传到GPU并更新页面表。
- GPU:计算辐射传输,将探针插入剪辑图并在延迟照明中采样。
其中在整个单元格列表执行辐射传输的过程如下:
植被是FC4的主要渲染关注点,使用了和以为完全完全不同的数据集。目标是近距离视觉逼真度,改进的LOD和替代物(imposter)以及各类模拟。其中骨骼和物理模拟的流程如下:
对于替代物(imposter),从九个角度截图,八根垂直于这棵树,另外一个自顶向下:
G-Buffer屏幕截图:捕捉反照率、法线和材质属性。
深度公告板:公告板几何图形细分为16x16网格,在GBuffer截图期间捕获深度,根据原始树深度置换顶点。深度数据和渲染图如下:
此外,还要为植被生成AO体积:尺寸范围从16x16x16到64x64x64,从体积周围的32个方向捕获阴影贴图,采样阴影和平均值。
从视觉上看,植被很难完全正确,模拟了部分特性:光在草叶间反射,光在树叶中散射等等,需要TA的魔法。
抗锯齿上,使用了HRAA,详见14.4.3.5 特殊技术的Hybrid Reconstruction Anti Aliasing部分。
SIMD at Insomniac Games分享了Insomniac公司的游戏所使用的SIMD技术,包含SSE、技巧、最佳实际等。
SIMD编程在Insomniac公司的工作室中有悠久历史,如PS2 VU、PS3 SPU+Altivec、X360 VMX128、SSE(+AVX),关注本周期的SSE编程,当PC+主机共享ISA时,更大的激励,当使用SIMD时,PC工作站的速度快得离谱,许多旧的最佳实践不适用于SSE。
当时的趋势是都是GPGPU,但许多问题太小,无法转移到GPU,并且不息在控制台上浪费x86内核。永远不要低估暴力+线性访问,CPU SIMD可以大大提高性能,不能只把性能留在PC上。当时SSE和AVX SIMD的选项有:
- 编译器自动向量化。乌托邦式的想法在实践中并不奏效,编译器是工具不是魔杖,在维护期间经常中断,只获得一小部分性能提升!编译器支持/保证=糟糕,VS2012中没有支持,VS2013中有些支持,不同的编译器有不同的怪癖。
- 英特尔ISPC。SSE/AVX类着色器编译器,编写标量代码,ISPC生成SIMD代码,需要在另一个抽象层次上进行投入,注意:容易生成低效的加载/存储代码。主要优点:SSE/AVX自动切换,例如英特尔的BCT纹理压缩器, 在AVX工作站上自动运行速度更快。
- 内部函数。掌握控制权,而不必去汇编,Insomniac游戏中编写SIMD的首选方式,可预测,没有无形的性能退化,灵活地公开所有CPU函数。难以学习和实践,但不是真正的反对理由,所有好的编程都很难(而糟糕的编程很容易)。
- 汇编。永远是一个选择!64位VS编译器上没有内联汇编,需要外部汇编程序(例如yasm)。对于初学者来说,有很多陷阱:在OS之间保持ABI可移植性很难、相对稳定(Non-volatile)的寄存器、64位Windows的异常处理、堆栈对齐、调试…
为什么SSE没有被更多地使用?对个人电脑领域碎片化的恐惧,每个x64 CPU都支持SSE2,但通常支持更多, “它不符合我们的数据布局”,传统上,PC引擎不太重,在OO设计中嵌入SIMD代码很尴尬,“我们试过了,但没用”。
// SSE版Vec4声明
class Vec4
{
__m128 data; // 有X/Y/Z/W,是4D向量
operator+ (…)
operator- (…)
};
// 【不正确】的SSE版Vec4点积
Vec4 Vec4Dot(Vec4 a, Vec4 b)
{
__m128 a0 = _mm_mul_ps(a.data, b.data);
__m128 a1 = _mm_shuffle_ps(a0, a0, _MM_SHUFFLE(2, 3, 0, 1));
__m128 a2 = _mm_add_ps(a1, a0);
__m128 a3 = _mm_shuffle_ps(a2, a2, _MM_SHUFFLE(0, 1, 3, 2));
__m128 dot = _mm_add_ps(a3, a2);
return dot; // WAT: the same dot product in all four lanes
}
// 【良好】的SSE版Vec4点积
__m128 dx = _mm_mul_ps(ax, bx); // dx = ax * bx
__m128 dy = _mm_mul_ps(ay, by); // dy = ay * by
__m128 dz = _mm_mul_ps(az, bz); // dz = az * bz
__m128 dw = _mm_mul_ps(aw, bw); // dw = aw * bw
__m128 a0 = _mm_add_ps(dx, dy); // a0 = dx + dy
__m128 a1 = _mm_add_ps(dz, dw); // a1 = dz + dw
__m128 dots = _mm_add_ps(a0, a1); // dots = a0 + a1
不要把时间浪费在SSE类上,试图用AOS数据抽象SOA硬件,注定是笨拙和缓慢的。SSE代码想要自由,实现无包装器或框架的最佳性能,只需根据需要编写小的助手例程。“它不符合我们的数据布局”,空粒子(float pos[3],…),存储在结构粒子{float pos[3];…},在SSE中使用粒子数组很难,所以避免这样做。保留spawn函数,更改内存布局,问题在于结构粒子,而不是SSE。内存中的结构粒子(AOS):
内存中的粒子(SOA):
数据布局选择:对于SSE代码来说,SOA形式通常要好得多,自然映射到指令集,SOA SIMD代码与标量参考代码紧密映射。AOS形式通常更适合于标量问题,尤其是对于查找或索引算法,单缓存未命中以获取一组值。如果需要,在转换中局部地生成SOA数据,通过调整输入/输出来平衡SIMD效率。
举个具体的案例——门。自动打开的门,当右翼演员“忠诚”在某个半径范围内时,想想《星际迷航》,典型博弈问题,最初是作为面向对象解决方案实现的,开始出现在性能雷达上,约100扇门 x 约30个角色测试 = 3000次测试!下面是有性能问题的版本及解析:
上图的原始更新中输入数据的内存关系如下:
SIMD准备工作:将门数据移动到中心位置,实际上只是SOA形式的一包价值观,很好的方法,因为门很少被创建和破坏,每个门都有一个进入中央数据仓库的索引,在更新中本地构建参与者表,每次更新一次,而不是100次, 隐藏在堆栈上的简单数组中(分配用于可变大小)。
// 门更新数据设计
// In memory, SOA
struct DoorData
{
uint32_t Count;
float *X;
float *Y;
float *Z;
float *RadiusSq;
uint32_t *Allegiance;
// Output data
uint32_t *ShouldBeOpen;
} s_Doors;
// On the stack, AOS
struct CharData
{
float X;
float Y;
float Z;
uint32_t Allegiance;
} c[MAXCHARS];
SIMD门新的更新可以一次完成所有的门,在内部循环中测试4个门和1个参与者,数据布局带来的巨大好处,所有的计算都会自然而然地以SIMD操作的形式出现。
// 外循环
for (int d = 0; d < door_count; d += 4)
{
// 加载4扇门的属性,清除4个“打开”累积器
__m128 dx = _mm_load_ps(&s_Doors.X[d]);
__m128 dy = _mm_load_ps(&s_Doors.Y[d]);
__m128 dz = _mm_load_ps(&s_Doors.Z[d]);
__m128 dr = _mm_load_ps(&s_Doors.RadiusSq[d]);
__m128i da = _mm_load_si128((__m128i*) &s_Doors.Allegiance[d]);
__m128i state = _mm_setzero_si128();
// 内循环
for (int cc = 0; cc < char_count; ++cc)
{
// 加载1个角色的属性,广播到所有4个线程(lane)
__m128 char_x = _mm_broadcast_ss(&c[cc].x);
__m128 char_y = _mm_broadcast_ss(&c[cc].y);
__m128 char_z = _mm_broadcast_ss(&c[cc].z);
__m128i char_a = _mm_set1_epi32(c[cc].allegiance);
// 计算角色和四扇门之间的平方距离
__m128 ddy = _mm_sub_ps(dy, char_y);
__m128 ddz = _mm_sub_ps(dz, char_z);
__m128 dtx = _mm_mul_ps(ddx, ddx);
__m128 dty = _mm_mul_ps(ddy, ddy);
__m128 dtz = _mm_mul_ps(ddz, ddz);
__m128 dst = _mm_add_ps(_mm_add_ps(dtx, dty), dtz);
// 对比开门半径和忠诚=>或进入状态
__m128 rmask = _mm_cmple_ps(dst, dr);
__m128i amask = _mm_cmpeq_epi32(da, char_a);
__m128i mask = _mm_and_si128(_mm_castps_si128(amask), rmask);
state = _mm_or_si128(mask, state);
}
// 为这4扇门存储“应该开门”,为下一组4扇门做好准备。
_mm_store_si128((__m128i*) &s_Doors.ShouldBeOpen[d], state);
}
内循环的汇编代码生成如下:
vbroadcastss xmm6, dword ptr [rcx-8]
vbroadcastss xmm7, dword ptr [rcx-4]
vbroadcastss xmm1, dword ptr [rcx]
vbroadcastss xmm2, dword ptr [rcx+4]
vsubps xmm6, xmm8, xmm6
vsubps xmm7, xmm9, xmm7
vsubps xmm1, xmm3, xmm1
vmulps xmm6, xmm6, xmm6
vmulps xmm7, xmm7, xmm7
vmulps xmm1, xmm1, xmm1
vaddps xmm6, xmm6, xmm7
vaddps xmm1, xmm6, xmm1
vcmpps xmm1, xmm1, xmm4, 2
vpcmpeqd xmm2, xmm5, xmm2
vpand xmm1, xmm2, xmm1
vpor xmm0, xmm1, xmm0
add rcx, 10h
dec edi
jnz .loop结果获得了20-100倍加速比,更可能的是,现在可以对数据进行推理,蛮力SIMD代表“合理的事物”,游戏中有很多“合理的”问题!删除缓存未命中+SIMD ALU可能是一个巨大的胜利,解决“千刀之死”(death by a thousand cuts)问题,这种类型的转换通常会让它远离雷达。
文中还例举了过滤数据的案例。
最佳实践:分支。一般避免分支,预测失误的分支在大多数H/W上仍然非常昂贵,不想在内部循环中很难预测分支,如果非常可预测,可以进行分支,分支应正确预测99%以上才能有意义,例如,数据海洋中的一些昂贵的东西。如果在SSE2上,请_mm_movemask_X(),还可以考虑m_testz_si128()和SSE4.1+。
分支的替代方案:GPU风格的“计算两个分支”+选择,用于许多较小的问题,每个问题单独输入数据+内核,尽可能产生最佳性能,考虑对索引集进行分区,运行fast内核将索引数据划分为多个集合,在每个子集上运行优化的内核,除非访问了大多数索引,否则预取可能很有用。
最佳实践:预取。对上一代硬件来说绝对必要,盲目地将其推广到x86不是一个好主意,准则:不要预取线性数组访问,在某些硬件上可能存在严重的TLB未命中成本机会,芯片已经在缓存级别免费预取。指南:可能预取即将发布的PTR/索引,如果知道他们之间的距离足够远/不规则,AMD/Intel之间的预取指令有所不同,仔细测试是否从所有硬件中受益。
最佳实践:展开。在VMX128/SPU样式代码中常见,为了让机器隐藏延迟,很有意义,也有很多寄存器!对于SSE/AVX来说,通常不是个好主意,只有16个(命名)寄存器——硬件内部有更多寄存器,无序的执行在一定程度上为你展开。指南:仅展开至整个寄存器宽度,例如展开2x 64位循环以获得128位循环,但不能再展开,可以根据需要对非常小的循环进行例外。
最佳实践:流式读写。一定要使用流式读取(>SSE 4.1)和写入,有助于避免缓存垃圾,特别是对于使用大型查找表的内核,但别忘了围栏!!针对不同体系结构的不同选项_mm_fence()总是有效,但速度很慢,流绕过了强大的x86内存模型,如果不加以限制,微妙的数据竞争就会发生。
结论:SIMD不是魔法,所有人都可以成为性能的英雄!小投资可以带来巨大的收益,现代SSE好处多多!
Strategies for efficient authoring of content in Shadow of Mordor分享了游戏Middle-earth: Shadow of Mordor中的内容创作策略和效率,包含同步、加载、性能、资产处理、内容依赖等内容。
随着游戏日益复杂,游戏的资源尺寸、数量、数据记录等都呈数十倍的增长曲线,其中纹理占据约55%,音频占据35%,其它约10%。而纹理和音频中,占用尺寸从高到低依次是动画、关卡、模型、数据记录、行为、特效、着色器。(下图)
文中采用了智能加载的策略,检查源文件格式,强调最小占用、最大加载速度,如数据进入内存(磁盘)的速度有多快,数据的解释速度有多快(CPU)等。LTA(LTA–Lith Tech ASCII)源文件格式,类似xml的文本,人类可读、文件大、解读缓慢,使用编码、压缩的ASCII码,磁盘上更小,更快地进入内存,解读速度较慢。压缩二进制表示法,磁盘上更小,速度快得多,无需解析文本,独立压缩的文件树根(Zlib),并行或部分加载/解压缩,CRC检查暴露文件损坏,提供转换为人类可读格式的实用程序。对于压缩格式,占用小10倍,加载快10倍!
按需加载:在需要之前不加载大多数数据,需要用户操作来提示额外加载,非常适合独立的工作流程,树控制对此很有效。需要适当的源文件粒度,很难处理单个文件。延迟加载时间(快15倍):
在后台加载,仅提前加载部分数据,让用户在加载初始块后开始编辑,需要数据,但不是立即需要,如有必要,请用户屏蔽,例如视觉辅助、Visual Studio智能提示。后台加载可以提升5倍的速度。
千刀之死:需要加载大量小文件,硬盘在这方面的性能很差,异步IO将有所帮助,建立一个“检查点”,单个文件,压缩后占用最小,在其过期的情况下进行修补,90k文件/800m到30m的检查点。
磁盘SSD可以提速序列化,但容量更小。CPU线程池实现并行化,不适用于任何地方,复杂有开销。
线程池可以减少CPU等待时间,可以提高线性速度,如果合适容易丢弃,缺点是不会抵消糟糕的算法选择,核心竞争,更复杂,只和最慢的作业一样快。
文中提及的资源构建管线如下:
还采用了数据继承:
Piko: A Framework for Authoring Programmable Graphics Pipelines分享了一种可编程的GPU渲染管线。文中提到当前市面上高效的图形管线有:
渲染器 | 平台 | 算法 | Unreal Engine 4 | GPU | 延迟着色的光栅化 | Unity 5 | GPU | 前向和延迟着色的光栅化 | Disney Hyperion | 多核CPU | 延迟着色的路径追踪 | Pixar RenderMan | 多核CPU | 光线追踪的Reyes | Solid Angle Arnold | 多核CPU | 路径追踪 | Media Molecule Dreams | GPU | 基于点的延迟着色渲染 | 但问题是高效的图形管道实现很难编写,设计空间也很难探索。
GPU上的软件光线有:
引入灵活的图形管线,在类型中抽象各个阶段,通过队列抽象通信。
高性能的基础是并行、执行局部性、数据局部性、生产者消费者局部性。而Piko框架真是解决以上问题的桥梁:
Piko的运行流程如下:
管线中的每个阶段都有三个步骤:
Piko管线易于表达和定制:
利用空间分块,可以提升并行度和局部性,从而提升图形管线效率:
Rendering the Alternate History of The Order: 1886分享了游戏The Order: 1886的渲染迭代历程。
该游戏引擎使用了深度预通道、顶点法线和速度,按分块列表计算,使用深度缓冲区剔除,透明材料的单独列表,异步计算->基本免费,更低开销的MSAA。生成每材质像素着色器,完全优化的材质+照明管道,更难手动优化所有案例。存在一些GPR问题,照明所用参数的函数。
造成画面闪烁的原因有高频信号、欠采样(时间或空间)、移动采样、不良的过滤方法(需要考虑频率响应,可以使用后处理抗锯齿)。
重建过滤器是重采样(上采样、下采样、过采样)的重要部分,生成输出(影响渐变、影响感知的细节、影响锯齿)。有时无法选择重建过滤器,因为它是整个系统固有的东西,其中一个例子是显示器。显示器会采集离散采样信号,并将其转换为连续信号。通过查看屏幕上的实际物理模式,可以了解正在使用什么样的过滤器。在LCD显示器中,矩形像素图案的普遍使用是我们有时认为像素是“小正方形”的原因之一。
以下是常见的几种重建过滤器,分别是盒子、三角形和Sinc:
锯齿的来源:光栅化(几何锯齿)、镜面反射(镜面锯齿)、阴影、纹理、SSAO、后处理特效和采样!
几何锯齿的来源:几何采样不足(通常频率非常高,无法预过滤三角形),光栅化器是一个阶跃函数,二进制-开/关,丑陋的楼阶梯图案,相机移动时的时间瑕疵,改变覆盖范围=闪烁!可以用MSAA进行过采样。
镜面锯齿的来源:低粗糙度=非常高的频率,移动采样点将闪烁,采样不足的几何体+法线贴图变得更糟,可以使用LEAN/CLEAN/Toksvig进行预过滤,很难解释所有法线方差的来源,但你绝对应该这么做!
The Order使用的抗锯齿是EQAA(2倍片元、4倍覆盖率、质量与性能/内存的平衡)、自定义解析(高阶过滤、用一些细节换取稳定)、TAA(进一步减少闪烁,与MSAA解决方案整合)。
使用了MSAA的中间通道有:延期贴花,累积到非MSAA RT;低分辨率透明和AO,放大每深度子样本,合成一次;Alpha测试,按深度子样本测试,或使用A2C;景深,使用所有子样本中最小的CoC。
带MSAA的DOF。
MSAA自定义解析:使用计算着色器而不是硬件解析,样本颜色片元和覆盖率样本,2像素宽的立方滤波器,覆盖相邻像素的子集,没有负波瓣–HDR的振铃(ringing)太多。更多请参阅:MSAA Resolve Filters。
The Order想要选择更宽、更平滑的重建滤波器,对比了点、盒子和高斯重建滤波器之后,最终选用了后者,因为它更宽更平滑,在时域中转换为更平滑的过渡,从而提供更好的稳定性。
HDR的MSAA:非线性色调映射,在极端情况下“杀死”AA,夹紧(clamp)特别糟糕!需要在postFX之后进行色调映射,后处理需要HDR,昂贵的解决方案:MSAA分辨率的后处理,在色调映射后立即解析。廉价的解决方案:色调映射子样本,解析,然后逆转色调映射,复杂的运算符不容易逆转。更便宜的解决方案:使用简单运算符的近似色调映射(Reinhard),基本上按1 / (1 + Luminance)来计算样本权重,好处是抑制小高光以减少闪烁。
左:HDR下的MSAA;右:逆转亮度过滤器。
还需要需要考虑曝光!不想过度渲染高光,以更好地匹配最后的色调映射步骤,仍然可以将曝光偏移1或2档,平衡过度压暗和抑制高光。
左:逆转亮度过滤器,中:逆转亮度过滤器(曝光+10),右:修复的逆转亮度过滤器(曝光+10)。
// 解析过滤样本代码
float3 sample = InputTexture.Load(uint2(samplePos), subSampleIdx).xyz;
float weight = Filter(sampleDist); // Bicubic, Gaussian, etc.
float sampleLum = Luminance(sample);
sampleLum *= exposure * exp2(ExposureOffset); // ExposureOffset ~= -2.0
weight *= 1.0f / (1.0f + sampleLum);
sum += sample * weight;
totalWeight += weight;TAA的主要目标是减少镜面闪烁,预过滤还不够,主要启发是TXAA、SMAA 1TX[Sousa13]、Dust 514[Malan12]、Killzone: Shadow Fall [Valient14]。积累多个样本,指数移动平均数,使用速度缓冲区重新投影前一帧,使用最小/最大邻域进行加权和夹紧,MSAA解析期间计算的最小/最大值,没有抖动(主要是引擎团队没有时间集成,不管怎样,摄像机总是在移动)。
后处理AA锐化:宽的解析导致“柔和的外观,与视觉风格一致,可以增加后处理AA锐化,不锐化的遮罩非常简单,但注意不要太极端!
从左到右的锐化值是0.0、0.5、1.0。
阴影:16个聚光灯阴影投射,带4个级联的1个平行光,最多支持2个平行光。保持简单:1个用于聚光灯的纹理数组,1个用于级联,前向通道中采样。缓存聚光灯阴影,基于距离的不同频率更新,如果没有移动,就不会重新生成阴影。
预计算阴影可见性:用于主要可见性的系统扩展,在关卡构建期间预计算,仅适用于非移动光源。对于聚光灯,从光源POV栅格化网格ID,使用模板标记投射者(计数>=2)。对于平行光,在光源空间中将关卡拆分为NxN的tile,确定潜在的阴影投射者,对于每个摄影机采样点:渲染摄影机可见网格,渲染潜在的阴影投射者,如果模板值>=2,则添加到最终阴影可见性列表中。
用于所有阴影的EVSM,好处是没有偏倚、更少条纹,硬件过滤(三线性、各向异性),预过滤非常适合正向渲染,有助于降低GPR压力,非常适合缓存。
采样了SDSM来实现CSM,分析深度缓冲区,基于可见曲面约束级联。计算视图空间的的最小/最大XYZ,带优化原子的单通道,理想情况下,逐联计算光源空间中的AABB。在回读时附带1帧延迟,可以使用GPU路径,减少瑕疵的“时间扭曲”技巧:计算每个像素的速度,预测下一帧位置,展开AABB以覆盖下一帧位置。不稳定,需要进一步探讨。
左:普通CSM,右:SDSM。
文中还探讨了一种改良的贴花渲染方法——混合贴花:延迟通道累加,使用深度进行投影,添加混合到fp16渲染目标中,主前向通道读取贴花缓冲区,修改材质属性。
Far Cry 4 and Assassin’s Creed Unity: Spicing Up PC Graphics with GameWorks由Nvidia呈现,讲述了Far Cry 4和刺客信条两款游戏利用GameWorks在PC上增强了不少图形特性,如HBAO+、PCSS、TXAA、角色渲染、光照等。
Far Cry 4的喜马拉雅山脉场景。
刺客信条的中世纪宏伟场景。
NVIDIA GameWorks包含多个组件,如ShadowWorks、PostWorks、Godrays、HairWorks等等。其中,ShadowWorks和PostWorks非常适合Far Cry 4和刺客信条,HairWorks发型和Godrays非常适合Far Cry 4。
NVIDIA ShadowWorks由不同的技术组成,以提供电影级阴影、HBAO+、高级软阴影等。基于地平线的环境遮挡+(Horizon-Based Ambient Occlusion+,HBAO+)是当时最先进的SSAO方法,有最佳性能,可伸缩。调整HBAO+:半径用HBAO内核的大小,偏移隐藏低细分瑕疵,指数遮挡衰减,细节遮挡是高频遮挡组件的权重,粗糙遮挡是低频遮挡分量的权重。
高级软阴影:最先进的软阴影,基于更近的软阴影百分比(PCS),支持级联阴影图,简单但功能强大的界面。调整高级软阴影的参数:光源尺寸、最大阈值、最低百分比、混合百分比、边界百分比。修复漏光:当灯光尺寸过大时,会发生灯光泄漏,PCSS内核太宽,在级联之外采样,调整边界百分比以限制内核,如果仍存在漏光,需减小灯光大小和最大阈值。
时间抗锯齿(TXAA)专为减少时间锯齿而设计的胶片式抗锯齿技术,NVIDIA PostWorks家族成员。
NVIDIA HairWorks使用户能够模拟和渲染毛发,以提供真正的交互式游戏体验,运行时库和内容创建工具的组合。
HairWorks集成流程:
HairWorks允许自定义着色模型,支持正向着色和延迟着色。在Far Cry 4中,依靠Dunia渲染机制来执行着色,使用定制材质,HairWorks参数存储在GBuffer中。
HairWorks的GBuffer数据:压缩的漫反射、法线、镜面指数及缩放、切线、最终成像。
抗锯齿:最好的解决方案是在一个单独的、启用抗锯齿的过程中渲染HairWorks毛发。在Far Cry 4中,毛发在主管线中渲染,并依赖于全局抗锯齿,以对抗闪烁。
左:无AA,中:4xMSAA,右:4xTXAA。
NVIDIA Godrays可以渲染出逼真的太阳光束,巨大的调整空间,可扩展的性能,首次整合使用了游戏中的烟雾颜色,使用太阳色显示出最好的效果。
Godrays需要找到平衡点:场景看起来完全模糊了,事实上,只是增加了太多的密度,使光线强度依赖于白天
14.4.3.2 光影技术
2010年,A Real Time Radiosity Architecture for Video Games阐述了Frostbite引擎实现的实时辐射光照的架构,包含介绍Enlighten的概述、架构及如何集成到Frostbite中。
文中提到Enlighten有4个特点:分离的光照管线、带回馈的单次反馈、光照图输出和来自目标几何体的重建光照。Enlighten的管线如下图,预计算阶段包含分解场景到系统、投影细节几何到目标几何以重建光照、提取目标几何以实时计算辐射,运行时阶段包含GPU渲染直接光、CPU异步生成辐射、在GPU组合直接和非直接光。
运行时的管线如下图,
上图涉及的各个节点和最终组合效果如下系列图:
光照图输出如下所示:
Frostbite集成Enlighten的因素包含工作流和工作时间、动态环境、灵活的架构。Frostbite的预计算流程如下:
- 收集静态和动态物体。静态物体接受和反弹光照,而动态物体只接受光照。
- 生成辐射系统。并行处理和更新,输入依赖关系控制光传输,用于辐射粒度。
- 参数化静态几何。静态网格使用目标几何,利用目标几何图形来计算辐射,投影细节网格到目标网格以获得uv,系统打包成单独的uv图集。
- 生成运行时数据。每个系统一个数据集(流友好),使用Incredibuild的XGI进行分布式预计算,数据只依赖于几何形状(不是光或反照率)。
渲染时,分离直接光照和辐射度光照,CPU计算辐射度,GPU计算直接光。Frostbite使用延迟渲染,所有光源都可以动态反弹辐射度。分离光照图和光照探针渲染,光照图在前向Pass中渲染,光照探针被添加到3D纹理中,并在延迟渲染中执行。运行时管线分为三步:
- 辐射度Pass(CPU)。更新非直接光照图和光照探针,将光照探针注入到3D纹理中。
- 几何Pass(GPU)。增加非直接光照图到单独的GBuffer中,使用模板缓冲遮蔽掉动态物体。
- 光照Pass(GPU)。渲染延迟光源,从GBuffer增加光照图,从3D纹理中增加光照探针。
以上几个阶段的效果图如下:
无独有偶,Pre-computing Lighting in Games也探讨了游戏引擎中的预计算光照。文中提到使用烘焙光的原因有3个:
- 光照工作流。烘焙照明是一种让艺术家访问全局照明 (GI) 的方式,根据实际光源定义照明,没有人工补光灯,将光照与几何体/材质分离。烘焙照明还允许更丰富的光源集,基于物理的软阴影,阴影投射HDR光探头。
- 质量。允许最高质量的光模拟算法,GI效果,多次反弹,允许高质量的直接照明。
- 性能。运行时性能非常好,独立于灯光设置,独立于GI算法,好看的光照贴图与不好看的光照贴图具有相同的性能。
- 性能可预测。运行时性能往往非常强大,艺术家可以根据需要添加任意数量的灯光,实时阴影贴图性能和GI难以预测,光照角度和位置影响阴影渲染的性能,玩家位置会影响灯光需要的分辨率。
- 性能可伸缩。可在Quake 1、手持设备、高端游戏中使用。
烘焙光面临的挑战有:
- 更改灯光设置。可以烘焙一天中的不同时间,如果引入更多可变灯,则组合会爆炸。可将移动和强度变化的灯光视为普通运行时灯光,适合组合爆炸或闪烁的灯光,没有间接照明。
- 移动/变形几何。区分局部和全局更改,局部的包含在房间里移动的角色、小家具、弹孔,全局的包含被毁的建筑物、被毁的墙壁。
局部几何体改变时存在两个问题:物体如何受环境影响?物体如何影响其环境?
朴素的方法只是为移动的物体添加直接照明,但会使使角色看起来格格不入,同样在没有直射光的区域,角色是完全黑色的。Light probes优雅地解决了这些问题,并为照亮角色和其它移动物体提供了一个很好的管线(下图)。一些游戏将关键灯置于光探头之外,并将它们添加为更传统的直射灯。
对于移动物体上的入射光,在房间里烘焙光照探头,使用最近的来照亮物体,将入射照明近似为整个物体的一个光照探头,与环境相比,适用于较小的物体,非常大的物体可能需要特殊处理。编码可采用球面谐波(通常为3阶),每个面使用1个像素的立方体贴图,只有单一的环境色。
移动物体也可以影响环境,光照探头中的直接照明照明可选,允许对动态对象进行自阴影,允许对象在环境上投射阴影,也可以从光探头中提取最强的光方向,也可能可以提供间接照明的自我阴影,有些方法只为环境上的角色阴影很重要的灯光烘焙间接光,角色的间接照明通常微不足道。
对于全局几何体改变,高动态游戏倾向于避免全局烘焙光照,其它子系统也倾向于依赖静态几何或在静态几何上表现更好(路径寻找,碰撞检测,游戏故事通常需要玩家遵循某些路径)。
- 内存占用。照明是全局性的,包含材质纹理(实例共享材质纹理,多个对象可以共享纹理,纹理可以平铺和镜像)、照明纹理(每个实例必须是唯一的,不能平铺、镜像等,可根据分辨率要求优化分辨率)。
法线贴图非常适合增加几何细节级别,法线贴图在光照中引入高频细节,高频照明需要高纹理分辨率。
对于定向光照图,细节在几何体中,而不是在入射光中,将每个纹素的入射光半球存储在光照贴图中,允许近似不同法线方向的照明。定向光照图的典型编码有辐射度法线贴图 (RNM)、SH(一般为 2 个波段,4 个分量)、每像素环境光和定向光、SH基,允许使用真正的BRDF,半球会模糊的,但也可以从中获得合理的镜面反射效果。
上:烘焙光;中:法线贴图;下:低分辨率定向光照贴图与法线贴图相结合。
- 灯光重建时间。可以采用混合的方案:
- 仅烘焙间接照明。间接光通常比直接照明更平滑,锐利的阴影需要更高的纹理分辨率。
- 太阳的特殊处理。阳光往往是对户外场景影响最大的光线,阳光直射通常是锐利阴影和动态范围差异的来源,仅从太阳烘焙间接光,直接光作为运行时光添加。
该文提出的烘焙光管线如下:
管线的影响有:
- 光源构建阶段可能很耗时。在CPU小时为单位的数量级,取决于算法、分辨率、关卡大小、灯光设置、反弹次数等。
- 加快速度的工具,选择性灯光构建,预览质量构建,预览工具,相机渲染工具,渐进式光照贴图生成,分布。
- 自动重建以确保照明始终是最新的。
- 用于管理GI特定光源属性的工具。直接和间接照明的比例因子,以放大和分离光的贡献。
- 用于管理GI特定材质属性的工具。在屏幕上产生良好发光效果和正确外观的东西并不一定会在环境中产生所需的光发射,增加或减少场景的整体反射率。
- 纹理烘焙形状需要唯一的UV。可以在一定程度上实现自动化,易于展开的内容更可取,如果可能,将细节保留在法线贴图层中。
- 顶点烘焙很常见。由于纹理分辨率不足而没有接缝,法线贴图和定向光照贴图有助于在低分辨率光照下提供细节,不适合多边形内的阴影和其他照明不连续性。
Real-time Diffuse Global Illumination in CryENGINE 3提出了级联光照传播体积(Cascaded Light Propagation Volumes,CLPV)的技术。CLPV的核心思想在于:
1、采样照明表面,将它们视为辅助光源。为GI采样场景时,使用面元(又名point、disk, Surfel == 表面元素),所有光照面元都可以在光源空间中展平为2D映射图,使用反射阴影贴图(RSM)进行照明,RSM是在GPU上对光照面元进行采样的最快方法,甚至过度快O_O!
2、将样本分簇成一个统一的粗糙3D网格(grid),累加并平均每个单元格(cell)的辐射亮度(Radiance)。分簇面元时,以虚拟点光源(VPL)表示的光照面元,将每个面元分布到最近的单元格中(类似于PBGI, light-cuts and radiosity clustering),将所有VPL转换为输出辐射分布,以较低频带的球面谐波表示,在拥有者的单元格的中心进行累加,使用光栅化完全在GPU上完成。
3、迭代地将辐射亮度传播到相邻单元格(仅适用于漫反射)。RSM是一组从灯光位置规则地采样的场景VPL,通过规则网格和SH离散地初始VPL分布,将光照从一个单元格迭代传播到另一个单元格。(下图)
跨3D网格的局部单元格到单元格的传播,类似于参与媒体照明的SH离散坐标法[GRWS04],6个轴向方向,轮廓面作为传播波前(wave front),将得到的SH系数累加到目标单元格中以进行下一次迭代。
4、用生成的网格点亮场景。使用LPV进行最终场景渲染,使用硬件三线性插值在特定位置查找生成的网格3D纹理,将辐照度与被照表面法线的余弦波瓣进行卷积,应用抑制因子(dampening factor)以避免自溢出(self-bleeding),计算朝向法线的方向导数,基于与强度分布方向的梯度偏差进行抑制。
注入后迭代8次的效果:
为了稳定光照结果,可以采用以下方法:
- 空间稳定。将RSM捕捉一个像素以进行保守光栅化,通过一个网格单元捕捉LPV以实现稳定注入。
- 自发光(Self-illumination)。在RSM注入期间偏移半单元格VPL到法线方向。
- 时间连贯性和重投影。对RSM注入执行重投影的时间SSAA。
此方法的局限性:仅漫反射相互反射,稀疏空间和低频角度近似(光扩散:光传输溅射在各个方向,空间离散化:对于遮挡和非常粗糙的网格可见),次级AO信息不完整。
可以采用多分辨率方法,以不同的分辨率渲染多个嵌套的RSM,受级联阴影贴图技术的启发,在GPU上模拟不均匀的多分辨率渲染,根据对象的大小将对象分配到不同的RSM。将RSM注入相应的LPV,创建绑定RSM视锥体的嵌套LPV网格,独立进行传播和渲染,从内部LPV传播到外部。
LPV还可以扩展到:
- 透明物体。
- 用于大规模光照近似的光照缓存,将分析辐射注入被光线覆盖的网格单元。
- 具有附加遮挡网格的二级遮挡,使用相同的技巧可以多次反弹。
- LPV中部分匹配的光泽反射。
- 参与介质照明,来自传播过程的本质。
CLPV效果这么好的原因有:
- 人类对间接照明的感知。对接触照明非常敏感(角落、边缘等),间接照明主要是低频,即使是间接阴影,平滑渐变而不是阴影中的平坦环境,近似为参与媒体的扩散过程。
- 级联:基于重要性分簇。发射器根据其大小分布在级联中。
离线的PBRT和实时的LPV的对比如下图,两者差异不太明显:
光照图、PRT、LPV在图像质量、内存、动态光照支持、动态物体支持、次级遮蔽、多反射、区域光等参数的对比如下表:
LPV还可以和其它技术相结合:
- 与SSAO相乘以添加微遮挡细节。
- 延迟环境探针。结合后可增强远距离GI。
- 间接光源和延迟光源。在某些地方使用间接光灯模拟GI,对GI风格化的艺术家很重要。
总之,LVP是全动态方法,改变场景/视图/照明,GPU和控制台友好,极快(在PlayStation 3上大约需要1毫秒/帧),符合生产要求(用于实时调整的丰富工具集),高度可扩展,与质量成正比,稳定、无闪烁,支持复杂的几何形状(例如树叶)。
Physically-Based Shading Models in Film and Game Production是Naty Hoffman等人在Siggraph上分享的PBR实时化的演讲,演讲中详细地阐述了PBR的物理理论基础和数学化建模,以及如何在GPU中实现出来。
不同物质对光的吸收和散射的表现。
基于微观几何建模的光照模型。
漫反射和次表面散射的转变关系。
经典的Cook-Torrance BRDF公式。
Crafting Physically Motivated Shading Models for Game Development也是Naty Hoffman的演讲,涉及了PBR在游戏引擎的实现、改进和优化等内容。文中说到使用PBR的原因有:更容易实现照片写实/超写实,在照明和观察变化下保持一致,更少的调整和“捏造因素”,为艺术家提供更简单的材质界面,更容易排除故障,更容易扩展。
PBR需要一些前置基础,包含伽玛校正渲染、支持HDR值,良好的色调映射(最好是电影)。伽玛校正渲染的特点是着色输入(纹理、浅色、顶点颜色等)自然创作、预览和(通常)使用非线性(伽马)编码存储,最终帧缓冲区也使用非线性编码,这样做是有充分理由,感知一致等于有效使用比特,还有历史遗留原因(如工具、文件格式、硬件)。
如果着色默认为Gamma空间,着色结果不正确,产生“1+1=3”的效果:
高动态范围 (HDR) 可以产生逼真的渲染,但需要处理远高于显示白色 (1.0) 的值,着色前:光照强度、光照贴图、环境贴图,着色产生影响光晕、雾、景深、运动模糊等的高光,存在廉价的解决方案。
文中对比了Phong和Blinn-Phong的效果,发现在某些情形Blinn-Phong的效果更真实:
文中提到对镜面高光,除了菲涅耳项之外,还引入了归一化因子(\alpha_p+2)/8,归一化因子非常重要,若没有它,镜面反射亮度会从4倍太亮到数千倍太暗,具体取决于\alpha_p的值,误差如此之大,菲涅耳因子变得无关紧要。没有归一化使得创建看起来逼真的材质变得非常困难,尤其是当每个像素的\alpha_p变化时。下面分别是有无归一化的曲线和效果对比图:
笔者的另一篇文章已经详细深入地探讨过PBR:由浅入深学习PBR的原理和实现。
OIT And Indirect Illumination Using Dx11 Linked Lists和Real-Time Order Independent Transparency and Indirect Illumination Using Direct3D 11讲述了使用DirectX 11的特性来实现间接光的效果。文中提到了没有间接阴影的间接光方案:
1、绘制场景G-Buffer。
G-Buffer需要允许重建:世界/相机空间位置、世界/摄影机空间法线、颜色/反照率,DXGI_FORMAT_R32G32B32A32_FLOAT位置可能需要用于间接阴影的精确光线查询。
2、绘制反射阴影图(RSM)。RSM显示从光源接收直射光的场景部分。
RSM需要允许重建:世界/相机空间位置、世界/摄影机空间法线、颜色/反照率,仅绘制间接光源的发射器,间接阴影的光线精确查询可能需要DXGI_FORMAT_R32G32B32A32_FLOAT的位置。
3、以1/2的分辨率绘制间接光缓冲区。RSM纹素用作G-Buffer像素上的光源,用于间接照明。步骤如下:
- 延迟渲染1/2分辨率的间接光(IL)。
- 将G-Buffer像素转换为RSM空间。
- G-Buffer像素的空间转换顺序:Screen Space -> Light Space -> 投影到RSM纹素空间。
- 使用RSM纹素的内核作为光源。
- RSM纹素也称为虚拟点光源 (VPL)。
- 内核大小取决于所需速度、想要的效果外观、RSM分辨率。
在G-Buffer的一个像素上计算IL,然后累加内核中所有VPL的贡献:
下面的计算项与辐射度形状系数(form factor)计算中使用的项非常相似:
平滑IL的简单解决方案需要考虑四个中心位于t0、t1、t2 和t3的VPL内核:
大的VPL内核的计算速度很慢:
可以采用下图的技巧:
4、上采样间接光 (IL)。
间接光缓冲为1/2的分辨率,执行双边上采样步骤,结果是全分辨率的IL。
5、绘制添加IL的最终图像。
组合直接照明、间接照明和阴影。
左:没有间接光;右:组合了间接光。
添加间接阴影的步骤:
- 使用CS和链表技术。
- 将IL的遮挡几何图形(使用遮挡者的三角形)插入到3D列表网格中,查看备用数据结构的备份。
- 再读取一个VPL的内核。
- 只累加被遮挡者三角形遮挡的VPL的光。
- 通过3d网格追踪光线以检测被遮挡的VPL。
- 仅渲染低分辨率缓冲区。
- 从IL缓冲区中减去被遮挡的间接光。
- 使用了低分辨率遮挡的IL的模糊版本,模糊是双边模糊/上采样的组合。
上排:3D网格、非直接光缓冲区、被遮挡的非直接光;下排:非直接光缓冲区、减去被遮挡的非直接光、最终成像。
Uncharted 2: Character Lighting and Shading阐述了神秘海域2使用的角色渲染技术,包含皮肤、头发、布料等材质的渲染。
神秘海域2中不同角色的渲染效果。
其中皮肤采用了次表面散射模型。其中下图是NV使用了纹理空间的模糊来近似次表面散射效果:
然后通过RGB分量各不相同的卷积核来累加获得最终的次表面散射效果:
// 直接光:像素自身的权重最大,并且B > G > R.
diffColor = direct*float3(.233,.455,.649);
// 散射光:lm1~lm5就是上图模糊后的纹理.
diffColor += lm1 * float3(.100,.336,.344);
diffColor += lm2 * float3(.118,.198,.0);
diffColor += lm3 * float3(.113,.007,.007);
diffColor += lm4 * float3(.358,.004,.0);
diffColor += lm5 * float3(.078,0,0);
从左到右:仅直接光、仅散射光、组合了两者。
由于NV在模糊的过程使用了太多通道,性能和销毁无法满足要求,为此,神秘海域2采用了12-Tap的近似方法:
12-Tap抖动的权重如下:
float3 blurJitteredWeights[13] =
{
// 像素自身的权重.
{ 0.220441, 0.437000, 0.635000 },
// 12-Tap的权重.
{ 0.076356, 0.064487, 0.039097 },
{ 0.116515, 0.103222, 0.064912 },
{ 0.064844, 0.086388, 0.062272 },
{ 0.131798, 0.151695, 0.103676 },
{ 0.025690, 0.042728, 0.033003 },
{ 0.048593, 0.064740, 0.046131 },
{ 0.048092, 0.003042, 0.000400 },
{ 0.048845, 0.005406, 0.001222 },
{ 0.051322, 0.006034, 0.001420 },
{ 0.061428, 0.009152, 0.002511 },
{ 0.030936, 0.002868, 0.000652 },
{ 0.073580, 0.023239, 0.009703 },
}
12-Tap有两种实现方法:
- 分离模糊(Separate Blur)。
- 渲染到光照贴图。
- 12-Tap模糊光照贴图。
- 渲染最终场景。
- 组合模糊。
- 渲染到光照贴图
- 渲染最终场景,从光照贴图中使用12-Tap采样。
以上两种方法都有不错的渲染效果:
左:分离模糊;右:组合模糊。
Uncharted 2还尝试了弯曲法线(Bent Normal),以伪装R/G/B各分量来自不同的法线,R更接近几何,G/B更接近法线贴图(下图),漫反射计算3次。
但是,对R/G/B使用不同的法线似乎会导致一些蓝色斑点:
出现蓝色斑点的原因是点积在极端角度,可能会遇到diffuseR=0和diffuseB=1的情况,或相反亦然。
另一种方法是混合法线(Blended Normal),对几何和法线映射法线进行漫反射计算,并在它们之间从几何法线中获取更多红色,从法线映射法线中获取更多绿色/蓝色。具体做法是:Diffuse(L, G)、Diffuse(L, N) 然后Lerp,蓝色/绿色保持不变、红色溢出,可以有红色但没有蓝色/绿色,不能有蓝色/绿色但没有红色。这种方法可以显著降低蓝色斑点:
在头发渲染上,Uncharted 2使用了Kajiya-Kay的光照模型,实现的细节和特点如下:
- 轻微环绕漫反射(Slight Wraparound Diffuse)。
- Kajiya-Kay镜面反射。
- 没有自阴影。看起来像具有额外偏差的最大级联。
- 漫反射贴图作为高光遮罩。部分去饱和。
- 使用Blinn-Phong的延迟光照。
在布料上,Uncharted 2使用了边缘光 + 内部光 + 漫反射的组合:
布料光照。从左到右:边缘光、内部光、漫反射、组合光。
布料光照的伪代码如下:
VdotN = saturate( dot( V, N ) );
Rim = RimScale * pow( VdotN, RimExp );
Inner = InnerScale * pow( 1-VdotN, InnerExp );
Lambert = LambertScale;
ClothMultiplier = Rim + Inner + Lambert;
FinalDiffuseLight *= ClothMultiplier;
但以上布料的实现方式忽略了光线方向,因此布料光照不能随光源方向的改变而改变。
CryENGINE 3: reaching the speed of light主要是CryENGINE 3在控制台上纹理压缩和延迟照明的改进。
纹理压缩改进:
- 颜色纹理。创作精度、最佳色彩空间、DXT块压缩的改进。
建议根据直方图选择正确的颜色空间,按照经验是如果75%以上的像素高于中值(线性空间是116/255=0.45),则使用线性空间。
- 法线贴图纹理。法线精度、3Dc法线贴图压缩的改进。
以前,艺术家将法线贴图存储到8bpc纹理中,导致法线从一开始就被量化了!将工作流更改为始终导出16bpc法线贴图!修改工具以默认导出,对艺术家透明。
上:8bpc法线纹理;下:16bpc法线纹理。显然后者的高光更细腻平滑。
可以改进用于法线贴图的3Dc编码,3Dc比ARGB8好很多,在多数GPU上以16位精度生成插值!
常规的3Dc编码器:将x和y独立压缩为两个alpha通道——不将x-y视为法线!
建议改进3Dc编码器:将两个alpha块视为一个整体x-y法线,计算正常而不是“色差”的误差:
$$
\triangle N = \arccos\bigg(\cfrac{(N_c \cdot N)}{||N_c|| \ ||N||} \bigg)
$$
为了加速压缩,可以采用自适应方法:压缩为2个alpha块,测量法线的误差。如果误差高于阈值,则运行高质量编码器。
a:原始纹理;b:常规编码;c:建议编码;d:误差。
CryEngine 3的遮挡剔除使用软件z缓冲区(又名覆盖缓冲区),步骤如下:
- 在控制台上缩小前一帧的z缓冲区。使用保守遮挡避免错误剔除。
- 创建mip并使用分层遮挡剔除,类似于Zcull和Hi-Z技术,使用AABB和OOBB测试遮挡。
- 在PC上:手动放置遮挡物并在CPU上光栅化,CPU和GPU之间的延迟使z缓冲区无法用于剔除。
SSAO的改进包含将深度编码为2通道16位值 [0;1],作为有理数的线性深度:depth=x+y/255。以半屏分辨率计算SSAO,将SSAO渲染到同一个RT(另一个通道),双边模糊同时获取SSAO和深度。具有4个样本的体积遮挡,简单重投影的时间累积,整体性能是在X360上1ms,PS3上1.2ms。
对于颜色分级(color grading),将所有全局颜色转换烘焙到3D LUT,事实证明16x16x16 LUT已足够,尽量使用硬件3D纹理,颜色校正通道是一种查找:newColor = tex3D(LUT, oldColor)。
CryEngine 3使用Adobe Photoshop作为色彩校正工具,从Photoshop读取转换后的颜色LUT:
文中谈到了延迟渲染管线的问题包含
- 不支持抗锯齿,MSAA对于延迟管线来说过于繁重,后处理抗锯齿不会完全消除锯齿,大多数情况下需要超采样。
- 有限的材料变化,无各向异性材料。
- 不支持透明对象。
延迟渲染的GBuffer的每像素数据越小越好,CryEngine 3最小化GBuffer到64 bits / pixel,其中RT0存储Depth 24bpp和Stencil 8bpp,RT1存储Normals 24bpp和Glossiness 8bpp。用于标记照明组中的对象的模板:门户/室内、自定义环境反射、不同的环境和间接照明。光泽度不可延迟,照明累积通道需要,否则镜面反射是非累积的。这种G-Buffer布局的问题:仅Phong BRDF(正常 + 光泽度)、没各向异性材质、24bpp的法线过于量化、照明带状/低质量。
对于着色的法线精度,24bpp的法线过于量化,光照质量低。24bpp的精度本应该足够了,为什么会出现光照质量低?原因是存储了标准化的法线!立方体是256x256x256个单元格 = 16777216个值,在这个立方体中只使用单位球体上的单元格:16777216个中的约289880 个单元格,即约1.73%!!
我们有一个包含256^3个值的立方体!最佳拟合是找到一条光线误差最小的量化值,可以离线执行,使用3D-DDA中的约束优化。将其烘焙到结果的立方体贴图中,立方体贴图应该足够大(显然 > 256x256)。
提取这个对称立方体贴图最有意义和唯一的部分,保存为2D纹理,在G-Buffer生成期间查找它,缩放法线,将调整后的法线输出到G-Buffer。
法线的最佳匹配支持Alpha混合,尽管最合适的会被破坏,但通常不是问题,重构只是一种归一化!可以应用于一些选择性平滑的物体,例如禁用带有细节凹凸的对象,不要忘记为结果纹理创建mip-maps!
几种法线存储技术的对比如下表:
法线存储技术 | 有效单元格 | 有效单元格占比 | Normalized normals | 约 289880 / 16777216 | 约 1.73 % | Divided by maximum component | 约 390152 / 16777216 | 约 2.33 % | Proposed method (best fit) | 约 16482364 / 16777216 | 约 98.2 % |
标准化法线(上)和最佳匹配(下)法线的渲染对比图。
标准化法线(上)和最佳匹配(下)法线的对比图。
该文还谈到了一种用于光照的计算:裁剪体积(Clip volume)。没有阴影的延迟光照往往会溢出,但阴影开销很大。解决方案:使用艺术家定义的剪裁几何体——剪裁体积,除了光照体积遮罩之外,还由遮罩模板。非常低开销,提供四倍的模板标记速度。
裁剪体积示例。左上:裁剪体积几何体;右上:模板标记;左下:光照累积缓冲;右下:最终成像。
CryEngine 3为了高效地实现各项异性材质,还将BRDF复杂度与光照复杂度解耦,BRDF复杂性完全从光照通道中消除。(下图)
在抗锯齿上,CryEngine 3使用了混合抗锯齿的解决方案。
- 近处物体用后处理AA。不超采样,适用于边缘,使用MLAA。
- 远处物体用TAA。进行时间超级采样,不区分表面空间阴影变化。
- 用模板和无抖动相机将它们分开。
距离分离保证了远处物体的视图矢量的微小变化,减少反向时间重投影的基本问题:着色域中的视图相关变化。原因是重投影是基于深度缓冲区的,因此,不可能考虑物体的着色空间局部变化。如果将它应用到特写物体上,可能会导致重影、反射等。
在逐物体基础上分开,一致的物体空间着色行为,使用模板标记物体以进行时间抖动。
上:用于远处物体的TAA;下:用于近处物体的后处理AA。
Sample Distribution Shadow Maps是Intel提出的一种改进的阴影渲染方法。
Sample Distribution Shadow Maps(SDSM)是样本分布阴影图,通过分析阴影样本分布,找到紧凑Z的最小值/最大值,基于紧凑Z边界的对数分区,无需调整即可适应视图和几何形状。计算紧凑的光源空间边界,每个分区都有紧凑的轴对齐边界框,大大提高有用的阴影分辨率。
上:PSSM(平行阴影图)的分区、光源空间、光源空间分区;下:SDSM的分区、光源空间、光源空间分区。
分区变体有:
- K均值(K-means)的分簇。在Z中有大量样本的地方放置分区,平均误差的好结果,但有玻璃钳口(glass jaw)。
- 自适应对数。与基本对数类似,但要避免Z中的间隙,只适用于特殊情形,通常不值得尝试。
上面的方案需要深度直方图。
SDSM有两种不同的实现:
- 对数的简单“减少”实现。可以在DX9/10硬件上的像素着色器中实现。
- 一般深度直方图实现。共享内存原子使这成为可能,太慢且依赖于DX11之前的硬件。
SDSM产生更紧密的光源空间截锥体,渲染到阴影贴图中的几何体更少。
在GPU上生成的分区边界数据,CPU不能用于截锥体剔除!阻塞并读回分区边界数据(非常小),听起来糟糕,但就是当时所做的,而且速度相当快。未来在GPU上进行截锥剔除。
关于时间一致性的说明:
- 改变分辨率会导致时间锯齿。
- 量化光源空间中的分区边界仅适应于定向灯?根本无法移动或调整分区大小,存在一些相机变换的问题,过于限制和次优。
- 将分区量化为2的幂大小?可以工作,但苛刻,且浪费了很多分辨率。
- 以亚像素阴影分辨率为目标。需要足够的分区分辨率(约等于屏幕分辨率),使用良好的过滤和阴影贴图抗锯齿!
未来的改进方向:
- 更好的分区方案?虽然尝试了很多方法,但可能有更好的算法在实践中运行良好。
- 解决投影锯齿的混合算法。在误差高的地方使用更昂贵的算法。
Toy Story 3: The Video Game Rendering Techniques是介绍了迪斯尼的游戏玩具总动员3所使用的SSAO、环境光及阴影等渲染技术。
文中提到,SSAO面临的挑战及对应的解决方案如下:
使用线性积分。
蓝点仍然是正在采样的像素,想象一个围绕它的概念体积球体,是在2D中采样而不是在3D中采样。每个样本都有相应的体积,并且根据样本的深度,该体积的一小部分将被遮挡。
使用线积分,每个样本都会产生一小部分遮挡与非遮挡,因此遮挡量会平滑地变化。
线性积分的算法过程:采样 (x,y) 坐标,计算沿 [0,1] 的距离,样本的对应行是将该数量乘以相应的体积以获得样本的遮挡贡献。
使用2D随机旋转。先创建具有2D旋转的纹理(使用4x4 G16R16F纹理来编码每个角度的正弦和余弦)。
以4x4纹理编码的旋转:
具有4x4偏移的顺序旋转:
为了避免旋转的样本过于集中或规则,可以随机旋转:
随机旋转后的效果:
还可以对旋转添加抖动(下图左没有抖动,右添加了抖动):
有没旋转样本的对比(左无右有):
以往(如CryTek)的做法是对于大距离深度差的AO直接设为0,但问题是如下图所示的平面应该被1/2遮挡,因此将不可用的样本设置为零会使结果偏向过于未遮挡,从而导致光晕。
当平面与视图平面平行时,使用0.5效果很好,但会因倾斜表面而失效。在下图中,结果将是遮挡太少,如果遮挡物在另一侧,则会导致遮挡过多。
为了能够以估算丢失的样本,需要对采样模式施加约束,即每个样本都是一对的一部分(配对采样),解释了之前看到的奇怪的“猎户座”采样模式:
在环境光方面,辐照度光源使用了SH,每个轴沿 +/- 的一个定向光,单色环境光,仅用于环境照明。可实时调整,负光源(negative light),SH可以实时混合。其中负光源主要用于从负y方向指向上方的光,使光源的底部变暗,并给一切带来了轻微的阴影。
渲染Wii上的环境光采用SH,且每一帧在视图空间中生成一个可以用法线查找的球面贴图。环境存在局部区域的问题,某些地方漏光导致过亮,原因是每个世界只有一个环境配置(ambient rig),预期的效果是无论位置如何,一切都呈现在相同的氛围中。可能的解决方案是在两个环境配置之间混合,基于相机距离的混合,根据位置切换环境配置,烘焙环境照明,实时辐射度或全局照明,指定的环境光。
但没有采取以上方法,而是增加约束:只有两种类型的光源,即暗光源和亮光源。
该解决方案的工作原理是将体积渲染到延迟缓冲区中,并根据缓冲区的值在明暗环境绑定之间进行混合。主要优点是艺术家可以更好地控制环境照明。
延迟技术的步骤:
上述的体积包含了多种几何类型和操作:立方体、球体、旋转和缩放。
下面是有无使用体积的对照图:
在阴影方面,文中还分享了没有光照贴图的动态阴影、用于主角的投射阴影(Drop Shadow)、柔和并保留阴影的形状、以牺牲整体距离为代价近距离获得更高质量的阴影。
在柔和并保留阴影的形状方面,传统的方案是以绝对最高分辨率渲染阴影贴图、添加过滤以减少瑕疵。但存在需要更柔和的阴影,ToyStory 3包含多达300万个顶点的场景,在300m的距离上拉伸4个级联非常昂贵,有限的LOD和遮挡剔除技术。ToyStory 3也考虑过虚拟阴影图(Virtual Shadow Map,VSM),但VSM也在当时也存在诸多限制,如模糊高分辨率阴影贴图过于昂贵,没有 2x 深度写入,艺术家不喜欢的视觉效果,漏光很难管理等,最终未被采纳。最终采用了组合解决方案:3个640x640阴影贴图、4x4的高斯PCF、5x5交叉双边滤波。(下图)
此外,ToyStory 3还采用了延迟阴影(Deferred shadow)的技术,R通道存储SSAO,G通道存储世界阴影,B通道存储角色阴影。
延迟阴影着色步骤如下:
- 渲染全屏四边形。
- 从视图空间深度缓冲区重新生成世界位置。
- 包围盒级联选择。
- 动态深度偏差计算。
- 4x4 高斯PCF到最终阴影值。
文中还对阴影的条纹、深度偏差等瑕疵进行了优化和改善。
Real-Time Order Independent Transparency and Indirect Illumination Using Direct3D 11分享了基于DX11的OIT透明渲染和带有间接阴影的全局光照技术。间接阴影可以帮助感知场景中发生的细微动态变化,为深度感知添加有用的提示,场景像素上的间接光贡献更准确,当环境光线昏暗或动作发生在远离直射光的情况下,这对于视觉体验和游戏玩法尤其重要。
Dynamic lighting in GOW3讲述了游戏战神3(God of War III)所使用的动态光照技术,包含环境光、点光源、定向光等。
其中环境光被组合成一个RGB插值器,该文并不涉及。点光源和定向光表示为混合顶点光源(Hybrid vertex light)。
混合顶点光源可支持1个与像素灯相同的光源,也支持多光源:计算每个顶点的距离衰减,每个顶点组合成一个聚合光,插值每个像素的聚合光位置,在片元程序中执行N\cdot L、N\cdot H等,就好像有一个单像素光一样。
在插值任意三角形两个点的位置时,使用默认的插值会产生错误的结果,需要特别处理光源方向:
对于光源的衰减函数,希望它是光滑的、便宜的,希望一阶导数接近0,因为函数本身接近0。下图是相同的定向灯直射而下的衰减,右边是文中采纳的衰减,衰减函数设置为在相同距离处达到零:
为了更好地表示聚合的光源,从每个世界光位置减去世界顶点位置以创建相对向量,计算长度和重量(记住两个灯的光强度都是 1),将相对向量乘以权重以进入方向域,添加灯光方向,并累积权重,将聚合方向乘以累积权重以返回位置域,最终得到了聚合光的相对光向量,将顶点世界位置添加到它以获得聚合光的世界位置。
相关的计算公式、符号说明、图例如下:
解决计算聚合光位置的方法,选择了合适的衰减函数之后,需要解决背面光源,因为光源是在不考虑阴影的情况下聚合的,消除背向顶点的灯光的贡献很重要。采用以下公式:
接下来还需要解决不同方向的两个光源的过渡问题。假设下图是完全对称的,在片元程序中计算的N dot L将正好是1,比A或B处的N dot L值高得多,因此将在P处看到意想不到的亮紫色高光:
修复以上问题的过程是:在片段程序中,得到从顶点插值的光位置,然后计算光向量并在计算 N dot L 之前对其进行归一化,如果插值光向量短于阈值,则停止归一化,可以很好地解决上述问题。
接下来处理聚合光源颜色的问题。用于计算“物理上正确”值的数据丢失,需要解决一些在片段程序中插值时会给出合理结果的东西。计算聚合光位置,计算归一化光方向,计算点积:
相加多个向量,其结果的长度等于投影的总和:
最后拟合出的公式如下:
扩展到RGB:
GOW3实现时,在EDGE作业中将每个顶点的光照计算作为自定义代码运行,高度优化,仍然保持PPU版本运行以供参考和调试。
Physically-based Lighting in Call Of Duty: Black Ops陈述了在不断发展的使命召唤图形的背景下基于物理的照明和阴影以及经验教训。
COD的运行时光照策略是:所有主要照明都在着色器中计算,每个主要的运行时阴影贴图会覆盖相机周围半径中的烘焙阴影。因此,主要可以改变颜色和强度,移动和旋转小范围,仍然看起来正确,静态和动态阴影很好地融合在一起。
对于漫反射,主要漫反射使用经典的兰伯特项,由阴影和漫反射反照率调制。次级漫反射由具有逐像素法线的光照贴图/光照网格二次辐照度重建,由漫反射反照率调制。
对于镜面反射,主要镜面反射使用微平面BRDF,由阴影和“漫反射”余弦因子调制。次级镜面反射从具有逐像素法线和菲涅耳项的环境探针重建,也与二次辐照度相关,基于与主要高光相同的BRDF参数。
采用模块化方法,早期实验性使用Cook-Torrance,然后尝试了不同的选项以获得更逼真的外观和更好的性能,由于BRDF的每个部分都可以单独选择,因此尝试了各种“乐高积木”(即组合)。
其中,D(法线分布)采用了Beckmann方程:
F(菲涅尔)采用了以下方程:
G(几何遮蔽)采用了Schlick-Smith联合公式:
对于环境贴图,以前有几十个环境探针来匹配照明条件,由于内存限制,分辨率低,过渡问题、镜面反射流行、大型网格的连续性。对于Black Ops,希望解决这些问题,并拥有更高分辨率的环境贴图来匹配高镜面反射指数。解决方案:
- 归一化(Normalize)——通过捕获点的平均漫射照明来划分环境贴图。
- 去归一化(De-normalize)——将环境贴图乘以从光照贴图/光照网格中重建的每个像素的平均漫反射光照。
归一化允许环境贴图更好地适应不同的光照条件,户外区域只需一张环境图就可以逃脱,室内区域需要更多特定位置的环境贴图来捕捉次级镜面光照。使用AMD/ATI的CubeMapGen预过滤和生成Mipmap,HDR角度范围过滤,面边修正。
根据材质光泽度选择mip:
texCUBElod(uv, float4(R, nMips - gloss*nMips));
对于非常光滑的表面,可能会导致纹理损坏,某些GPU具有获取硬件选择的mip的指令。环境贴图“菲涅耳”:
但会导致高光过多,可以采用法线方差(Normal Variance)解决。方差贴图可以直接对来自mipping法线贴图的丢失信息进行编码,方差图需要高精度和额外的成本才能在着色器中存储、读取和解码,如果离线将它们与光泽贴图结合起来会怎样?
可以从法线贴图中提取投影方差,总是从顶部mip中提取,最好使用NxN加权滤波器:
添加创作的光泽,转换为方差:
将方差转换回光泽度:
这种方法解决了大部分的高光强度问题,也可以用于镜面反射的抗锯齿,在对环境贴图的mip进行光泽控制时,最大限度地减少纹理损坏的机会。
基于物理的着色相对更昂贵(ALU 平均增加10-20%),使用特殊情况着色器对性能有所帮助,对于纹理绑定着色器,可以隐藏额外的ALU成本,为特定情况使用快速的Lambert着色器仍然是个好主意。
基于物理的着色是完全值得的,使镜面反射真正成为“下一代”,准备好在工程和艺术方面付出相当大的努力以获得收益。
Lighting the Apocalypse: Rendering Techniques for RED FACTION: ARMAGEDDON分享了游戏红色兵团(Red Faction)使用的光照技术,如推断光照。
推断光照(Inferred Lighting)是延迟光照的一个变种,也叫光照预通道渲染(Light Pre-Pass Rendering)。将照明与场景复杂性隔离开来,对于处理场景破坏至关重要,推断光照 = Light Pre-Pass++,可调照明分辨率、支持MSAA、Alpha照明。
1年后,Lighting & Simplifying Saints Row: The Third分享了推断照明的最新迭代,新增了几项优化和功能,以及自动化LOD管线,包含网格简化和实际执行问题。
原来的推断照明支持许多完全动态的光源、集成Alpha照明(无前向渲染)、硬件MSAA支持(即使在 DX9上)。而本文在此基础上新增了雨滴照明(需要IL)、更好的树叶支持(仅适用于IL)、屏幕空间贴花(由IL增强)、径向环境光遮蔽 (RAO)(由IL优化)。详见14.4.4.1 Inferred lighting小节。
对于LOD,以前的方法是主要由艺术家创作,耗时,实际创建的LOD并不多,大多选择淡入“细节集”。新方式实现了全功能网格简化器,在crunchers中运行,而不是在DCC应用程序中运行。大部分是自动生成的LOD,但艺术家可以调整:建筑物、人物、车辆,完全自动化(无艺术家干预):地形,还使用简化器生成地形碰撞船体、构建阴影代理。
网格简化主要使用误差度量,误差度量衡量网格近似的“糟糕”程度,用于计算收缩误差,确定哪个边先收缩,放置结果顶点的位置。二次误差度量概述:
实现的过程中可能出现UV边界的拉伸问题:
原因是UV不连续:
可以采用UV镜像,以便让边界不那么明显:
更好的做法是保持边界,以相同方式保留任何类型的边界,通过边界边缘添加“虚拟”平面:
连续区域:在每个顶点,跟踪具有连续UV的区域。连续区域问题是UV可能在顶点处是连续的……即使地区是分开的
材质数量:随着LOD变得更简单,材料成本占主导地位:
减少材质数量:积极寻找“小”面积材质,更换为同一网格上使用的较大材质,数量有所减少,但不会有大的节省。
补充细节层次,可将每个可流区域烘焙成单个网格,更加简化(大约是原始顶点的 5%),用顶点着色替换几乎所有材质。
CSM Scrolling: An acceleration technique for the rendering of cascaded shadow maps讲述了一种阴影优化技巧,通过滚动CSM和区分动态、静态物体来优化CSM的渲染。
对于阴影,大多数时候,相机不会跨帧进行彻底的改变,大多数几何图形在帧之间是相对静态的。可以识别从前一帧发生变化的几何图形,跨帧的光线方向相对稳定,空间查询的结果可以与阴影渲染在同一帧中使用,几何被分成小实例。CSM滚动步骤可结合下图加以说明:
1、在缓存阴影图中存储来自上一帧的静态几何体。
2、滚动缓存阴影图以匹配相机视图的变化。
3、在滚动过程中暴露的边缘中渲染额外的静态几何体(比如数字3旁边阴影图右上侧的圆柱体),然后在缓存区域中渲染新的静态几何体(比如数字3旁边阴影图左上侧的圆形)。
(以上阶段都是在持久的缓存阴影图中操作,以下阶段则是在临时的当前帧阴影图中操作)
4、复制缓存阴影图到当前帧的最终阴影图。
5、渲染非静态几何体到当前帧的最终阴影图。
现在假设相机没有移动或移动很少,则涉及4个主要阶段(下图数字标注)。
- 将前一帧的“静态”几何图形存储在缓存地图中(“静态” = 在 t 时间内没有移动,例如5秒)。(1和2)
- 每帧将非静态几何图形渲染到缓存副本。(3和4)
- 但是,上一帧的阴影图缓存在相机移动、相机FOV变化、“静态”几何体移动的情况下会失效。涉及阶段1。
- 解决方法是针对阶段2中新的静态几何体(红色圆形):
- 在缓存区域中渲染新的“静态”几何图形。
- 查询“静态”几何体的状态,区分当前“静态”与以前“静态”查询结果。
- 使用动态遮挡系统。
- 创建复制新的阴影图缓存以使用此帧。(3)
- 渲染动态”几何体到临时阴影图。(4)
现在假设相机移动了很多(但很慢),此时将涉及5个阶段(下图数字标注)。
- 插入CSM缓存:滚动阴影图,渲染到暴露的边缘。(2、3)
- 滚动缓存阴影图以考虑相机视图的变化。(2)
- 从前一帧采样阴影纹理。(2)
- 滚动区域是钳制到边框的(下图白色区域)。(3)
- 由于相机运动是3D,涉及横向滚动、深度滚动。(2)
- 横向滚动:垂直于光线的平移。
// input是已在delta相机在光源坐标系中转换的UV。
float ScrolledDepth_LateralOnly(float3 input)
{
float2 uv = input.xy;
// 简单的纹理查找(点采样)
return SampleShadow(uv);
}
- 深度滚动:平行于光线的平移。
// input是已在delta相机在光源坐标系中转换的UV。
float ScrolledDepth(float3 input)
{
float2 uv = input.xy;
// 深度滚动需要额外的处理. input.z是光源坐标系中的相机深度的差异值。
float depth_offset = input.z;
float old_depth = SampleShadow(uv);
// 抵消所有先前的深度(滚动深度)
float new_depth = old_depth + depth_offset;
// 防止深度超出远平面。
return (old_depth < 1.0f) ? new_depth : 1.0;
}
- 在滚动过程中暴露的边缘中渲染额外的静态几何体(右上侧红色圆柱体)。(3)
- “静态”几何体具有重叠边界体积:
- 几何体相对于视图的粗糙度:
- 有的具有大量的重叠体积(下图左),有的只有少量的重叠体积(下图右),有的具有明显的锯齿(下图中):
- 在缓存区域中渲染新的“静态”几何体。(3)
- 复制阴影图以用作当前帧的最终阴影图。(4)
CSM的滚动涉及2、3、4,渲染效果如下:
总之,直接添加到CSM缓存,关键是像2D位图一样滚动,可以减少约70%的静态几何体渲染到CSM。
Practical Physically Based Rendering in Real-time和Beyond a simple physically based Blinn-Phong model in real-time详细阐述了实时渲染领域的PBR的理论、依赖知识、特点、实现及优化。
实时PBR使整个渲染管道基于当前控制台的物理基础,包含以下几点:
- 基于物理的着色模型。基于物理的BRDF模型
- 基于物理的光照。基于物理的量,胶片模拟(基于频谱的色调映射)。
- 基于物理的相机模拟。基于真实相机系统的镜头模拟。
基于物理的光照要求使用的物理量:
- 正确的色彩空间。
- 在光谱域 (380nm – 1000nm) 处理胶片模拟(色调映射)。
- 基于真实电影资料的电影数据库,曝光、显影、复制、打印和投影。
- 基于瓦特。
- 其他单位(勒克斯、流明、色温)在引擎中转换为瓦特。
- 光源面积。用于延迟和前向光源的基于图像的照明和伪光源大小,金属或光泽物体不再需要环境贴图着色器。
基于物理的相机模拟要求基于真实相机系统:
- 基于镜头数据库的光学模拟:
- 真正的散景模拟。
- 基于透镜方程。真实的相机参数和镜头,对焦模拟。
- 孔径模拟。叶片数、圆形光圈、光圈机制。
- 暗角。桶形暗角,光学暗角。
基于物理的着色模型(已实现或正在研究的模型):
- Ashikhmin。
- 分层材质。
- Oren-Nayar、改良的Oren-Nayar。
- 逆向反射材料。
- 其它特殊材料。Marschner、金属、玻璃、印刷、NPR。
基于物理的Blinn-Phong:
文中还详细地剖析了基于物理的IBL的理论、公式、推导及实现。
基于物理的IBL公式推导及近似。
辐射率环境图(REM)的生成过程。
IBL效果。
镜面AO及效果对比。
不同漫反射模型的效果和性能对比。
Realtime global illumination and reflections in Dust 514讲解了利用特殊的高度场光线追踪来完成间接光和反射的计算。下面三幅图像从左到右分别显示了原始环境、理想的卷积环境和实时近似:
理想的间接项是使用前面描述的朗伯卷积离线计算的,计算需要几秒钟。右侧是锥形轨迹近似,在像素着色器中实时评估。过渡是基于表面法线的Z/up分量的天空颜色和地面颜色之间的线性混合;底色是通过对具有大量mip偏移的地面纹理进行采样获得的。毫无疑问,可以通过花费更多周期来建立更好的近似值。
无论如何,关键是有一种方法可以为空间中的任何点和任何表面法线方向提供间接项,为所有环境提供了一个解决方案,可以用一个平面水平面和一个天空立方体来近似。
在对高度场进行光线追踪时,将光线与一个水平平面相交,该平面的高度是通过对光线原点下的高度场进行采样来确定的。(下图)
另一种方法是使用单独的偏向光线进行向上和向下跟踪,因此从不显示水平光线跟踪的结果。
单样本光线追踪步骤在大多数情况下运行良好,但在某些情况下完全失败,例如下图的横截面。黑色矩形表示一个物体,例如一座桥; 橙色线是从下方渲染场景时生成的高度场。左下角的箭头表示一些样本射线,例如从下方经过的车辆顶部的反射。问题是当光线原点在屋顶下传播时,交点会不连续地变化,在该车辆的顶部,桥边缘下方的反射会出现明显的不连续性。虽然仍然可以对不准确的交点进行合理的反映,但不连续性非常明显并且显然是错误的。
解决方案是对高度场应用后处理,以确保高度不会突然变化,将产生下图显示的横截面。意味着反射永远不会有不连续性,因此会得到更合理的反射,还具有一步光线跟踪近似的好处。高度场过程是增量完成的; 高度场需要多次通过才能收敛到此处显示的结果, 这是它的工作原理。
高度场细化过程:
整个跟踪过程:计算上/下偏向光线向量,在射线原点采样压缩高度场,计算每一层的交点,计算每一层的mip偏差,采样四层纹理和天空纹理,合成结果以产生上下颜色(天空、天花板、下面的桥梁、地板、上面的桥),根据查询光线方向混合向上/向下颜色。
细化:时间切片分层更新,为阴影重用CSM,边缘淡出,量化运动。
总之,提供通用间接项,提供具有可变模糊的通用反射项,快速,不支持任意高度复杂度的场景,一般情况下,无法应对墙壁和垂直表面,动态对象不会促成间接或反射,反射质量有限。
三重缓冲的目标是GPU永远不会停止等待页面翻转发生,并且翻转发生在vblank上,因此不会出现撕裂。
两个全高清缓冲区A和B之间的放大和累积步进乒乓球。由于以30fps运行,并且只在帧的最后写入全高清缓冲区,因此要求没有限制写入之间至少有16.6毫秒的间隔。一旦完成了对其中一个缓冲区的写入,就请求在下一个vblank上翻转。在正常的事件过程中,它会在主720p渲染期间的某个时间点发生,当准备开始放大下一帧时,可以确信翻转将会发生。
需要一个栅栏来强制执行这种延迟,因为在RSX几乎无事可做的情况下,例如菜单和加载屏幕。从三个720p缓冲区变为两个1080p缓冲区和一个720p缓冲区,因此所需的额外内存约为9MB。
累积步骤:如果确定正在处理的当前像素是静态的,那么需要找出所有低分辨率样本(下图的红点)落在正在处理的高分辨率像素的区域内。如果有,希望将其混合到运行平均颜色中,如果没有,则保持当前像素不变。
累积的效果对比:
Rock-Solid Shading阐述了PBR的基础理论,分析了导致着色失真的问题,以及如何提升材质的真实可信度。
让东西看起来不错的因素有稳定、干净、没有锯齿,材质类型的表现力,简单、直观的模型。主要工具:Blinn-Phong、Banks、Ashikhmin-Shirley,大多数材质无论是否风格化都可以用简单的BRDF表示。
当前的着色模型存在的问题:物体太亮。锯齿:采样问题可能会导致法线突然成为亮点,闪闪发光,在 HDR 绽放过程中导致失真,亮点可能完全错过,防止使用高的镜面指数,经常会看到使用环境贴图完成的规范以获得清晰的亮点。
为什么离线渲染不会出现以上问题?采样率经常被锁定——例如REYES,即使是错误的,样本也是帧到帧的,从方程中消除了大部分时间锯齿。一切都过采样,每个像素一百个样本并不少见,在无穷大采样大多数问题都会消失。解决了吗? 不,但蛮力使之减缓。
没有达到目标的原因:不稳定——分辨率对大尺度效果影响很大,锯齿——时间和空间,缺乏表现力——无法使用广泛的力量。如何正确实现Blinn-Phong?可以做一个基于纹理的照明方法——一个la REYES,可以找到一个类似的BRDF,真实地表现自己吗?
LEAN映射的机制。LEAN(Linear Efficient Anti-aliased Normal Mapping)是线性高效抗锯齿法线贴图,在Sid Meier的文明V中应用过,为未来的所有资产生产进行部署。好处:时间稳定,分辨率稳定,可以使用高镜面指数(例如10000+),Blinn-Phong内容可以轻松转换,自动各向异性。
给定凹凸贴图的法线:N = (N.x, N.y, N.z),然后创建另一个图M:M = (B.x^2, B.x*B.y, B.y^2),其中: B = (N.x/N.z,N.y/N.z)。不是冗余数据,需要这些项的线性过滤版本!
存储5个通道:X、Y与中心凹凸的偏移量,可以是8位,X^2、Y^2、X*Y,如果想要很好的高镜面反射功率,需要16位。压缩也许可行,但由于使用了线性过滤,不能牺牲它。接下来要介绍的中间解决方案。
从Blinn Phong初始化内容:沿X^2和Y^2项添加的基本镜面反射功率s为1/s,所以M映射 (X^2, Y^2, XY) 变为 (X^2 + 1/s, Y^2 + 1/s, XY),存储逆幂意味着需要16位精度来获得大于256的幂,观察:即使使用Blinn-Phong,将功率存储为1/s也会导致MIP滤波器正常运行。
它与Blinn-Phong的近似程度如何?对于低power(例如 < 16),LEAN映射的响应与Blinn-Phong不同,可能需要重新调整一些内容
清晰映射:5个值可能开销大:修改,丢弃各向异性:
存储3个值:X、Y、(X^2 + Y^2)/2,只有 (X^2 + Y^2)/2需要以高精度存储。
对于高光过亮的问题,在最小化滤镜下高光更稳定;对于之前提出的锯齿问题,无锯齿。
达到目标。稳定:渲染的分辨率不影响大尺度效果,抗锯齿:线性硬件滤波器工作正常,表现力:可以使用大功率,并支持各向异性。还有一些问题:在低功率下与Blinn-Phong的分支,存储空间要求更高。
接下来阐述着色器锯齿匿名(Shader Aliasing Anonymous)。选项:纹理空间着色,关键思想是MIP照明!但开销大,用虚拟纹理缓存?可选项1: 拟合,关键思想是找到最合适的参数,例如法线、粗糙度、反射率,缓慢、脆弱、不连续。可选项2:直接方差估计,关键思想是估计方差 -> 新的粗糙度:
烘焙方差过程:对于每个MIP:
调整光泽度:多种多样……镜面AA “无处不在”!
- 动态反射:生成MIP,MIP偏移的查找,或是DX11:可变高斯? 图像空间收集?
- 体素锥追踪 [Crassin 11]
- 反射公告牌 [Mittring11]
- 反射遮挡?
选项:预过滤(LEAN),更好:更准确的结果,各向异性效果。
缺点是内存、额外的着色成本、切线空间。
LEAN的双变量法线分布:
可视化图:
LEAN的内存,烘焙,和Toksvig 一样……双线性模拟仍然很重要!存储协方差矩阵:[Σx, Σ y, Σz]?
可能有精度问题。可改成存储两个光泽值:
可以使用BC5或DXT5,可选择存储相关性:\rho = \cfrac{\sum_z} {\sqrt{\sum_x \cdot \sum_y}}。
除了LEAN,还涉及详细法线贴图、几何物体、漫反射、环境贴图。
其中几何物体锯齿(Geometry AA)是另一个方差来源!
想法1——预过滤几何法线:扩大,生成MIP,使用Toksvig,需要图集!
想法2——像素四边形消息传递 [Penner11A],访问邻居平均方差,平均代码:
float2 dir = 0.5 - frac(vpos*0.5 - 0.25)*2;
float3 n0 = N;
float3 n1 = ddx_fine(n0)*dir.x;
float3 n2 = ddy_fine(n0)*dir.y;
float3 n3 = ddy_fine(n1)*dir.y;
float3 nn = n0 + n1 + n2 + n3;想法3——来自 Kaplanyan & Valient:结合法向锥(曲率)和镜面反射波瓣锥,将规格功率转换为锥角,添加曲率角:
float3 dN = fwidth(N);
float3 new_normal = normalize(N + dN);
float curvature = acos(dot(new_normal, N))/(pi*0.5);转换回新的power,已投入实际应用中!优化后的代码:
float3 dN = fwidth(N);
float3 new_normal = normalize(N + dN);
float curvature = sqrt(1 - dot(new_normal, N));
float angle = 4.11893/sqrt(power) + curvature;
power = 16.9656/(ngle*angle);类似的结果。下面是不同方法的着色效果对比:
当前的漫反射存在误差,原因是完整的漫反射积分方程是:
而实际上,目前使用的漫反射积分方程是:
也就是忽略了(n_a\cdot \bold l)的积分。实际上,如果要正确计算漫反射,可以使用完整积分的等同变体:
漫反射积分的解决方案:法线方差圆锥,平均法线 (Na) 周围法线锥的光照积分,锥角公式:cos(θ) = 2*length(Na) - 1
可以预计算积分,就像预积分的皮肤着色 [Penner11B]一样:
float len = length(Na);
float3 N = Na/len;
tex2D(LUT, float2(dot(N, L)*0.5 + 0.5, len));结果样例:
缩小LUT,重要区域:0~25度(下图)。
也可以避免 LUT,使用曲线贴合:x^2。
float DiffuseAA(float3 N, float3 L)
{
float a = dot(N, L);
float w = max(length(N), 0.95);
float x = sqrt(1.0 - w);
float x0 = 0.373837*a;
float x1 = 0.66874*x;
float n = x0 + x1;
return w*((abs(x0) <= x1) ? n*n/x : saturate(a));
}
约18 条指令 (fxc) ,可以说太合适了,获得了大致近似。但有类似Toksvig的问题:压缩法线,存储预过滤的长度/方差。
另外,LEAN还可用于环境图中:
Calibrating Lighting and Materials in Far Cry 3阐述了育碧的Far Cry 3的基于物理的光照模型及优化。
为了获得更加接近物体本身的基础色(反照率),Far Cry 3使用数码相机对物体进行扫描,然后删除光照和镜头畸变,获得了更加物理的反照率贴图。
捕捉漫反射反照率时,使用麦克白颜色检测器(Macbeth ColorChecker)作为参考,由X-Rite制作,有24个已知sRGB值的色块。
ColorChecker旁边的照片材质,照明必须一致,使用ColorChecker的块寻找变换。
变换有两种:仿射变换(Affine Transform)和多项式变换(Polynomial Transform)。仿射变换的优点是消除通道间的串扰,缺点是线性变换。多项式变换的优点是精确调整级别,缺点是通道独立。
颜色校正工具:命令行工具,由Photoshop脚本启动,在xyY颜色空间中运行,应用以下变换[Malin11]:
下面是校正前(左)后(右)的对比:
天空的着色模型采用了CIE的模型:
光照模型也是Cook-Torrance,其中D项是:
F项有两种:Schlick近似和球面高斯近似:
可见性项:
然后简化了G项:
为了减少镜面反射的锯齿和保留其细节,采用了Toksvig的公式来缩放镜面power:
将这些缩放数据存储在纹理中,理想情况下,用于调整光泽贴图。添加额外纹理的成本太高,不是每个着色器都使用光泽贴图,无法将Toksvig贴图与现有光泽贴图组合。DXT5压缩法线贴图中存在自由通道,艺术家在法线贴图的alpha通道中绘制光泽,将光泽与Toksvig组合储存在R通道中:
法线的y分量的压缩受到影响:
光泽贴图是可选的,如果有,便与Toksvig结合,如果没有,将Toksvig平均为单个值。
Deferred Radiance Transfer Volumes: Global Illumination in Far Cry 3详细介绍了FarCry 3中使用的动态全局照明的近似值,分为两部分:首先是理论概述和离线预计算,其次是实时渲染实现和着色细节。使用稀疏体积的辐射传输探针,每个探针存储球谐系数矩阵,可以在运行中重新照亮探针,从而支持在一天中的时间变化下的照明,以及枪口闪光和爆炸等局部照明。使用重新照亮探针的着色是在屏幕空间中的GPU上完成的。使用混合CPU/GPU实现来确保系统在当前一代控制台上运行良好,在内存和性能方面都是高效的,提供详细的性能统计数据以及代码片段来说明系统的内部结构。
延迟辐照亮度传输体积的特点是近似全局光照,轻量级、主机友好,实时重照明,混合CPU/GPU。其中全局光照是低频的辐照亮度传输,支持太阳和天空反弹及天空直接光。
实时重照明是用太阳/天空颜色更新的全局光照,支持一天的周期时间,直接的艺术家反馈。
整个系统的概述。
离线时,将探针放置在世界中并为它们预先计算辐射转移,这一过程也称为烘焙。在游戏中,每当光照环境发生变化时,都会实时重新点亮探测器。将动态生成新的辐照度值插入到许多体积纹理中,然后GPU使用这些来遮蔽屏幕空间中的所有内容。探针烘焙时,预计算辐射传输和天空的能见度。
PC的局部辐射亮度传输:来自动态光源的全局照明,假设光源与探针处于同一位置,在每个探针位置存储白光PRT。
实时重照明:由太阳和天空驱动的照明,投影到二阶SH,艺术家创作梯度。
计算每个探针的光贡献,结果是颜色/强度数组,每个基本方向都有一种颜色,可以添加更多的基础方向,以获得更好的准确性。
体积纹理:GPU快速滤波,可以逐像素完成,适用于大型对象。连接到第一人称摄像机,体素是四个基础强度,96x96x16的RGBA,完全更新约7ms,占用5个SPUS。时间摊销:只更新那些没有数据可用的,使用包装避免偏移现有的切片。
体积纹理环绕:环绕采样器状态昂贵,模拟具有frac()的环绕,用于正确过滤的重复边界。
环境照明方面考虑了室内、远距离环境等因素。
下图是PS3的实现概览:
Practical Clustered Shading介绍了分簇着色的特点和实现。
分簇着色(Clustered Shading)是Olsson、Billeter、Assarsson等人在HPG 2012的论文Clustered Deferred and Forward Shading提出的一种扩展了分块着色的技术。它的特点是实时、鲁棒性好,支持的光源多,与视图低相关,可以处理嘈杂的深度分布。
分簇着色带来的海量光源,可带来全局光照、复杂的光源类型(如面光、体积光)、艺术家无约束、带光照的特效等。假设有以下的样例场景:
对于分块着色,支持延迟或前向,简单,某些情况快速,2D分块。
分块着色(Tiled Shading)的最大问题在于游戏是3D,而它是在2D平面上分块,导致单个块会和改块在深度上的所有光源相交,即便较远的光源被前面的物体遮挡!简而言之,分块是2D,几何样本、片元/像素是3D,光密度也是3D,视图依赖,不可预测的着色时间。
分簇着色(Clustered Shading)的核心思想是增加第三维,也在深度方向分块 = 分簇,也可大于3维(例如法线)。
分块和分簇在空间上的分布如下两图:
分簇着色的步骤如下:
- 光栅化G缓冲区。前向:pre-z通道。
- 分簇分配。
分簇键:ck = (i, j, k) 的整数元组,i, j = 2D分块的id,即gl_FragCoord.xy,k = \log(z_\text{viewSpace})。
全屏通道,标记使用的分簇,读取深度,计算ck,将网格中的单元格设置为1。向前:具有副作用的几何通道。
精简非零的分簇值,获得非空簇的列表,并行前置和Compute Shader。
许多簇和光源,可分层方法:光源的层次结构、32叉树(匹配GPU的SIMD),在GPU上动态重建,用BV测试给每个簇遍历光源树。
以下是在Crytek Sponza场景有树有10000+光源的情况下,不同方法的性能对比图:
上图显示,Tiled着色受着色时间支配,即使使用非常简单的像素着色器也是如此,意味着提高tile速度不会太大改变结果。另一方面,分簇着色在着色和算法的其它部分之间更加平衡。图中还显示了三个基于法线的更复杂剔除的变体,然而测试实现中没有得到回报,因为更复杂的剔除和更多簇的成本超过了着色成本的降低。随着更昂贵的着色和更好的分簇和剔除实现,它们可能仍然值得。
分块前向着色(Tiled Forward Shading)可用于透明物体,存在的问题是视图依赖、退化为二维、全屏不连续!
分簇前向着色(Clustered Forward Shading)在预几何通道执行,标记使用的分簇,片元着色器中的副作用,将网格中的单元格设置为1。
以下是分簇前向着色的性能数据:
总之,分簇着色高性能,低视图依赖性,良好的最坏情况性能,全动态,支持透明度,支持前向或延迟,或两兼而有之。潜在的优势有:样本的快速体素化,如阴影、锥体追踪的起点、近似阴影、自适应着色及其它用途。
Moving Frostbite to Physically Based Rendering 3.0阐述了Frostbite引擎改进了PBR关照,系统地梳理了其理论基础、相关技术、推导、优化及应用。PBR的范围包含了光照、材质和相机(之前的关注点只在光照和材质上)。
80%的外观类型是标准材质,镜面反射用带有GGX NDF的微面模型,漫反射用迪士尼模型,其它材质类型有次表面材质、单层涂层材料。其中镜面反射的公式如下:
下图是GGX-Smith和Height-Correlated Smith的G项对比效果:
上图显示差异很小,但对于高粗糙度值(右侧)来说很明显:
漫反射项上,不再使用兰伯特,用迪斯尼漫反射取而代之,因为后者使用了漫反射和镜面反射之间的耦合粗糙度和反向反射(retro-reflection)。
它们的漫反射对比如下图,在低粗糙度时有点暗,在高粗糙度时更亮,很微妙,但可以带来不同。
最初的迪士尼漫反射项存在一个问题,即能量不守恒:在某些情况下,反射光可能高于入射光。Frostbite应用了一个简单的线性校正,以确保当镜面反射和漫反射项相加时,半球方向的反射率小于1。
对于镜面反射和漫反射的输入值,Frostbite再次使用了Burley的近似方法,解耦了金属和非金属,更易于资产创作。
Frostbite选择向艺术家展示“平滑度”而不是粗糙度,因为白色的平滑对他们来说更直观。还尝试了各种重新映射函数,以获得感知线性度,最后,再次使用了Burley的方法来平方化粗糙度。
在光照方面,争取光照一致性——所有BRDF必须与所有光源类型正确集成,所有光源都需要管理直接照明和间接照明,所有照明均正确组合(SSR/本地IBL/...),所有光源之间的比例都正确。光照存在多种单位和参考系,常见的光度单位制如下:
而Frostbite使用了以上4种单位,它们的应用场景具体如下:
Frostbite支持四种不同的形状:球体、圆盘、矩形和管状。每种光源都可以有一个更简单的版本,但只有点和点使用准时(punctual)光照路径,因为它们更频繁,成本较低。
下面是关于准时光、光度光、区域光的描述:
对于IBL,关注点在近距离光照探针和远距离光照探针。其单位和光照来源和公式如下:
在运行时,不使用表面的镜像方向,而采用略有偏移的主导方向(dominant direction,下图青色箭头),有助于提高积分近似的精度。
对于摄像机,依旧考量了基于物理的模拟,纳入将场景亮度转换为像素值、光圈、感光元件、镜头、快门等因素:
到达传感器的场景亮度将由曝光确定,然后将曝光转换为像素值:
过渡到PBR的步骤:
1、标准材质 + 观察者优先 + 培养关键艺术家。
2、PBR/非PBR并行,自动转换。
3、向游戏团队宣传PBR + 验证工具。
Real-time lighting via Light Linked List阐述了使用光源链表来实现和优化海量光源的技术。使用Light Linked List(光源链表,LLL)可以获得半透明顺序正确的光照效果:
使用Light Linked List的前(左)后(右)对比。
将光源存储在逐像素链接列表中,其结构体如下:
struct LightFragmentLink
{
float m_LightDepthMax; // 光源最大深度
float m_LightDepthMin; // 光源最小深度
int m_LightIndex; // 光源索引
uint m_Next; // 下一个光源
};
// 压缩版本
struct LightFragmentLink
{
uint m_DepthInfo; // 深度信息
uint m_IndexNext; // 下一个光源
};
分辨率越低越好:四分之一、八分之一等等…内存消耗:4个缓冲区,分辨率为八分之一:2个RWByteAddressBuffer、1个RWStructuredBuffer、1个深度缓冲区(可选),平均每像素预分配40个光源。总成本:900P:约7.25megs,1080P:约10.15megs。
Insomniac引擎的渲染流程如下:
其中填充链表的消耗如下:
LLL的深度缓冲:生成较小的深度缓冲区,使用保守的深度选择,使用GatherRed。
LLL的着色步骤:软件深度测试、获取最小和最大深度、分配一个LLL片元。
LLL的深度测试:正面通过深度测试,背面未通过深度测试,禁用硬件深度剔除。
// 软件测试正面.
// 如果正面Z测试失败,跳过片元.
if((pface = true) && (light_depth > depth_buffer))
{
return;
}
如果两种深度都穿越,哪个深度优先?边界的RWByteAddressBuffer,编码深度+ID(16位ID、16位深度),
uint new_bounds_info = (light_index << 16) | f32tof16(light_depth);
使用InterlockedExchange交换新旧的边界值:
使用一个RWStructuredBuffer来存储:
struct LightFragmentLink
{
uint m_DepthInfo; // 深度信息,高位是最小深度,低位是最大深度。
uint m_IndexNext; // 下一个光源。
};
RWStructuredBuffer<LightFragmentLink> g_LightFragmentLinkedBuffer;
// 增加当前的计数
// 分配.
uint new_lll_index = g_LightFragmentLinkedBuffer.IncrementCounter();
// 不要越界
if(new_lll_index >= g_VP_LLLMaxCount)
{
return;
}
// 填充链接光源的片元并保存。
// 最终输出
LightFragmentLink element;
element.m_DepthInfo = (light_depth_min << 16) | light_depth_max;
element.m_IndexNext = (light_index << 24) | (prev_lll_index & 0xFFFFFF);
// 存储光源链表信息
g_LightFragmentLinkedBuffer[new_lll_index] = element;
计算光照的步骤:绘制全屏四边形、访问LLL、应用光源。访问LLL时,获取第一个链接元素偏移:第一个链接元素以较低的24位编码。
uint src_index = LLLIndexFromScreenUVs(screen_uvs);
unit first_offset = g_LightStartOffsetView[src_index];
// 解码首个元素索引
uint elemen_index = (first_offset & 0xFFFFFF);
启动照明循环:等于0xFFFFFF的元素索引无效。
while(element_index != 0xFFFFFF)
{
LightFragmentLink element = g_LightFragmentLinkedView[element_index];
element_indx = (element.m_IndexNext & 0xFFFFFF);
}
解码光源的最小和最大深度,比较光源的深度。
// 解码光源边界
float light_depth_max = f16tof32(element.m_DepthInfo >> 0);
float light_depth_min = f16tof32(element.m_DepthInfo >> 16);
// 执行深度边界检测
if((l_depth > light_depth_max) || (l_depth < light_depth_min))
{
continue;
}
// 获取完整的灯光信息
uint light_index = (element.m_IndexNext >> 24);
GPULightEnv light_env = g_LinkedLightsEnvs[light_index];
switch(light_env.m_LightType)
{
// ......
}
对于阴影,使用纹理数组,分配子区域。
Multi-Scale Global Illumination in Quantum Break说明了游戏Quantum Break的多种规模的全局光照,包含大规模光照和屏幕空间光照。
文中提到可能的全局光照解决方案有:
- 动态方法:
- Virtual Point Lights (VPLs) [Keller97]
- Light Propagation Volumes [Kaplaynan10]
- Voxel Cone Tracing [Crassin11]
- Distance Field Tracing [Wright15]
- 基于网格的预计算:
- Precomputed Radiance Transfer (PRT) [Sloan02]
- Spherical Harmonic Light Maps
- 无网格预计算:
- Irradiance Volumes [Greger98]
文中经过对比之后,选用了Irradiance Volumes。
辐照度体积原理示意图。
全局照明体积的好处是没有UV,适用于LOD模型,体积光照,与动态对象一致,但不适用于镜面,由于数据量太大。混合反射探头图例如下:
自动化放置探针,最大化可见表面积,尽量减少到地面的距离,选择K个最佳探针位置:
对于全局光照数据的存储(如镜面探针图集),可选的方案有:
- GPU体积纹理:由于压缩,无法使用原生插值。
- GPU稀疏纹理:对于细粒度树结构,页面太大,可能无法在未来游戏的目标平台上使用。
- 自适应体积数据结构有:
- Irradiance Volumes [Greger98, Tatarchuk05]
- GigaVoxels [Crassin09]
- Sparse Voxel Octrees [Laine and Karras 2010]
- Tetrahedralization, e.g., [Cupisz12], [Bentley14], [Valient14]
- Sparse Voxel DAGs [Kmpe13]
- Open VDB [Museth13]
自适应体素树:隐式空间划分,64的分支因子,多尺度数据。
体素树结构:
树遍历:
体素树可视化:
无缝插值:
文中的SSAO基于Line-Sweep Ambient Obscurance(LSAO)[Timonen2013],LSAO定位了最有贡献的遮挡体。
扫描36个方向,长步(~10px)和短线间距(相隔约2倍),GPU的调度友好,在Xbox One上,720p的扫描速度为0.75毫秒。对样本增加抖动,额外的近场样本(距离约2倍),样本垂直于加紧的遮挡体。
36个方向太贵,无法按像素采集,在3x3邻域上交错(4个方向/像素),使用深度和法线感知3x3盒过滤器进行收集。
屏幕空间漫反射照明,LSAO样本是“最可见的”,很适合对入射光进行采样,无法根据定义进行遮挡(提供自遮挡)。
效果对比:
屏幕空间反射:GGX分布的每像素1条光线,针对所有表面进行评估,线性搜索(7步),步进形成一个几何级数。
深度缓冲样本的处理,需要支持不同的粗糙度,计算圆锥体覆盖率,需要适应遮挡和颜色采样,还可以找到单色样本位置。深度厚度=a+b*(沿射线的距离),深度场延伸至/自摄影机,而不是沿视图z!
将线性项匹配视图空间的步长,否则,匹配实心几何体上的孔:
对于遮挡,计算圆锥体的最大覆盖率,将圆锥体的下限夹紧到曲面切线!
对于颜色,需要一个样本位置,首先选择覆盖大部分圆锥体的样品。将反射光线对准覆盖的中心,并与最后两个样本之间的直线相交,低采样密度:向相机方向插值(蓝色)。
光线上方的上一个示例:不插值。
优化交叉点,如果相邻光线的方向相同,交叉搜索,采取最近的命中距离。
Hybrid Ray-Traced Shadows阐述了混合光线追踪的阴影技术,以获得高质量更接近基准真相的阴影效果。常规的阴影图存在粉刺、彼得平移和锯齿等瑕疵:
传统的边界体积层次结构可以跳过许多光线三角形命中测试,需要在GPU上重建层次结构,对于动态对象,树遍历本身就很慢。
存储用于光线跟踪的图元,而无需构建边界体积层次!对于阴影贴图,存储来自光源的深度,简单而连贯的查找。同样地存储图元,一个深层图元图,逐纹素存储一组正面三角形。深度图元图绘制(N x N x d)包含3个资源:
- 图元数量图(Prim Count Map):纹理中有多少个三角形,使用一个原子来计算相交的三角形。
- 图元索引图(Prim index Map):图元缓冲区中三角形的索引。
- 图元缓冲区(Prim Buffer):后变换的三角形。
d够大吗?可视化占用率:黑色表示空的,白色表示满了,红色则超出限制,对于一个已知的模型,很容易做到这一点。
GS向PS输出3个顶点和SV_PrimitiveID:
[maxvertexcount(3)]
void Primitive_Map_GS( triangle GS_Input IN[3], uint uPrimID : SV_PrimitiveID, inout TriangleStream<PS_Input> Triangles )
{
PS_Input O;
[unroll]
for( int i = 0; i < 3; ++i )
{
O.f3PositionWS0 = IN[0].f3PositionWS; // 3 WS Vertices of Primitive
O.f3PositionWS1 = IN[1].f3PositionWS;
O.f3PositionWS2 = IN[2].f3PositionWS;
O.f4PositionCS = IN.f4PositionCS; // SV_Position
O.uPrimID = uPrimID; // SV_PrimitiveID
Triangles.Append( O );
}
Triangles.RestartStrip();
}PS哈希了使用SV_PrimitiveID的绘制调用ID(着色器常量),以生成图元的索引/地址。
float Primitive_Map_PS( PS_Input IN ) : SV_TARGET
{
// Hash draw call ID with primitive ID
uint PrimIndex = g_DrawCallOffset + IN.uPrimID;
// Write out the WS positions to prim buffer
g_PrimBuffer[PrimIndex].f3PositionWS0 = IN.f3PositionWS0;
g_PrimBuffer[PrimIndex].f3PositionWS1 = IN.f3PositionWS1;
g_PrimBuffer[PrimIndex].f3PositionWS2 = IN.f3PositionWS2;
// Increment current primitive counter uint CurrentIndexCounter;
InterlockedAdd( g_IndexCounterMap[uint2( IN.f4PositionCS.xy )], 1, CurrentIndexCounter );
// Write out the primitive index
g_IndexMap[uint3( IN.f4PositionCS.xy, CurrentIndexCounter)] = PrimIndex; return 0;
}需要使用保守的光栅来捕捉所有与纹素接触的图元,可以在软件或硬件中完成。硬件保守光栅化:光栅化三角形接触的每个像素,在DirectX 12和11.3中启用:D3D12_RASTERIZER_DESC、D3D11_RASTERIZER_DESC2。
软件保守光栅化:使用GS在裁减空间中展开三角形,生成AABB以剪裁PS中的三角形,参见GPU Gems 2-第42章。
光线追踪:计算图元坐标(与阴影贴图一样),遍历图元索引数组,对于每个索引,取一个三角形进行射线检测。
float Ray_Test( float2 MapCoord, float3 f3Origin, float3 f3Dir, out float BlockerDistance )
{
uint uCounter = tIndexCounterMap.Load( int3( MapCoord, 0 ), int2( 0, 0 ) ).x;
[branch]
if( uCounter > 0 )
{
for( uint i = 0; i < uCounter; i++ )
{
uint uPrimIndex = tIndexMap.Load( int4( MapCoord, i, 0 ), int2( 0, 0 ) ).x;
float3 v0, v1, v2;
Load_Prim( uPrimIndex, v0, v1, v2 );
// See “Fast, Minimum Storage Ray / Triangle Intersection“
// by Tomas Mller & Ben Trumbore
[branch]
if( Ray_Hit_Triangle( f3Origin, f3Dir, v0, v1, v2, BlockerDistance ) != 0.0f )
{
return 1.0f;
}
}
}
return 0.0f;
}
左:3k x 3k的阴影图;右:3k x 3k的阴影图 + 1K x 1K x 64的PM。
为了抗锯齿,使用额外的光线可行吗?开销太大了!简单技巧——应用屏幕空间AA技术(FXAA、MLAA等)。
混合方法;将光线跟踪阴影与传统的软阴影相结合,使用先进的过滤技术,如CHS或PCS,使用阻挡体距离计算lerp系数,当阻挡体距离->0时,光线跟踪结果普遍存在。插值因子可视化:
L = saturate( BD / WSS * PHS )
L: Lerp factor
BD: Blocker distance (from ray origin)
WSS: World space scale – chosen based upon model
PHS: Desired percentage of hard shadow
FS = lerp( RTS, PCSS, L )
FS: Final shadow result
RTS: Ray traced shadow result (0 or 1)
PCSS: PCSS+ shadow result (0 to 1)使用收缩半影过滤,否则,光线跟踪结果将无法完全包含软阴影结果,将导致在两个系统之间执行lerp时出现问题。
效果对比:
不同图元复杂度的效果、消耗及性能如下:
局限性:目前仅限于单一光源,不会扩大到适用于整个场景,存储将成为限制因素,但最适合最接近的模型:当前的焦点模型、最近级联的内容。总之,解决传统的阴影贴图问题,AA光线跟踪硬阴影的性能非常好,混合阴影结合了这两个世界的优点,无需重新编写引擎,今天的游戏速度足够快!
Advancements in Tiled-Based Compute Rendering阐述了基于分块的计算着色器的渲染,如当前的技术、剔除改进、分簇渲染等。
文中改进的目标有Z-Prepass(前向+)、深度边界、光源剔除、颜色通道。
首先来分析深度边界,以往的做法是根据每个tile确定深度缓冲区的最小和最大界限,原子操作的最小、最大值。以往的实现往往存在不少性能上的问题:
可以改成并行规约(Parallel Reduction),原子有用,但不是高效的,需要计算友好算法,当前已经有了很好的资料:
- Optimizing Parallel Reduction in CUDA [Harris07]
- Compute Shader Optimizations for AMD GPUs: Parallel Reduction [Engel14]
实现细节:第1个pass读取4个深度样本,需要单独的通道,写入边界到UAV,也许对其它操作也有用。
效率对比:
显卡 | Atomic Min/Max | Parallel Reduction | 变化 | AMD R9 290X | 1.8 ms | 1.6 ms | 提升11.1% | NVIDIA GTX 980 | 1.8 ms | 1.54 ms | 提升14.4% | 在3840x2160分辨率和2048个光源情况下的深度边界和光源剔除的综合成本,并行规约过程约需0.35ms,比测试的GPU上的原子最小值/最大值更快。
接下来分析光源剔除。
光源剔除需要涉及球体-视锥体的相交检测,存在以下几种方式:
上:裁剪平面;中:围绕长视锥体的AABB;下:围绕短视锥体的AABB。
除了以上几种,还有Arvo[Arvo90]相交测试方式,其代码如下:
左:球体-视锥体相交测试;右:Arvo相交测试。明显Arvo的更紧凑精确。
剔除聚光灯时,不要在聚光灯原点周围放置边界球体,在半径为r的球体内P处的紧凑包围聚光灯:
在深度裁剪时,存在2.5D和HalfZ方式,而文中使用了改良的HalfZ方式:像往常一样计算最小和最大Z,然后计算HalfZ,分别使用HalfZ和Max&Min的第二组最小值和最大值,测试近边界和远边界,写入其中一个列表或者两者,在深度边界通道重复一次,最坏情况收敛于HalfZ。
从上到下:2.5D、HalfZ、改进的HalfZ。
Unreal Engine 4的Infiltrator演示和不同方法的光源剔除可视化。
剔除结论:带有AABB的改良HalfZ通常效果最好,尽管生成MinZ2和MaxZ2会增加一些成本,即使在两个AABB而不是一个AABB中剔除每个光源,32x32的tile在剔除阶段节省了大量时间,以推送更多光源时的颜色通道效率为代价。
对于分簇渲染的光源剔除,视图空间AABB在二维网格上工作得最好,但在16切片时很糟糕,视图空间视锥体平面更好,逐tile平面计算,然后测试每个切片的近距离和远距离,(可选)然后测试AABB。
VRAM的使用:16x16像素的2D网格需要numTilesX x numTilesY x maxLights,1080p:120 x 68 x 512 x uint16 = 8MB,4k: 240 x 135 x 512 x uint16=32MB,每种灯光类型(点和聚光)的列表:64MB,32片:仅点光源为1GB,或者使用更粗的网格,或者使用压缩列。
压缩列表的选项1:在CPU上进行所有剔除,但其中一些灯光可能是由GPU产生的,CPU是宝贵的资源!选项2:在GPU上剔除,跟踪TGSM中每个切片的灯光数量,在灯光列表标题中写入偏移表,每个分块只需要maxLights x “安全系数”。
Z Prepass非常依赖场景,通常被认为开销太大,DirectX12有助于降低提交成本,应该已经有一个超级优化的阴影深度路径!仅位置流,索引缓冲区和材质一起批处理,部分Prepass确实有助于减轻几何体负载。
结论:并行规约比原子最小值/最大值更快,AABB球体测试结合改良的HalfZ是一个不错的选择。分簇着色可能会大大节省tile的剔除,低光源数量的开销更小,与2D分块相比,它还提供了其它好处。聚合裁剪是非常值得的,为昂贵的场景颜色提供了最佳的优化。
如何打造一款秒级别的全局光烘培软件由网易研究院在GDC中国2015呈现,讲述了实现快速烘焙的全局光照渲染。文中提到需要自己研发的原因是当时市场上的商业烘培软件烘培慢、包体大、材质有限,提出了CloudGI的烘焙流程:
使用了基于点云的GI:
基于点云的GI主要有3个步骤:点云生成、构建八叉树、计算间接光照。
点云实际是用的面元,包含了法线、位置、辐射率、面积等信息,使用了DX1的UAV 和原子相加的指令。对于面元的面积,在几何着色器中执行,公式如下:
$$
\text{Area} = \cfrac{\text{TriangleArea}}{\text{UVArea/UVPerPixel}}
$$
构建八叉树使用了GPU,高度并发,无需同步,不浪费显存。创建叶节点,3位一层编码,32位表示10层:
计算间接光照的过程:遍历八叉树,得到id映射表,然后从ID计算辐射率,最后用下面的公式获得平均值并乘以PI:
另外,采用了分块烘焙的方法来提升速度。
14.4.3.3 移动平台
The Benefits of Multiple CPU Cores in Mobile Devices阐述了移动设备对多核的需求、SMP多处理器、Tegra 2、Dual Core ARM Cortex A9架构等内容。
移动设备执行各种各样的任务,例如We浏览、视频播放、移动游戏、SMS文本消息和基于位置的服务。由于高速移动设备和Wi-Fi网络可用性的增长,移动设备也将用于以前由传统PC处理的各种性能密集型任务。下一代智能手机(称为“超级手机”)和平板电脑将用于各种任务,例如播放高清1080p视频、基于Adobe Flash 的在线游戏、基于Flash的流式高清视频、视觉丰富的游戏、视频编辑、同步高清视频下载、编码和上传以及实时高清视频会议。
当前这一代移动处理器并非旨在应对这种高性能用例的浪潮。当用户同时运行多个应用程序或运行性能密集型应用程序(如游戏、视频会议、视频编辑等)时,基于单核CPU的设备的体验质量会迅速下降。为了提高CPU性能,工程师采用了多种技术,例如使用更快和更小的半导体工艺、提高内核工作频率和电压、使用更大的内核以及使用更大的芯片缓存。
增加CPU内核或高速缓存的大小只能将性能提高到一定水平,超过该水平的热量和散热问题使得进一步增加内核和高速缓存大小变得不切实际。从基本的半导体物理学中我们知道,提高工作频率和电压可以成倍地增加半导体器件的功耗,即使工程师可以通过增加频率和电压来挤出更高的性能,但性能的提高会大大缩短电池寿命。此外,消耗更高功率的处理器将需要更大的冷却解决方案,从而导致设备尺寸的意外扩大。因此,提高处理器的工作频率以满足移动应用不断提高的性能要求,从长远来看并不是一个可行的解决方案。
为了满足移动设备对性能和外形时尚度快速增长的需求,业界已开始采用更新的技术,例如对称多处理(Symmetrical Multiprocessing,SMP)和异构多核(Heterogeneous Multi-core)计算。 NVIDIA Tegra是当时世界上最先进的移动处理器,从头开始构建为异构多核 SoC(片上系统)架构,具有两个ARM Cortex A9 CPU内核(下图)和其它几个专用内核来处理特殊任务,例如音频、视频和图形。
ARM Cortex A9 CPU内核架构图。
与用于音频、视频和图形处理等任务的通用处理内核相比,专用内核需要的晶体管更少、工作频率更低、性能更高、功耗更低。(下图)
双核CPU的电压和频率扩展优势。
对称多处理(SMP)技术使移动处理器不仅能够提供更高的性能,而且还能满足峰值性能需求,同时保持在移动电源预算之内。 具有SMP的多核架构由以下特征定义:
- 架构由两个或更多相同的CPU内核组成。
- 所有内核共享一个公共的系统内存,并由一个操作系统控制。
- 每个CPU都能够在不同的工作负载上独立运行,并且尽可能与其它CPU共享工作负载。
单核(上)和双核(下)运行网页浏览的对比图。
搭载ARM Cortex A9 CPU的移动设备和搭载其它芯片的移动设备的性能表现。图中表明前者有2.5倍的提升。
游戏Dungeon Defender开启双核之后,FPS是单核的2倍多。
总之,芯片制造商意识到,频率和内核尺寸的不断增加导致功耗呈指数级增长和过度散热, 因此,CPU制造商开发了多核CPU架构,以继续提供更高性能的处理器,同时限制这些处理器的功耗。智能手机和平板电脑等移动设备比PC设备从多核架构中获益更多,因为电池寿命和续航的收益巨大。该文预测双核处理器将在2011年成为标准,四核将在不久的将来出现。
为了进一步提高性能并保持在移动电源预算内,所有移动处理器最终都将不可避免地具有多核处理器。Android、Windows CE和Symbian等移动操作系统能够在多核环境中运行,并具有有效利用底层硬件的多个处理核心所需的功能。此外,流行的Web浏览器和大多数PC游戏已经是多线程的,如果将这些应用程序移植到基于多核CPU的移动处理器上运行,用户将看到性能的巨大改进。
NVIDIA Tegra旨在利用对称多处理的强大功能,提供非凡的Web浏览体验、响应速度快的用户界面、有效的多任务处理以及极大的电池续航时间。
在2012年的GDC中国上,《调教三国》产品研发及技术感悟分享了国内具有代表性意义的移动端游戏的研发过程、技术和经验教训。下图是该游戏的服务端使用的架构:
下图是该游戏使用的引擎架构:
在移动端游戏的引擎选择上,文中给的建议是不盲目选择商业引擎(Unity、Unreal、Flash),流行的开源引擎会更加符合实际,也不要盲目跨平台,iOS和Android足够。当时的Cocos2D-X是多少移动端游戏的首选,并建议不要自己重新造轮子写引擎。下图是当时移动端游戏引擎的常见模块和架构:
对客户端技术及语言的选择,建议在Windows开发环境,减少对于Object C的依赖(iOS),减少对于Java的依赖(Android),C/C++作为主要编程语言。
在游戏品质分级上,建议的分辨率是高端1136x640(iPhone 5)、中端960x640(iPhone 4S)、低端480x320(iPhone 3GS),内存控制在150M以内,包体大小在高中低端上分别是小于50M、约80M、大于100M。
用户体验上注重特殊操作方式(触摸点大小、操控范围)、适应新兴屏幕特点(小屏幕、高清晰)、性能优化(加载时间、内存局限)、网络优化(时延、断线及压缩)。
iOS and Android Development with Unity3D讲述了2012年用Unity开发移动平台游戏的攻略。当时付费下载过移动端app的情况如下图,其中40%多ios用户选择了是,而安卓30+%:
为了以后平台迁移,选择解决方案要考虑代码最少的大多数平台、不会吃掉利润的许可模式、广泛的社区支持等因素。同时对比了H5、Cocos2D、UDK、Flash、Corona等开发套件。说明选择Unity的原因:对关键平台的最佳支持,包含移动设备(iOS、Android)、网页(NaCL、Flash、网页播放器)、桌面(Steam、Mac App Store)、主机及原生插件、广泛的社区等。
2012年的Unity游戏画面。
插件方面,主要使用跨平台插件,访问平台特定功能(游戏中心等)。重构平台特定代码只花了几天,更换了适用于Android的iOS插件,运行时平台检查和#IF 编译器指令的组合,AndroidJava类。具备跨平台的导出工具,每个平台的资产设置,如压缩设置、过滤、缓存服务器、多平台工具包、特定于平台的资产、构建时资产更改。
总之,Unity有最佳商业模式,最广泛的平台支持,最佳社区支持,非常简单的移植过程。
Bringing AAA Graphics to Mobile Platforms阐述了移动端硬件的幕后工作原理及案例研究,软件如何应用这些知识将控制台图形引入移动平台。
移动图形处理器的功能有着色器、渲染到纹理、深度纹理、MSAA,性能也在慢慢变好。
移动GPU架构:基于Tile的延迟渲染(TBDR),与台式机或控制台截然不同,常见于智能手机和平板电脑,ImgTec SGX GPU属于这一类,还有其它基于tile的GPU(例如 ARM Mali)以及其他移动GPU类型(NVIDIA Tegra更传统)。
基于Tile的移动GPU:TLDR将屏幕分割成tile(例如16x16或32x32像素),GPU适合整块芯片,处理一个tile的所有绘制调用,对每个tile重复以填满屏幕,每个tile在完成时被写入RAM。
ImgTec处理过程。
顶点前端:顶点前端从GPU命令缓冲区读取,将顶点图元分布到所有GPU核心,将绘制调用拆分为固定的顶点块,GPU核心独立处理顶点,持续到场景结束。
顶点处理如下图:
各阶段描述如下:
- 顶点设置。从顶点前端接收命令。
- 顶点预着色。获取输入数据(属性和uniform)。
- 顶点着色器。通用可扩展着色器引擎,执行顶点着色器程序,多线程。
- 参数缓冲区。存储在系统内存中,但不能溢出这个缓冲区!
像素前端:读取参数缓冲区,将像素处理分配给所有内核,一次一整块tile,在一个GPU内核上完整处理一个tile,tile在多核GPU上并行处理。
像素处理(每个GPU核心)如下图:
其中:
- 像素设置。从Pixel Frontend接收tile命令,从参数缓冲区获取顶点着色器输出,三角光栅化,计算插值器值,深度/模板测试, 隐藏表面剔除(HSR)。
- 像素预着色。填充插值器和统一数据,启动非依赖纹理读取。
- 像素着色器。多线程ALU,每个线程可以是顶点或像素,每个GPU核心中可以有多个USSE。
- 像素后端。当tile中的所有像素都完成时触发,执行数据转换、MSAA下采样,将完成的tile的颜色/深度/模板写入内存。
着色器单元注意事项:没有动态流控制的着色器程序:每条指令4个顶点/像素,具有动态流控制的着色器程序:每条指令1个顶点/像素, 着色器中的Alpha混合:不分离专用硬件,切换状态时可能会发生着色器修补。
手机是新的PC,广泛的功能和性能范围,可伸缩的图形回归,用户图形设置回归,低/中/高/超,渲染缓冲区大小缩放,100个SKU测试回来了。
渲染目标已死,MSAA开销低且使用更少的内存,只有在内存中的解析数据,MSAA大约消耗0到5ms,当心缓冲区(颜色或深度)回存!Alpha混合没有带宽消耗,低开销的深度/模板测试。
“免费”去除隐藏表面(ImgTec SGX GPU专用),消除所有背景像素,消除过绘制,仅用于不透明。
移动与控制台:OpenGL ES API 的CPU开销非常大,100-300个绘图调用时的最大CPU使用率,避免每个场景的数据过多,顶点和像素处理之间的参数缓冲区,节省带宽和GPU刷新,着色器打包,某些渲染状态会导致着色器被驱动程序修改和重新编译,例如alpha混合设置、顶点输入、颜色写入掩码等。
Alpha测试/丢弃:有条件的z写入可能非常慢,在像素着色器确定当前像素的可见性之前,“像素设置”(PDM) 不会提交更多片段,而不是提前写出Z。使用alpha-blend而不是alpha-test,使几何体适合可见像素。
渲染缓冲区管理:
- 每个渲染目标都是一个全新的场景,避免来回切换渲染目标!
- 可能导致完全恢复(restore):在场景开始时将全部颜色/深度/模板从RAM复制到Tile Memory。
- 可能导致完全解析(resolve):在场景结束时将完全的颜色/深度/模板从Tile Memory复制到RAM。
- 避免缓冲区恢复。清除一切:颜色/深度/模板,清除只是在寄存器中设置一些脏位。
- 避免缓冲区解析。使用丢弃扩展(GL_EXT_discard_framebuffer)。
- 避免不必要的不同FBO组合,不要让驱动程序认为它需要开始解析和恢复任何缓冲区!
纹理查找:不要在像素着色器中执行纹理查找!让“pre-shader”提前排队,即避免依赖纹理查找。不要用数学操作纹理坐标,将所有数学运算移至顶点着色器并向下传递。不要将.zw组件用于纹理坐标,将作为依赖纹理查找处理,仅使用.xy并在.zw中传递其它数据。
移动端材质系统:完整的虚幻引擎材质太复杂,初步构想是预渲染为单个纹理,目前的解决方案是将组件预渲染为单独的纹理,添加特定于移动设备的设置,由艺术家推动的功能支持。
移动材质着色器:一个手写的uber shader,所有功能都有很多#ifdef,在艺术家UI中显示为固定设置,如复选框、列表、值等。
着色器离线处理:离线运行C预处理器,减少游戏内编译时间,在离线时消除重复。
着色器编译:启动时编译所有着色器,避免在运行时挂起在GL线程上编译,在Game线程上加载,编译还不够,必须触发虚拟draw call!记住某些状态如何影响着色器!可能需要尝试避免着色器补丁,例如alpha混合状态、颜色写入掩码。
文中谈及了耶稣光的渲染和具体步骤。其移动端优化包含:
- 将所有数学运算移至顶点着色器。没有依赖纹理读取!
- 通过插值器传递数据。但插值器数量有限。
- 将径向过滤器拆分为4个绘制调用,4x8 = 总共32次纹理查找(相当于 256 次)。
- 从30毫秒缩短到5毫秒。
文中还涉及了角色阴影。投影、调制的动态阴影,相当标准的方法,生成阴影深度缓冲区,模板潜在像素,比较阴影深度和场景深度,使受影响的像素变暗。具体步骤:
- 从光源角度投影角色深度。
- 重新投影到相机视图中。
- 与SceneDepth进行比较并进行调制。
- 在顶部绘制角色(无自阴影)。
阴影优化:
- 帧中阴影深度优先。避免渲染目标切换(解析和恢复!)
- 解析阴影之前的场景深度:
- 将tile深度写入RAM以作为纹理读取。
- 保持在同一个tile中渲染。
- 不幸的是,OpenGL ES中没有这方面的API。
- 优化阴影的颜色缓冲使用:
- 只需要深度缓冲区!
- 不必要的缓冲区,但在OpenGL ES中是必需的。
- 清除(避免恢复)并禁用彩色写入。
- 使用glDiscardFrameBuffer()避免解析。
- 可以用F16/RGBA8颜色编码深度。
- 绘制屏幕空间四边形而不是立方体,避免依赖纹理查找。
工具提示:在PC上使用OpenGL ES包装器。几乎“所见即所得”,在Visual Studio中调试。Apple Xcode GL调试器,iOS 5,完整捕获一帧,在单独的窗格中显示每个绘制调用、状态,显示每个drawcall使用的所有资源,显示着色器源代码+所有统一值。
ImgTec 6xxx 系列:100+ GFLOPS(可扩展到 TFLOPS 范围),DirectX 10、OpenGL ES “Halti”,PVRTC 2,提高内存带宽使用率,改进的延迟隐藏。
Accelerate your Mobile Apps and Games for Android on ARM由Arm呈现,讲解了在Android系统上如何优化App的技术。
移动应用程序需要特殊的设计考虑,这些考虑因素并不总是很清楚,解决日益复杂的系统的工具也很有限。动画和游戏丢帧,联网、显示、实时音视频处理耗电,应用程序不适合内存限制。幸运的是,谷歌、ARM 和许多其他公司正在开发这些问题的分析工具和解决方案。应用程序是CPU/GPGPU受限、I/O 或内存受限或省电的吗?能用什么方式来修复它?
Java SDK Android应用分析:使用SDK Lint工具进行静态分析,使用DDMS进行动态分析(分配/堆、进程和线程利用率、Traceview(方法)、网络),层次结构查看器,系统跟踪。
这个性能瓶颈是否可并行化?是Java还是Native? 反过来会更好吗?以前有这样做过吗? 不要重新发明轮子。对资源使用很智能吗?应该针对哪个版本的Android?静态分析:LINT(下图):
超越静态分析Dalvik Debug Monitor Server (DDMS),DDMS 线程分析(类似“top”但更好):
DDMS:Traceview追踪每种方法消耗多少CPU时间。
分配和HEAP是否以高频方法进行分配:
Dumpsys gfxinfo:将dumpsys数据列放入电子表格并可视化……例如动画是否会掉帧:
Systrace:已经尽所能在应用程序内部进行分析,但仍然找不到瓶颈,Systrace来救援!Systrace.py将生成一个5秒的系统级快照:
ARM DS-5社区版免费Android原生分析器和调试器:
流线型的概览:
Mali GPU的图形分析工具:
应用程序资源优化器 (ARO),免费/开源以网络为中心的诊断工具(由AT&T提供,但不需要AT&T设备),pcap/数据收集需要root,设备上的APK,用于捕获数据分析的Java桌面应用程序。
网络资源:
- 关闭连接。80% 的应用程序在完成后不会关闭连接,LTE功率增加38%(3G功率增加18%)。
- 缓存数据。17 的移动流量是重复下载相同的未更改HTTP内容 ,“这只是一个6 KB的徽标”——6KB * 3DL/会话 * 10000个用户/天 = 3.4GB/月,从本地缓存读取比从Web下载快75-99%,即使支持缓存 - 默认情况下它是关闭的。
- 管理每个连接。将连接分组,节省电池,加快应用程序。
缓存方法:每个文件都有一个唯一标签,在服务器上为每个请求重新验证。高性能网站:规则1 – 减少HTTP请求 ,添加连接会耗尽电池电量,增加500-3000毫秒的延迟。仔细分配Max-Age时间很重要,在达到Max-Age之前,应用程序不会检查服务器上的文件,检索是严格的文件处理时间。
分组连接:下图,红色:每60秒下载一张图片,蓝色:每60秒下载一个广告,绿色:每60秒向服务器发送一次分析。
未分组:使用了38J的能量,分组:使用了16J的能量,节省58%的电量!!
其它网络优化:删除对文件的重定向,它们每次请求大约2-3秒,预取常用文件,线程文件下载而不是串行下载,不应出现4xx、5xx的http响应错误代码,小心定期连接,定期3分钟轮询更新可能会在一天中保持连接1.2 小时,消耗大约20%的电池电量。
适用于ARM的原生开发套件 (NDK):NDK是一个全面的工具包,使应用程序开发人员能够直接为ARM处理器编写。
SMP和并行化:当今市场上几乎所有的Android和移动设备都是多核的,而且这一趋势将继续下去——设计多线程应用程序Davlik Java线程和IPC,AsyncTask通常是将任务快速推送到后台工作线程的最简单方法,且IPC复杂性极低,仿生C库实现了Pthreads API的一个版本,大多数pthread和sem_函数都已实现,但没有SysV IPC,如果它在pthread.h 或 semaphore.h中声明,它将大部分按预期工作。
OpenGL ES 2.0支持可编程嵌入式GPU的完全可编程3D图形,免版税的跨平台API,2D和3D图形,支持Android的框架API和NDK。
为移动/嵌入式/电池编写java:新的方法,应避免,至少不在CPU密集/频繁的活动中,尝试使用静态变量或仅预先分配或在活动的自然停顿时分配,避免触发垃圾收集(使用DDMS)。在JellyBean中为图形使用新功能,例如用于垂直同步脉冲的android.view.Choreographer、myView.postInvalidateOnAnimation(),不要画不会显示的东西c.quickReject(items...)、Canvas.EdgeType.BW。
SIMD: NEON——适用于许多应用的通用SIMD处理,支持用于互联网应用的最广泛的多媒体编解码器,许多软编解码器标准:MPEG-4、H.264、On2 VP6/7/8、Real、AVS、……在软件中支持所有互联网和数字家庭标准。更少的周期,NEON将在复杂的视频编解码器上提供 1.6x-2.5x 的性能,单个简单的DSP算法可以显示更大的性能提升 (4x-8x),处理器可以更快地休眠 => 整体动态节能。直接编程,干净的正交向量架构,适用于广泛的数据密集型计算。不仅适用于编解码器 - 适用于2D/3D图形和其他处理,32个寄存器,64位宽(双视图为16个寄存器,128位宽),现成的工具、操作系统、商业和开源生态系统支持。
线程文件下载(左)与串行下载(右)的对比。
OpenGL ES 3.0 - Challenges and Opportunities分享了OpenGL ES 3.0在移动端的特性、应用和优化。
OpenGL ES是嵌入式系统的开放式图形库,图形硬件的底层软件接口,OpenGL的子集,OpenGL ES驱动的GPU的各种使用,用于智能手机/平板电脑、电视机、汽车等等。桌面GPU计算在移动设备中还需要多长时间?计算能力和带宽速度曲线及趋势预测如下:
OpenGL ES 3.0于2012发布规范,基于OpenGL3.3/4.x的功能集,减少扩展需求,完全向后兼容OpenGL ES 2.0。着色语言GLSL ES 3.00模式如下:
ETC2纹理压缩是标准纹理压缩,支持Alpha、1或2个通道,消除ETC1的限制(不支持Alpha、质感差),理论上不再需要专有纹理格式,更小的文件大小,没有不同的资产包。
布尔遮挡查询:用于基于硬件的可见性测试的软件接口:
glGenQueries
glDeleteQueries
glBeginQuery
glEndQuery
glGetQueryObjectuiv
...
int qid[NUM_OBJECTS];
unsigned int result = 0;
// 生成查询
glGenQueries(NUM_OBJECTS, &qid[0]);
for (int i = 0; i < NUM_OBJECTS; ++i)
{
// 开始查询
glBeginQuery(GL_ANY_SAMPLES_PASSED, qid);
// 以低细节渲染物体.
RenderOccluders();
// 结束查询.
glEndQuery(GL_ANY_SAMPLES_PASSED);
// 等待查询结果有效.
// 注意:这种同步等待结果的方式会导致【严重的性能问题】!!
while (result == GL_FALSE)
{
glGetObjectuiv(qid, GL_QUERY_RESULT_AVAILABLE, &result);
}
// 获取结果。
glGetObjectuiv(qid, GL_QUERY_RESULT, &result);
if (result == GL_TRUE)
{
// 以全精度渲染物体。
RenderObjects();
}
}
...
遮挡查询的CPU和GPU时序图。
ES3比2的遮挡查询能够有明显的提升。
实例化渲染:尽量减少绘图调用,强大的具有大量相同几何图形的场景,相关接口:
glDrawArraysInstanced(GLenum mode, Glint first, GLsizei count, GLsizei primcount)
glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primcount)
glVertexAttribDivisor(GLuint index, GLuint divisor)
gl_InstanceID
可能难以在现有(2013年)渲染管道中实现。
// OpenGL ES 2.0
for ( int i = 0; i < numInstances; i++ )
{
// set for each instance the model-view-projection matrix
glDrawElements(GL_TRIANGLES,mesh->indx_count,GL_UNSIGNED_SHORT,mesh->indx);
}
// OpenGL ES 3.0
glDrawElementsInstanced(GL_TRIANGLES,mesh->indx_count,GL_UNSIGNED_SHORT,mesh->indx, numInstances);
// 非实例化渲染
// Vertex shader
#version 100
uniform mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;
MVP = glGetUniformLocation( programObj, &#34;u_matViewProjection&#34; );
glUniformMatrix4fv(MVP, 1, GL_FALSE, &mvpMatrix );
// 实例化渲染
// Vertex shader
#version 100
// uniform -> attribute
attribute mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;
// glGetUniformLocation -> glGetAttribLocation
MVP = glGetAttribLocation( programObj, &#34;u_matViewProjection&#34; );
// 填充实例化数据.
for (int i = 0; i < 4; i++)
{
glEnableVertexAttribArray(MVP + i);
glVertexAttribPointer(MVP + i,
4, GL_FLOAT, GL_FALSE, // vec4
16*sizeof(GLfloat), // stride
&(matArray + 4*i*sizeof(GLfloat)); // offset
glVertexAttribDivisor(MVP + i, 1);
}
#define LTP_ARRAY 0
#define VERTEX_ARRAY 4
layout(location = LTP_ARRAY) in highp mat4 inLocalToProjection;
layout(location = VERTEX ARRAY) in highp vec3 inVertex;
void main()
{
gl_Position = inLocalToProjection * inVertex;
}
多重渲染目标(MRT)在一次绘制调用中渲染到多个缓冲区,提供在下一代中执行视觉效果的可能性,如延迟照明、细胞阴影、延期贴花、实时局部反射......
// C++
...
unsigned int fb;
unsigned int initializedTexture2D_1;
unsigned int initializedTexture2D_2;
GLenum buffs[] = {GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1};
glGenFrameBuffer(1, &fb);
glBindFramebuffer(GL_FRAMEBUFFER, fb);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, GL_TEXTURE2D, initializedTexture2D_1, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, GL_TEXTURE2D, initializedTexture2D_2, 0);
glDrawBuffers(2, buffs);
// render calls
...
// fragment shader
#version 300 es
layout(location = 0) out lowp vec4 color;
layout(location = 1) out highp vec4 normal;
in lowp vec4 v_color;
in highp vec4 v_normal;
main()
{
color = v_color;
normal = v_normal;
}
OpenGL ES 3.0特性的消耗收益比。
挑战:
- 现有引擎中的实现并非易事。
- 需要对产品管线进行改造。
- 在某些情况下,OpenGL ES 3.0特性不会获得更好的性能。
- 图形部门也需要理解MRT。
- OpenGL ES 3.0设备目前比较少同时支持ES2/ES3。
机会:
- 更好的性能。
- 更小的能耗。
- OEM喜欢看到开发人员使用的最新技术。
- 当前控制台和移动设备之间的差距越来越小。
- 通过扩展,一些3.0功能可在当前的通用硬件上使用。
2013年,大型多人在线游戏 (MMOG) 等视频游戏已成为文化媒介,手机游戏为应用市场带来大量下载和潜在收益。尽管移动设备的处理能力增加了带宽传输,但网络连接不佳可能会成为游戏即服务 (GaaS) 的瓶颈。为了提高数字生态系统的性能,处理任务分布在瘦客户端设备和强大的服务器之间,An Architecture Approach for 3D Render Distribution using Mobile Devices in Real Time基于“分而治之”的方法,即使用游戏中场景序列的树KD对体积曲面进行细分,从而将曲面缩减为小点集。提高了重建效率,因为数据的搜索是在局部和小区域中进行的。流程通过一组有限状态建模,这些状态使用隐马尔可夫模型构建,域由启发式配置。进行了六个控制每个启发式状态的测试,包括间隔数,以验证所提出的模型。该验证得出的结论是,所提出的模型在一系列交互中优化了每秒的响应帧。
节点的架构全连接(遍历)模型。
桌面虚拟现实(Virtual Reality,VR)历来是消费级3D计算机图形的主要显示技术。近来,立体视觉和头戴式显示器等更复杂的技术已变得更加普及。然而,大多数3D软件仍然仅设计用于支持桌面VR,并且必须进行修改以在技术上支持这些显示器并遵循其使用的最佳实践。Virtual Reality Capabilities of Graphics Engines评估了现代3D游戏/图形引擎,并确定了它们在多大程度上适应不同类型的负担得起的VR显示器的输出,表明立体视觉得到了广泛的支持,无论是原生还是通过现有的适应。其它VR技术,如头戴式显示器、头部耦合透视(以及随之而来的鱼缸VR)很少得到原生支持。但是,该文确定并描述了一些方法,例如重新设计,通过这些方法可以添加对这些显示技术的支持。
2013年虚拟现实显示技术有桌面VR(串流)、立体视觉、头部耦合透视、头戴式显示器等几种,它们在模拟模型和用户感知方面的差异如下图:
立体视觉(Stereoscopy)是适用于双目视觉的桌面VR范式的扩展。立体镜通过两次渲染场景来实现这一点,每只眼睛一次,然后以这样的方式对图像进行编码和过滤,使每张图像只能被用户的一只眼睛看到。这种过滤最容易通过特殊的眼镜实现,眼镜的镜片设计为选择性地通过匹配显示器产生的两种编码之一。当前的编码方法是通过色谱、偏振、时间或空间。这些编码方法经常被分类为被动、主动或自动立体。被动和主动编码之间的区别取决于眼镜是否是电主动的:因此被动编码系统是颜色和极化,而唯一的主动编码是时间。自动立体显示器是不需要眼镜的显示器,因为它们在空间上进行编码,这意味着眼睛之间的物理距离足以过滤图像。
消费者立体显示器与计算机的接口方式与桌面VR显示器相同(通过VGA或DVI等视频接口)。由于这些接口中的大多数都没有特殊的立体观察模式,因此将两个立体图像以显示硬件可识别的格式打包成一个图像。此类帧封装格式包括交错、上下、并排、2D+深度和交错。由于这些标准化接口是软件将渲染图像传递给显示硬件的方式,因此软件应用程序不需要了解或适应编码系统的显示硬件。相反,图形引擎支持立体透视所需要的只是它能够从不同的虚拟相机位置渲染两个具有相同模拟状态的图像,并将它们组合成显示器支持的帧封装格式。
头部耦合透视(Head-coupled perspective,HCP) 的工作原理与桌面VR和立体视觉略有不同, 定义了一个虚拟窗口而不是虚拟相机,其边界是虚拟的窗口映射到用户显示器的边缘。因此,显示器上的图像取决于用户头部的相对位置,因为来自虚拟环境的对象会沿用户眼睛的方向投影到显示器上。这种投影可以使用桌面VR中使用的投影数学的离轴版本来完成。
为了做到这一点,必须实时准确地跟踪用户头部相对于显示器的位置。用于此目的的跟踪系统包括电枢、电磁/超声波跟踪器和图像- 基于跟踪。HCP的一个限制是,由于显示的图像取决于用户的位置,因此任何其他观看同一显示器的用户将感知到失真的图像,因为他们不会从正确的位置观看。
头戴式显示器(Head-mounted display,HMD)是另一种单用户VR技术,将立体视觉的增强功能与类似于HCP的大视场和头部耦合相结合。HMD背后的感知模型是完全覆盖用户眼睛的视觉输入,并将其替换为虚拟环境的包含视图。通过将一个或两个小型显示器安装在非常靠近用户眼前的镜头系统来实现的,以实现更自然的聚焦。由于显示器非常靠近用户的眼睛,显示器的任何部分只有一只眼睛可见,使系统具有自动立体感。
头饰中还嵌入了一个方向跟踪器,允许跟踪用户头部的旋转,允许用户通过将虚拟相机的方向绑定到用户头部的方向来使用自然的头部运动来环顾虚拟环境。它与HCP不同,HCP跟踪的是位置,而不是方向。支持HMD的软件要求与立体观察相同,但附加要求是图形引擎必须考虑HMD的方向,以及要校正的镜头系统引起的任何失真。
通过确定可以使用哪些扩展机制来实现所需的VR显示技术来衡量支持级别,已经结合了差异可以忽略不计的扩展机制(例如脚本和插件),并引入了两个额外的级别,不需要扩展(本机支持)和没有引擎内支持(重新设计)。扩展机制按引擎代码相对于实现VR支持的非引擎代码的比例排序,产生的支持级别及其排序如下:
5、原生支持。在原生支持VR技术的引擎中,引擎的开发人员特意编写了渲染管线,使用户只需最少的努力即可启用VR渲染。所需要做的就是检查开发人员工具中的选项或在引擎的脚本环境中设置变量。除了轻松启用该技术外,这些引擎还旨在避免常见的优化和快捷方式,这些优化和快捷方式在桌面VR显示器中并不明显,但随着更复杂的技术变得明显,一个常见的例子是渲染具有正确遮挡但深度不正确的对象,会导致立体镜下的深度提示冲突。
4、通过引擎内图形定制(包括节点图)。一些引擎的设计方式使得可以使用具有图形界面的自定义工具来更改渲染过程,一种方法是通过节点图,其中渲染管道的不同组件可以在多种配置中重新排列、修改和重新连接。根据支持的节点类型,有时可以配置节点以产生某些 VR 技术的效果。下图显示了虚幻引擎的材质编辑界面,该界面配置为将红青色立体立体渲染作为后处理效果。
3、通过引擎内编码(脚本或插件)。每个引擎都可以使用自定义代码进行扩展,使用定义明确但受限制的扩展点。两种常见的形式是在受限环境中运行的脚本,以及引擎加载并运行外部编译的代码插件,两种形式都可以访问引擎功能的子集,但是,插件也可以访问外部API,而脚本不能。由于通常实现特定于应用程序功能的机制,因此可用于自定义代码的引擎功能可能更多地针对人工智能、游戏逻辑和事件排序,而不是控制确切的渲染过程。
2、通过引擎源代码修改。除了免费的开源引擎,一些商业引擎通过适当的许可协议向用户提供其完整的源代码。通过访问完整的源代码,可以实现任何VR技术,尽管所需的修改量可能很大。
1、通过工程改造。对于不提供上述任何定制入口点的引擎,仍然可以通过重新设计进行一些更改。工程改造是逆向工程的一种形式,除了学习程序的一些工作原理之外,还修改了它的一些功能。对渲染管道进行完全逆向工程所需的工作量可能很大,因此更可取的是微创形式的再工程。其中一种方法是函数挂钩,即内部或库函数的调用被拦截并替换为自定义行为。由于很大一部分实时图形引擎使用OpenGL或Direct3D库进行硬件图形加速,因此这些库为通过函数挂钩实现纯视觉VR技术提供了可靠的入口点。事实证明,这种方法可以有效地将立体视觉添加到3D游戏。本文还展示了以这种方式实现头耦合透视也是可能的,通过挂钩加载投影矩阵(glFrustum和glLoadMatrix)的OpenGL函数,并用头部耦合矩阵替换原始程序提供的固定透视矩阵。
影响用户体验的因素有很多,虽然质量因素本质上与显示硬件相关,但适当的软件设计可以缓解这些问题,而粗心的设计可能会引入新问题。可以通过软件减轻的硬件质量因素的示例是串扰(立体)、A/C故障(立体)和跟踪延迟(HCP和HMD)。由于这些因素对于它们各自的显示技术来说是公认的,因此有众所周知的技术可以最大限度地减少它们引起的问题。解决方案分别是降低场景对比度、降低视差和最小化渲染延迟。
不正确的软件实现也会影响VR效果的质量,可能是由于粗心或桌面VR优化的结果。这方面的一个示例是任意位置的特殊图层(例如天空、阴影和第一人称玩家的身体)不同通道的深度。虽然在桌面VR中产生正确的遮挡,但在立体镜下添加双目视差提示会显示不正确的深度,并在这两个深度提示之间产生冲突。由于桌面VR的主导性质,这不是一个不常见的问题,并且可以作为另一个例子,说明简单的第三方实现可能不如原生VR支持。从这些方面应该注意到,虽然非原生VR实现可能满足必要的技术要求,但也必须考虑其它因素。
下表是2013年的主流引擎对VR的支持情况:
引擎 | 立体视觉 | 头部耦合透射 | 头戴式显示器 | UDK | 4:图形定制。可以使用Unreal Kismet创建双摄像头装备,并使用材质编辑器打包输出。 | 1:工程改造。无法从引擎访问自定义相机投影,因此如果无源代码访问权限,则需要工程改造。 | 3:引擎编码。通过自定义实现立体化,可以通过自定义DLL获得头部方向并通过脚本绑定到相机。 | Unity | 3:引擎编码。 | 3:引擎编码。 | 3:引擎编码。 | CryENGINE | 5: 原生。 | 3:引擎编码。 | 3:引擎编码。 | OGRE | 3:引擎编码。 | 3:引擎编码。 | 3:引擎编码。 | Developing Virtual Reality Experiences with the Oculus Rift详细地分享了在Oculus Rift上进行VR开发的技术、过程及实操。
2014年的Rift技术包含开发工具包2,1920x1080 OLED屏幕,每只眼睛一半,广角圆形透镜,90-110度FOV,GPU可校正镜头变形,低持久性–每个像素每帧的亮度小于3毫秒,1000Hz陀螺追踪朝向,60Hz位置跟踪:外部摄像头可以看到HMD上的LED阵列,软件融合、方向和位置预测。
善待VR玩家:虚拟现实开发者每天花数小时观看HMD,大部分时间,到处都会有bug,大脑很快就会学会忽视疯子,但玩家不会!他们的大脑新鲜而天真,他们希望事情是真实的,希望你已经调试了所有东西,并且拥有真正的“存在感”。如果你把每件事都推到11,你会给他们带来创伤,他们会停止玩游戏,给你一星的评价!
每个人都大相径庭,有些人无法忍受的事情,而其他人甚至看不见,没有一个“VR公差”滑块,对一个方面非常敏感的人可能会容忍另一个方面,例如上下楼梯,宽容不仅仅是一种可以学习的技能,可能会有负面反馈:人们对暴露的容忍度会降低。最佳实践指南包含当前已知道的内容,将其用作至少需要认真思考的事项清单。
温和错误,过于激烈的虚拟现实让人更难理解剧情和游戏机制,让紧张体验成为可选,尽量避免“在你脸上”的粒子和爆炸,更少、更慢的移动。默认低难度,让更有经验的VR用户“选择加入”,而不是让新手“选择退出”,便于随时更改,在受“VR打击”之后,允许降低强度以实际玩游戏。
前庭光学反射(Vestibulo-Optical Reflex,VOR):眼球和肌肉、反射神经元、耳内半圆管等部位和交互如下图:
用于“固定”,如静止物体、移动头部,通过耳朵检测头部旋转,<10毫秒后,眼睛转动顺畅,不是扫视!非常平滑,卓越的视觉质量。
VOR增益是耳朵运动和眼睛反应之间的比率,通常给予1:1的补偿,+10头部运动 = -10眼动,在固定过程中获得微调,尝试产生零“视网膜流”,调整速度非常慢。
如果视图被压缩怎么办?一副新眼镜,VR中的渲染比例不正确,10头部运动现在需要-5保持注视的眼球运动,VOR增益现在导致视网膜流,导致定向障碍,增益适应需要1-2周(假设持续使用!)。
保持VOR增益:显示器上的游戏通常有一个“FOV”滑块,监视器上可接受–不会直接影响VOR增益,显示器不会随头部移动——不会发生“虚拟注视”,房间周边视觉提供真实光流真实性检查…但即便如此,它也会给一些人带来问题。在Rift中,唯一需要关注的是虚拟现实,VR对象的视网膜流必须与真实世界的运动相匹配。虚拟现实中的视场比例不是任意选择!它必须与HMD+用户特征相匹配,“医生,当我这样做的时候,会伤害我的玩家的大脑……”
Rift显示屏有一个物理间距,即“每可见度像素数”,准确值取决于失真、用户的头部和眼睛位置等。通过用户配置工具找到,SDK将帮助您精确匹配此音高,对于给定的设备和用户大小,它将提供正确的视野和比例,避免任何改变视野或“缩放”效果,头部旋转10度必须产生10度光流,即使每度像素的微小变化也会给大多数用户带来问题。
IPD——瞳孔间距,实际上每只眼睛有两种成分:从鼻子到瞳孔——“半IPD”,视距——从透镜表面到瞳孔的距离,与HMD的尺寸无关!从中心到眼向量,在用户配置期间设置,存储在用户配置文件中。很少对称,有点人的视距可能相差2毫米,下图的那个家伙的鼻子和瞳孔相差1个像素:
中央瞳孔-SDK报告的位置,头盔显示器的中心线,左右视距的平均值。大致是玩家“感觉”到自己的位置,音频侦听器位置,视线检查,十字线/十字线光线投射的原点。
保持帧率:存在是一个相当二元的东西——有或没有,坚如磐石的高FPS对虚拟现实中的存在感至关重要,以75FPS的速度进行立体声显示很有挑战性,积极删除细节和效果,以保持帧速率和低延迟,保持状态给玩家带来的乐趣远远超过额外效果,主要成本是绘制调用和填充率。
对于绘制调用,双倍的眼睛,双倍的调用,新的API应该可以降低多次提交的成本:Mantle(Vulkan的前身)、DX12等。有些事情只需要做一次:剔除——使用包括双眼的保守平截体、动画、阴影缓冲区渲染、一些远距离反射/光泽贴图/AO渲染——但不是全部!一些延迟照明技术。
对于填充率,更改虚拟相机渲染的大小,而不是帧缓冲区大小,例如对于DK2,帧缓冲区始终为1080x1920–不要更改此设置!但相机眼睛通常每只眼睛渲染1150x1450,取决于用户面部形状和眼睛位置——由个人资料和SDK设置。缩放此渲染效果非常好,失真校正过程将对其进行重新采样和过滤,每帧动态缩放也很好——几乎看不见。如果该帧中有大量粒子/爆炸,请降低大小。使用相同的RT,只需使用一小部分即可,SDK明确支持此用例。
Virtual Reality and Getting the Best from Your Next-Gen Engine阐述了如何在游戏引擎种集成VR渲染。文中提到可调节的眼镜佩戴者:
无需调整即可耐受瞳孔间距的变化:
下图则是关键的(上)和可容忍的参数示意图:
对于立体3D而言,在摄影立体和头盔显示器的固定设置下的所需的焦距要求:
结合下图,(a)大多数HMD的视野都很窄,(b)实现宽视场需要更高分辨率的显示器,(c)或更大的像素。(d)如何做到两全其美?利用眼睛的可变敏锐度,(e)使用扭曲着色器压缩图像的边缘,(f)光学元件应用反向失真,使边缘看起来再次正确,(g)中心像素较小,边缘像素较大。
VR开发需要注意以下事项:
- 不要控制玩家的头部!
- 注意第一人称动作。
- 照片现实主义是没有必要的。
- 不要使用电影级的渲染效果!比如可变焦距、过滤器、镜头光斑、泛光、胶片颗粒、暗角、景深等。
立体渲染质量检查:
- 左右方向正确吗?
- 双眼中的元素相同吗?
- 两幅图像代表同一时间吗?
- 刻度正确吗?
- 深度一致吗?
- 避免快速深度变化了吗?
如果要在游戏引擎集成VR功能,需要注意的是每个引擎都是不同的,但总会有一些相似之处。什么是良好的虚拟现实引擎?答案如下:
- 高质量的视觉效果。
所说的高质量视觉效果是什么意思?没有任何东西会分散你的注意力,让你沉浸在游戏中,良好的着色效果(但不一定是真实照片),通常意味着良好的抗锯齿。
为什么良好的抗锯齿至关重要?人类感知的本质意味着我们很容易被高频噪点分心,分心会降低存在感,使用立体渲染时,锯齿伪影可能会更严重,它们会导致视网膜竞争,良好的抗锯齿比本机分辨率更重要。
抗锯齿方法有:边缘几何AA,通常硬件加速;图像空间AA,非常适合大多数渲染管线,如FXAA、MLAA、SMAA等;时间AA,使用再投影进行时间超采样。
MSAA在高频几何体、几乎垂直的线条、对角线看起来更好,但内部纹理/着色仍然有锯齿。
FXAA在边缘几何体、纹理/着色细节看起来更好,但有时会丢失高频数据中的细节。
超采样反走样渲染到更大缓冲区的效果良好……如果负担得起的话,与良好的下采样过滤器一起使用。
抗锯齿结果:镜面AA也可以大大改善图像,一个很好的起点是研究LEAN、Cheap LEAN (CLEAN)和Toksvig AA,扭曲着色器减少边缘锯齿,在某些游戏中,可能需要更多地关注LOD。
几种AA方法的组合可能会产生更好的结果,每种不同的AA解决方案都能解决不同方面的锯齿问题,使用最适合引擎的方法。
- 一致的高帧速率。
为什么一致的高帧速率至关重要?在虚拟现实中,低帧速率看起来和感觉都很糟糕,如果没有高帧率,测试就很困难。在整个开发过程中保持高帧率,缺少V-sync也更为明显,因此,请确保启用了V-sync。
在当前的引擎中,“通道”的概念被广泛接受,如反射渲染、阴影渲染、后处理等,每个通道都有不同的要求,每个通道都要找出瓶颈所在。CPU?DrawCall?状态设置?资源设置?GPU?顶点处理受限?几何处理受限?像素处理受限?
绘制调用、状态设置或资源设置时CPU受限?考虑如何使用几何着色器,可以减少绘制调用的总数,阴影级联渲染:drawCallCount/n,其中n是层叠的数量,立方体贴图渲染:drawCallCount/6。降低资源设置成本,它还有其它特性可以帮助将处理从CPU上移开。
几何渲染单元将一个图元流转换为另一个可能更大的图元流,在像素着色器之前发生,即在直接顶点像素绘制调用中的顶点着色器之后,如果启用了细分,则在Hull着色器之后。
几何体着色器功能,渲染目标索引/视口索引,用于单程立方体贴图渲染、阴影级联、S3D、GS实例,允许逐图元运行同一几何体着色器的多次执行,而无需再次运行上一个着色器阶段。
用于立体3D渲染的几何体着色器,一种使引擎立体3D兼容的简单方法,为每种材质添加一个GS(或调整已有材质的GS),如下所示:
[maxvertexcount(3)]
void main(
inout TriangleStream<GS_OUTPUT> triangleStream,
triangle GS_INPUT input[3])
{
for(uint i = 0; i < 3; ++i)
{
GS_OUTPUT output;
output.position = (input.worldPosition , g_ViewProjectionMatrix);
triangleStream.Append(output);
}
}
顶点/几何体受限?通过压缩属性来减少顶点大小,在着色器阶段之间打包所有属性,如果正在使用用于放大或细分管道的几何体着色器,这一点很重要。考虑使用延迟获取(late fetch)法:将顶点属性数据绑定为使用的着色器阶段中的缓冲区,高度依赖硬件,始终调试性能,看看是否有影响!减少在GPU周围移动的数据。
像素受限?降低像素着色器的复杂性,减少每帧着色的像素数,一个使用较小渲染目标的实验上采样与高质量视觉冲突,引入光晕、微光和视网膜冲突。
考虑使用重新投影来加速立体3D渲染的方面,在PlayStation 3立体声3D游戏中获得巨大成功,然而,它只能在视差较小的情况下成功使用。
- 出色的跟踪和标定。
项目Morpheus SDK处理跟踪,使用SDK提供的跟踪矩阵。游戏定义的默认观看位置和方向:
追踪玩家头部与摄像机的偏移量:
玩家眼睛相对于头部矩阵的偏移量:
跟踪器重置功能:设置头部位置,使其与游戏摄像机的位置和偏航对齐。
标定:重置位置和方向跟踪,重新调整游戏世界与现实世界的关系,以便固定的玩家位置,传递和游戏(Pass-and-play),匹配不同身高的玩家。
- 低延迟。
为什么减少延迟如此重要?延迟是输入和响应之间的时间间隔,重要的不仅仅是始终如一的高帧率。不仅用于虚拟现实头部跟踪,提高响应能力在游戏中至关重要,游戏编程人员了解响应控制的必要性,网络程序员了解对响应性对手的需求等等。
多上下文渲染:从引擎的角度来看,减少延迟的一种方法是使用延迟上下文在多个线程上异步构建命令列表(又称命令缓冲区),作为即时上下文,在命令缓冲区中将命令排队时会产生渲染开销,相比之下,在回放期间,命令列表的执行效率要高得多,适用于“通道”的概念。多上下文渲染允许GPU在帧中更早地开始处理,从而减少延迟。
单上下文(上)和多上下文(下)渲染的对比。
多上下文渲染最简单的测试用例:为每只眼睛的视图并行创建和提交命令列表,立即减少CPU帧时间,如果引擎受CPU受限,意味着帧延迟会立即减少。如果引擎是GPU受限,但GPU现在在帧中完成得更早,因为它启动得更早,意味着帧延迟立即减少。
虚拟现实的延迟考虑:是否有任何特定于虚拟现实的方法来应对延迟?采样跟踪数据和使用该数据渲染帧之间的时间需要尽可能短,不要使用超过两倍的缓冲,使用最新的方向数据重新投影图像可以改善明显的延迟和帧速率。尽可能降低被跟踪外围设备的延迟,是否有任何特定于平台的方法来应对延迟?
预测(Prediction):带有Project Morpheus的PlayStation 4是一个已知的系统,硬件中存在任何延迟,库/软件中存在任何延迟,需要想方设法减少这些延迟。提供CPU和GPU性能分析工具,使开发者能够计算并减少游戏中的延迟,可以用它来预测图像显示时HMU的位置。减少引擎延迟是关键,但使用预测来掩盖任何微小的剩余延迟都可以很好地发挥作用,指定的预测量越小,其质量越好。
假设现在拥有一个非常高效、高帧率、低延迟、超高质量的下一代引擎中拥有了出色的跟踪功能,该引擎针对虚拟现实进行了优化……引擎的工作完成了吗?当然不是!还有特定于平台的优化、跟踪外围设备、社交方面、游戏性/设计元素等工作。
异步计算:仍受CPU限制?也许Compute可以帮助将可并行任务卸载到GPU上。仍然受限于GPU?Compute允许你从不同的、更通用的角度来思考GPU任务,在GPU未被充分利用的地方使用它,阴影渲染通常需要顶点/几何体,因此它是安排异步计算任务的好地方。
Diving into VR World with Oculus阐述了Oculus下的VR开发攻略。
虚拟现实中最重要的因素有:短余辉(Low Persistence)、延迟、现实。
Oculus SDK 0.4.x的特色:支持DK1和DK2,新款C接口; 用新款SDK来对游戏进行重新编译。位置跟踪,位置原点目前距离摄像头1米,SDK用预测的Pose状态来报告传感器状态,包括方向、位置和导数,超出跟踪范围时,给出旗标提示。依靠头部模型,Direct-to-Rift和扩展模式,OVR配置工具。
OVR软件堆栈如下图,C接口: 容易与其它语言连接,驱动程序DLL: 自动支持硬件和功能的变更,OVR服务: 在各应用之间的Rift分享和虚拟现实转换。
SDK渲染与游戏渲染的比较:SDK 0.2未做任何渲染,只提供适当渲染所需的参数;SDK 0.4中的新渲染后端,延续了关键的渲染特色,Game(App) layer gives层将SDK左、右眼纹理ovrHmd_EndFrame()。
SDK的一般工作流程:
- ovrHmd_CreateDistortionMesh。通过UV来转换图像,比像素着色器的渲染效率更高,让Oculus能更灵活地修改失真。
- ovrHmd_BeginFrame。
- ovrHmd_GetEyePoses。
- 基于EyeRenderPose(游戏场景渲染)的立体渲染。
- ovrHmd_EndFrame。
用后面的特性在SDK 0.4上完成渲染:利用色差和时间扭曲来实现桶形畸变,内部延迟测试及动态额预测,低延迟垂直同步(v-sync)和翻转(用direct-to-rift,甚至会更好),健康与安全警告。
SDK易于集成,无需创建着色器和网格,通过设备/系统指针和眼睛纹理,支持OpenGL和D3D9/10/11,必须为下一帧重新申请渲染状态。好处:与今后的Oculus硬件和特性更好地兼容,减少显卡设置错误,支持低延迟驱动显示屏访问,例如前前缓冲区渲染等,支持自动覆盖:延迟的测试、摄像头指南、调试数据、透视、平台覆盖。
支持Unreal Engine 3、Unreal Engine 4、Unity等主流游戏引擎使用SDK渲染。
扩展模式:头戴设备显示为一个OS Display,应用程序必须将一个窗口置于Rift监视器上,图标和Windows在错误的位置,Windows合成器处理Present,通常有至少一帧延迟,如果未完成CPU和GPU同步,则有更多延迟。
Direct To Rift:输出到Rift,显示未成为桌面的一部分。头戴设备未被操作系统看到,避免跳跃窗口和图标,将Rift垂直同步(v-sync)与OS合成器分离,避免额外的GPU缓冲,使延迟降到最低,使用ovrHmd_AttachToWindow,窗口交换链输出被导向Rift,希望直接模式成为较长期的解决方案。
延迟:Motion-to-photon的延迟,涉及多阶段:动作、传感器、处理与合并、渲染、Scanout、传输、像素变化时间、像素余辉。
将延迟保持在低值是提供良好虚拟现实体验的关键,目标是< 20毫秒,希望接近5毫秒。
渲染延迟- 时间扭曲(TimeWarp):将渲染重新延迟到后面一个时间点,与变形同时进行,减少感受到的延迟,负责DK2滚动快门,SDK 0.4处理方向、位置。在帧结束前,使用传感器是否有其它方式?时间扭曲– 预测的渲染(John首创)。
Programming for Multicore & big.LITTLE阐述了ARM的大小核心的特殊CPU并行架构,可区别地处理不同计算密集度的任务,以达到省电和性能的均衡。
多核和big.LITTLE之多处理的情况:平台趋势:从中端到高端的四核+内核明显增加,一切都在变大——LTE、GPU、摄像头、显示屏,单线程性能改善正在减少——关注多核,这不仅仅关乎性能——热量约束用例现在已经司空见惯。软件趋势:操作系统供应商更多地利用多核,更广泛地了解多处理支持库,增加设备的组合使用,例如增强现实。
利用并行性,在核心内可以使用NEON、SIMD,使用并行的工具(OpenMP、Renderscript、OpenCL等),尽可能地多线程,从来都不容易,但越来越有必要。
2014-2015年的多核心趋势:Cortex-A15/Cortex-A7 big.LITTLE的2014年的高端产品,芯数范围:4(2+2)、6(2+4)和8(4+4)核心,Cortex-A17/Cortex-A7(32b)将于2015年上市。2014年,ARMv8-A(64b)芯片组在所有细分市场中崭露头角,四核和八核Cortex-A53进入入门级和中端,高端移动设备预计将在2015年向A57和A53 big.LITTLE迁移,多个big.LITTLE预期的拓扑结构。新的小型处理器提供与Cortex-A9类似的性能,使用大处理器(如Cortex-A15)显著提升性能。
从程序员的硬件视角看big.LITTLE系统:高性能Cortex-A57 CPU集群,节能Cortex-A53 CPU集群,CCI-400保持集群之间的缓存一致性,GIC-400提供透明的虚拟中断控制。
big.LITTLE:来自4+4MP系统与Quad Cortex-A15的证据:
big.LITTLE开发 / 关于全局任务调度(GTS)的一般建议:
- 相信调度程序。Linux将为性能和效率制定时间表,所有新任务都从大屏幕开始,以避免延迟,快速适应任务的需要。
- 除非你知道线程是密集的,但不是紧急的,可以在小核执行则永远不要使用大核,例如可能将小核用于在单独的线程上加载资产。
- 小核心是伟大的。你会经常使用它们,Cortex-A53的性能比Cortex-A9高20%,大多数工作负载将运行在很少的服务器上,为其它SoC组件提供更多热量空间。
- 大核心是重要的动力。把它们想象成短脉冲加速器,例如基于物理的特效,在设计过程中考虑权衡。
需要避免:
- 共享公共数据的不平衡线程。集群一致性很好,但不是免费的。
- 如果有实时线程,请注意实时线程不是自动迁移的,实时线程是一个设计决策,请仔细考虑亲和性。
- 避免在大内核上执行长时间运行的任务。很少需要长时间的处理能力,这个任务可以并行化吗?
在2014年的big.LITTLE和Global Task Scheduling(或HMP)设备:精彩的巅峰表现,针对长时间运行的工作负载的节能、可持续计算;多处理:超越单线程性能的限制,避免对性能的热量约束。
NEON是一种广泛的SIMD数据处理体系结构,ARM指令集的扩展,32个寄存器,64位宽(双视图为16个寄存器,在ARMv7中为128位宽),NEON指令执行“打包SIMD”处理,寄存器被视为相同数据类型元素的向量,数据类型:有符号/无符号8位、16位、32位、64位、单/双精度、浮点还是整数。指令在所有线程上执行相同的操作。
通用SIMD处理适用于许多应用:
- 支持用于互联网应用的范围最广的多媒体编解码器。许多软编解码器标准:MPEG-4、H.264、ON2VP6/7/8/9、Real、AVS…,在软件中支持所有互联网和数字家庭标准。
- 需要更少的周期。NEON将在复杂的视频编解码器上提供1.6x-2.5x的性能,单个简单的DSP算法可以显示更大的性能提升(4x-8x) ,处理器可以更快地休眠=>整体动态节能。
- 易于编程。清晰正交向量结构,适用于广泛的数据密集型计算,不仅适用于编解码器——适用于2D/3D图形和其它处理,现成的工具、操作系统、商业和开源生态系统支持。
NEON的优化路径:
- 开源库,例如OpenMAX、libav、libjpeg、Android Skia等,免费提供的开源优化。
- 矢量化编译器。利用现有源代码自动利用NEON SIMD,状态:发布(在DS-5 armcc、CodeSourcery、Linaro gcc和现在的LLVM中)。
- NEON指令集。NEON操作的C函数调用接口,支持NEON支持的所有数据类型和操作,状态:发布(在DS-5和gcc中),LLVM/Clang正在开发中。
- 汇编程序。对于那些真正想在低级别上进行优化的人,状态:已发布(在DS-5和gcc/gas中)。
- 商业渠道。优化并支持现成的软件包。
Arm的各代架构图:
ARMv7-A到ARMv8-A的演变:
异常级别和交互处理:
OpenGL ES 3.0 and Beyond: How To Deliver Desktop Graphics on Mobile Platforms阐述了用OpenGL ES 3.0在移动端开发出桌面般的图像特性的说明。
OpenGL ES 3.1在2014年发布,当时的安卓应用占比达62%,ES 3.0的使用率达到8%:
OpenGL ES 3.0的新功能:
- 主要新功能:
- 多重渲染目标
- 遮挡查询
- 实例渲染
- 统一缓冲区对象(UBO)和统一块
- 变换反馈
- 基本重启
- 序二进制
- 增强的纹理功能:
- Swizzle、3D纹理、2D阵列纹理、LOD/MIP级别夹具、无缝立方体贴图、不变纹理、NPOT纹理、采样器对象
- 新的渲染缓冲区和纹理格式:
- 浮点格式
- 共享指数RGB格式
- ETC/EAC纹理压缩
- 深度和深度/模板格式
- 单通道和双通道纹理(R和RG)
- ES着色语言3.00版:
- 完全支持32位整数/浮点数据类型(IEEE754)
- 输入/输出存储限定符,复制到/来自后续/上一管道阶段的值。
- 数组构造函数和操作
- 新的内置函数
其中实例化和非实例化的对比:
英特尔Bay Trail平台上的OpenGL ES 3.1:
OpenGL ES 3.1-计算着色器模型:
OpenGL ES 3.1 EXT Extensions–细分着色器:
OpenGL ES 3.1英特尔扩展–像素同步:
- 概念
- 英特尔OpenGL | ES扩展:GL_INTEL_fragment_shader_ordering
- 允许从着色器中同步无序的内存访问
- 在同步点向着色器添加单个内置项:beginFragmentShaderOrderingINTEL();
- 好处
- 使用无序内存访问映射到同一像素的片段可能会导致数据竞争
- 片元可以按顺序进行阴影处理
Assassin&#39;s Creed Identity: Create a Benchmark Mobile Game!讲述了如何制作一个高性能的移动端游戏,包含引擎选择、内容创作、架构概述、游戏逻辑、统一编辑器扩展等内容。
使用的引擎要求可以快速原型化支持:易于学习的环境,支持动画驱动的游戏,编辑器框架应该易于理解。移动端友好型,艺术家驱动,专注于可扩展工具,灵活的许可条款(例如iOS和Android仅适用于部分工程师)。
在设计架构时,遵循几条优先级。优先事项1:协作促进跨部门工作;优先事项2:让人们继续工作,不要破坏构建;优先级3:保持敏捷,解耦计划和流线处理。
前提条件:尽可能将游戏逻辑从Unity逻辑分离,控制和调试Unity引擎的Mono行为被认为是一项挑战。首先,是优化游戏逻辑实体的更新时间:
(重新)使用有限数量的3D角色:
为什么要重写Unity引擎的实体组件模型?启动、唤醒、更新和修复更新不允许刺客信条团队想要的控制粒度,复合实体只能通过预置进行克隆,并且需要像下面这样:
public class PlayerCharacter: AssassinCharacter, IPlayerCharacterStateMachinesCarrier,IPlayerEventHandlerCarrier, IParticipantCarrier, IVisionCarrier
{
public PlayerCharacter() : base(entity=> newPlayerCharacterStateMachines(entity))
{
...
文中还对Unity的C#提出一些深层次的优化建议和注意视线,感兴趣的童鞋可阅读全文。
Frostbite on Mobile分享了Frostbite在移动端化的相关技术和经验,包含从GL迁移到Metal、着色器、光照等。
在GL迁移到Metal过程中,有两大挑战:1、引擎在内存消耗方面已经开始与xbox 360时代有所不同;2、许多着色器都是用纯HLSL编写的,使用YACCGL作为着色器转换器。
利用Metal经验改进OpenGL ES 3.0后端,花时间将Metal和GL后端与控制台/PC对齐。管理磁贴内存:glInvalidateFramebuffer,glClear,所有平台上的延迟渲染/正向渲染,ES上的大多数功能,但性能较低。
在光照方,许多光源都支持使用灯光分块优化,所有游戏都转移到基于物理的渲染。光源类型有点光源、聚光灯、区域光源、阴影投射等效物、平面反射、局部反射体积等。光照分块的向前vs延迟如下图:
交叉编译许多复杂着色器,计算用于光源剔除/装箱(binning)的着色器,在Deferred / Forward / Forward+之间切换。用于局部反射的立方体贴图数组unw(ra/ar)ped到2d lat-long纹理数组,采样时有些alu开销,但支持硬件寻址/过滤/MIPMAP。
将延迟光积累从cs重写为vs/ps,在tile内存中累积光照,在Metal上没有间接的drawcalls/Dispatch,使用早期的顶点着色器模拟。相关优化:
- 后端优化。公开tiler提示api并大量使用(非tiler上的nop:s),合并尽可能多的渲染过程,减少状态变化。
- 着色器代码。尽可能多地使用内部函数/内置函数,使用标量数学,仔细打包、对齐数据。
总之,在深入细节之前先了解全貌,今天的移动硬件和api支持完整的引擎功能集,许多特定于tile内存的优化都可以在不偏离桌面/控制台代码基础的情况下完成,如果为多个平台构建,请使用交叉编译器。新API如Vulkan/ES 3.1、spir-v,特定于tile的着色器优化(延迟着色),使用tile本地存储进行高效渲染,特定于移动设备的着色器优化(fp16/fp32使用、alu/带宽平衡),未来可以考虑Tesselation、异步计算、间接绘制等。
Advanced VR Rendering阐述了Valve的VR平台上的尝试和改进,包含立体绘制、时序(调度、预测、VSync、GPU气泡)、镜面锯齿和各向异性照明以及其它VR渲染主题。
Valve的VR有3年多的研究,联合了硬件和软件工程师,专为VR设计的定制化光学元件,显示技术——低持久性、全局显示,跟踪系统(基于基准的位置跟踪、基于点的桌面跟踪和控制器、激光跟踪耳机和控制器),SteamVR API–跨平台、OpenVR。
HTC Vive开发者版规格:刷新率是90赫兹(每帧11.11毫秒),低持久性,全局显示,帧缓冲区的分辨率是2160x1200(每只眼睛1080x1200),离屏渲染的宽高约1.4倍:每只眼睛1512x1680 = 254万个着色像素(蛮力),FOV约为110度,360 房间尺度跟踪,多个跟踪控制器和其它输入设备。
光学与变形:Warp通道分别为RGB使用3组UV,以考虑空间和颜色失真。
可视化1.4倍的渲染目标。其中上图是扭曲前,下图是扭曲后。
每秒着色可见像素数:30赫兹时720p:2700万像素/秒,60Hz时1080p:1.24亿像素/秒,30英寸监视器2560x1600@60赫兹:2.45亿像素/秒,4k监视器4096x2160@30赫兹:2.65亿像素/秒,90赫兹时的VR 1512x1680x2:4.57亿像素/秒,我们可以将其降低到3.78亿像素/秒,相当于非虚拟现实渲染器在100赫兹时的30英寸监视器。
没有“小”的影响:跟踪允许用户接近跟踪体积中的任何内容,无法实现超昂贵的效果,并声称“这只是角落里的一个小东西”,即使是最低画质也需要比传统创作的更高的逼真度,如果在跟踪体积中,必须是高保真的。
虚拟现实渲染目标:最低GPU最低规格,希望虚拟现实取得成功,但需要客户,最低规格越低,客户就越多,客户不应注意到锯齿,客户将锯齿称为“闪烁”,算法应该扩展到多个GPU上。
立体渲染(单GPU):强力运行CPU代码两次(错误),使用几何体着色器放大几何体(错误),重新提交命令缓冲区(很好的解决方案),使用实例将几何体加倍(更好,API调用减少一半,提高了VB/IB/texture读取的缓存一致性),来自High Performance Stereo Rendering For VR。
立体渲染(多GPU):AMD和NVIDIA都提供DX11扩展以加速跨多个GPU的立体渲染,AMD实现的帧速率几乎翻了一番,但还没有测试NVIDIA的实现。非常适合开发人员,团队中的每个人都可以在他们的开发盒中使用多GPU解决方案,在没有不舒服的低帧率的情况下打破帧率。
预测(Prediction):目标是尽可能缩短HMD和控制器变换的预测时间(渲染为光子)(精度比总时间更重要),低持久性全局显示:在11.11毫秒帧中,面板仅点亮约2毫秒。
上面的图像不是最佳的VR渲染,但有助于描述预测。
管线架构:渲染当前帧时模拟下一帧:
在提交之前,会重新预测转换并更新全局cbuffer,由于预测限制,虚拟现实实际上需要这样做,必须保守地在CPU上减少大约5度。
等待VSync:最简单的VR实现,在VSync之后立即预测,模式#1:Present(),清除后缓冲区,读取像素;模式#2:Present(),清除后缓冲区,在查询上自旋转(spin)。非常适合初始实现,但避免这样做,GPU不是为此而设计的。
“运行开始”的VSync:怎么知道离VSync有多远?很棘手,图形API并不直接提供这一点。Windows上的SteamVR/OpenVRAPI在一个单独的进程中,在调用IDXGIOutput::WaitForVBlank()时旋转,记录时间并递增一个帧计数器。然后,应用程序可以调用getTimeSincellastVsync(),该函数也会返回一个帧ID。GPU供应商、HMD设备和渲染API应该提供这一点。
“运行开始”的细节:要处理坏帧,需要与GPU部分同步,在清除后缓冲区后注入一个查询,提交整个帧,在该查询上旋转,然后调用Present(),确保在当前帧的VSync的正确一侧,现在可以旋转直到运行开始时间:
为什么查询题很关键?如果有一帧延迟,查询将在下一帧的VSync右侧,确保预测保持准确(下图橙色部分):
开始运行总结:具有一个稳定的1.5-2.0毫秒GPU性能增益!正常情况,可以分别在NVIDIA Nsight和微软的GPUView中看到下图所示:
锯齿是VR的头号敌人:相机(玩家的头)永远不会停止移动,因此,锯齿会被放大。虽然要渲染的像素更多,但每个像素填充的角度比以前做的任何事情都大,以下是一些平均值:2560x1600 30英寸显示器:约50像素/度(50度水平视场),720p 30英寸显示器:约25像素/度(50度水平视场),VR:约15.3像素/度(110度视场,是非VR的1.4倍),必须提高像素的质量。
4xMSAA最低质量:前向渲染器因抗锯齿而获胜,因为MSAA正好有效,如果性能允许,使用8xMSAA,必须将图像空间抗锯齿算法与4xMSAA和8xMSAA并排进行比较,以了解渲染器将如何与业内其它渲染器进行比较,使用HLSL的“sample”修饰符时,抖动的SSAA显然是最好的,但前提是可以节省性能。
法线贴图依然可用,大多数法线贴图在虚拟现实中效果都很好。无效的情况:跟踪体积内大于几厘米的特性细节不好,以及被跟踪体积内的表面形状不能在法线贴图中。有效的情况:无法近距离查看的被跟踪体积外的远处物体,以及表面“纹理”和精细细节。法线贴图映射错误:
任何只生成平均法线的mip过滤器都会丢失重要的粗糙度信息:
用Mips编码的粗糙度:可以存储一个各向同性值(可视为圆的半径),是所有2D切线法线与促成该纹理的最高mip的标准偏差,还可以分别存储X和Y方向标准偏差的二维各向异性值(可视化为椭圆的尺寸),该值可用于计算切线空间轴对齐的各向异性照明!
添加艺术家创作的粗糙度,创作了2D光泽=1.0–粗糙度,带有简单盒过滤器的Mip,将其与每个mip级别的法线贴图粗糙度相加/求和,因为有各向异性光泽贴图,所以存储生成的法线贴图粗糙度是免费的。
左:各向同性光泽度;右:各向异性光泽度。
切线空间轴对齐的各向异性照明:标准各向同性照明沿对角线表示,各向异性与任一相切空间轴对齐,只需要2个附加值与2D切线法线配对=适合RGBA纹理(DXT5>95%的时间)。
粗糙度到指数的转换:漫反射照明将Lambert提高到指数(N\cdot L^k),其中k在0.6-1.4范围内尝,试了各向异性漫反射照明,但不值得这么做,镜面反射指数范围为1-16384,是具有各向异性的修改的Blinn-Phong。
void RoughnessEllipseToScaleAndExp(float2 vRoughness, out float o_flDiffuseExponentOut,out float2 o_vSpecularExponentOut,out float2 o_vSpecularScaleOut)
{
o_flDiffuseExponentOut=((1.0-(vRoughness.x+ vRoughness.y) * 0.5) *0.8)+0.6;// Outputs 0.6-1.4
o_vSpecularExponentOut.xy=exp2(pow(1.0-vRoughness.xy,1.5)*14.0);// Outputs 1-16384
o_vSpecularScaleOut.xy=1.0-saturate(vRoughness.xy*0.5);//This is a pseudo energy conserving scalar for the roughness exponent
}各向异性的光照计算过程:
几何镜面锯齿:没有法线贴图的密集网格也会产生锯齿,粗糙度mips也无济于事!可以使用插值顶点法线的偏导数来生成近似曲率的几何粗糙度项。
float3 vNormalWsDdx = ddx(vGeometricNormalWs.xyz);
float3 vNormalWsDdy = ddy(vGeometricNormalWs.xyz);
float flGeometricRoughnessFactor = pow(saturate(max(dot(vNormalWsDdx.xyz, vNormalWsDdx.xyz), dot(vNormalWsDdy.xyz, vNormalWsDdy.xyz))), 0.333);
vRoughness.xy=max(vRoughness.xy, flGeometricRoughnessFactor.xx); // Ensure we don’t double-count roughness if normal map encodes geometric roughness
flGeometricRoughnessFactor的可视化。
MSAA中心与质心插值并不完美,因为过度插值顶点法线,法线插值可能会在轮廓处导致镜面反射闪烁。下面是文中使用的一个技巧:
// 插值法线两次:一次带质心,一次不带质心
float3 vNormalWs:TEXCOORD0;
centroid float3 vCentroidNormalWs:TEXCOORD1;
// 在像素着色器中,如果法线长度平方大于1.01,请选择质心法线
if(dot(i.vNormalWs.xyz, i.vNormalWs.xyz) >= 1.01)
{
i.vNormalWs.xyz = i.vCentroidNormalWs.xyz;
}法线贴图编码:将切线法线投影到Z平面上仅使用2D纹理范围的约78.5%,而半八面体编码使用2D纹理的全部范围:
缩放渲染目标分辨率:事实证明,1.4x只是HTC Vive的一个建议(每个HMD设计都有一个基于光学和面板的不同建议标量),在较慢的GPU上,缩小建议的渲染目标标量,在速度更快的GPU上,放大建议的渲染目标标量,尽量利用GPU的周期。
各向异性纹理滤波:提高了显示器的分辨率(别忘了,VR的每度只有更少的像素),对于颜色和法线贴图,强制启用此选项,默认使用8x。禁用其它所有功能,仅三线性,但需要测量性能。如果在其它地方遇到瓶颈,各向异性过滤可能是“免费的”。
噪点是你的朋友,在虚拟现实中,过渡很可怕,带状(banding)比液晶电视更明显,当像素着色器中有浮点精度时,可在帧缓冲区中添加噪点。
float3 ScreenSpaceDither(float2vScreenPos)
{
// Iestyn&#39;s RGB dither(7 asm instructions) from Portal 2X360, slightly modified for VR
float3 vDither = dot(float2(171.0, 231.0), vScreenPos.xy + g_flTime).xxx;
vDither.rgb = frac(vDither.rgb / float3(103.0, 71.0, 97.0)) - float3(0.5, 0.5, 0.5);
return (vDither.rgb / 255.0) * 0.375;
}环境图:无穷远处的标准实现 = 仅适用于天空,需要为环境图使用某种类型的距离重新映射:球体很便宜,立方体更贵,两者在不同的情况下都很有用。
模板网格(隐藏区域网格):用模板屏蔽掉实际上无法透过镜头看到的像素,GPU在提前模板拒绝时速度很快。或者,可以渲染到接近z的深度缓冲区,以便所有像素都可启用提前z测试,透镜会产生径向对称变形,意味着可以有效地看到投影在面板上的圆形区域。
模板网格图例。从上到下从左到右依次是:扭曲视图、理想扭曲视图、浪费的空间、无扭曲视图、无扭曲视图(屏蔽无效像素)、最终无扭曲视图。
模板网格(隐藏区域网格):SteamVR/OpenVRAPI提供此网格,填充率可以降低17%!无模板网格:VR 1512x1680x2@90Hz:4.57亿像素/秒,每只眼睛254万像素(总计508万像素),带模板网格:VR 1512x1680x2@90Hz:3.78亿像素/秒,每只眼睛约210万像素(总计420万像素)。
扭曲网格,依次是:镜头畸变网格、暴力、剔除0-1之外的UV、剔除模板网格、收缩扭曲。
需要性能查询!总是保持垂直同步,禁用VSync查看帧率会让玩家头晕,需要使用性能查询来报告GPU工作负载,最简单的实现是测量从第一个到最后一个draw调用。理想情况下,测量以下各项:从Present()到第一次绘图调用的空闲时间、从第一次绘图调用到最后一次绘图调用、从上次绘图调用到现在的Present()的空闲时间。
总结:立体渲染、预测、“运行开始”(每帧节省1.5-2.0毫秒)、各向异性照明和Mipping法线贴图、几何镜面抗锯齿、模板网格(节省17%的渲染像素)、优化的扭曲网格(降低15%的成本)。
14.4.3.4 并行技术
UFO Invasion: DX11 and Multicore to the Rescue讲解了DirectX 11下的多线程特性。文中对比了多线程和单线程的任务执行模式,多线程下不再串行地执行任务,而是划分成若干各子任务,并在多帧直接重叠:
在多线程模式下,当有的工作线程处于饥饿(空闲)状态时,应该可以从其它满负载的线程偷取任务执行:
文中提及了Entity的概念和性质,Entity包含两种数据:State和Mind。State是公开给游戏的其它系统的数据,Mind是Entity的私有数据。Entity更新时,可以并行地更新State和Mind,但需遵循的规则如下:
- 永远没有其它实例读取Mind。
- 更新Mind时不要改变State。
- 更新Mind时不要关注其它实例。
Entity更新允许依赖,有时上一帧的信息不够,但知道想知道的,Entity可以声明自己依赖于另一个(或其它)Entity。
Render是完全并行的,因为每个线程都在自己的队列中收集数据,排序稍后会将所有内容放在一起。每个条目的权重为128位(64位密钥、32位实体ID、32位参数),对于4096个条目,需要排序的64K数据,勉强足以证明并行排序的合理性……许多昂贵的渲染部分同时发生在此,最显著的是剔除。
渲染本质上是在一个线程上连续发生的,查看当前密钥与前一个密钥之间的差异,引擎可以非常有效地更新管线的状态。从一个越界的“Last Key”开始,它使引擎选择正确的渲染目标、视口和各种状态。Entity被回调以绘制它需要绘制的任何内容,当前键成为最后一个键,重复直到完成。实例化实际上只是累积具有相同实例化id的键并使用结果列表回调Entity的问题。
DirectX 11的多线程非常简单:
- 多线程渲染到延迟上下文。
- 延迟上下文生成命令列表。
- 主线程将它们提交给立即上下文。
适应DirectX 11多线程模型的渲染流程如下:
最巧妙的是,这种方法还处理命令列表不依赖于当前管线状态的要求:总是从一个超出范围的“最后一个键”开始,即每个命令列表都以完整的管线状态开始。
Shears - Squeeze the Juice Out of the CPUs: Post Mortem of a Data-Driven Scheduler介绍了一种通过解决多线程环境中的数据争用来安排引擎循环的创新方法。这种数据驱动的调度最大限度地减少了数据竞争,并最大限度地增加了包括Cell的SPU在内的硬件占用。使用无锁算法实现,与更传统的调度程序相比,实现了更好的性能。
当前引擎在循环上主流的做法是采用重叠,利用多线程并行:
执行分布在多个线程上,然后收集有关同步点的数据,即使它可以很好地在微观和宏观上扩展,但有数据访问问题:处理的只读权限或写访问但需要锁同步原语,需逐项目调整。
下图的架构即使它的伸缩性比同步点好,但和上一个问题一样,仍然需要同步点和/或锁,逐项目的调整:
可以做些什么来避免这些问题?使用剪刀(Shear):将大任务切割成更小的块。结合下图看一下前面的任务序列,推送一堆数据,任务C锁定直到A&B完全完成,所以,识别2个数据流D0&D1。
改变视角,从数据流看。数据流表示自上而下,任务仍然存在,添加了重要元素:任务访问,任务访问定义调度。现在放入数据流,任务A&B先运行然后C,结果和以前一样。
现在将数据倒入数据流中,结果 => 任务C可以更早开始,不等待任务A&B,调度程序的输入意味着数据访问声明而不是任务序列声明。
以游戏循环引擎为例:数据从顶部 => 流向底部,一切都与数据流有关,没有数据 => 没有进程 => 只会执行必要的代码。
文中还给出了详细的动画说明Lock-free的执行过程(下图是静态的,无法展示动画):
下表是在Intel Core 2 Quad Processor Q6600测试临界区和无锁原子的性能对比,单位是操作数/毫秒,2个线程:不是20%或50%,快36倍 - 超过3000%,57%的空闲时间调用锁函数,即使锁成功。
随着硬件开发人员远离创建更快的处理器来代替多核架构,游戏开发人员必须利用多线程技术来利用这些新设备。 对于多核移动设备,对基于网络的多线程游戏引擎的需求已成为现实。Building a Multithreaded Web Based Game Engine Using HTML5, CSS3 and JavaScript讨论各种多线程Web引擎架构的设计,这些架构允许使用线程控制器在线程中动态处理请求。利用WebWorkers、WebSockets和WebGL等HTML5和JavaScript API可以在基于浏览器的3D游戏中建立一个新标准,这些游戏功能齐全,真正跨平台支持移动和桌面设备,无需插件。
Web端的应用架构和OS内核之间的关系。
嵌套和共享类型的工作。
Web端的多线程架构和运行模型。
应用层、OS、第三方扩展的层次结构。
我们知道CPU擅长任务并行,GPU擅长数据并行。而GPU Task-Parallelism: Primitives and Applications偏偏剑走偏锋,不按套路出牌,尝试在GPU上引入任务并行,阐述其概念、重要性、实践等技术。
什么是任务并行?
- 任务:在单个上下文中执行的一组逻辑相关的指令。
- 任务并行:任务并行处理。调度组件确定如何将任务分配给可用的计算资源。
- 示例:Cilk、英特尔TBB、OpenMP。
GPU是数据并行的,围绕数据并行处理构建的GPU硬件,CUDA是一种数据并行抽象,基于任务的工作负载被忽略(到当时为止)。
GPU任务并行性:扩展了GPU编程的范围,许多任务并行问题仍然表现出大量的并行性,将GPU编程为任务并行设备,分为两部分:原语(primitive)和应用程序。
原语的目标是构建一个任务并行系统,它可以处理不同的工作流程,处理不规则的平行度,遵从任务之间的依赖关系,对所有这些进行负载平衡。
原语并行:
- 任务粒度。处理任务的正确并行粒度是多少?每个线程一个任务是好的做法,每个warp一个任务更好。重视SIMD,将warp视为具有 32宽矢量通道的MIMD线程。
- 任务管理器(启动、退出)。如何继续处理任务直到没有任务为止?持久线程编程模型:
while(stillWorkToDo)
{
// 运行任务
}
将启动范围与工作量分离,谨防死锁!
- 任务通信。如何在SM之间平均分配任务?具有工作捐赠程序的分布式队列。也可用单个块队列,因为原子现在足够快,也很简单。
- 任务依赖。如果任务有依赖关系怎么办?如何增强当前的系统以尊重依赖关系?
依赖决策:放入队列的所有任务都在没有依赖关系的情况下执行,依赖关系影响哪些任务可以放入工作队列,维护一个任务依赖映射,每个warp必须在排队其它工作之前检查该映射。
while(Q is not empty)
{
task t = Q.pop()
Process (t)
Neighbors tnset = dependencyMap(t)
For each tn in tnset
tn.dependencyCount--;
if(tn.dependencyCount == 0)
Q.push(tn);
}
对于应用并行,有多种场景需要任务并行:Reyes渲染、延迟照明、视频编码,只使用必要的原语。
对于Reyes渲染,需要任务并行的原因是不规则并行、动态通信。需要的原语是持久线程、动态任务队列。
Reyes的任务并行。
对于延迟光照,不同的灯光影响屏幕的不同部分,所以我们用太多的灯光细分tile。需要任务并行的原因是不规则并行、动态通信。需要的原语是持久线程、动态任务队列。
总之,任务并行性很重要,包含多种应用场景。几种基本原语:调度任务粒度、持久线程、动态排队、依赖解析。
Killzone Shadow Fall: Threading the Entity Update on PS4分享了PS4游戏Killzone Shadow Fall使用的线程化更新Entity的技术。
实体是大多数游戏对象的基类,例如玩家、敌人、武器、门,不用于静态世界,具有组件,例如模型、移动器、破坏性,以固定频率更新(15、30、60Hz),几乎所有频率均为15Hz,玩家更新频率更高,以避免延迟。实体和组件具有代表性,控制渲染、音频和VFX,在实体未更新的帧中插入状态,插入比更新更便宜,但会引入延迟,始终与上次更新保持一致。
在PS3中,一个Entity = 1个纤程,大部分时间花在PPU上,没有明确的并发模型,读取部分更新状态,实体相互等待。
在PS4上,一个Entity = 一个作业,无纤程,实体整体更新,如何解决竞争条件?
有依赖的实体不能并发更新,但没有依赖的可以。无(间接)依赖=无访问权限,工作方式有两种:武器也可以接近士兵,创建依赖项有1帧延迟,全局系统需要锁。
但是,有少量实体会导致大量的瓶颈:
非排它依赖项,进入“子弹系统”的通道必须有锁保护。
还可以使用弱引用,两个坦克相互开火(下图),循环依赖发生时,更新顺序颠倒,不经常使用(每帧<10)。
非更新实体,实体可以跳过更新(LOD),实体可以在其它帧中更新,正常调度!下图是各种依赖的总结:
调度算法:具有独占依赖关系的实体合并到一个作业中,依赖关系决定排序,非排它性依赖成为作业依赖,先开启开销大的作业!
边界情况:非周期性依赖变成周期性作业依赖,作业1和作业2需要合并。
跨帧平衡实体,防止所有15Hz实体在同一帧中更新,实体可以移动到其它帧,1次更新的增量时间更短,将父子关联的实体保持在一起,如士兵的武器、骑枪士兵、锁定近距离战斗。
性能问题:内存分配互斥,消除了许多动态分配,使用堆栈分配器,锁定物理世界,主模拟世界的R/W互斥,第二次“子弹碰撞”宽相位+锁定,大量依赖实体,玩家更新非常大开销等。
可以采用切分场景(Cut Scene)的策略。切分场景实体需要依赖关系,切分场景中的10多个角色创造了巨大的作业!
以上问题的解决方案是为非交互实体创建子切分场景,主切分场景决定时间和流程,在时间线中向前扫描1帧以创建依赖关系。
使用对象,不可能依赖可用对象(太多),获取可用对象的列表,受锁保护的全局系统,“使用”图标出现在屏幕上,玩家选择,建立依赖关系,启动“使用”动画,1帧后开始交互(依赖关系有效),隐藏1帧延迟!
以上各类更新方式的性能对比如下:
整帧的时间线和实体更新的时间线如下两图:
总之,易于在现有引擎中实现,游戏程序员可以像单线程一样编程,几乎没有多线程问题。
Multithreading for Gamedev Students讲述了多线程编码的相关技术,如硬件支持、常见游戏引擎线程模型、竞争条件、同步原语、原子与无锁、危险。
多处理器(Multiple processors):成本高、功耗高、芯片间延迟、缓存一致性问题,通常仅限于高端台式机和超大型计算机。
多核(Multiple cores):多核可更有效地利用可用硬件资源,内核可能共享二级/三级缓存、内存接口,台式机和游戏机最常见的设置。
多个硬件线程(Multiple hardware threads):同步多线程(SMT),英特尔领地上的“超线程”,线程共享core的资源,如执行单元、一级缓存等。更有效地利用核心资源,暂停的线程不会浪费资源,与单硬件线程相比,通常快10%-20%,但变化很大。
多个软件线程(Multiple software threads):操作系统可以创建多个进程,游戏通常作为一个进程运行。一个进程可以产生多个线程,共享内存地址空间。线程可以在多个线程之间迁移,或固定到具有线程关联的特定线程。
硬件样例:
- L1 & L2 per-core
- Shared L3
- PlayStation 4 / Xbox One
- AMD Jaguar architecture
- 2 quad-core &#39;modules&#39;
- 8 hardware threads
- L1 cache per core
- L2 cache shared by all cores in module
- AMD GCN GPUs
- Used by both PS4 & Xbox One
- 18 & 12 compute units respectively
- Rendering is inherently parallel
- Hardware exploits this to achieve high speed & throughput
- Extensible to non-graphics workloads
- Compute & async compute
游戏内的多线程:30/60fps目标,必须使用所有可用资源,许多相互作用的系统,有限的共享数据集,一些常见的线程模型。游戏内常见的几种并行方式:
竞争条件:系统的输出取决于时序,时间受到很多因素的影响,未定义的行为-所有赌注均已取消,随机=不可预测=错误结果,调试噩梦。下图是典型的竞争条件案例:
同步:
- 自旋锁(Spinlock)
- 紧密旋转,试图获得锁定,通常通过原子变量。
- 可能会造成问题,CPU和内存带宽使用。
- 正确使用时是轻量级。
- 互斥(mutex)
- 锁定/解锁配对。
- 保护代码的关键部分,提供单线程访问(互斥)。
- 信号(Semaphore)
- 维护内部计数器。
- 等待(递减)和信号(递增)操作。<= 0个线程睡眠,大于0个等待线程继续。
- 信号可以唤醒线程。
- 用于线程之间的信令,或者控制可以执行任务的线程数。
- 条件变量(Condition variable)
- 线程等待条件满足。
- 监视器:互斥+条件变量。
- 游戏中使用的平台特定事件。
- GPU围栏(Fence)
- 用于CPU和GPU交互。知道共享数据何时产生或使用。
- 特定于平台的API,DX12和OpenGL自3.2以来的核心。
编写多线程代码时,还需要注意内存顺序(Memory ordering),以下面代码为例:
// global
int data = 0;
int readyFlag = 0;
// thread A
data = 32;
readFlag = 1;
// thread B
if(readFlag == 1)
{
Output(data); // 这里的data并非唯一的值,可能是0或32!!
}
以上错误的出现正是多线程之间的内存顺序问题。编译器可以重新排序指令,CPU可以重新排列指令,CPU可以重新安排内存访问顺序。内存模型确定哪些读写操作可以相对于其它操作重新排序,硬件和软件:处理器只有一个内存模型,语言可能还有另一个原因。
顺序一致性(Sequential consistency):内存访问所见即所得,没有明显的重新排序,对可能的优化的后续限制,除非性能另有要求,否则请使用。
内存屏障:用于在编译器和CPU上强制执行内存排序,隐含在某些函数中,如std::atomic<>操作不是memory_order_relaxed的操作。明确获取和释放栅栏。
无锁编程:实现无锁、无阻塞的多线程算法,没有线程可以通过被中断来阻止全局进程。使用无锁的原因是免于锁争用、可扩展性、性能(无约束锁的性能非常好)。但无锁的缺点是复杂!
多线程的危险(Hazard)包含:
- 死锁。获得了两把锁,但顺序不同,一个线程锁定A并等待B,另一个锁B并等待A。
- 活锁(Livelock)。多线程在局部处理,每个线程的活动都会导致其它线程多次无法取得全局处理。经典类比:走廊里有两个人。
- 优先级反转(Priority inversion)。低优先级线程获取高优先级线程所需的锁,然后由于其优先级而进入睡眠状态,系统性能最终由低优先级线程而不是高优先级线程决定。
- 虚假分享(False sharing)。多个线程在同一缓存线中修改内存,导致持续缓存失效和不必要的内存流量,会显著影响性能。
- ABA问题。
多线程的复杂性:只知道一点知识是危险的。错误容易犯,但很难调试。没有不可能的事情, “百万分之一”,每帧50次,每秒30帧…~11分钟,如果你运气好,坏事就会发生。即使是简单的事情也会引起问题:
enum{EValueA, EValueB};
// ...
Assert(foo==EValueA || foo==EValueB);
上面代码乍一看,不会有太多问题,但它开始偶尔会触发断言。可能本能会想到是内存损坏或其它一些内存问题,比如对齐(GPU通过栅栏设置foo的值),因为该值只能是这两个值中的一个。更让人困惑的是,无论何时报告断言,foo实际上等于EValueA。实际上,在断言逻辑的中间,foo被从EValueB改为EValueA——在第一次测试之后,在第二次测试之前!这表明,当你放松警惕时,事情很容易出错!!
调试性:考虑到所有这些复杂性,提前计划,始终尝试保持单线程路径处于活动状态,所以你知道问题是逻辑还是线程,运行时可切换(如果可能),有时,仅仅思考比调试更好。
Concurrent Interactions in The Sims 4讲述了The Sims 4(模拟人生4)架构的并发技术,包含互动、约束、交互队列、转换、社交等。
The Sims的世界是用游戏对象建立起来的,游戏对象提供交互,模拟人生也是对象!模拟人生运行交互,互动是行为的基本单位。多任务是很自然的事情,人们同时做多种事情,频繁请求的功能,系统方法是有价值的,临时实现需要大量工作,结果不一致。
不是真正地并发执行会很棘手,可能导致诸如死锁、竞争条件等,多任务涉及上下文切换和协作等。
角色多任务的串行和并行图例。
多任务中使用了子动作(Sub Action)的概念:
规则:我能执行一个动作吗?状况→ 行动。如何执行动作?行动→ 条件。避免重复逻辑。
约束:数据驱动的规则,运行互动的先决条件。回答问题:我可以进行互动吗?如何运行互动?
约束创作:数据驱动。动画:位置、姿势、携带,XML调优:几何、方向、表面,脚本:评分功能、视线。
约束组合:多任务组合约束,支持操作:交集、并集。
交互队列:每个模拟都有一系列激活的互动和等待交互的有序队列。互动具有优先级:高(用户导向)、低(自动)、空闲(已完成但仍在运行)。
队列处理和交互处理。
生成行为:约束定义了执行交互的先决条件,可以生成性地使用,需要能够找到到约束的转换。
转换图:每个对象上的约束都存储在一个抽象图中,边是状态变化,搜索图形以生成转换序列。
使用转换图:
图搜索:多个节点可以满足需求,边缘按成本加权,按近似距离加权的路线,搜索决定最佳路径。
搜索优化:双向搜索,使用携带(carry)、插槽(slot)简化,节点查询索引。
Parallelizing the Naughty Dog Engine Using Fibers讲述了Naughty Dog引擎利用纤程来实现并行化的技术。
新作业系统的设计目标是允许将无法移动到SPU的作业化代码,作业可以在执行过程中让渡给其它作业,例如玩家使用kick更新并等待光线投射,游戏编程人员易于使用的API,用户没有内存管理,同步/链接作业的一种简单方法,性能仅次于API的易用性。
纤程(Fiber)就像一个局部的线程,用户提供堆栈空间,包含纤程状态的小型上下文和节省寄存器。由线程执行,协作多线程(无抢占),纤程之间的切换是明确的(PS4上的sceFiberSwitch),其它操作系统也有类似的功能。最小化开销,在纤程之间切换时没有线程上下文切换,只有注册保存/恢复。(程序计数、指针堆栈、gprs…)
Naughty Dog引擎的作业系统有6个工作线程,每个都锁定在一个CPU内核上,线程是执行单元,纤程是上下文,作业始终在纤程的上下文中执行,用于同步的原子计数器。有160个纤程(128 x 64k堆栈,32 x 512k堆栈),3个作业队列(低/正常/高优先级),没有作业窃取。
一切都是作业:游戏对象更新、动画更新和骨骼混合、射线投射、命令缓冲区生成,除了I/O线程(套接字、文件I/O、系统调用…),这些是系统线程,像中断处理程序一样实现(读取数据、发布新作业),总是等待,从不进行昂贵的数据处理。
新作业系统的优点:极易更新现有游戏玩法,深度调用堆栈没有问题,让一个作业等待另一个作业是直截了当的:WaitForCounter(...)。超轻量,可更换纤程,系统支持的操作,如PS4上的sceFiberSwitch(),保存/恢复程序计数器和堆栈指针和其它所有的寄存器。缺点是无法再使用系统同步原语:互斥、信号量、条件变量…锁定到特定的线程,纤程在线程之间迁移。同步必须在硬件层面上完成,原子自旋锁几乎无处不在,特殊作业互斥锁用于持有时间较长的锁,如果需要,将当前作业置于睡眠状态,而不是旋转锁定。
对纤程的支持:可以在调试器中查看纤程及其调用堆栈,可以像检查螺纹一样检查纤程,纤程可以命名/重命名,指明当前作业,异常处理,纤程调用堆栈与线程一样保存在核心转储中。纤程安全线程本地存储(TLS)优化,问题是TLS地址允许在测试期间缓存,默认情况下,该函数将运行,在功能中间切换纤程用错误的TLS指针醒来。目前不受Clang支持,解决方法:对TLS访问使用单独的CPP文件。在作业系统中使用自适应互斥体,可以从普通线程添加作业,旋转锁->死锁,在进行系统调用之前,旋转并尝试抓住锁,解决优先级反转死锁,由于初始旋转,可以避免大多数系统调用。
引擎的管线如下,游戏逻辑向渲染逻辑向GPU依次发送任务:
以帧为中心的设计,每个阶段都是完全独立的,不需要同步,一个阶段可以立即处理下一帧,简化了引擎设计的复杂性,由于并行性,几乎不需要锁,锁仅用于在大量作业的阶段更新中进行同步。下面是新旧设计的对比图:
Naughty Dog引擎对帧的定义:“经过处理并最终显示在屏幕上的一段数据”,要点是“一段数据”,时间不长,帧由数据成为显示图像所经过的阶段定义。
帧参数(FrameParams)是每个显示帧的数据,最终显示的每个新帧的一个实例,通过引擎的各个阶段发送,包含每帧状态:帧序号、增量时间、蒙皮矩阵,每个阶段访问所需数据的入口点。无竞争资源,由于每个阶段都在一个独特的实例上工作,因此不需要锁,状态变量会在每一帧复制到此结构中,例如增量时间、摄像机位置、蒙皮矩阵、要渲染的网格列表,存储每个阶段的开始/结束时间戳:游戏、渲染、GPU和翻转。如果帧已完成特定阶段,则易于测试:HasFrameCompleted(frameNumber),现在可以很容易地跟踪生命周期,如果在第X帧中生成GPU要使用的数据,则等待HasFrameCompleted(X)为真,有16个帧参数,可以在它们之间旋转,但只能跟踪最后15帧的状态。
内存生命周期:单游戏逻辑阶段(临时内存)、双游戏逻辑阶段(低优先级光线投射)、渲染逻辑阶段的游戏(对象实例数组)、游戏到GPU阶段(蒙皮矩阵)、渲染到GPU阶段(命令缓冲区)及同时用于CPU和GPU的内存!
内存不足:许多不同的线性分配器,许多不同的生命周期,所有尺寸都适合最坏的情况,从未同时遇到所有分配器的最坏情况,100-200 MiB的浪费内存。
标记堆(Tagged Heap)是基于块的分配器,2M的块大小,2MiB是PS4上的“大页面”–>1 TLB条目,每块都有一个标记(uint64_t),没有“Free(ptr)”接口,只能释放与特定标记关联的所有块(下图)。
所有分配器都使用标记堆,从共享标记堆中分配2M块,并在分配器中局部存储,99%的分配都小于2MB,大于2M的分配会从标记的堆中连续分配2M的块,在此局部块中进行分配,直到为空,像这样共享一个公共块池允许动态调整分配器的大小。
分配器给多个工作线程分配标记堆的示意图。如果单个2M的块被多个线程同时使用,则需要加锁保护,防止竞争。
优化:在分配器中为每个工作线程存储一个2M块,使用工作线程索引来选择要使用的块,该线程上的所有分配都将进入该线程的块,从而避免竞争,99.9%的内存分配不需要锁,实现高容量、高性能分配器。
逐线程块的分配器示意图。
总结:纤程很棒,以帧为中心的设计简化了引擎,使用FrameParams之类的方法可以大大简化数据生命周期和内存管理,在处理多帧引擎设计时,基于标记的块分配器非常有用。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|