


















前言
上一篇粗读简单覆盖了渲染管线的核心思想和LOD自动化构建的方面,这次继续深入运行时的很多方面。
开始之前简单的谈一谈对复杂系统做概念性把握的好处和弊端。好处是这么复杂的游戏引擎系统,在一定程度上做归纳之后,我们对其能有概念上的把握,例如“渲染的改进目标是逐像素规模”,“LOD的改进是自动化构建一套基于DAG结构的数据”等,这样起码不至于盲目崇拜,能有对一个技术框架的有基本的理性把握(复杂度、创新、代价之类的)。
但反过来如果认为已经抽象总结出了这些内容,就可以通过个人做整体把握了,显然也不对——如果这个概念是图论呢,如果是前人论文总结出的一套方案呢?这显然也不是一时半会能学会的东西,很多可能也是前人几代人接力得到的成果。我个人认为,不局限于任何学科,“长求总”是有效但是也是不够的——提炼知识之后学到一些,也引入了更多之前从未涉及的知识,最终有一种既不迷信也不恐惧的敬畏感,并始终保持好奇心是比较合适的。
下面正文开始,内容还是以翻译页面内容或解说稿内容为主,打星号的部分是我个人的补充说明。
1 运行时LOD选取
*整篇文章中对于error这个词的使用是挺不常见的,无论如何将其理解为一个用来比较的值即可。虽然我还是翻译成“误差”,但很多时候不仅仅是这个意义。
(构建时)有两套包围盒相同的子网格,它们各自有不同的LOD
基于屏幕空间的容差范围来选择:通过简单投影到屏幕上来计算误差(包围盒占屏幕比例);在球形包围盒上处于极端位置的点,基于距离和角度失真的情况进行修正
集群内的所有组必须选择同样的LOD:由于后续是并行计算的,但显然不能在线程之间进行通信;所以唯一的方法是保证同样的输入的情况下能有同样的输出(无论多线程中的先后顺序如何)
*如果做过基于随机数种子和玩家输入的帧同步逻辑,应该能理解这种思路;不过区别在于帧同步一般都必须使用定点数来保证跨平台的一致性,图形计算上可能没这么严格。
LOD选择对应于切割DAG(关于DAG上一篇介绍过):如何并行计算?不希望在运行时遍历整个DAG
如何定义切割方法?
We don’t want to draw all low error clusters. Many of them represent the same area but at different detail.
我们并不希望绘制全部低误差的集群,它们很多有着相同的面积但细节程度并不相同。
The cut happens at the point where a parent’s error is too high, but it’s child’s error is small enough to be valid to draw. Parent says no but the child says yes. This is entirely local, does not depend on the entire path to this node, and thus can be evaluated in parallel.
切割发生于这样的节点:父节点的误差值过大,但子节点的误差值足够小以被选取为待绘制的节点——即父节点判定不通过而子节点判定通过的情况。这一过程是完全局部的,不依赖于路径上的其它节点,因此可以在并行计算中被评估执行。
需要决定唯一的一种切割结果
为此需要强制节点误差值是单调的(这里指严格由大到小)
That sounds complicated but really means we need to make the error calculation for parents to always be at least as large as their children. This is forced during the offline DAG building by modifying the parent’s stored error and bounds used for projecting it.
这听起来很复杂,不过实际上这意味着父节点的误差计算结果需要始终不小于其子节点。这在离线构建DAG的过程中被强制保证——通过修改父节点的预存储的误差范围和用于投影的包围盒。
每一帧对于父子节点都是做二元选取,如何规避帧切换时的视觉跳跃(在父子节点直接切换)?
是否可以通过几何形变或交叉渐变来作平滑过渡?(这些过渡方案在运行时会有较高的开销,或是需要一些额外的数据,或是两者皆是)
如果小于1像素的误差范围,待绘制的集群内容(LOD之间)就仅有细微的不同
TAA is built to blend subpixel differences over time. It does our work for us so long as the error is subpixel. This is the reason why getting an accurate error estimate is so important.
TAA被内置以用于混合不同帧之间次级像素的差异。由于误差也是次级像素级的,因此它能胜任我们的(平滑过渡)的工作。这也是精确的误差估计非常重要的原因。
网格简化过程中的集群误差是物体空间标量:(标量意味着)无方向信息;位置误差可以是有向的;混入了参数误差导致计算困难
投影到屏幕的过程无法很好的覆盖表面角的信息:类似mipmaps仅仅是基于距离的方法;也常见于曲面细分系数的计算
意味着掠射角表面会被过度拼构(网格重组)
解决方法需要各向异性的LOD:无法在集群选择的过程中实现;集群选择类似mip选择,必须是各向同性的
掠射角的额外开销在其它场合也会出现:基于点的渲染中会导致overdraw;在SDF和SVO中会导致表面“浮动”
*这一段翻起来很绕,主要是词语没有合适的名词对应,也没有配解说稿。其实分享人大概想说的就是夹角很大(或很小)的表面没有很好的LOD方案,要么就是有额外开销,要么就是无法并入集群考虑。这里SVO是稀疏体素八叉树(Sparse Voxel Octree),SDF上一篇介绍过。
*关于什么是各向异性和各向同性,可以去看Games101中关于纹理的这两方面特性的介绍。
——可见的集群可能是:
靠近时:所有单独的实例
远离时:不同实例中的所有根集群
——需要有一个层级结构,但DAG遍历是比较复杂的
——记住:LOD的决定是完全局部的,这意味着我们可以使用任何能加速这一过程的数据结构
*这一段原文也稍微有点绕,实际上在说即使有了LOD切割,很多节点也是不需要考虑的,可以通过剔除进一步加速这一过程。
2 LOD剔除
The clusters we can cull are exactly the ones that fail the LOD selection test from before. Any cluster whose ParentError is already small enough can be culled. Interestingly, this means that an acceleration structure for LOD culling should be based on ParentError, not the ClusterError itself.
那些不通过LOD选择检测的集群就正好是可以被剔除的集群。任何父节点误差值足够小的集群都可以被剔除;有趣的是,这意味着LOD剔除的加速结构也应该基于父节点误差值,而不是集群本身的误差值。
With that we build a BVH over the clusters. As with any BVH, the parents conservatively bound their children which in this case also includes ParentError.
基于这点我们在集群的基础上构建了BVH。对于任何BVH而言,父节点都保守地包围了所有子节点,这一点对于父节点误差值也是一样的。
Traversing this tree is a classic parallel expansion work scheduling problem. Implemented naively looks like this with many passes, each processing a single level of the tree, appending any passing children nodes to a buffer to be processed by the next pass.
遍历这棵树(BVH层级的)是一个经典的并行展开调度问题。一种不成熟的实现如图,需要通过很多pass,每个pass处理一层树节点,并把子节点附加给缓冲区给下一层pass。
Each pass depends on the previous, so the GPU is completely drained at every level of the tree. Because the CPU doesn’t know how deep the recursion will go, enough dispatches have to be issued to cover the worst case. This means we can very easily end up with empty dispatches that don’t do any processing at all!
每一层pass的计算都需要基于前一层的结果,因此GPU在每一层树都处于耗尽状态。由于GPU不知道递归的深度,因此需要保证足够的pass派遣数以覆盖最坏的情况。这意味着大多数时候可能会有很多不进行任何处理的空派遣调度。
This can be mitigated somewhat by choosing a higher branch factor, but this also results in inefficiencies.
选择更大的分支系数(大于8)可以缓解这一情况,但结果仍然很不效率。
——理想状态
在父节点执行完毕后立刻开始子节点执行
直接从计算中产生子线程
——采用持久化线程(池)作为替代方案
不能产生新线程,必须重用线程(线程池)
管理调度队列
一次派遣足够的工作线程以填充GPU
使用简单的多产多销(MPMC)任务队列模式来在线程间通讯
——当工作队列不为空时
从队列中取出一个节点
检测
把通过检测的子节点加入队列
——单次派遣
没有递归深度或散开数的限制
不需要重复抽取GPU资源
节省10-60%(通常是25%)性能,基于场景复杂度
——依赖调度行为
一旦一个线程组开始执行,需要它不会无限地处于饥饿状态(等待数据)
调度行为在D3D或HLSH中是未被定义的(不支持)
在主机和所有相关GPU上都通过测试
这只是一个可选的优化,不是Nanite中必须的组件
*上面最后一段主要是介绍线程阻塞算法(blocking algorithms)及调度优化方面的情况。
——叶子节点是有共享父节点的集群
作为节点经过相似的剔除检测过程
输出可见的集群
——在相同的常驻Shader中进行集群剔除(减少切换shader的开销)
活跃的BVH节点或许一开始并不能填满GPU
执行时间可以在确认最深的遍历层级后确定
集群剔除可以更早开始以填补BVH剔除执行过程中的空洞(执行完毕的线程)
——两个队列
从集群队列开始执行,并等待节点出现在节点队列中
合并成64个批次
Because the BVH traversal can go deep and the number of active nodes at a given time can be small relatively to the width of the GPU, the BVH culling phase will not always be able to fill the GPU.
由于BVH遍历可能进入很深的层级,并且一个给定时间内的活跃节点数可能与GPU的带宽相关度较小,因此BVH的剔除步骤并不能始终填满GPU。(解释了需要提早开始集群剔除的原因)
——明确追踪前一帧的可见状态变得很复杂
LOD选择可能不同
前一帧的可见集群甚至可能不在内存中
——最终的剔除检测流程看起来如下:
基于前一帧的物体变换数据检测前一帧的HZB
绘制可见物体,存储被挡住的备用
从深度缓冲中构建这一帧的初始HZB
使用这一HZB来检测之前视为被挡住的物体
绘制检测后认为可见(之前认为被挡住)的物体
最终,构建这一帧的完整HZB并用于下一帧使用
*除了光栅化(图中的rasterizer,下一节)的部分,其它就对应前面介绍的各个步骤。这个双pass方案的核心就是基于前一帧的HZB做可见性预测,先绘制一部分潜在可见的物体,并生成中间HZB;而整个多线程LOD剔除的核心思路就是充分利用线程池,尽量高效地进行BVH剔除和集群剔除。
3 光栅化
*狭义的光栅化包含从三角面变换到屏幕空间像素的过程,广义的还包含一些这个过程的其它图形处理。
*这一段主要介绍了需要像素尺寸的三角形的原因
Tiny triangles are terrible for a typical rasterizer, HW rasterizers included. They are designed to be highly parallel in pixels not triangles since that’s the typical workload.
微小三角形对传统光栅化来说很恐怖(包含硬件光栅化)。它们是被设计成在像素上高度并行化而不是三角形上,因为(并行绘制像素)是主要的工作负担。
Modern GPUs setup 4 tris/clock max and outputting primitive ID needed for vis buffer makes this even worse.
现代GPU设置了4三角每单位时钟的上限,并且可见性缓冲需要输出primitive ID,都使这一问题变得更糟。
Primitive shaders or mesh shaders can be faster but are still bottlenecked and not designed for this. Could we possibly beat the hardware with a software rasterizer?
原始的shader或网格shader可以更快但仍然有瓶颈瓶颈,并且它们也不是为这一特性(微小三角形)设计的。我们是否可以使用自定义的软件光栅化来击败硬件限制?
*后面略去了一页,介绍软件光栅化是硬件光栅化速度的3倍。
丢弃像素和写入最终像素需要的工作一样多
即使一个单独的向量特性都会产生检测上的浪费(对于微小三角形)——基础包围盒计算更快
在tile层级序列化以处理深度和ROP(ROP:Render Output Unit,渲染输出单元 )
输出2X2的像素四边形
(这种设计)通常的目的:
VS和PS调度(VS是 Vertex Shader ,PS是 Pixel Shader)
输出格式、顺序、混合
裁剪
(传统光栅化)优化是基于大三角形覆盖很多像素的情况
我们预期的是很多三角形各自覆盖较少像素的情况
没有ROP或硬件深度检测
仍然需要深度缓冲:不能序列化tile阶段(必须能并行以保证效率);即使在单个tile或单个像素也需要能支持多三角形的并行计算。
Instead we use 64b atomics! Specifically a global image InterlockedMax to the visibility buffer .
与之相对的我们使用了64b的原子单元(多线程概念)。尤其是一个全局的可见性缓冲的关联锁映射。
This 64b integer has Depth in the high bits which is what gives us the depth test, And the payload in the low bits. In our case the payload is the visible cluster index and triangle index.
这64b的整形数在最高位记录了深度用来做深度检测,低位部分则是有效荷载(用于计算参数)。在我们的情况中有效荷载用于可见集群索引和三角形索引。
With that detail the visibility buffer shows its true power. The payload needs to be small enough to pack in 34 bits or less. Without that we wouldn’t be able to do fast software rasterization.
基于这一细节,可见性缓冲显示出它真正的威力。有效荷载需要减少至34位或更低,否则我们无法实现足够快的软件光栅化。
128三角形集群,意味着线程组容量是128
每个顶点一个线程:对坐标做空间变换,存储在组共享中。如果多于128个顶点(大于线程数)则循环执行(最多2次)
每个三角形一个线程:获取索引、获取变换后的坐标、计算边的方程和深度渐变、计算屏幕包围盒矩形、对于所有矩形内的像素——如果在所有边内则写入像素
对于大三角形,使用硬件光栅化
逐集群决定使用软件方式或硬件方式
也使用64b的原子单元用于写入UAV(Unordered Access View 无序访问视图)
——多大是过大(指软件光栅化弱于硬件的阈值)?
比想象的大很多
边缘小于32像素的集群都可以使用软件光栅化
——迭代覆盖矩形范围的过程会检测很多像素
最好的情况一半的检测是有效的
最差的情况全部像素都不可用
*因此产生了对于极值情况的预判和规避的需要。
逐行扫描可以更快?
传统的不规则四边形是复杂的——需要很多设置和边缘遍历的过程
So, here is our scanline rasterizer. Instead of the inner loop iterating from rect min to max testing whether this pixel is in or out, we solve for the x interval that passes and only iterate over those.
这就是我们的扫描线光栅化流程。相比于在内循环从矩形最小值到最大值迭代检测像素是否在其中,我们计算出一行里X的最小最大值,并仅迭代这部分像素。
Although this isn’t exact fixed point math anymore due to a divide we haven’t found any issues in practice.
虽然这已经不是定点数计算了(由于用到了除法),不过我们并没有在实测中遇到问题。
*之前并行计算的部分介绍过,为了保证输入输出一致性的问题,定点数是需要的,否则(用到了浮点数)同样的输入都可能算的不一致。不过图形学上毕竟看起来对就能接受,所以他们才会这么说。
——没有逐三角形的剔除
——没有基于硬件的Hiz像素剔除
——我们的软件HZB是从前一帧来的
剔除的是集群而不是像素
分辨率基于集群的屏幕尺寸
——会带来过度overdraw的因素:大的集群、部分重叠的集群、聚集、快速的动量
——overdraw的开销:
小三角形:顶点变换和三角形设置边界
中三角形:检测边界的像素覆盖
大三角形:原子计算单元的限制
*基于这种设计,由于他们特化了小三角形的光栅化过程,因此处理大三角形的效率是持平或不如传统的光栅化方案的(overdraw多的时候)。不过他们这套设计的核心就是为了处理更多细节小三角形的场合。
当整个网格仅覆盖很少几个像素时又如何呢?
DAG(访问)在一个根集群上就停止了:(最大)128个三角形;停止基于分辨率的缩放
能否因为特别小就直接剔除?对于结构性的建筑块就不能剔除
*解说稿中也进一步说明,因为不能简单剔除,而要考虑的三角形还是很多,所以这也是一个优化点。
显然我们需要混合一些点——即使在渲染规模上已经是次线性的,在内存中并不是
实例内存增加得很快(图中所示)
未来可能需要层级化的实例管理(实例的实例的...)
Nanite也没有混合的特殊方案:混合后的唯一代表物必须替代原始实例(在极限距离的情况);核心的改进就是把这个距离推得很远。
*解说稿中也解释了,为了尽量不损失画面细节,因此他们只在极限远的距离才应用这一优化。但这也不是最终方案。
——12X12不同视觉方向,合并在一个图集中
XY图集位置对应将各个面映射到视觉方向的结果(octahedral 八面体的)
(结果是)不同方向抖动离散化的
——每个方向(最小单元)12X12像素
正交投影
最小差别扩展以符合网格的AABB(轴向包围盒)
包含8:8深度,以及三角形ID信息(根集群里的,可以对应替代了哪些三角形)
每个网格的替代信息占内存40.5KB
——通过Ray march来调整不同方向的视差
——直接从实例剔除的pass中绘制,基于确定的可见性实例列表
——将来可能替代成某些更好的方案
*再补充点解说稿的说明:这里存储的信息不包含颜色和gbuffer,整体运作是通过注入可见性缓冲执行的步骤进行替代,支持纹理映射和材质(基础颜色还是从纹理映射中来);另外这一方案在某些情况确实会带来可见的视觉跳跃,因为不像前面LOD方案是自然有平滑过渡的,这个替换物方案就没有。
4 延迟渲染材质
——这个像素是什么材质如何确定?
——可见性缓冲解码:
可见集群 => 实例ID,集群ID
集群ID+三角形ID => 材质槽ID
实例ID+材质槽ID => 材质ID
——可以标明每种材质在全屏范围内占的多边形范围
——跳过不满足这一材质ID的像素
——如果某些材质没有可见像素,CPU也无法得知
材质的drawcall只能是发出后不管的模式
这是GPU驱动的管线带来的不好的副作用
——如何变得更效率?不希望为每一个材质pass都检测每一个像素
使用模板检测?不希望每一个材质都重置一次
我们把硬件深度检测作为我们的方案——材质ID视为深度值
构建材质深度缓冲——Compute shader同时输出了标准深度和HTILE(Hiz加速结构)
对所有材质而言,它们是:全屏范围的,Z值等于材质深度,将深度检测条件设置为“等于”
*关于模板缓冲和深度缓冲这里不展开了,简单理解它们都是基于像素级别做值比较进行取舍的方案。这里深度缓冲的硬件加速特性被他们采用了。
材质只覆盖屏幕一小部分时:Hiz可以处理这类问题(LOD或剔除),不过我们也可以做改进
大颗粒度的tile标明或剔除:每个材质渲染8X4格子的tile;为全屏范围应用同样的着色方法
在顶点shader中基于一个32b的遮罩来过滤(低于标准的)一部分tile
*这里我的理解是32b就是一个tile的材质数据阈值。
*另外这一方案他们在做分享时其实都只在主机上是完全跑通的,实际上在PC和移动端因为API的差异,他们虽然已经有了备选方案,但还在调整和优化(这部分在解说稿中有说明)。
Because material are still coherent pixel shaders we still have finite difference based derivatives to use for texture filtering.
由于材质仍然是使用连贯的像素shader,因此我们仍然能确定有限差分的派生并能应用纹理过滤。
Unlike traditional rasterization the pixel quads span across triangles. This is a very good thing because with tiny triangles quad overdraw can get out of hand very quickly.
不像传统光栅化,像素多边形是跨越三角形的。这是一个很好的特性因为(如果不是如此)微小三角形的overdraw可能很容易就变得失控。
But quads also span depth discontinuities, UV seams, and even different objects. And that is not good.
不过这种像素多边形展开的方式也会带来深度不连续、UV有接缝等问题。而这方面是不好的。
Finite differences of discontinuities are nonsense and often huge which results in high mip levels being used like the artifacts in this image.
有限差分的不连续(通常表现为边界)是无意义的,但往往数量很庞大。这会导致(错误的)采用高级别的mip而导致图中所示的视觉错误。
计算分析派生情况——在三角形之间的参数过渡
通过链式法则在材质节点图中传播
如果派生情况无法被解析评估,则退回到有限差分的情况
通过SampleGrad方法来采样纹理(带有过渡的)
额外的开销很小:对材质pass而言小于2%的开销;只影响纹理采样过程的计算;虚拟纹理的代码已经包含了SampleGrad
*管线和性能情况主要看图中数字。图中的25M的三角形数量是从原始10亿的量级减下来的,这是UE5在UE4基础上的主要革新。
5 阴影
Unfortunately there are more shadow rays than primary since there are on average more than 1 light per pixel. We need something at least as fast as what we have for primary.
(对于阴影的管线最终)不幸的是对于光线追踪需要每像素多于一条射线(相对于像素的基准光线追踪是一条射线)——我们需要一些至少和基准光线追踪一样快的替代方案。
在光线追踪方面,DXR不够有弹性,无法适应复杂的LOD逻辑和自定义的三角形编码,也不支持局部BVH更新。
我们需要一个基于光栅化的方案——能利用到我们其它的工作产出
大部分光源是不会移动的,因此我们需要尽可能的进行缓存(预计算)
Nanite supports normal shadow map drawing but this new architecture enables new techniques that weren’t practical before. It allowed us to implement efficient virtual shadow maps.
Nanite 也支持普通的阴影纹理方案,不过这个新架构启用了之前为被实践过的全新的技术方案。它使我们的能实现高效的虚拟阴影纹理。
We use 16k shadow maps for everything now. Depending on the light type there might be one or more shadow maps.
我们在各处使用了16K的阴影纹理。基于光源类型的不同可能需要一张或更多的(虚拟)阴影纹理(图中列举了几种光源不同的参数,spot 聚光灯、point点光源、directional方向光)。
The resolution we rasterize into the shadow map is made to match the screen pixels that those triangles cast onto. If that region of the shadow map doesn’t cast onto anything on screen we don’t draw to it.
我们光栅化的阴影纹理的分辨率是符合相应的三角形对应的屏幕像素的。如果有一块区域阴影纹理不投射到屏幕中任何位置,我们就不会绘制对应的阴影(基于剔除等)。
These 16k shadow maps are virtualized and sparse.
这些16K的阴影纹理是虚拟化的和松散的。
(虚拟阴影纹理的)页容量是128X128
页表的大小是128,包含mip层级
标记需要的页:屏幕像素投影到阴影纹理空间;选择对应mip级别,1图素对应1像素;标记所在的页
对需要的页分配物理内存(显存)
如果存在缓存的页,则进行重用:(上一帧的阴影纹理)不是直接被弃用,而是基于需要的阴影遮罩计算来移除。
深层管线目前有着显著的同步开销
阴影视图的数量 = 灯光数 * 阴影纹理数 * Mip数
不同视图不能同时剔除和光栅化:(分开执行)可以分摊性能开销,为此需要通过视图ID来标记出对应元素
That’s tiny proportional to the cost of a full resolution primary view but spinning up the pipeline for a minor amount of work can be very inefficient.
(阴影)对于整个屏幕的原始视图渲染而言只是一个小比例的额外开销,但为了少量内容而进行管线轮转(which is 需要同步)可能是非常低效的。
Now not only can Nanite draw the entire scene with a single chain of dependent dispatch indirects. It can render all shadow maps for every light in the scene, to all of their virtualized mipmaps at once.
(由于有了多视图渲染)Nanite不仅可以在一个间接分派指令链中绘制整个场景,也可以在为每个灯光绘制所有的阴影纹理时,一次性绘制到对应的虚拟mipmap中。(这里一次的概念都是指不用切换管线或shader,以减少切换的开销)
如果没有和需要的页重叠,则进行剔除——类似HZB检测(例如某个mip没内容)
The physical texture we are writing to isn’t contiguous in virtual space. This means clusters that overlap multiple pages can’t expect the addressing of a pixel to be direct.
(内存中的)待写入的物理纹理在虚拟空间中可能不是连续的。这意味着跨越了很多页的集群不能期待对其中像素的寻址是直接的。(即可能不能同时完成,为此才在两种光栅化中找了方案)
For the software rasterizer it is best to keep the inner loop as simple as possible. We’ve found even a single additional shift in the inner loop is measurable. So instead we emit 1 visible cluster to the rasterizer per overlapped page, do the page translation once for the cluster and scissor to the page pixels. SW clusters are small so most overlap a single page.
对于软件光栅化,最好保持程序的内循环尽量简单——我们发现即便一个单独的额外切换都会导致内循环产生可度量的开销。因此作为替代我们对于每个重叠的页只发出一个可见性集群进行光栅化计算,对集群的页只做一次转换,之后写入页像素中。这里的集群一般都足够小,大部分时候就与一个页重叠。
Hardware clusters are bigger, often overlap multiple pages and duplicating the vertex and triangle cost doesn’t make sense. Instead we do the virtual to physical page table translation per pixel. Because we are doing atomic UAV writes, even in the HW path, we are free to scatter them.
硬件(光栅化的)集群更大,通常能覆盖很多页,并且重复顶点和三角形的开销也不合理。作为替代我们在像素级别做物理页表的虚拟化。由于我们的方案中是基于计算原子的UAV写入,因此即使在硬件路线中我们也能自由的将其(内存地址)打散。
*这部分主要在说如何在光栅化阶段处理物理纹理中不连续的情况。
Just like in the primary view, Nanite picks the LOD matching 1 pixel error. In the case of shadows this means the pixels of the mip level it is rasterizing to. This maintains the property of roughly scaling cost with screen resolution not scene complexity.
类似基础视图中的那样,Nanite选择像素作为LOD误差判断的尺度。在阴影渲染的情况下,这意味着选择光栅化的mip级别。这能保证大致的尺度缩放的开销是基于分辨率进行改变而不是场景复杂度。
That does not mean the triangles drawn to the shadow map are exactly the same as those drawn in the primary view. That mismatch can cause incorrect self shadowing. We address that discrepancy with a short screen space trace to span the zone where they could differ.
这并不意味着阴影纹理中的三角形的绘制是和基础视图中完全一样的——这种错配可能导致自阴影的问题。我们通过一个短的屏幕空间追踪来定位这种差异,以跨越这些可能有差异的区域。
结语
原文中还有流式加载(Streaming)和压缩(Compression)两节,因为篇幅原因这里就不翻译了,有兴趣的可以去看看原文,干货也不少。后续Epic肯定也把一些计划改进内容逐步实现了,不过我也没有一条一条去核对。
在当初看到UE5的宣传DEMO时(包括之后下载下来实际体验了),我也并没有一种“这下妥了、UE天下无敌了”的感觉;反过来读到这篇分享后,尽管这些年应许的图形次世代还在路上,但我确对它的到来多了很多信念感。这份感觉来源于他们这套系统构建时充满了实践的智慧——他们没有任何一个步骤是搭在空中楼阁上的;很多细节中的魔鬼他们也是围绕这个大胆的想法一步步试出来的。
如果听过Games202系列课程,可能对于闫老师时常提到的“学术界”和“工业界”的划分——两者的关系,简单来说可以概括成学术界提供思想和论文成果,工业界结合实际情况进行采用和改进。Epic这家公司无疑是属于工业界的,而且是强大的先行者和实践者;而其竞争对手的Unity,虽然也有很多学术和实验的成果(也有不少paper),但由于过于下沉和追热点的商业策略的原因,它的引擎产品可以说是完完全全被甩下车了——很多复杂的渲染特性,Unity都不提供(或做的不好),需要大公司的开发者自己深度定制修改了。
最后谈一点关于3D和高清化的看法——虽然现在是一个游戏开发高度平民化的时代,但没有任何个人表达是不需要吸引人的包装的。把粗糙的3D和精巧的2D相比也不公平,因为2D开发比3D更容易,所以事实上2D的赛博垃圾的数量肯定也是大于3D游戏的;只是精巧的2D游戏经历了地狱一样的筛选脱颖而出,才会给人一种惊艳感,会让人觉得相比更大规模的3D游戏比较平庸——而这种筛选实际上是一种平台排序机制和人们兴趣点长期混合的结果。从我个人的一些了解来说,2D游戏的商业化实际是地狱难度的,如何展示给玩家看到就是一个大问题——毕竟游戏销量不能靠鉴赏家来支撑;所以好的2D游戏实际需要创意和视觉都是S级的,这可能比做一个中庸的3D动作游戏要更难(除非是大学生练习作品,不考虑成本)。多年以前《失落城堡》这样的项目还能成功,放到现在不在规模、视觉、玩法上翻个十倍恐怕是不行的了;《猫里奥》(俗称)这样的游戏,出过一次之后这类创意就被占用了,留给后人可用的全新有冲击力的创意肯定是越来越少的。
反过来说,即使高清化的战车甩下去了很多大厂,其实这个赛道还是在的。因为这个赛道天然的就等于主机市场本身——想象一下如果没什么3A大作,那玩家买主机的意义何在呢?如果COD系列没有顶尖的写实画面,这游戏还成立么?由于创意和玩法这东西是需要一定的理解成本和整体性的(因此游戏都需要包装和宣传),游戏行业如果少了卷画面质量的维度,那么更“接地气”的卷服务、卷买量、卷其它粉圈情绪什么的,只会是一个更无理、残酷和充满铜臭的环境;反过来,高清大作只要排好发售日期,并做好自己品质,始终还是能以更接近“出版物”的方式进行发售(以规避更多现代传播学上的玄学),而比拼品质的市场总归对于其中的从业人员是更有信仰更正向的一种状态。
自游戏进入3D化时代以来,(当时的)高清化3A游戏就像游戏业的黄金树,它够大的时候整个产业才能滋养更多的中小型游戏;如今它有所残损,表现出的就是业界整体的一种想跳车的氛围,是一种腐败的气息——殊不知跳到的就是更地狱更难成功的赛道,会把本来不是红海的领域做成红海。在我看来,高清化这个“黄金树”属于修补一下还能继续存在的,它可能需要的是产品的转型,而不是整个游戏机产业完全不行了。从这个意义上,我还是期待Epic这波图形技术的革命能成功。
最后是资料链接:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。