2018-10-24 21:48:00 u012614151 阅读数 2
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2377 人正在学习 去看看 杨石兴

@author: 白袍小道

转载悄悄说明下

随缘查看,施主开心就好

   

说明:

本篇继续Unreal搬山部分的渲染模块的Shader部分,

主要牵扯模块RenderCore, ShaderCore, RHI, Materia.

可能分成N篇。

(这里放入的UE的模块框)

(下一篇主要是UE灯光和着色简要[ush以及对应结构,和UE代码和DX部分],然后是巴拉巴拉)

   

前言:

部分算法和流程的实现原理,和细节(往往这部分会成为优化的处理口)。

梳理UEShader的结构,底层的接入,分层。

UE着色使用和细节部分。

对比反观差距和原因

   

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

第一部分、渲染管线和着色

Shading Models

描述物体的颜色如何根据表面等因素而变化 (例如方向,视图方向,和照明)

1、通常具有控制外观变化的属性。

2、控制这些变化的属性而决定了变化。

   

评估

设计着色实现时,需要根据计算结果进行划分他们的评估频率。

首先,确定一个给定的结果在整个绘制调用中,计算总是不变的。在这种情况下,计算应用程序可以通过GPU计算(通常在CPU上)执行吗。着色器可以用于特别昂贵的计算。结果被传递给图形API通过统一着色器输入。

   

说明

1、虚拟机

a\ 目前的着色语言都是 C-likelike 的着色语言,比如 HLSL,CG 和 GLSL GLSL,其被编译成独立于机器的汇语言 语言,也称为中间( IL )。

这些汇编语言在单独的阶段,通常是驱动中被转化成实际机器 )。

这些汇编语言在单独的阶段,通常是驱动中被转化成实际机器 语言。这样的安排可以兼容不同硬件实现些汇编被看做是定义一个作为着色译器.

b\ 着色语言虚拟机可以理解为一个处多种类型寄存器 和数据源、预编了一系列指令的处理器。

2、单指令多数据( SIMD), 寄存器.

a\ 一次DrawCall会通过API做一次(一系列)的图元绘制,从而驱使图形管线运行。[如Draw,DrawInstance, …..]

b\ 输入,输出和交接。

c\ Shader的语句、加载和编译。

DX10 虚拟机

  

   

语法树

划分

1、顶点着色

发生在图元装配后

顶点着色器(VS)阶段处理来自输入汇编器的顶点,执行每个顶点的操作,如转换、蒙皮、变形和每个顶点的光照,并编制到输出结构交付给下一个管道。

(顶点着色器总是操作单个输入顶点并产生单个输出顶点。顶点着色器阶段必须始终处于活动状态,以便管道执行。

如果不需要顶点修改或转换,则必须创建一个直通顶点着色器并将其设置为管道。)

   

a\ 输入和输出:

每个顶点着色器输入顶点可以由16个32位向量组成(每个顶点最多4个分量),每个输出顶点可以由16个32位4分量向量组成。

所有顶点着色器都必须至少有一个输入和一个输出,可以是一个标量值。

 

b\ vertexshader阶段可以使用输入汇编程序生成的两个系统值:VertexID和InstanceID(参见系统值和语义)。

由于VertexID和InstanceID在顶点级别都是有意义的,并且硬件生成的ID只能被输入到理解它们的第一阶段,因此这些ID值只能被输入到vertexshader阶段。

 

c\ 顶点着色器总是在所有顶点上运行,包括带有邻接的输入基元拓扑中的相邻顶点。

顶点着色器执行的次数可以使用VSInvocations管道统计从CPU查询。

   

   

2、可选镶嵌

描述:

Direct3D 11运行时支持三个实现镶嵌的新阶段,它将低细节细分曲面转换为GPU上的高细节原语。

镶嵌瓷砖(或分解)高阶表面成适合渲染的结构。

通过在硬件中实现镶嵌,图形管道可以评估较低的细节(较低的多边形计数)模型和渲染较高的细节。

虽然可以进行软件镶嵌,但是由硬件实现的镶嵌可以生成大量的视觉细节(包括对位移映射的支持),而不需要向模型大小添加视觉细节和刷新率。

   

概括:镶嵌使用GPU计算更详细的表面从一个由四边形斑块,三角形斑块或等值线构成的表面。

   

HullShader:

一个可编程的着色器阶段,它产生一个几何贴片(和贴片常量),对应于每个输入贴片(四边形、三角形或直线).

外壳着色器(每个补加点调用一次)将定义低阶表面的输入控制点转换为构成补加的控制点。

ID3D11Device::

CreateHullShader

   

ID3D11DeviceContext::HSSetShader

  

  

Tessellator

一个固定的函数管道阶段,它创建表示几何patch的域的抽样模式,并生成一组较小的对象(三角形、点或线)来连接这些样本

将一个域(四边形、三边形或直线)细分为许多较小的对象(三角形、点或直线)。将uv(和可选的w)坐标和表面拓扑(归一化的坐标域)输出到域着色器阶段。

域着色器在每个tessellator阶段点调用一次,并计算表面位置。

  

Domain

一个可编程着色器阶段,计算对应于每个域样本的顶点位置

域着色器计算输出补丁中细分点的顶点位置,其中每个tesselator阶段输出点运行一次,并且对tesselators阶段输出UV坐标、船体着色器输出补丁和船体着色器输出补删常量具有只读访问权限。

ID3D11Device::

CreateDomainShader

  

  

 

   

   

3Geometry Shader

几何着色器(GS)阶段运行应用程序指定的着色器代码,将顶点作为输入,并能够在输出上生成顶点。

(不像顶点着色器操作单个顶点,几何着色器的输入是一个完整语义的顶点(两个顶点表示线,三个顶点表示三角形,或者单个顶点表示点)。

几何体着色器还可以将边缘相邻基元的顶点数据作为输入(对于一条直线,额外的两个顶点,对于三角形,额外的三个顶点)。

下图显示了一个三角形和一条有相邻顶点的直线。)

   

a\ 可以使用由IA自动生成的SV_PrimitiveID系统生成的值。

b\ 能够输出多个顶点,形成单个选定的拓扑(可用的GS阶段输出拓扑有:tristrip、linestrip和pointlist)。

c\ 输出可以通过流输出阶段反馈到光栅化阶段和/或内存中的顶点缓冲区。提供给内存的输出被扩展到单独的点/线/三角形列表(它们将被传递给光栅化器)。

   

4Pixel Shader

像素着色器阶段(PS)支持丰富的着色技术,例如逐像素光照和后期处理。

像素着色器是一个程序,它结合了常量变量、纹理数据、每个顶点值的插值和其他数据来产生每个像素的输出

对于一个原语覆盖的每个像素,rasterizer阶段只调用一次像素着色器,但是,可以指定一个空着色器来避免运行一个着色器。

   

a\ 当对纹理进行多采样时,每个覆盖的像素只调用一次像素着色器,同时对每个覆盖的多采样进行深度/模板测试。通过深度/模版测试的样本将使用像素着色器输出颜色进行更新。(所以适当减少)

   

b\ 像素着色器固有函数产生或使用屏幕空间x和y的数量导数,导数最常用的用途是计算纹理采样的详细程度计算,在各向异性滤波的情况下,选择沿各向异性轴的样本。

通常,硬件实现同时在多个像素(例如2x2网格)上运行一个像素着色器,以便在像素着色器中计算的量的导数可以合理地近似为相邻像素中同一执行点上的值的增量。

   

c\ 输入:

输入将被限制(超过会截取)在16(无配置Geo着色)or 32、 32位、4个组件。

关键词:顶点属性,插值,覆盖,抽样,

   

d\ 输出

像素着色器最多可以输出8 32位4分量的颜色,如果像素被丢弃,则不输出颜色。像素着色器输出寄存器组件在使用之前必须声明;每个寄存器都允许有一个不同的输出-写入掩码。

使用depth-write-enable状态(在输出合并阶段)来控制是否将深度数据写入深度缓冲区(或使用丢弃指令丢弃该像素的数据)。

像素着色器还可以输出可选的32位、1个组件、浮点数、深度值来进行深度测试(使用SV_Depth语义)。

深度值在深度寄存器中输出,并替换深度测试的插值深度值(假设启用深度测试)。无法在使用固定函数深度和着色器深度之间动态变化。

像素着色器不能输出模板值。

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

第二部分、Unreal 着色器相关

DX

1、编译,读写

D3DCompileFromFile(_In_ LPCWSTR pFileName,

_In_reads_opt_(_Inexpressible_(pDefines->Name != NULL)) CONST D3D_SHADER_MACRO* pDefines,

_In_opt_ ID3DInclude* pInclude,

_In_ LPCSTR pEntrypoint,

_In_ LPCSTR pTarget,

_In_ UINT Flags1,

_In_ UINT Flags2,

_Out_ ID3DBlob** ppCode,

_Always_(_Outptr_opt_result_maybenull_) ID3DBlob** ppErrorMsgs);

D3DCompile(_In_reads_bytes_(SrcDataSize) LPCVOID pSrcData,

_In_ SIZE_T SrcDataSize,

_In_opt_ LPCSTR pSourceName,

_In_reads_opt_(_Inexpressible_(pDefines->Name != NULL)) CONST D3D_SHADER_MACRO* pDefines,

_In_opt_ ID3DInclude* pInclude,

_In_opt_ LPCSTR pEntrypoint,

_In_ LPCSTR pTarget,

_In_ UINT Flags1,

_In_ UINT Flags2,

_Out_ ID3DBlob** ppCode,

_Always_(_Outptr_opt_result_maybenull_) ID3DBlob** ppErrorMsgs);

   

D3DCompressShaders(_In_ UINT uNumShaders,

_In_reads_(uNumShaders) D3D_SHADER_DATA* pShaderData,

_In_ UINT uFlags,

_Out_ ID3DBlob** ppCompressedData)

   

D3DReadFileToBlob(_In_ LPCWSTR pFileName,_Out_ ID3DBlob** ppContents)

D3DWriteBlobToFile(_In_ ID3DBlob* pBlob,_In_ LPCWSTR pFileName, _In_ BOOL bOverwrite)

   

2、创建

CreateVertexShader(

_In_reads_(BytecodeLength) const void *pShaderBytecode,

_In_ SIZE_T BytecodeLength,

_In_opt_ ID3D11ClassLinkage *pClassLinkage,

_COM_Outptr_opt_ ID3D11VertexShader **ppVertexShader)

CreateXXXShader类似

   

Unreal

UnrealShader处理包括了:ush的包含,编译得到转储的着色器usf,平台编译器编译,创建和使用(绑定)。

1、Global Shaders

全局shader是用于操作固定几何体(如:full screen quad)的shader,并且它不需要与materials产生交互,例如:阴影过滤、后处理。

这种类型的shader在内存中只有一份实例.

   

2Materia

材质由一些控制材质如何渲染的状态和一些控制材质如何与渲染Pass shader交互的材质输入(material inputs)组成。

   

3、Vertex Factories 和 MeshTypes

Material必须能够赋给各种类型的Mesh Types, 这个是通过和Vertex Factories 配合完成的。 一个FVertexFactoryType表示唯一的Mesh Type。

一个FVertexFactory的实例存有每个几何体的数据,以便支持特定的Mesh Type。

   

引擎内Shader位于:Engine\Shaders\Private(Public)

(详细分析和说明在下一篇)

使用部分

   

   

ShaderCore

FShader、FGlobalShader、

FShaderType, FVertexFactory

FShaderPipeline, FShaderTarget

   

   

   

   

   

材质的编译

   

过程说明:

1、生成材质的Shader代码【只是Shader文件】

2、构建编译环境,且添加相关文件路径

3、编译[可以异步],成功就缓存起来

3.1 创建了一个临时引用计数指针[主要是确保操作一个引用的着色器映射[或查找]将导致这个着色器映射被删除]

3.2 往ShaderMapsBeingCompiled添加[占个位置]

3.3 分配唯一编译ID,配置编译环境

3.4 迭代处理所有顶点工厂的类型,编译这个材质和顶点工厂类型组合的所有网格材质着色器。---------FVertexFactoryType

3.5 迭代处理所有材质Shader的类型 ------FShaderType

3.6 迭代处理所有材质Shader管线的类型 ------FShaderPipelineType

3.7 进入映射编译着色器

   

类关系:

   

   

2018-10-20 19:36:00 u012614151 阅读数 4
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2377 人正在学习 去看看 杨石兴

@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

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

1 灯光信息的可见分配。

   

   

2、预处理可见性

位于文件SceneVisibility.cpp

   

FSceneRenderer::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList)

   

2.1

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

FSceneViewState::GetPrecomputedVisibilityData

完成任务:

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

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

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

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

如何完成:

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)如果几何体标记为Hideview.PrimitiveVisibilityMap标记

2.4c

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

2.4d

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

2.4e

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

2.4f

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

   

2.5

OcclusionCull

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

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

bFetchVisibilityForPrimitives处理(还有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\ 修正三角形:ClippedVertexToScreenTestFrontfaceAddTriangle

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

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

2FOcclusionQueryBatcher::BatchPrimitive (一个算法)

2.6

StereoPass

若视图开启了InstancedStereoPass

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

->ComputeAndMarkRelevanceForViewParallel;

   

2.7

GatherDynamicMeshElements

FSceneRender::GatherDynamicMeshElments

   

3、排序BasePass,实现HiZ剔除

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

.

FDeferredShadingSceneRenderer::SortBasePassStaticData

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

.

FDeferredShadingSceneRenderer::AsyncSortBasePassStaticData

   

4、提交视野的自定义数据

(略)

   

5RHI资源(构建)

(略)

   

检验:

   

   

总结:

   

2019-07-26 18:45:41 UWA4D 阅读数 141
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2377 人正在学习 去看看 杨石兴

这是侑虎科技第581篇文章,感谢作者王文涛供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/wang-wen-tao-70,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!


动机和目的

之前写过一篇文章,讲的是UE4里的HISM的工作方法。

UE4的HISM深入探究:https://zhuanlan.zhihu.com/p/58963258

为了实现植被的遮挡剔除、Instance还有距离剔除的功能。HISM Component几乎成为了UE4里最庞大的一个文件,然而实际上HISM对于很多游戏场景里的需求还是很难满足,特别是HISM需要满足静态数据的前提,十分依赖美术的工作和参数调整,对于越来越大和高自由度的游戏世界非常难受。

很多AAA级游戏引擎都有动态合并Instance的功能,也就是动态判断绘制的物体材质,Vertex Buffer(VB)等是否一样,一样的物体合并到一起绘制。但是对于4.21前的管线,动态物体和静态物体没法合并,然后运行时判断是否合并Instance的效率太低。

为了解决像动态Instancing、渲染提交效率等等的问题,EPIC终于在4.22痛下杀手改动这个陈旧庞大的渲染管线代码。知乎上有很多讲UE4渲染的文章,看过一些代码的人应该也清楚,UE4.21之前的渲染数据是基于DrawingPolicy的框架,这样的框架的好处是面向对象的设计,思路很直接,但是它的缺点却是显而易见的:

1、通过大量模板实现Shader的选择和permutation,代码难以阅读,对于现代CPU体系来说性能堪忧(非Data-oriented 设计)。

2、分成Dynamic和Static两个draw list,Instancing/Batch等优化,只能对static物体做手脚。

3、DrawPolicy直接决定渲染线程设置到GPU的数据,难以和GameThread数据解耦,冗余数据很多,对性能也是大问题。

 

老的UE4一个Draw命令的数据流程

 

 

除此之外,一些新技术的需求,这种DrawingPolicy的架构也是难以胜任的:

1、DXR技术,Shader所需Constant等数据必须是全场景的,同时,Ray Tracing技术是不能简单地针对可见性作剔除,光线会反弹,同样需要全场景信息。

2、GPU Driven Culling的技术,CPU必须在不知道可进性的情况下,把场景数据发送到GPU的Buffer,由GPU进行剔除计算。

GPU-Driven Rendering Pipelines

3、Rendering Graph或者说可编程的渲染管线技术,需要能够灵活的抽象出Render Pass等概念,而现在的DrawingPolicy代码去写一个简单的自定义pass可能就需要好几百行,太过沉重。

总结起来就是,我们需要一种更简洁、更紧凑、更利于CPU访问的数据结构去表示场景数据,包括Transform、Material(可以理解成Shader参数)、RenderState、顶点Buffer等等。而它们最好是一个数组,并不需要包括太多Game Thread的信息。而我们的Renderer代码需要在渲染每一个Frame开始前cache全场景的数据。

UE4给出的解决方案叫做MeshDrawCommand

FMeshDrawCommand类缓存了一个DrawCall所需要的最少资源集合:

  • VertexBuffer和IndexBuffer
  • ShaderBindings(Constant,SRV,UAV等)
  • PipelineStateObject(VS/PS/HS/GS/CS 和 RenderState)

FMeshDrawCommand的好处有:

1、独立的结构体,不用依赖于DrawPolicy那一大堆模板;

2、Data-oriented设计,在提交的时候就是一堆数组,效率高。

基于FMeshDrawCommand的新管线:

 

 


Caching MeshDrawCommand

构造好PrimitiveSceneProxy之后,就可以用FMeshPassProcessor的AddMeshBatch去创建一个或者多个MeshDrawCommand。

继承自己的FMeshPassProcessor就可以创建一个新的自定义MeshPass,创建一个MeshDrawCommand的流程如下:

 

 

例如,阴影绘制里的DepthPass:

 

 

注意原来UE4.21的Uniform那一套东西在MeshDrawCommand里被替换成了ShaderBinding。我们不用一个个去调用setParameters接口,而统一用SetShaderBindings的API即可。

FMeshDrawShaderBindings是一个存放了若干个内存Buffer的数组,数组的大小和该Command用的Shader数量相同。Buffer里面的内容就是构造好的RHIUniformBuffer的refrence,包括SRV、Constantant等等,使用MeshDrawShaderBindings的好处就是可以快速比较两个DrawCommand的Uniforms是否数目一样,内容一致,从而实现DrawCall Merge。

GetShaderBindings的例子:

FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
ShaderBindings.Add(RenderOffsetParameter, ShaderElementData.RenderOffset);

FMeshDrawShaderBindings里存放的是FRHIUniformBuffer结构体的指针,这是为了在update uniform时,只用更新一个UniformBuffer的内容即可,而不是更新引用到这个UniformBuffer的所有MeshDrawCommand。

 

 

有了MeshDrawCommand,我们就可以只根据物体的动态或者静态属性区分每一帧需要更新的东西。对于场景大部分的静态物体,只需要判断Uniform是否与view相关去更新view即可,动态物体则一般需要每帧更新MeshBatch和MeshDrawCommand。


Dynamic Instancing和GPUScene

1、GPUScene

上面这些Data-oriented向的优化只是提高了绘制的效率,但是要真正实现Dynamic Instancing,我们需要的一个个GPUSceneBuffer来存储PerInstance的场景数据。UE4用一个GPUTArray来表示PerInstance的数据,实际上它是个float4的数组,这样的方法在几年前Ubi的引擎里就实现过:

 

Ubi的GPU Driven Pipeline

 

 

只不过Ubi做的更彻底,包括Mesh信息VB IB等都全部扔到GPU去,然后用MultiDrawInstanceIndirect去绘制。UE4的GPUScene Buffer则主要包括世界矩阵、Lightmap参数、包围盒等PerInstance相关的Shader数据,4.23之后添加了一些CustomData可以用于你自定义的Instance算法。

 

 

UE4的GPUTArray代码实现在ByteBuffer.cpp和ByteBuffer.usf中,在DirectX中用的是StructureBuffer来实现,OpenGL下则用的是TextureBuffer。

Update/Resize一个Buffer的代码也很简单。Map一段CPU数据到GPU然后执行一个ComputeShader更新场景里已经绑定的Buffer:

SetShaderValue(RHICmdList, ShaderRHI, ComputeShader->NumScatters, NumScatters);
SetSRVParameter(RHICmdList, ShaderRHI, ComputeShader->ScatterBuffer, ScatterBuffer.SRV);
SetSRVParameter(RHICmdList, ShaderRHI, ComputeShader->UploadBuffer, UploadBuffer.SRV);
SetUAVParameter(RHICmdList, ShaderRHI, ComputeShader->DstBuffer, DstBuffer.UAV);
RHICmdList.DispatchComputeShader(FMath::DivideAndRoundUp<uint32>(NumScatters, FScatterCopyCS::ThreadGroupSize), 1, 1);
SetUAVParameter(RHICmdList, ShaderRHI, ComputeShader->DstBuffer, FUnorderedAccessViewRHIRef());

这里UE4对GPU Buffer的代码实现是相对比较简单的,实际上对于动态更新的GPU数据可以用一个GPU RingBuffer去实现,可以节省每帧重新分配内存和map操作的时间:

 

GPU RingBuffer示意

 

 

2、MeshDrawCommand Merging

有了PerInstance的数据,我们还需要知道哪些MeshDrawCommand可以merge,MeshDrawCommand可以简单的判断,去比较是否相等从而merge。

 

 

注意因为MeshDrawShaderBindings都是指针,所以比较只要简单判断command绑定的bindings是否相等即可。

Merge好的MeshDrawCommand存在场景里的一个HashSet里,如果两个command能merge,简单的把这个command的StateBucket+1即可:

    /** Instancing state buckets.  These are stored on the scene as they are precomputed at FPrimitiveSceneInfo::AddToScene time. */
    TSet<FMeshDrawCommandStateBucket, MeshDrawCommandKeyFuncs> CachedMeshDrawCommandStateBuckets;

判断command是否能merge是比较费时的操作,因此只会在物体AddToScene的时候执行。此外,现在的MeshDrawCommand是逐个比较相等去判断能否merge的,之后也可以直接用Hash值去merge来提高效率。

3、PrimitiveId Buffer

UE4是用DrawIndexedInstanced最终去绘制物体的,那么我们需要按照某个物体的ID去索引GPUSceneBuffer中的内容,UE4的做法也很直接,在支持DynamicInstance的物体的VertexFactory中添加一个VertexStream作为PrimitiveId:

if (GetType()->SupportsPrimitiveIdStream() && bCanUseGPUScene)
{
    Elements.Add(AccessStreamComponent(FVertexStreamComponent(&GPrimitiveIdDummy, 0, 0, sizeof(uint32), VET_UInt, EVertexStreamUsage::Instancing), 13));
    PrimitiveIdStreamIndex[Index] = Elements.Last().StreamIndex;
}

每个MeshDrawCommand在构造时去初始化这个VertexBuffer即可:

for (int32 DrawCommandIndex = 0; DrawCommandIndex < NumDrawCommands; DrawCommandIndex++)
{
const FVisibleMeshDrawCommand& VisibleMeshDrawCommand = VisibleMeshDrawCommands[DrawCommandIndex];
PrimitiveIds[PrimitiveIdIndex] = VisibleMeshDrawCommand.DrawPrimitiveId;

}

例如场景的Scene Buffer有一千个物体的数据,我们只要知道我们的MeshDrawCommand在这一千个index中的offset,然后对command里的primitives ID依次加一即可。

最后是SubmitMeshDrawCommands:

RHICmdList.SetStreamSource(Stream.StreamIndex, ScenePrimitiveIdsBuffer, PrimitiveIdOffset);
RHICmdList.SetStreamSource(Stream.StreamIndex, Stream.VertexBuffer, Stream.Offset);
RHICmdList.DrawIndexedPrimitive(MeshDrawCommand.IndexBuffer,MeshDrawCommand.VertexParams.BaseVertexIndex,0,MeshDrawCommand.VertexParams.NumVertices,MeshDrawCommand.FirstIndex,MeshDrawCommand.NumPrimitives,MeshDrawCommand.NumInstances * InstanceFactor

4、新管线的优势和发展方向

4.22的新管线在场景复杂度较高的情况下有非常好的减少CPU overhead的效果,收益主要来自于Data-Oriented设计带来的cache miss的减少和动态Instancing实现。而且这个收益不仅限于PC端,据Epic的工程师说,在堡垒之夜上使用Depth Prepass和新的Dynamic Instancing管线,让渲染效率提升了30%左右。

将更多的工作交给GPU来完成也是游戏引擎发展的一个趋势,下一篇计划用4.22这些新轮子去实现一些新的东西。

参考文档:

[1]GPU Driven pipeline
[2]GPU Dynamic resource management


文末,再次感谢王文涛的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

也欢迎大家来积极参与U Sparkle开发者计划,简称“US”,代表你和我,代表UWA和开发者在一起!

2018-08-07 21:45:49 smilejiasmile 阅读数 461
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2377 人正在学习 去看看 杨石兴

转载自: https://blog.csdn.net/nikoong/article/details/79776873

渲染管线入门

 

  渲染管线中一些技术名词大部分是英文直译,光看中文很容易被误导。所以我们必须在了解一个技术的具体作用之后,才能了解它到底是什么。这篇博客我主要翻译自Minh Tri Do Dinh的GPUs - Graphics Processing Units一文。也就是一些论坛里推荐给新手阅读的图形处理器架构(GPU_Architecture)与图形管线(Graphics_Pipeline)。在此之上我还添加一些注解,方便大家阅读。

基础名词

  • 3D物体:任何物体,其几何形状都是由三角形组成的。三角形,或者任何图形,都由顶点组成。 
    这里写图片描述 

  • 顶点:具有空间坐标和其他信息(如颜色和纹理坐标)的点

  • 纹理:映射到3D物体表面的图像,这会造成该物体由某种材料组成的幻觉。物体的顶点存储着纹理坐标(2维向量),用于指定纹理如何映射到任何给定表面。 
    这里写图片描述 

  • 齐次坐标:3D空间中的点通常需要3个坐标来指定位置。如果继续用3个坐标表示,有时会遇到问题。例如:顶点平移时的操作是加一个向量,顶点旋转时的操作是乘以3x3矩阵)。所以我们通过扩展三维向量来绕开这个问题。我们添加另一个坐标(w坐标,从而得到所谓的齐次坐标[注1]。这样一来,顶点的所有变换都可以通过将其四维坐标与特定的4x4矩阵相乘来进行,使得计算更容易。齐次坐标的另一个用途是明确区分向量和点。

渲染管线

  渲染管线(Graphics pipeline/rendering pipeline),其实贴切的翻译应该是渲染流水线。渲染流水线就是将数据从3D场景转换成2D图像,最终在屏幕上显示出来的总过程。它分为几个阶段:应用阶段、几何阶段和光栅阶段。 
  应用阶段:主要是CPU与内存打交道,例如碰撞检测,计算好的数据(顶点坐标、法向量、纹理坐标、纹理)就会通过数据总线传给图形硬件 。 
这里写图片描述

几何阶段(Geometry Stage)

  这个阶段也被称为“变换和光照”阶段。为了从3D场景转换到2D,场景中的所有物体都需要转换到几个空间。每个空间都有自己的坐标系。这些转换是通过一个空间的顶点转换到另一个空间的顶点来实现的。 
  光照(lighting),是这个阶段的另一个主要部分。是使用物体表面的法向量来计算的。通过摄像机的位置和光源的位置,可以计算出给定顶点的光照属性。 
这里写图片描述 

  对于坐标系变换,我们从物体坐标系开始,每个物体都有自己的坐标系,这有利于几何变换,如平移,旋转和缩放。 
  之后,我们进入到世界坐标系,场景中的所有物体都具有统一的坐标系。 
  下一步是转换到视图空间,即摄像机坐标系。想象一下:先在世界空间中放一个虚拟摄像机,然后进行坐标变换,使得摄像机位于视图空间的原点,镜头对准z轴的方向。现在我们定义一个所谓的视体(view frustrum),它用来决定了我们能够通过虚拟的3D摄像机所能看到的场景,我们只需要把这些内容渲染出来就行。 
这里写图片描述 

  之后,所有的顶点将被转换剪切空间,并通过图元装配,组装成图元(primitive,三角形或线)。当物体在视体的外,说明我们不需要渲染这个物体,所以就简单丢弃它。当一个物体有部分在视体内,我们需要进行剪切,同时生成新的顶点。新顶点具有适当的的纹理坐标和颜色坐标。 
  接着我们执行透视分割操作,其实就是实现了一个投影变换,把”近大远小“的效果实现出来。把一个视体转换为归一化的立方体。这个立方体的坐标范围:x和y在-1和1之间,z在0和1之间。同时视体内的物体也一同进行缩放。这样一来得到的立方体,便于裁剪操作,也便于将来投影到平面(立方体只需要“扁平化”)。 
这里写图片描述 

  最后,我们转换到屏幕空间。执行的操作:根据窗口大小将x和y坐标缩放到合适的坐标,这样就得到2D图片了。(请注意,顶点的z坐标会保留,用于以后的深度操作) 
  请注意,除了剪裁之外纹理坐标也需要进行转换。还要注意除去不可见的表面[注2]。如出去立方体的背面,就是所谓的背面剔除。

光栅阶段(Rasterization Stage)

  光栅这个名词不太好理解,新手可以想象成”透过一把梳子看世界“,类似于将连续的数据转化成离散的数据。这里光栅阶段是指矢量图形转化成像素点的过程。 
  GPU需要遍历2D图像并进行转换将数据转化为大量“像素候选”,即所谓的片段(Fragment,片段可以理解为像素的原型,但绝对不是指一大片像素)。片段是包含位置,颜色,深度,纹理坐标等属性的数据结构。片段可能会最终成为图像上的像素。片段是通过检查原始图元和和屏幕像素是否相交来生成的。如果一个片段与一个基元相交,但不与它的任何顶点相交,那么它的属性必须通过顶点之间的属性插值来计算得出。 
这里写图片描述 

  最后还有一些步骤来得到最终的像素。最终像素颜色的计算:结合纹理和颜色、光照等其他属性,或片段与另一个半透明片段的结合(所谓的α混合),亦或是可选的雾化效果(另一个图形效果)。[注3]


注1:齐次坐标和四元数长得很像,但却是不同的概念,适用领域也不一样。

注2:可见性检查的一些具体操作: 
* 剪刀测试(检查矩形面罩的可见度) 
* 模板测试(类似于剪刀测试,仅针对缓冲区中的任意像素模板) 
* 深度测试(比较片段的z坐标,丢弃更远的片段) 
* Alpha测试(检查半透明碎片的可见度)

注3:在我们获得最终结果之前,可以应用像抗锯齿这样的附加程序:将一些像素写入内存供以后显示。

reference

2019-04-05 03:25:54 cpongo11 阅读数 5
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2377 人正在学习 去看看 杨石兴

我的专栏目录:

小IVan:专题概述及目录zhuanlan.zhihu.com图标

简介:

Unreal engine的渲染模块是越来越复杂,最开始只有延迟管线,随后加入了mobile,现在又加了很多实时光线追踪的东西,各种DX12,VK新的RHI也纷纷加入。

虚幻4的4.22版本对渲染模块进行了较大重构,废弃了之前的DrawList&&Draw Policy,取而代之的是MeshDrawPipline。与之前的DrawPolicy相比,MeshDraw更为简洁,灵活。新旧Pipline对比图如下:

4.22

v2-4741d01b86c2a0838f69f62994217103_b.jpg

<=4.22

v2-6842757d6cdbbe664bf881c9cc7e3eb0_b.jpg

我之前有写老的drawingpolicy的文章:小IVan:虚幻4渲染编程(Shader篇)【第十卷:绘制策略】。如有错误还请巨佬们斧正。

因为涉及到的细枝末节的东西很多,所以这篇文章我会在一个月内持续更新修改。


首先先过一下渲染过程中主要的类和步骤。

【1】FPrimitiveSceneProxy

这个类的功能没有什么改变,依然在通过GetDynamicMeshElements等函数从每个primitivecomponent中收集渲染资源

v2-080a2aeb3ed579f31529be4e4e34c893_b.jpg

【2】InitViews

FPrimitiveComponent的GetDynamicElements这个收集操作通过渲染器的InitViews在可见性剔除之后发起

v2-d6dabdcae9c1cf6622c7c99ba7db08ce_b.jpg
v2-189412e0445d583e8feead60ea730d3f_b.png

FMeshElementCollector会把场景里的渲染数据收集起来

v2-cd4cee4741e940730114974215da5e0a_b.jpg

最后会调用SetupMeshPass来创建MeshPassProcessor

v2-dc6ab7b11df504f6b43baba7f8826b62_b.png

【3】FMeshPassProcessor

v2-3b49a186b5f6ec56e0c6b1bea7547ded_b.jpg

FMeshPassProcessor有两个作用,第一个是选择绘制时所使用的shader,第二个是搜集这个pass绑定的顶点工厂,材质等。可以从一个FMeshBatch创建多个FMeshDrawCommands

v2-abdcff6aa0929355b1cba70dfa5fcec1_b.jpg

搜索引擎就会发现,每个pass都会有一个对应的FMeshPassProcessor。FMeshPassProcessor是所有pass的FXXXMeshPassProcessor基类。

在一个pass在初始化的时候,会往MeshPassProcessor里填充BatchMesh渲染单元。

以depthpass为例,DepthPass的PassProcessor创建代码如下:

v2-a3d4bd08a60bdbc4d62faf3731134541_b.jpg

在渲染管线重会调用Processor的AddBatchMesh把渲染资源加进去

v2-8ffbe4a6821ddd27a88f6f0065d392ed_b.png
v2-19232eef47238492f433fe500f17c2f4_b.png

在AddMeshBatch函数中会进行Passfilter和SelectShader,分别对应下图的1,2部分。

v2-4bbf753c277566e76c536c728a84b441_b.jpg

shader等设置好以后会通过BuildMeshDrawCommands创建command然后通过FinalizeCommand完成command的添加

v2-adc9709fa091950ba56f148fe3d5b432_b.jpg
v2-3bc2e944f7313912849a60db6803ef64_b.jpg

【4】FMeshDrawCommand

v2-0c7e20b858b3de3301a485f256a49f32_b.jpg

代码注释已经解释得很清楚了。一个MeshDrawCommand描述了一次绘制所需要的所有资源。FMeshDrawCommand类中主要负责管理了三类资源:(1)ShaderBindings,记录了这次Command各个阶段绑定的参数集。(2)VertexStreams 记录了VertexBuffer的信息。IndexBuffer记录了IndexBuffer的相关纤细。CachedPipeline用于索引GraphicPipelineState。

【5】ShaderBindings

v2-b24e4393235cf5c91297d3a051b729e4_b.jpg

可以从新老方式中看到,新的方式是通过收集参数而不是直接设置RHICmdList来设置Paramerters了。ShaderBinding是作为MeshDrawCommand的组成部分之一,在这里面需要绑定管线所需要的所有参数集。

v2-4f60c34422624d137f243d4052b720ed_b.jpg

【GPUScene】

v2-1b3a12c20d2ad2417a785cc4f998cd23_b.jpg

GPUScene有一个PrimitiveBuffer,它会跟踪场景Primitive的添加,移除操作,会在GPU端有一个镜像场景。虚幻使用一个Computeshader来更新primitivebuffer给下一帧使用。Primitivedatabuffer要求一个primitive的所有shader只能有一个primitiveID

v2-cb0e8f3d3e65632c29c0d458180fbe3a_b.jpg

这个primitiveID只有LocalVertexFactory才有

v2-e71eeb565b7bcc312fb12336361528c3_b.jpg
v2-33cc1b62c9f081fa1773c1bcf77000e0_b.png

这里用CustomDepthPass作为一个例子看一下静态物体的绘制流程。

【CustomDepthPass】

首先准备好个空场景然后拖一个盒子进去

v2-681e8a20f22bd4567b5bec51b6dcb6c5_b.jpg

这时会调用AddPrimitive的逻辑

v2-04307e21e9f52dbfb84ac922fea5acf9_b.jpg

会创建场景代理,各种渲染数据,矩阵包围盒等。并且会更新GPUScene

v2-0d406cd0e19f6113cd53ca87135e3986_b.jpg

并且会CacheMeshDrawCommands

v2-56818a180c431e306b1d6f2714bdf78c_b.png

然后创建PassProcessor然后AddMeshBatch,这个Processor会创建MeshDrawCommands

v2-e1a576499b75b51dfd34afa1bf7cb4ad_b.jpg

完成commands的创建后销毁这个Processor。这便完成了对Commands的添加。

v2-511e1eb2ab9196e90c2c235a4786e526_b.jpg

要绘制时直接调用这些commands即可。这些commands会去调用RHI,RHI层会去调用各大图形接口。

v2-07d4d4c8f33911066bbf56016ca21b5a_b.png

最终就完成了绘制。

这里会发现,我们添加的时候是往TSet<FMeshDrawCommandStateBucket, MeshDrawCommandKeyFuncs> CachedMeshDrawCommandStateBuckets;里添加,真正使用的时候不会直接使用这个数据,而是经过剔除和各种处理后把一帧画面里所需要的数据放到ParallelMeshDrawCommandPasses里。


DrawCall的发起者ParallelMeshDrawCommandPasses

【ParallelMeshDrawCommandPasses】

v2-fe2b7bd95c4cd3ba2dad4660c29b653b_b.jpg

DispatchPassSetup是一个关键函数,它负责为每个pass管理MeshDrawCommands,为绘制每个pass做准备。

可以看到我们有很多种pass类型。一个pass里有很多MeshCommand

v2-857a64092c73e4230104611eff74af34_b.jpg

在InitView的时候,我们把我们的command按照不同的pass类型分别塞倒ParallelMeshDrawCommandPasses里,这样就可以方便地统一调用command来完成各个pass的绘制了。

v2-3787a8f6fdf436b5da6cf2b486cdf66f_b.jpg

PrePass:

v2-43778ba03642bf6dd9e8824439f69660_b.png

DepthPass:

v2-00896070b0764ab7dc4f73915597432c_b.png

CustomDepthPass:

v2-d11c0c37b01246b35e6987cfb48c9e84_b.png

BasePass:

v2-d1d67f7a603d45151991197ad19c987b_b.png

还有很多我这里就不例举了。

我在视口中加入一个球一个盒子,他们的vertex buffer不一样材质一样,所以无法被加入到一个batchmesh中,所以可以理论上customdepthpass会有两次绘制

v2-cce48ca03b263974a80630a131d2e288_b.jpg
v2-4d98f33504f1e447ccca20aa432995f2_b.jpg

我在视口中再拖了一个盒子,现在视口中有两个盒子一个球体。BasePass只画了两次,因为两个盒子被DrawInstance了,然而CustomDepthPass却画了三次:

v2-43af65d28c7ccd21bcef0244947a57f3_b.jpg
v2-047dea6113aca7e691d55118406d1ef9_b.jpg

对于美术制作层面来说,如果是做端游,我们可以把模型切成碎块然后搭建场景不需要batch,因为虚幻的basepass已经dynamicbatch了的(前提是这些碎片的vertexbuffer和材质是一样的)比如搭建一个木头结构的房子其实也就用几个木头模型罢了。这样切模型和非在DCC中batch模型的区别在于,其它有些pass不会以dynamicbatch规则去合并这些dc。


综上虚幻的渲染模块经过这次重构后,灵活性更强,逻辑更加紧凑。

v2-520e526b6d25884e480f0d1e6695bb66_b.jpg

Enjoy it.

没有更多推荐了,返回首页