0%

原文链接

The Reyes image rendering architecture

前言

我是这两天才得知今年的图灵奖被授予给了两位图形学界的大佬,看了他们的事迹后,感觉激动得说不出话,今天就让我们回顾一下其中一名图灵奖得主Edwin E. Catmull所参与的关于图形管线的论文,这篇论文中所提出的图形管线中的核心思路被沿用至今。

论文要解决的问题

模型复杂性:由于之前无论多复杂的渲染图像相比于真实画面都显得过于简单,这是由于真实画面包含丰富的图形和纹理。为实现画面的真实感,需要数十万个几何图元。
模型多样性:包括图元、分形、粒子系统等
着色复杂性:由于表面反射特性过于复杂,因此需要一个可编程的着色器。
最小化光线追踪:许多非局部光照效果都可以用纹理映射来近似。在自然场景中,只有少数物体需要光线追踪。
速度:动画渲染需要严格的速度需求。
画面质量:需要保证画面质量,避免锯齿等错误。
灵活性:架构需要灵活兼容新的技术。

设计原则

自然坐标系:每种计算都需要一个坐标系系统以自然地进行这种计算。比如纹理映射需要使用uv坐标系,可视表面计算需要屏幕空间坐标系等。
向量化:架构应该利用向量化、并行化和管线,相似的计算应该一起进行,比如对于同一画面的计算通常是相似的,因此需要同时进行着色。
同一表示:许多算法只能处理简单的几何形状,因此需要将几何图元转换为多个微多边形表示的近似,所有的着色计算和可视化计算都在微多边形上进行。
局部性

  1. 几何局部性:每个几何图元的计算不应该依赖于其他几何图元,并且每一个几何图元只能计算一次。
  2. 纹理局部性:只有当前需要的纹理在内存中,纹理应该中从磁盘中读取一次。

线性关系:渲染时间应该与模型的大小成线性关系
大模型:模型不应限制几何图元的数量
后门:架构中应该有”后门”,以便于其他程序可以被用来渲染一些物体。
纹理贴图:纹理贴图的访问应该是有效的,本文期望每个surface可以使用几个纹理。此外,纹理是定义复杂着色性质的强有力的工具。

overview

设计细节

几何局部性

当光线追踪反射或折射任意表面时,都有可能产生次级光线。虽然这些次级光线命中的物体可以很快被计算出来,但是必须从数据库中访问该对象。随着模型越来越复杂,访问这些模型的开销会越来越大,因此光线追踪不适合渲染极度复杂的环境。

使用纹理贴图可以近似这些非局部计算,比如反射、折射和阴影的纹理贴图可以很好地近似这些效果,并且可以避免大量的访存和计算。

点采样

点采样具有简单,功能强大,并能够简单地适用于不同类型的图元。但是,采样可能会导致失真。因此本文采用蒙特卡洛方法(随机采样)。

本文采用一种名为jittering的随机采样方法,像素被分为多个子像素,每一个子像素有一个采样点,这个采样点的位置就由jittering决定。该方法被用于采样为多边形覆盖的子像素。屏幕中每个采样点的当前可见性信息被存在z-buffer中。

微多边形

微多边形是算法的几何基本单元,是平面可着色的四边形,其中的每一条边约为1/2的像素。这是因为半个像素是一个图像的奈奎斯特极限,表面着色可以被每个微多边形的单一颜色充分表示。
将几何图元划分为微多边形的过程被称为dicing,其结果是一个二维微多边形数组,被称为grid。此外,由于相邻微多边形的共享顶点只被表示一次,因此grid中的微多边形需要更少的存储。
dicing在eye space空间中实现,并根据屏幕中图元大小的估计值进行dice,即需要创建多少个微多边形。
使用微多边形并被着色具有以下几个优势:

  1. 向量化着色:如果每一个点的着色计算操作相似,则着色操作可以被向量化(这里可能是并行化的意思)
  2. 纹理局部性:通过按顺序访问大型连续纹理块进行纹理请求,因为着色可以按物体顺序执行,避免了其他算法中发生的纹理颠簸(即需要反复从磁盘中读取相同的纹理)。比如当纹理请求分成小块且在不同的几种纹理贴图间切换时,就会发生纹理颠簸。
  3. 纹理过滤
  4. 细分一致性:由于可以一次对整个表面进行细分,因此可以通过正向差分等有效技术细分。
  5. 裁剪:不需要像某些算法所要求那样需要沿着像素边界裁剪对象
  6. 位移贴图类似于凹凸贴图,表面的位置和其法向量会发生改变,这使得纹理贴图称为建模表面或者存储建模程序结果的一种手段。由于位移贴图可以改变表面位置,因此必须在隐藏表面(应该是深度检测)计算之前计算。
  7. 不需要进行透视畸变矫正:由于微多边形比较小,因此不需要对插值的透视畸变进行矫正。

纹理局部性

假设,s、t表示纹理贴图上的坐标,u、v表示一个surface上的纹理参数坐标。
纹理通常被划分为两类:一致性访问纹理(CATs)和随机访问纹理(RATs)。对于一致性访问纹理来说s=au+b,t=cv+d,其中a、b、c、d分别为常数。其他的纹理均为RATs。
此外,CATs相比于RATs处理更容易且速度更快,因为如果通过uv的顺序进行着色计算,则可以顺序地访问纹理贴图。

CATs

RATs比CATs更加通用,比如,环境贴图是RATs,因为根据反射的方向进行索引。再比如,decal,即世界空间上纹理在surface上的平行投影,其中s、t依赖于x、y、z,而不是u、v。

算法描述

algorithm

下面将讨论上述算法所用到的几个过程:

Bound:图元可以计算它的视角空间的bound,屏幕空间的bound由视角空间转换而来,图元必须在其bound内,但这个bound不一定是紧的。
Dice:不是所有类型的图元需要是diceable的,唯一的要求是每一个图元可以能够将自己split成其他图元,这种split最终可以让图元完全被dice。
split:图元能够将自己split成一个或多个相同类型或其他类型的图元。
Diceable test:这个测试决定图元是否应该被dice或split。如果一个图元被分割后将产生太多的微多边形或特别大的微多边形,则这个图元被认为是not diceable的。

split
sample

如果一个物体完全视椎体外,则被剔除; 如果一部分在视椎体外一部分在视椎体内则依旧被保留、着色和采样。只有在视椎体内的区域才有可能被采样。
有时物体一部分在眼睛后面,但另一部分在视椎体内。其处理过程如下图:

split-until-culled

实现方式

由于当时的内存不够大,但z-buffer需要大量的内存,因此屏幕被分为rectangular buckets,这些bucket被放在内存或者磁盘中。在初始的过程中,检测每一个图元的边界,并将对应其放入对应的bucket中,在剩下的计算中,按需对桶bucket进行处理。首先,对bucket中的图元进行split或者dice。当图元被dice后,其微多边形被着色,并放入其覆盖的bucket中。当bucket中的所有图元被split或diced后,这个bucket中的微多边形被采样。因此,只需要一个bucket对应屏幕大小的z-buffer即可。

总结

本文所提的图形管线的一些设计原则和设计思路被沿用至今。

由于本文部分内容的表达方式可能与现在的表达方式有所出入,如果本篇分享存在问题,欢迎提出。

论文1:LiveRender: A Cloud Gaming System Based on Compressed Graphics Streaming

论文要解决的问题

服务器端通过使用对图形流(Graphic Stream)进行压缩传输至客户端,并由客户端进行渲染,以解决传统基于流的远端渲染产生的高带宽和可扩展性差的问题

系统架构

系统架构如下图所示,其中LiveRender需要处理三种数据:图形命令、图形数据(包括顶点、indices、法向量等)、纹理数据

LiveRender

服务器侧

LiveRender在服务器侧实现一个代理库用于拦截图形命令和数据,并将其传输至客户端进行渲染。Cache管理器模块负责存储可以重用的图形命令、图形数据和纹理数据。几何压缩器模块用于对顶点和indices数据进行帧内和帧间压缩。

客户端侧

用于处理输入和图形渲染

客户端-服务器交互

本文对比了LiveRender和GamingAnyWhere两种云游戏系统因为交互导致的响应延迟。
对于GamingAnyWhere,SP过程包括处理输入、渲染图形和生成H.264视频流。CP过程表示解码和显示图像。
对于LiverRender,PS过程包括图形命令以及相关数据的打包分批发送,客户端收到第一个batch便开始渲染,PO表示传输过程、服务器处理过程和客户端处理过程的并行过程,PE表示从全部数据传输至客户端到渲染完所有batch。

Interaction

图形压缩和状态同步

帧内压缩

基于二次误差度量实现帧内压缩,以简化图形的顶点数据

Intra-frame Compress

算法如下

algorithm1

此外,本文针对帧内压缩提出两种策略:

  1. 对每个模型只进行一次简化,如果模型后续发生变化,则重新通过indices进行映射。
  2. 只对更新频繁的模型进行简化

帧间压缩

由于连续帧之间的关联性,采用帧间压缩可以减少几何数据的冗余

将帧分为原始帧(O帧)和衍生帧(D帧),将帧序列转换为O,D1,D2,…,Dk,O…,这里的k是O帧的间隔。D帧只保留图形命令数据和纹理数据。几何数据通过相邻的两个O帧恢复出来。 实验表明k=1可以在开销和性能之间取得一个权衡。

Inter-frame Compress

缓存机制(Caching)

LiveRender缓存机制处理三种数据:图形命令,几何数据和纹理。

图形命令

在较短的时间段内,渲染所使用的图形命令较为接近,部分图形命令会被频繁调用。
缓存过程:Server端维护一个hash表用于插入,查找和删除缓存。客户端使用数组维护缓存。当一个新的命令被拦截,服务器端首先计算这个命令对应的hash code,然后查找在hash表中查找此项,如果命中,则服务器端向客户端发送一个index,用于在客户端检索命令存放的位置。如果没有命中,则服务器端向客户端发送完整的命令。

algorithm2

Cache

图形数据和纹理

对于图形数据:如果一个模型只有一小部分的数据被更新,则只传输这一部分数据即可。
对于纹理:纹理在游戏开始前预加载到客户端,后续新的纹理随着游戏的进行不断传输至客户端,注意每一个纹理只被传输一次。

延迟同步

在服务器侧,维护一个抽象设备模型,用于模拟客户端GPU所有的状态(包括光照,视角,转换矩阵等,个人认为这里的状态表述不明确,可以理解为GPU 常量缓冲区所存的数据),服务器侧检查状态值,只有被修改的数据将被传输到客户端以保证同步。

实验及分析

这里就不枚举实验了,直接上结论。
由于本系统的渲染过程不需要在服务器端运行,因此服务器端可同时服务更多的客户端,此外,由于对需要传输的数据进行了压缩和缓存,因此大幅地减少了带宽。

以下是个人见解:本系统虽然有效地降低了带宽传输,但是由于游戏的主要部分“渲染”被放到了本地,这导致一部分在shader中需要进行复杂运算的游戏并不适用于本系统。此外,随着带宽的不断提高,服务器端压缩数据的过程便成为瓶颈

论文2: Kahawai: High-Quality Mobile Gaming Using GPU Offload

论文要解决的问题

通过移动客户端和服务器端协同渲染以减少传输带宽和移动客户端能耗

系统架构

Kahawai实现了两种协同渲染技术,分别是delta encoding和client-side I-frame rendering

delta encoding

移动客户端渲染低细节帧,服务器端分别渲染低细节帧和高细节帧,并将其差值通过H.264编码传输至移动客户端。

delta encoding Architecture

client-side I-frame rendering

移动客户端渲染高分辨率的I帧;服务器端渲染所有帧,在进行H.264编码后丢弃所有的I帧,转换为placeholder,并传输至客户端;客户端接收到仅包含P帧的输出后,将I帧插入进去,并进行H.264解码得到最终的输出。

client-side I-frame rendering Architecture

实现细节

delta encoding

由于具有不同细节等级的输出具有相似的信息,因此编码高质量和低质量版本的帧的差值比编码高质量帧所需要的bit更少。虽然在某些情况下会出现反例,但大多数都符合这个情况。

此外,delta encoding具有以下两种挑战:

  1. 差值的压缩导致在最终恢复图像时产生黑点
     这是由于高细节帧的像素和低细节帧的像素之间的差值可以是正的也可以是负的,需要额外的符号位,因此为了简化,采用模运算作为一个简单的解决方案。下图是原文中的一个例子。

example1

 本文通过如下公式(1)将值转换为正值,并通过式(2)在客户端进行恢复。

equation1
equation2

  1. 被编码的差值无法精确表示高细节帧和低细节帧的差异
     其主要原因是有损压缩,压缩产生的误差与高清晰度和低清晰度帧的相似程度成反比。如果高清晰度和低清晰度的帧之间差距较大,相比于直接传输H.264编码的高细节帧需要更高的带宽,并会产生更大的误差。比如,下面两个例子:

figure4
figure5

其中图5的例子的差值相比于高清晰度帧来说更难压缩。

Client-side I-frame rendering

移动客户端能够渲染的I帧帧率越高,服务器端传输所需的带宽越低。

在传统的H.264中如果I帧越多,由于I帧所含的信息量较高,因此所需的带宽越高。但是在Client-side I-frame rendering就不会存在这种问题,这是由于I帧在客户端渲染,并不会被服务器端传输。

Kahawai实现

基于开源游戏引擎idTech4和闭源游戏街霸4分别实现了该系统。

该系统的实现主要存在以下3个挑战:

  1. 支持确定性的图形输出;
  2. 输入处理
  3. 针对两种协同渲染技术实现编解码支持

确定性图形输出

对于delta encoding,客户端的低分辨率输出必须与服务器端的低分辨率输出相匹配。
对于Client-side I-frame rendering,客户端的I帧必须与服务器端的I帧相匹配。

本文通过服务器端的游戏实例重播移动客户端的输入来达到输出的匹配。但由于在底层实现重播会产生较高的开销,这会导致用户体验变差。另一方面,将重播与游戏的内部逻辑纠缠在一起是不切实际的。

Kahawai采取了一种折中的方案,即kahawai不需要完全重播所有的游戏执行,只要产生相同的画面即可。比如,服务器端的渲染不需要产生于客户端相同的文件写入或线程调度。

渲染线程的代码如下:
code1

其中Frame()用于更新游戏状态(如光照,视角,转换矩阵等),UpdateScreen用于渲染并输出至屏幕。

有三种不确定性因素会影响idTech引擎的渲染线程的输出:

  1. 系统时间:Kahawai通过拦截渲染中的GetSystemTime调用以确保重播执行时使用相同的时间值。
  2. 伪随机数生成器:正确的重播键盘和鼠标输入便可以使得伪随机数生成器的是确定的。
  3. 游戏音乐:有些游戏的光照等依照于音乐的响度,Kahawai将音乐播放时对应的系统时间记录下来,依照该系统时间进行渲染。

当服务器连接中断时,客户端将继续采取低分辨率的渲染继续执行,并执行以下步骤:

  1. 游戏继续执行直到游戏被保存
  2. 暂停执行
  3. 将当前状态发送至服务器端
  4. 当服务器确定已经完成恢复游戏状态后继续执行。

输入处理

挑战:代码中,当FrameUpdateScreen处理第N帧时,系统在缓存第N+1帧的事件。为保证服务器端和客户端同步,Kahawai必须确保当客户端再处理第N帧的输入时,服务器端页必须处理第N帧的输入。为保证这一点,客户端需要将渲染线程的事件戳输入和适当的帧号发送至服务器端,当接收到下一帧的输入之前,服务器将暂停。

pipeline的总结

pipeline

图中不同的符号意思如下

sign

编解码支持

对于client-side I-frame,本文使用x264在可预测的间隔生成I帧
,然后将编码视频中的I帧丢弃,将被压缩的P帧和placeholder传输至移动客户端。在移动客户端,使用帧捕捉来确定提取被渲染的I帧,并将其插入至视频流中,最终通过ffmpeg进行解码。

实验及分析

直接上结论:

  1. 本文实现的两种系统在用户体验方面与thin-client策略类似;
  2. 在相同的画面质量下,本文实现的两种系统所需的带宽更低,但随着画面质量的增加,本文实现系统与thin-client的差距会减小,这是由于delta encoding的细节变多
  3. Kahawai部署在在局域网或附近的CDN的服务器中性能最好

以下是个人见解:delta encoding由于高清晰度和低清晰度帧的差值较大时表现不佳,因此该方法不能轻松适配于全部游戏。此外,该系统没有考虑负载均衡,这使得系统具有更进一步优化的空间。

论文3:Layered Coding for Mobile Cloud Gaming

论文要解决的问题

提出一种分层编码的框架,以减少传输码率和带宽,并证明该框架在云游戏场景下优于H.264/AVC编码

系统架构

Layered Coding

服务器端分别渲染一个高质量IHQ和低质量(base-layer)IBL的画面,客户端只渲染与服务器端同样的低质量画面。高质量和低质量画面的差值被称为(enhencement layer)IEL
关系如下:

enhencement layer

IEL使用H.264编码进行转换,并传输至客户端,由客户端进行解码,并与低质量画面合并得到最终的高质量画面。

encoding and decoding

此外,为客户端能够渲染低画质的画面,需要将3D模型和渲染指令传输至客户端。

Base layer pipeline的设计

本文认为,在多边形的建模中,base layer的计算复杂度与多边形的数量(这里的数量可以理解为base layer中使用的多边形与高质量画面使用的多边形的比例)线性相关。

通过衡量IEL的信息熵以估计被压缩的IEL的码率。实验表明,base layer中使用10%的多边形,IEL便会产生相比于IHQ50%的信息。

HEL

光照算法

phong光照模型

本文衡量了当客户端采用简化的光照模型时,enhencement layer所需包含的信息量。

phong

Table 1

exp2

当IBL渲染完整的光照模型时,enhencement layer包含的信息量HEL为0,如果IBL不进行渲染时,则HEL = HHQ

全局光照和纹理映射

全局光照由于计算过于复杂,仅考虑放在服务器端。
纹理信息也仅考虑放在服务器端

实验及分析

直接上结论,分层的框架相比于thin-client模式,在相同的画面质量下,需要更少的码率。

以下是个人见解本文分析了不同渲染操作所需计算量与画面包含信息量的关系,但论文中的表述存在部分问题,比如没有确切证据可以表明”画面的计算复杂度与多边形数量相关”,因为在顶点着色器后,片元着色器的计算负载通常更高

对比

三篇论文均考虑到云游戏场景中带宽的限制导致在thin client模式下游戏帧率主要取决于网络延迟,因此分别使用了不同的方法将渲染的部分计算量offload到client端。

对于论文1而言,由于全部的渲染计算均在客户端进行,服务器端相当于充当传统渲染管线中的内存和CPU的角色,而客户端相当于充当GPU的角色,因此当采用全局光照等高计算负载的渲染逻辑时,该系统并不能很好的发挥其性能。

对于论文2和论文3而言,他们分别将一部分渲染负载offload到client端,而另一部分在服务器端,通过在客户端合并的方式,将最终画面呈现给用户。其中论文2的所提出的两种模式并不能很好地保证负载均衡,而论文3中所提的负载的计算存在些许问题,需要改进。

完成日期:2020-7-2 15:30

原文链接

Visibility Rendering Order:Improving Energy Efficiency on Mobile GPUs through Frame Coherence

论文要解决的问题

从硬件层面解决early-z在渲染物体顺序不是从前到后时的overdraw问题。

经典Graphic Pipeline的回顾

GP1
传统的深度检测在片元着色阶段会产生不必要的计算量(overdraw)

earlyz
如果将深度检测提前至片元着色之前(early-z),没有通过深度检测的片元,则不需要进行片元着色。但是只有当物体的渲染顺序为从前到后时,才能完全避免overdraw。但是,大多数情况下不会通过软件对物体进行排序。下图是在不同的渲染场景下,No Early-z和early-z的渲染效率的对比。其中Unordered表示物体没有进行排序,Ordered表示物体已经被排序。图中的Overshading表示平均每个像素需要渲染多少个片元(包括透明物体,注意early-z只优化不透明的物体)

exp1

Early-z常见的两种改进措施

  1. z-prepass:
    • 是一种软件技术
    • 方法:将一帧分成两次独立的渲染。第一次渲染只对不透明的objects的位置信息进行光栅化,且在Fragment Shader中不做任何处理(即所得最终结果为一帧每个pixel的深度值),并将其结果存入depth buffer。第二次渲染利用第一次渲染所得的depth buffer进行early depth检测,并将通过检测的fragment进行Fragment Shader。
    • 优点:只会渲染所有最终可见的不透明fragment所对应的像素,将overshading降到最小。
    • 缺点:需要进行两次Vertex Shader、光栅化和深度检测。
  2. Deferred Rendering:
    • 是一种硬件技术
    • 方法:将一帧分成tile(硬件中过程),并在early-depth之前增加hidden surface removal(HSR)阶段,在这个阶段中只对一个tile中的objects的位置信息进行光栅化,并将结果进行深度检测,最终写入depthbuffer中。在此之后进行正常的光栅化、early-depth和fragment Processing。如下图,deferred rendering有两种实现方式,图(a)为串行方式,这种方式会产生额外的时间开销,图(b)为并行方式。

DR

文献所提方法 Visibility Rendering Order

  • 假设:下一帧objects的深度顺序(Visibility Order),与当前帧相差较小。(帧间的连续性,文章作者通过实验发现该情况发生的概率为99%)

VRO

  • 实现过程:

    1. 根据objects的深度关系构建Visibility Graph。具体过程为,在对第N帧进行early-depth的过程中得到不同objects在同一像素位置的深度关系,将该深度关系作为一条边(edges)通过edges Inserter单元构建一个有向图,并将该信息以邻接表的形式存入Graph buffer中。

    2. 对Visibility Graph执行拓扑排序算法,生成一个关于objects的front-to-back的List,第N+1帧将按该顺序对objects进行渲染。

      VisG
      VisG

      • 拓扑排序算法:该算法只能适用于有向无环图,然而应用场景中可能生成有环图(比如物体自遮蔽、两个objects相互遮蔽、两个物体以上相互遮蔽)
        Kahn
      • 破除环路方法:
        • 对于自遮蔽:不为其建立edge;
        • 对于两个objects相互遮蔽:仅记录其中的一组关系。比如object A与B相互遮蔽产生(A,B)和(B,A)那么只记录(A,B);
        • 对于两个物体以上的相互遮蔽:正常生成Graph,在拓扑排序算法遇到环路时,选取入度最小的节点进行处理,然后正常处理后续步骤。经实验表明这种情况占13%。
    3. Visibility Rendering Order的调整:

      • 对于前一帧有但当前帧没有的objects:从List中移除;
      • 对于前一帧没有但当前帧有的objects:放到List的末尾;
      • 对于半透明的objects(这种情况不能通过深度检测进行去除,而需要通过blend过程对这些objects的颜色进行合成,从后向前渲染):在不透明的物体之后渲染,保证其相对位置顺序。

实现过程

Baseline TBR GPU

TBRGpu

Objects经过Geometry Pipeline(包括视角转换、clipping、面剔除等),传入Polygon List Builder单元,该单元负责将图元调度到不同的tile当中,并将该信息存入Parameter Buffer中。该buffer存在显存当中,由tile cache进行访问。随后根据调度信息,进行Raster Pipeline。

该GPU有一个Vertex Processor和四个Fragment Processor。

Deferred Rendering TBR GPU

DRGpu

Tile i执行正常的渲染操作。HSR阶段用于计算Tile i+1的z-buffer的最终状态。当渲染Tile i+1时,两个z-buffer进行交换。

此外,Tile scheduler通过轮询的方式向两个光栅化器传输数据。在此期间,需要访问两次Tile Cache,因此平均数据传输时间与Baseline GPU和后续的VRO GPU相比较慢。

Visibility Rendering Order TBR GPU

  • Edge Inserter: 负责插入边,并将其存入Graph buffer中。

  • Edges Filter:由于Early-Depth过程可能产生大量相同边,因此可以通过该模块对边进行一个初步过滤。

  • Visibility Sort unit:执行拓扑排序算法,其结果用于下一帧渲染的object的排序。

VRO TBR GPU的实现细节

  • Graph Buffer:由于Visibility Graph过于稀疏,因此Graph Buffer以邻接表的形式进行组织。

GraphBuffer

  • Edge Inserter
    将边(Nsrc,Ndst)插入Visibility Graph可分为以下三步:

    1. 将节点Nsrc对应的entry从Graph Buffer存入Adjacency-Reg。

    2. 判断是否该邻接表中是否有这条边的信息(这个过程使用一组相等比较器只需要一个时钟周期便可)

      • 如果已存在,则丢弃;
      • 否则,长度信息加1,并将这条边的信息加入该邻接表,并存入Graph Buffer。
    3. 将Ndst对应的入度加1

      GraphBuffer

  • Visibility Sort Unit
    初始化:将所有入度为0的节点对应的objects-id存入Roots Queue。

    1. 将这个节点对应的邻接表读入AdjacencyList-Reg,并将对应的object-id通过Order-Queue存入Tile Scheduler

    2. 将该节点的Child节点的入度读入InD-Reg并减1,若此时入度为0,则将其存入Roots-Queue。

    3. 从Roots-Queue读入一个节点到AdjacencyList。

      VSU

实验部分

  • 使用Teapot GPU模拟器
  • 使用McPAT框架计算能耗

实验结果:

exp2
exp3
exp4
exp5
exp6

对于图18而言,需要注意DR读取一个图元较慢的原因是Tile scheduler通过轮询的方式向两个光栅化器传输数据。

Z-prepass的概念

在分享怎么实现之前,我们首先来看一下z-prepass到底是什么。
我们知道,在传统的图形管线中,被渲染的物体会在执行完像素着色器后执行深度检测,但这种方法有可能会在同一像素位置计算多个像素点,但实际显示在屏幕中的只有一个像素点,因此会产生大量的不必要的计算。
有一种优化策略是将深度检测的位置提前到像素着色器之前,即在执行完光栅化后便判断对应像素的深度,如果该像素的深度更小,则将其深度值写入深度缓冲区的对应位置,并对该像素进行像素着色过程。否则,便不会进行任何操作。这个过程被称为early-z。但是early-z依旧存在一些问题,比如当物体按照离屏幕由远及近的顺序渲染时,每一个物体的像素依旧会通过深度检测,因此并不会减少像素着色过程。
因此,z-prepass便是一种优化ealy-z的方法。z-prepass首先对需要被渲染的物体执行一遍渲染管线,但这个管线的像素着色器不执行任何操作,通过这种方法,借助深度检测机制,将离屏幕最近的物体的深度值写入深度缓冲区。然后执行第二遍渲染管线,这次的管线使用第一遍渲染产生的深度缓冲区,并利用early-z技术,使得只有与深度缓冲区中深度值相等的被渲染。

技术要点

z-prepass的所需要实现的技术要点如下:
1、 实现两个渲染管线,因此需要在Direct3D 12中创建两个pipeline state;
2、 需要创建两个CommandList以便于实现两个不同的渲染管线;
3、 需要实现两个像素着色器,其中一个用于z-prepass,另一个用于正常渲染;

具体实现

创建两个pipeline state

用于z-prepass的渲染管线需要打开深度检测,其代码如下

1
2
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc_zprepass = {};
psoDesc_zprepass.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);

用于正常渲染的渲染管线也需要进行一些修改,其代码如下

1
2
3
4
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL;

其中,用于正常渲染的管线需要禁用深度缓冲区的写入,即将D3D12_GRAPHICS_PIPELINE_STATE_DESC中的DepthStencilState.DepthWriteMask置为D3D12_DEPTH_WRITE_MASK_ZERO。此外,需要将深度检测的判定条件修改为若像素深度值与深度缓冲区的深度值相等则通过深度检测。因此需要将DepthStencilState.DepthFunc置为D3D12_COMPARISON_FUNC_EQUAL

当然,上述的代码并不完整,但是pipeline state的其他状态像往常一样设置即可。

CommandList中需要注意的问题

由于需要两次渲染,因此每次渲染对应一个CommandList。
对于z-prepass的CommandList在绑定渲染目标时,可以只绑定深度缓冲区,其代码如下

1
2
prepassCommandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
prepassCommandList->OMSetRenderTargets(0, nullptr, true, &dsvHandle);

对于正常渲染的CommandList正常地绑定渲染目标即可,但是要注意,不要清除深度缓冲区,其代码如下

1
mCommandList->OMSetRenderTargets(1, &rtvHandle, true, &dsvHandle);

像素着色器中需要注意的问题

对于z-prepass的像素着色器,只需随意return一个float4的变量即可。
对于正常渲染的像素着色器而言,需要在像素着色器的声明之前添加[earlydepthstencil]属性,以表明GPU应该做early-z,虽然现在的大多数GPU默认使用early-z,但建议还是加上这个属性以防万一,其代码结构如下

1
2
3
4
5
[earlydepthstencil]
float4 PShader(VertexOut pin) : SV_Target
{
// 你的程序逻辑 ...
}

大家好!

大家好,我是一名自学游戏引擎的在读研究生小白,这个blog将被用来记录自己所学的一些东西(包括但不限于知识分享、论文学习、代码实现等等),也可能会有一些生活方面的分享,欢迎大家来交流。