(原)Unreal渲染模块 管线 - 程序和场景查询

2023-03-10,,

@author: 白袍小道

查看随意,转载随缘

 
 

第一部分:

这里主要关心加速算法,和该阶段相关的UE模块的结构和组件的处理。

What-HOW-Why-HOW-What(嘿嘿,老规矩)

1、渲染模块这里有个主要任务需要完成:将需要在屏幕上(或者某设备)显示出来的Primitives(几何体,或者绘制图元)输入到pipeline的下一阶段。

 
 

2、渲染的每帧,计算Camera,Light,Primitives输出到几何阶段(非几何着色)

插一句:Geometry State包含了视点变换,顶点着色,投影、裁剪、映射

 
 

3、几个空间数据结构和算法:

层次包围入口裁剪、QuadTree, 空间分隔树, Kd树,八叉树,场景图、细节裁剪

 
 

空间数据结构:

是将几何体组织在N维空间中的一系列层次(上抱下,下抱下下,类推)数据结构

 
 

层次包围体BVH

 
 

入口裁剪PortalCulling

 
 

细节裁剪( 屏幕尺寸裁剪 )

具有包围体的问题,将这个包围体投射到投影平面,然后以像素为单位来估算投影面积,如果像素的数量小于用户定义的阈值,那么不对这个物体进行进一步处理。

 
 

遮挡删除

遮挡剔除必要性:

不难理解,可见性问题可以通过Z缓冲器的硬件构造来实现,即使可以使用Z缓冲器正确解决可见性问题,但其中Z缓冲并不是在所有方面都不是一个很"聪明"的机制。例如,假设视点正沿着一条直线观察,其中,在这条直线上有10个球体,虽然这10个球体进行了扫描转换,同时与Z缓冲器进行了比较并写入了颜色缓冲器和Z缓冲器,但是这个从这个视点渲染出的图像只会显示一个球体,即使所有10个球体都将被光栅化并与Z缓冲区进行比较,然后可能写入到颜色缓冲区与Z缓冲区。

上图中间部分显示了在给定视点处场景的深度复杂度,深度复杂度指的是对每个像素重写的次数。对于有10个球体的情形,最中间的位置,深度复杂度为10,因为在这个地方渲染了10个球体(假设背面裁剪是关闭的),而且这意味着其中有9次像素写入是完全没有必要的。

 
 

两种主要形式的遮挡裁剪算法,分别是基于点的遮挡裁剪和基于单元的遮挡裁剪

 
 

 
 

伪代码--------------------------------------------------------------------------------

----------------------------------------------------------------------------------------

下面是常用几种遮挡算法

1、硬件遮挡查询(UE中有,也可以自己先写写,然后测试对照)

硬件遮挡查询的基本思想是,当和Z缓冲器中内容进行比较时,用户可以通过查询硬件来找到一组多边形是否可见的,且这些多边形通常是复杂物体的包围体(如长方体或者k-DOP)。如果其中没有多边形可见,那么便可将这个物体裁剪掉。硬件实现对查询的多边形进行光栅化,并且将其深度和Z缓冲器进行比较

 
 

2、HZB(同上)

层次Z-缓冲算法用八叉树来维护场景模型,并将画面的Z缓冲器作为图像金字塔(也称为Z-金字塔(Z-pyramid)),该算法因此在图像空间中进行操作。其中,八叉树能够对场景的遮挡区域进行层次剔除,而Z-金字塔则可以对单个基元和边界体积进行层次Z缓冲。 因此Z-金字塔可以作为此算法的遮挡表示。

通过从前到后遍历八叉树裁剪遇到的八叉树节点,此算法可以仅访问可见的八叉树节点及其子节点(右上角的节点),

的容器只对可见包围体中的多边形进行渲染。

 
 

3、遮挡地平线算法

通过从前到后渲染一个场景,我们可以定位到地平线在哪里进行渲染,而任何在当前地平线之后和之下的物体都可以被裁剪掉。

 
 

4、遮挡物收缩与视锥扩张算法(也有类似处理)

可以使用基于点的遮挡算法来生成基于单元的可见性,根据给定的量来缩小场景中所有遮挡物来达到延伸有效可见点的目的,

通常与Occluder Shrinking算法一起配合使用

 
 

5、LOD这个放到后面细说。【篇幅不少】

 
 

6、裁剪图策略:后面加入

 
 

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第二部分:

下面按照延时渲染来过一下。

涉及主要类

DeferredShadingSceneRenderer

FSceneRender,FSceneViewFamily,FViewInfo,Fscene,FView

FMeshElementCollector

SceneOcclusion

FSceneViewState

TStaticMeshDrawList

FRenderTask\FDrawVisibleAnyThreadTask

DrawingPolicy,FPrimitiveSceneProxy

 
 

一、FDeferredShadingSceneRenderer::InitViews

这里主要通过检测可见性,透明排序等,完成视图初始化。这里我们关注检测可见性


灯光信息的可见分配。

 
 

 
 

、预处理可见性

位于文件SceneVisibility.cpp

 
 

FSceneRenderer::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList)

 
 

2.1

得到当前预处理可见性的数据

FSceneViewState::GetPrecomputedVisibilityData

完成任务:

*返回给定视图位置的可见性数据数组(数据),如果不存在,则返回NULL。

*数据位通过场景中每个原语的VisibilityId进行索引。

*此方法在必要时解压缩数据,并基于视图状态中的bucket和chunk(优化时候也需要注意)索引缓存数据

(这里若需要查看可以通过几个调试选项【详细在后面备注】,然后烘培)

如何完成:

a、计算盒子的ViewOrigin是否在格子中,这里利用反过来将盒子散列到bucket去减少了计算量。

这里盒子偏移,Bucket索引计算可以看看。

b、若有必要解压数据

2.1后(构建有时会覆盖截锥体就是迭代覆盖视口的图元可见性Map【view.PrimitiveVisibilityMap】)

 
 

2.2

更新HLOD转换/可见性状态

这里开启后是为了允许HLOD在踢裆删除阶段可以使用

 
 

2.3

计算使用标准视锥体裁剪的图元的数目---FrustumCull

 
 

 
 

接下来是引擎特性(处理view.PrimitiveVisibilityMap)

2.4a

更新了视图的原始fade状态(略)。

2.4b

(扫描VisibilityMap后面就说VMP)如果几何体标记为Hide,view.PrimitiveVisibilityMap标记

2.4c

视图属性标记了只显示几何体的,那其他同样标记

2.4d

反射捕获(Reflection Captures)的处理:只对接受非移动的.

2.4e

剔除线框中的小框对象【就一个投射矩阵判断】【主要是提高编辑器,因为线框模式的禁止了遮挡,我去】

2.4f

(不在线框中的)进行剔除

 
 

2.5

OcclusionCull

这里才是算法实现的开始,包括使用前面说的预计算数据,根据特征级别(opengl,dx)做不同的OC处理

a、软处理(自己整CPU处理)FSceneSoftWareOCclusion。

b、FetchVisibilityForPrimitives处理(还有FHZBOcclusionTester)

c、标记到非OC(没得整)组

 
 

2.5.1

FSceneSoftWareOCclusion

几个重要事情:

SubmitScene

CollectOccludeeGeom

Sort potential occluders by weight

Add sorted occluders to scene up to GSOMaxOccluderNum

reserve space for occludees vis flags

 
 

ProcessOcclusionFrame:

上面基本是数据的规整,这里才是执行算法过程

2.5.1.1

ProcessOccluderGeom,对着算法来一遍(嘿嘿)

每个模型

a\ 转换模型到裁剪空间【矩阵操作】

const FMatrix LocalToClip = Mesh.LocalToWorld * SceneData.ViewProj;

VectorRegister mRow0 = VectorLoadAligned(LocalToClip.M[0]);

VectorRegister mRow1 = VectorLoadAligned(LocalToClip.M[1]);

VectorRegister mRow2 = VectorLoadAligned(LocalToClip.M[2]);

VectorRegister mRow3 = VectorLoadAligned(LocalToClip.M[3]);

 
 

for (int32 i = 0; i < NumVtx; ++i)

{

VectorRegister VTempX = VectorLoadFloat1(&MeshVertices[i].X);

VectorRegister VTempY = VectorLoadFloat1(&MeshVertices[i].Y);

VectorRegister VTempZ = VectorLoadFloat1(&MeshVertices[i].Z);

VectorRegister VTempW;

// Mul by the matrix

VTempX = VectorMultiply(VTempX, mRow0);

VTempY = VectorMultiply(VTempY, mRow1);

VTempZ = VectorMultiply(VTempZ, mRow2);

VTempW = VectorMultiply(GlobalVectorConstants::FloatOne, mRow3);

// Add them all together

VTempX = VectorAdd(VTempX, VTempY);

VTempZ = VectorAdd(VTempZ, VTempW);

VTempX = VectorAdd(VTempX, VTempZ);

// Store

VectorStoreAligned(VTempX, &MeshClipVertices[i]);

 
 

uint8 VertexFlags = ProcessXFormVertex(MeshClipVertices[i], W_CLIP);

MeshClipVertexFlags[i] = VertexFlags;

}

 
 

每个三角形

b\ 修正三角形:ClippedVertexToScreen,TestFrontface,AddTriangle

【满足裁顶点加或修正三角形:按深度(获取离屏最远的)】

uint16 I0 = MeshIndices[i*3 + 0];

uint16 I1 = MeshIndices[i*3 + 1];

uint16 I2 = MeshIndices[i*3 + 2];

 
 

uint8 F0 = MeshClipVertexFlags[I0];

uint8 F1 = MeshClipVertexFlags[I1];

uint8 F2 = MeshClipVertexFlags[I2];

 
 

if ((F0 & F1) & F2)

{

// fully clipped

continue;

}

 
 

FVector4 V[3] =

{

MeshClipVertices[I0],

MeshClipVertices[I1],

MeshClipVertices[I2]

};

 
 

uint8 TriFlags = F0 | F1 | F2;

 
 

if (TriFlags & EScreenVertexFlags::ClippedNear)

{

static const int32 Edges[3][2] = {{0,1}, {1,2}, {2,0}};

FVector4 ClippedPos[4];

int32 NumPos = 0;

 
 

for(int32 EdgeIdx = 0; EdgeIdx < 3; EdgeIdx++)

{

int32 i0 = Edges[EdgeIdx][0];

int32 i1 = Edges[EdgeIdx][1];

 
 

bool dot0 = V[i0].W < W_CLIP;

bool dot1 = V[i1].W < W_CLIP;

 
 

if (!dot0)

{

ClippedPos[NumPos] = V[i0];

NumPos++;

}

 
 

if (dot0 != dot1)

{

float t = (W_CLIP - V[i0].W) / (V[i0].W - V[i1].W);

ClippedPos[NumPos] = V[i0] + t*(V[i0] - V[i1]);

NumPos++;

}

}

 
 

// triangulate clipped vertices

for (int32 j = 2; j < NumPos; j++)

{

FScreenTriangle Tri;

float Depths[3];

bool bShouldDiscard = false;

 
 

bShouldDiscard|= ClippedVertexToScreen(ClippedPos[0],        Tri.V[0], Depths[0]);

bShouldDiscard|= ClippedVertexToScreen(ClippedPos[j-1],        Tri.V[1], Depths[1]);

bShouldDiscard|= ClippedVertexToScreen(ClippedPos[j],        Tri.V[2], Depths[2]);

 
 

if (!bShouldDiscard && TestFrontface(Tri))

{

// Min tri depth for occluder (further from screen)

float TriDepth = FMath::Min3(Depths[0], Depths[1], Depths[2]);

AddTriangle(Tri, TriDepth, Mesh.PrimId, 1, OutData);

}

}

}

else

{

FScreenTriangle Tri;

float Depths[3];

bool bShouldDiscard = false;

 
 

for (int32 j = 0; j < 3 && !bShouldDiscard; ++j)

{

bShouldDiscard|= ClippedVertexToScreen(V[j], Tri.V[j], Depths[j]);

}

 
 

if (!bShouldDiscard && TestFrontface(Tri))

{

// Min tri depth for occluder (further from screen)

float TriDepth = FMath::Min3(Depths[0], Depths[1], Depths[2]);

AddTriangle(Tri, TriDepth, Mesh.PrimId, /*MeshFlags*/ 1, OutData);

}

}

 
 

2.5.1.2

按深度整理【最接近屏幕的在前】

栅格化遮挡删除

 
 

 
 

2.5.2

FetchVisibilityForPrimitives

这里先略(那啥,放到另外一个地方)

1、若支持并行处理:构建数据FVisForPrimParams,利用上多任务FetchVisibilityForPrimitivesTask处理FHZBBound

2、FOcclusionQueryBatcher::BatchPrimitive (一个算法)

2.6

StereoPass

若视图开启了InstancedStereoPass

->确保右眼视图中的图元在左眼(实例化)视图中可见。

->ComputeAndMarkRelevanceForViewParallel;

 
 

2.7

GatherDynamicMeshElements

FSceneRender::GatherDynamicMeshElments

 
 

、排序BasePass,实现HiZ剔除

按是否使用线程且还有可用渲染线程

.

FDeferredShadingSceneRenderer::SortBasePassStaticData

如果我们不使用深度(EarlyZPassMode==None)仅通过排序静态绘制列表桶大致从前到后,以最大化HiZ剔除。不会干扰状态排序,且每个列表都是单独排序的

.

FDeferredShadingSceneRenderer::AsyncSortBasePassStaticData

 
 

、提交视野的自定义数据

(略)

 
 

、RHI资源(构建)

(略)

 
 

检验:

 
 

 
 

总结:

 
 

(原)Unreal渲染模块 管线 - 程序和场景查询的相关教程结束。

《(原)Unreal渲染模块 管线 - 程序和场景查询.doc》

下载本文的Word格式文档,以方便收藏与打印。