float4 position : POSITION; float3 normal : NORMAL; float4 model_matrix0 : TEXCOORD0; float4 model_matrix1 : TEXCOORD1; float4 model_matrix2 : TEXCOORD2; float4 model_matrix3 : TEXCOORD3; float4 instance_color : COLOR0;
静态实例化步骤如下:在编辑态或游戏打包时选取一组空间邻近的完全复用的物体合并它们的刚体变换数据和Shader参数为Instance Data并存盘创建一个Instance物体索引原始模型的Mesh、Material和刚创建好的InstanceData,使用Instance物体替换选中的这组物体Instance渲染时使用双VertexStream,VertexStream0为原始几何数据,VertexSteam1为InstanceData,在Shader中通过InstanceData访问每个实例自己的特有数据进行T&L变换及着色静态实例化虽然解决了内存和包体和内存问题,但它和静态合批一样增加了GPU的消耗,因为其合并范围内相同的物体而增大了单个模型的包围盒,故影响了Lod切换和不易剔除和静态合批完全一样Static Instance算法分析可以参考Nvidia Gpu Gems 2的《Inside Geometry Instancing》,实现可以参考UE4 ISM的相关实现动态实例化(Dynamic Instance)动态实例化和静态实例化的不同之处在于以下2点:运行时机:游戏运行时进行实时合并InstanceInstance Data存储方式:动态实例化的Instance Data一般存储在全局的UniformBuffer中或Texture中动态实例化的数据结构如下所示:这样在HLSL中做如下Vertex Input声明,用于访问几何数据和Instance IDfloat4 position : POSITION; float3 normal : NORMAL; float4 instance_id : TEXCOORD0;
Instance Data的访问如下所示float4 matrix_row0 = instance_buffer[instance_id instance_data_size + 0]; float4 matrix_row1 = instance_buffer[instance_id instance_data_size + 1]; float4 matrix_row2 = instance_buffer[instance_id instance_data_size + 2]; float4 matrix_row3 = instance_buffer[instance_id instance_data_size + 3];
动态实例化发生的时机放在LOD计算和剔除之后,这样它就不会增加额外的渲染面数和Overdraw,可以规避掉静态合批所增加的GPU消耗动态实例化不是银弹,因为其需要在运行时计算哪些渲染数据可以合批及动态更新Instance Data,故它会增加CPU消耗注意到Batch数量的增加也是影响CPU消耗,这就是说如果动态Instance在Batch减少的消耗上如果不及更新Instance Data的消耗,那它就是妥妥的负优化另一方面因为在Shader中索引InstanceData需要一次间接查找或纹理采样,也可能增加额外的GPU消耗分层资源代理(Hierachical Resource Proxy)以下是巫师3的截图:如下图红框所示的每个植被群,可以为每个红框生成一个简化的模型来替代简化的模型可以使用Lod生成算法进行自动生成,或者更极端的使用一些简单的Billboard或面片来替代不失一般性,这一简化方式我们把它叫做资源代理,它们的思想来源也很简单——结合空间分割、LOD、静态合批三种思想,就可以得到这一低消耗表达中远景的技术方案分层资源代理(HPR)实现Tips1.HPR不止可以用于显示,也可以用于加载和剔除优化2.HPR可以是真正意义上的多层结构而不是只代表单层,距离相机越远,则加载和显示的资源代理层级越高3.HPR的空间分割方式除了可以使用标准的距离划分之外,也可以考虑到遮挡情况和物体本身的聚合情况,使用基于数据聚类进行空间分割如KMeans或GMM算法这对物体的剔除会更为友好聚类划分算法的运行结果如下图所示4. HPR是用于表现远景的,所以它除了通用的LOD生成算法可用,它还很显然有以下优化可用:清理掉所有封闭曲面中所包含的内表面和内部模型数据,如果不是FPS类游戏,半封闭曲面也同样可以做同样优化在已知HPR的切换距离和Fov的前提下,可以在生成HPR的时候离线计算出每个模型的屏占比,使用Screen Size Cull可以剔除掉视觉贡献过小的物体保留大平面和远处剪影,去掉非边界的小突起使用HPR方式可以在真正意义上做到无限视距的大世界它的最主要缺点是会额外生成Mesh和纹理 ,从而提高游戏发布的包体大小UE4中实现的HLOD,Level Proxy,Imposter均可视为HPR的不完备子集5、剔除(Culling)Culling指的是在某一个处理步骤中去掉在显示时无贡献或贡献甚微的多边形,其目是减少渲染数据传输成本和渲染管线执行的GPU计算成本本文内容里不会深入到剔除算法的具体实现方式,但会介绍每个算法的基本思想、算法的适用场合及算法在实际工程应用上的一些主要缺陷和可能的改进方式,以作为技术选型和实现之参考因个人水平有限,错漏在所难免,望多指正Culling时机通用的质量管理有一项基本的流程优化原则:在流程的越早阶段进行所需的质量保证,其所负担的成本越低,效益越高这一原则同样适用于剔除优化:越早阶段进行剔除,剔除的收益越大回顾一下简化后的经典的渲染资源处理流程:上述流程中可用于Culling的时机为:更新阶段GPU的光栅化阶段之前GPU的光栅化阶段之后像素着色阶段之前在使用GPU Driven Pipeline时,GPU的使用在单帧中由一次变为2次:一次基于ComputeShader的数据组织和一次渲染过程,流程简述如下:上述流程中把Culling全部移交给了GPU去执行,Culling时机则基本不变因主机和PC平台支持异步ComputeShader的缘故,GPU Driven Pipeline的执行流程和在更新和提交渲染之间并不一定完全串行Culling的粒度Culling算法作用的粒度包括:一个场景分块或物体分组一个场景物体一些三角形一些像素对于前两种情形而言,Culling就一定和场景组织的数据结构有莫大的关联,三角形级别的剔除只有在GPU Driven Pipeline的前提下才有实用价值,而像素级的剔除则完全嵌入在GPU硬件和驱动层以下先简要介绍场景组织的数据结构场景组织数据结构在2011年Dice《Culling the Battlefield: Data Oriented Design in Practice》之前,实时渲染引擎中的主流场景组织方式是SceneGraph、Octree、Quadtree或BSP等在那之后引擎开始引入了面向数据(DOP)的思想,一窝蜂的疯狂追求极致的CacheMiss率和极少的分支(从而规避掉CPU分支预测失败),许多引擎的场景组织方式回退到使用线性数组的时代,并使用SoA(Structure of Arrays)替代AoS(Array of Structure)SceneGraph的根节点为所有场景物体的双亲节点,叶节点表示的是具体的每个场景模型,中间的Group节点可能是场景分块或类型集合,SceneGraph示图如下所示:Quadtree按把场景按与地形所平行的平面,四等分场景,如此循环往复直至满足最大的拆分层级深度或是节点中的场景物体数量小于约定的最大叶节点物体数量才停止拆分Quadtree的示意图如下所示Octree是Quadtree的3D扩展版本——它把场景按包围盒在X,Y,Z三轴上进行等分,这样一个节点就有2^3=8个子节点,如此循环往复直至满足最大的拆分层级深度或是节点中的场景物体数量小于约定的最大叶节点物体数量才停止拆分Octree的示意图如下所示:SoA的核心思想是把相同的数据存在一起,因为Culling本质上需要遍历场景,这样在做遍历访问的时候同类数据放在连续的内存中可以保证更高的Cache命中率,下图是SoA的一个示意图:多提一句,ECS在性能上的收益大多也同样源于相同类型数据存储的连续性Culling算法Culling算法的核心在于计算一个渲染数据集对最终画面渲染贡献量,当贡献量小于给定阈值时丢弃掉这部分数据,从而节省数据传输带宽和渲染所消耗的计算量下面开始介绍常见的Culling算法,为方便阅读,大抵以实用程度前后介绍Frustum Culling视锥体(Frustum)确定了屏幕上的可见区域的范围在相机的Fov和Aspect、近、远裁剪面共同确定了这一锥棱台的形状使用这6个面进行物体的可见性Culling即为Frustum Culling和Lod计算类似,Frustum Culling一般使用的包围盒为Sphere或AABB,但也有使用OBB的,OBB更贴近物体,对剔除掉非轴向的长条形物体尤为有效以下为Frustum Culling执行的示意图左图为开启Frustum Culling之前,可以看到所有物体都被提交给渲染管线;右图为开启Frustum Culling之后,只有和视锥体相交的这些彩色的物体通过了Culling测试,未通过测试的黑白线框所标示物体就不会被提交给渲染管线,从而节点了传输带宽及后续的渲染计算量Frustum Culling计算量和参与Culling的物体数量成正比,算法时间复杂度为O(n)而对一个正常的FPP/TPP游戏来说,其在Frustum内的可见集大多数时候都会小于全体数据集的50%,所以Frustum Culling带来的收益至少是可以去掉一半以上的不可见物体,其发生时机也在于所有数据开始传输和渲染之前,故适用于所有类型的游戏应用场景Frustum Culling对于大世界来说,因为物体数量众多,使用传统分层的场景结构(Graph 或Tree)要比直接使用线性数组要处理的数据总量会小许多,从而更有效率Distance CullingDistance Culling的思想非常简单:当物体离相机超过设定的距离阈值,就把物体从可见列表中剔除Distance Culling执行时和Frustum Culling不同,Frustum Culling剔除掉的是完全看不到的物体,所以不需要做任何过渡性处理,但Distance Culling一般需要做淡入淡出处理以防止物体消失和出现时的画面跳变Distance Culling适合处理半透物体、镂空物体、草丛、贴花等面积大,Overdraw严重的物体,因为这些物体虽然包围盒很大,但是其在稍远一点,对最终画面的贡献微弱却消耗不低,在平衡画面表现和性能开销之后可酌情对上述物体开启Screen Size CullingScreen Size Culling和Distance Culling类似,它所用的阈值不是距离而是物体包围盒在屏幕上的投影面积即它的思想是:当物体最终在屏幕上的投影面积小于事先设定好的阈值,就把物体从可见物体中删除Screen Size Culling和Distance Culling一样在物体出现和被剔除时会存在画面跳变,所以同样需要做淡入淡出处理Screen Size Culling在实际应用过程中的适用于所有物体,它能快速使投影面积小的物体从画面中消失对于物体密度高、小物体多的场景,它的收益高的往往超乎想象虽然在复杂时间复杂度方面它和Distance Culling和Frustum Culling一样都是O(n),但在单个物体所需的计算量方面这两者都远小于Frustum Culling虽然Screen Size Culling从理论上来说可以和Distance Culling相互替换,但这要求每个物体去个性化设置这些数据,可用性不佳,故二者往往是叠加使用,用于不同分类物体的剔除PVSPVS的全称是Potentially Visible Set的缩写,意为潜在可见集PVS的基本思想为:离线的把场景划分成许多个块,这些分块的划分可能是均匀的3D网格,也能是自适应大小的3D网格完成网格划分之后会计算网格之间的可见性或场景中每个物体对当前网格的可见集并存盘,PVS即得名于此在游戏运行时读取存盘的可见性数据,根据当前相机所在的网格及它的可见网格(或物体)集来剔除掉不可见的物体,实现Culling功能下图所示的蓝色网格即为UE4的PVS分块:PVS是典型的空间换时间的两步式的算法:对实时渲染程序而言,它在离线生成时的时间可以忽略不计,在运行时它只需要查询当前网格的可见性,故算法时间复杂度是O(1)PVS的缺点是会消耗大量的内存,它存储可见集的空间复杂度是O(mn),其中m表示网格数量,n表示的是物体或网格的数量(取决于存储的是网格可见性还是物体可见性)试以存储物体可见集为例,场景大小为10km10km ,网格大小为10m10m,则就算只生成一层网格,其数量为100万 ,场景中共计30万个物体,物体使用32位索引做唯一标识,每个网格使用数组存储其可见性,则总的PVS数据集大小可能高达1T当然我们可以使用诸如红黑树之类的数据结构来存储可见集数据,但这样PVS在运行时的算法时间复杂度将不再是O(1)而是O(log n)注意到PVS存储的空间复杂度来源于网格数量和它所存储的可见性数据总量的和对其优化可以来自于以下这些方面:网格在划分过程中会考虑到当前网格中是否存在场景物体,那些不存在物体的网格则会被丢弃掉这样可以减少网格的总数量某些实现还会限制网格只能生成在接近地形的平面上或潜在的玩家可行走区域进一步减少网格数量网格划分考虑自适应几类大小而不是均分,在空旷的地方生成大的网格,在城市、孔洞区域生成更小的网格以用于减少网格总量把PVS当作粗略的剔除算法,只存低层级的Scene Graph或Octree的节点的可见性而不是存物体或网格本身的可见性如只存Octree第3级的可见性,则可见集总大小只有512个把PVS当作精确的剔除算法,但只存周围区域的可见性(如只存1平方公里内的物体可见性,则上例同样的物体密度和网格密度,PVS数据总大小可能只有10M左右)对PVS数据开始流式加载,因为视角移动缓慢且连续,流式加载可以缓解PVS的内存占用,但其无法减少游戏包体的大小Hi-Z CullingHiz的全称是Hierarchical-Z map,它依赖于Z buffer的数据来做剔除,这是一个两步算法,简单整理如下:读取上一帧的Z Buffer或使用延迟管线当前帧Pre-Z Buffer,生成其Mipmap ,即Hi-Z Map(或Pyramid Z Buffer)在Mipmap取值的时候和普通Mipmap使用的双线性或其它均值分布Filter不同,它的Filter是取四个像素中的最大值(Reversed-Z 则取最小值)这样采样的目的是为了防止错误的剔除Hi-Z map示意如下图所示:通过上一帧的ViewProjection矩阵,我们可以把物体的包围盒投影到屏幕空间,使用根据物体包围盒的大小,选取合适的Hi-Z map对应的mipmap使得包围盒投影后占一个像素大小,方便后续剔除处理的时候,只需采样周围2 x 2 的Hi-Z map像素就足以进行深度剔除包围盒一般可使用AABB或OBB使用Hi-Z进行剔除示意如下:在执行完Hi-Z剔除之后,数据可以回读取CPU端进行渲染提交,也可以使用GPU-Driven模式直接修改渲染Buffer本身从而减少GPU->CPU数据传输的开销使用上一帧Z buffer进行剔除有一个较为明显的问题是处理不了相机的高速移动和转动如果使用的是CPU端回读Hi-Z剔除结果的方案,则数据回读也可能带来卡顿,使用异步回读则可能带来更多帧的延迟,从而使剔除失效的情况增加无法处理相机高速运动的问题不光是Hi-Z的问题,接下来所述及的两个遮挡剔除算法同样被这个问题所困扰Software Occlusion Culling接下来介绍的两种都是遮挡剔除算法,从被工程上提出和使用的顺序来看,应该先介绍硬件的遮挡查询(Hardware Occlusion Culling,HOC)再介绍软件遮挡查询但因为从已有的无论PC还是手机项目的使用的效果来看,软件遮挡查询(SOC)在性能上都显著优于HOC,故在此介绍顺序如是反转SOC的基本方法为:选中一些物体做为遮挡体,使用CPU端执行的软件光栅化渲染器做Depth Only的渲染,即只渲染出一张和当前屏幕等比的一张小分辨率的Depth Map(如320 180)同样使用CPU端的软件光栅化渲染器光栅化待渲染物体的包围盒,据其与相对于第一步所生成的Depth Map比较以返回可见性SOC的流程示意图如下所示:SOC的执行过程完全在CPU完成,一般它会是运行在一个或一些单独的线程里其实现的关键在于如何执行更有效的软件光栅化渲染器、判别出最有价值的遮挡体集合、包围盒在做遮挡查询的时候需使用保守光栅化以避免在小分辨率Depthmap中出现误剔除SOC的光栅化部分是比较典型的计算密集型应用,一般都会使用SSE或Neon等向量指令集进行加速Hardware Occlusion CullingHOC和SOC类似,只不过它的执行是在GPU端而不是CPU端——现存的渲染API大多已支持硬件遮挡查询HOC相比SOC,其遮挡体的会带来额外的Batch,从而带来额外的消耗,对于复杂场景或大场景来说,其查询的效率很多时候会超过渲染这些数据本身的消耗,现在已很少在项目中看到其实装Portal CullingPortal Culling在卡神的Quake时代就已经存在且可以自动生成了其基本思想和Frusutm Culling基本上一致,它通过构造类似视锥体的棱台来做裁剪,不同的是视锥的上下左右平面来源是Fov和Aspect,而Portal的上下左右平面来源于场景中的孔洞正因如此,它的应用场景也受限于纯室内或从室内看室外(或相反的情形),但不适合开阔的野外场景Portal的示意如下所示:小结:以上8种Culling技术剔除的粒度是最细只能到物体级别接下来介绍的的剔除算法为三角形或三角形簇级别,但因其计算量大,且如果在CPU端实现的话资源会拆的非常碎从而不利于制作流程优化,故仅适用于GPU Driven Rendering PipelineMesh Cluster Culling在Mesh Shader出来之前,Mesh Cluster Culling就已经成型,它把要渲染的Mesh拆成一些空间上邻近的三角形簇,把一个球型Mesh分簇的示意图如下所示,每种颜色表示一个Cluster然后针对这些三角形族进行实施剔除算法,如Sceen Size Culling ,Hi-Z Culling和Backface Culling其中Backface Culling用于剔除模型背向相机的一面对上例的球体来说,Backface Culling能总是能减掉一半的面下例是Backface Culling的示例:下图是逐三角形Screen Size Culling的示例:Mesh Cluster Culling的一个优化是使用Cone来加速计算Backface如下图黑线所示的一个Cluster,计算Cone的Normal 和ViewDirection的夹角可以一次剔除掉一簇三角形总结:除上述软件实现的Culling算法之外,硬件层面也还有Viewport Culling ,Early-Z/HSR/Forward PixelKill等算法软件层面也还有用于剔除Alpha Test密集的区域的Pre-Z(图片来源网络,侵删)
0 评论