量子计算9 发表于 2022-2-27 14:00

UE4 Rendering系列教程笔记

原文链接:https://medium.com/@lordned/unreal-engine-4-rendering-overview-part-1-c47f2da65346
知乎有许多大佬已经搬运了,这里只是简单的笔记。初学ue4,可能存在诸多错误,望看到的读者大大们指正qaq
Part1 Introduction

这个部分介绍了这个教程的一些基本内容,并且推荐了一些常用的插件以及配置。
Engine Setup

下载源码编译,设置模式为“Debug Editor”而非“develop editor”。(两者差别日后查阅更多资料之后补充)
HLSL Tools for Visual Studio

"工具 -> 扩展和更新… -> 联机" 搜索“HLSL Tools for Visual Studio",下载并根据提示安装。
在"工具 -> 选项 -> 文本编辑器 -> 文件扩展名"中做如下编辑:


RenderDoc

截帧工具。在UE4里面使用的相关教程:https://blog.csdn.net/u013412391/article/details/108504585
github:https://github.com/baldurk/renderdoc
Modify ConsoleVariables.ini

"\Engine\Config"路径下。(以下变量貌似默认情况下都是设置好的)
主要关注参数:
r.ShaderDevelopmentMode = 1 :当材质缺省导致编译失败时可以重新编译。
r.Shaders.Optimize = 0 & r.Shaders.KeepDebugInfo=1 :让Unreal使用低优化编译器,保留调试信息。这样可以降低编译时间,并且保存debug信息,这样可以更好地在RenderDoc里面debug。
r.DumpShaderDebugInfo = 1 & r.DumpShaderDebugShortNames = 1 :在某些情况下很有用。DumpShaderDebugInfo会将生成HLSL写入硬盘(占用两个G的内存),但可以帮助RenderDoc调试;第二个变量可以缩短变量名称,以符合OS的最大路径长度限制。
Modify BaseEngine.ini


bAllowCompilingThroughWorker = True & bAllowAsynchronousShaderCompiling = True, 这两个变量可以让UE4进行多线程编译所有需要的shader。如果你需要只调试某个C++ shader管线,把这两个变量设置为False会非常有用,但这会使shader的编译时间变长。

Part2 Shader and Vertex Data

Vertex Factory

Unreal使用Vertex Factory来控制数据从Vertex Shader上传到GPU中。
Shaders


[*]所有Shader的基类:FShader --- FGlobalShader(只有一个实例存在) & FMaterialShader(与材质绑定)。

[*]FShader与FShaderResource成对出现,FShaderResource追踪一个Shader在GPU上的相关资源。如果FShader编译的结果与已经存在的结果一致,那么它们就可以共享一个FShaderResource。
[*]FGlobalShader////
[*]FMaterialShader:使用SetParamaters函数来用C++代码修改HLSL中的参数。参数的绑定是通过FShaderParameter / FShaderResourceParameter类在shader的构造函数中完成。
[*]FMeshMaterialShader:可以将Mesh的顶点数据引入到Shader端。(from UE4渲染代码逻辑总结(上) | 风蚀之月 (ch-wind.com))

Caching and Compilation Environments


[*]ShouldCache函数:如果Shader、Material、Vertex Factory全都同意某个特定组合被缓存,那么FShader才会创建这个特定组合;相反,只要有其中之一不同意,那么这个组合就会被跳过。

[*]Unreal会自动为一个Shader编译大量可能的组合,这个有它的好处在,但同时会生成许多无用的Shader。
[*]ShaderCache函数是一个可以在FShader、FMaterial、FVertexFactory中实现的静态函数。(按官方文档的示例说明,就是返回值是true时编译当前shader,否则就不编译。



FVertexFactory


[*]包含顶点数据,并且被链接到顶点着色器中(Vertex Shader)。
[*]FLocalVertexFactory:提供从局部空间到世界空间的转换;Static Mesh、Cables、Procedual Mesh使用。
[*]FGPUSkinVertexFactory:用于Skeletal Mesh(需要更多的顶点数据)。
[*]FLandscapeVertexFactory:Landscape是基于VTF(Vertex Texture Fetch),使用高度图来修改顶点位置实现的,所以需要额外的处理。
[*]FParticleVertexFactoryBase:粒子系统使用。
FPrimitiveSceneProxy


[*]渲染线程版本的UPrimitiveComponent(虽然我也不知道这个东西是个啥),负责维护每个Component在渲染线程上需要的数据。





[*]Unreal存在游戏线程和渲染线程,两者不能直接访问对方线程中的数据(除非使用一些特定的异步宏)。为了解决这个问题,Unreal在游戏线程中使用UPrimitiveComponent,通过重写CreateSceneProxy()函数来创建自己的FPrimitiveProxy类。

[*]UCableComponent / FCableSceneProxy:在CreateSceneProxy()函数中创建FCableSceneProxy并进行初始化,然后在SendRenderDynamicData_Concurrent()中将数据发送到渲染线程并借由SetDynamicData_RenderThread()进行数据构造。
[*]UImagePlateFrustrumComponent / FImagePlateFrustrumSceneProxy:由于是用于渲染2D材质,不需要VertexFactory。它仅使用渲染线程的回调函数来计算一些数据画线。FImagePlateFrustumSceneProxy只是在GetDynamicMeshElements时返回演算的结果。

Binding C++ to HLSL


[*]C++宏

[*]IMPLEMENT_MATERIAL_SHADER_TYPE(TemplatePrefix, ShaderClass, SourceFilename, FunctionName, Frequency)

[*]例如:IMPLEMENT_MATERIAL_SHADER_TYPE(,FDepthOnlyPS,TEXT("/Engine/Private/DepthOnlyPixelShader.usf"),TEXT("Main"),SF_Pixel)

[*]这个宏将FDepthOnlyPS绑定到DepthOnlyPixelShader.usf中,Main是shader端的入口函数。
[*]Unreal使用“Frequency”来区分shader的类型:



enum EShaderFrequency {   
SF_Vertex            = 0,      
SF_Hull                     = 1,      
SF_Domain               = 2,      
SF_Pixel               = 3,      
SF_Geometry            = 4,      
SF_Compute             = 5,      
SF_NumFrequencies = 6,      
SF_NumBits               = 3,
};




[*]第一个参数“TemplatePrefix”用于模板类与shader的绑定。有些情况下,宏指定一个模板类,然后使用另一个宏实例化来创建特定的实现。(例如为每个可能的lighting type创建变体)



IMPLEMENT_VERTEX_FACTORY_TYPE(FactoryClass, ShaderFilename, bUsedWithmaterials, bSupportsStaticLighting, bSupportsDynamicLighting, bPrecisePrevWorldPos, bSupportsPositionOnly)



[*]将C++形式的VertexFactory绑定到一个具体的HLSL文件(Shader)中。例如:


IMPLEMENT_VERTEX_FACTORY_TYPE(FLocalVertexFactory,”/Engine/Private/LocalVertexFactory.ush”,true,true,true,true,true);





[*]这个宏将FLocalVertexFactory与LocalVertexFactory.ush进行关联。
[*]因为MeshShader有许多实例,每一个实例会根据自己的光照类型、数据类型进行不同的绑定。shader与FVertexFactory不是一一对应的关系,使用相同渲染路径的类会在这里共用shader。





Part3 Drawing Policies

Drawing Policies


[*]并不是继承于一个基类。决定哪个shader变体用于绘制物体。
FDepthDrawingPolicy


[*]获取Material的shader
VertexShader = InMaterialResource.GetShader<TDepthOnlyVS<false>>(VertexFactory->GetType());
PixelShader =InMaterialResource.GetShader<FDepthOnlyPS>(InVertexFactory->GetType());


[*]如果是用于tessellation(曲面细分)则还需要Hull Shader和Domain Shader(?)

[*]使用SetSharedState()和SetMeshRenderState()来传递参数
FBasePassDrawingPolicy


[*]继承模板类:template<typenameLightMapPolicyType> classTBasePassDrawingPolicy : public FBasePassDrawingPolicy

[*]通过识别不同的光照类型来获取不同的shader





[*]template <ELightMapPolicyType Policy> void GetUniformBasePassShaders (...)

[*]使用这个函数获得对应光照的shader。


Drawing Policy Factory


[*]检查材质或者VertexFactory的状态,然后创建正确的Drawing Policy。
FdepthDrawingPolicyFactory

void FDepthDrawingPolicyFactory::AddStaticMesh(FScene* Scene, FStaticMesh*StaticMesh)


[*]获取static mesh,根据属性和设置确定对应的Drawing Policy(在这里有FDepthDrawingPolicy和FPositionOnlyDepthDrawingPolicy),然后将其加入到FScene的列表中。




[*]bool FDepthDrawingPolicyFactory::DrawMesh(...) / bool FDepthDrawingPolicyFactory::DrawDynamicMesh(...) / boolFDepthDrawingPolicyFactory::DrawStaticMesh(...)

[*]被调用用于进行渲染。

[*]也可以绘制mesh batch。但mesh batch不会被加入到列表中,而是通过RHI layer建立GPU状态,并带哦用其他的drawing policy来绘制mesh。

Part4 The Deferred Shading Pipeline

相比前向渲染,可以支持多光源,复杂度为n+m。n场景物体数量,m场景光源数量。
Vertex Factories

Changing Input Data


[*]CPU:顶点数据保存在FVertexFactory中;GPU:FVertexFactoryInput --- 来自C++端的数据输入,在不同的Shader头文件中会有不同的定义,使用方式也不同。
[*]存在通用的函数被定义来处理输入数据:GetVertexFactoryIntermediates, VertexFactoryGetWorldPosition, GetMaterialVertexParameters。
Changing Output Data


[*]FBasePassVSOutput:可以根据是否使用Tessellation将其定义为FBasePassVSToDS或FBasePassVSToPS。(曲面细分会需要在VS和PS之间引入Hull Shader和Domain Shader两个过程)=> 使用#define,




[*]这里我们可以看到FVertexFactoryInterpolants*和FBasePassInterpolants*,这些定义的数据保存了顶点插值相关的内容。

Base Pass Pixel Shader

Material Graph to HLSL


[*]MaterialTemplates.ush

[*]%s => string format。由引擎将材质编辑器中的节点填充到这里。结构体、函数中都会存在。结构体中存放材质编辑器中需要的数据,函数用于计算(调用在材质编辑器中使用到的节点)。





The "Primitive" Variable



在C++端用宏声明。在每个图元(primitive)在GPU上被绘制前,这些宏会定义一个由渲染器设置的结构体。
Creating the GBuffer


[*]GBuffer(几何缓冲区):存储关于几何的不同信息,如world normal、base color、roughness…在计算照时,Unreal会采样这些缓冲来计算最终的shading。在这之前,Unreal会通过几个步骤创建并填充GBuffer。
[*]GBuffer的内容时根据项目设置变化的。通常的情况是5纹理Gbuffer(5 texture GBuffer)(后缀A-E):

[*]GBufferA.rgb = World Normal, GbufferA.a = PerObjectGBufferData;
[*]GBufferB.rgba = Metallic, Specular, Roughness, ShadingModelID;
[*]GBufferC.rgb = BaseColor, GBufferC.a = GBufferAO;
[*]GBufferD = custom data;
[*]GBufferE = precomputed shadow factors。

[*]在入口函数FPixelShaderInOut_MainPS中,先调用MaterialTemplate.ush中声明的函数来完成获取BaseColor、Metallic、Specular、AO、Roughness等值;如果使用了次表面散射,则会继续获取数据计算此表面散射的颜色,否则默认值会被设置为0;接着如果项目中启用了DBuffer,Unreal会允许DBuffer Decals(贴花)来修改GBuffer中的结果;这之后,Unreal会计算Opacity,并进行体积光照贴图的计算;最后创建FGBufferData结构,并将数据打包其中,每个FGBufferData实例代表一个像素。










Setting the GBuffer Shading Model


[*]SetGBufferShadingModel(...)

[*]函数声明在ShadingModelsMaterial.ush中。
[*]获取GBuffer的数据,并将其按照shading model的需要重新分配。

[*]大部分模型只是简单地分配模型数据,而不进行修改;
[*]某些模型(次表面散射等)将自定义数据通道附加到GBuffer中。

[*]将ShadingModelID写入GBuffer --- 每像素存储的整数值,允许延迟传递查找每个像素稍后应当使用的shading model。
[*]如果需要使用GBuffer的CustomData通道,则需要修改BasePassCommon.ush中的WRITES_CUSTOMDATA_TO_GBUFFER,如果未在此处添加shading model,自定义数据会被丢弃。

#define WRITES_CUSTOMDATA_TO_GBUFFER (USES_GBUFFER &&(MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN ||MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || MATERIAL_SHADINGMODEL_CLEAR_COAT ||MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || MATERIAL_SHADINGMODEL_HAIR ||MATERIAL_SHADINGMODEL_CLOTH || MATERIAL_SHADINGMODEL_EYE))Using the Data


[*]EncodeGBuffer()
Deferred Light Pixel Shader


[*]DeferredLightPixelShaders.usf 计算光对像素影响的地方。
[*]Non shadow-casting lights --- indirect lighting --- shadow casting lights
[*]Unreal使用类似的Shader来处理Shadow casting lights 和 non shadow-casting lights,唯一不同的是预处理阶段,Unreal会计算一张ScreenShadowMaskTexture,它是场景中阴影像素的屏幕空间表示。

[*]为了计算这个,Unreal会渲染几何图形。这并不是重新渲染物体,而是对GBuffer进行采样,结合给定像素的深度,来查看它是否会出现投射光影。


Part6 Adding a New Shading Model

1、在EngineType.h中找到EMaterialShadingModel,在其中添加MSM_xxxx(在MSM_MAX前)--- 这个会反映在材质编辑器的shading model下拉菜单里。


2、给材质编辑器添加引脚:在Material.cpp中找到UMaterial :: IsPropertyActive(...),给switch中添加判断使用自定义的shading model的语句。(这个步骤仅仅只是修改了UI,需要确保着色器使用提供给这些引脚的数据)

[*]修改引脚名字则可以在MaterialGraph.cpp中寻找对应的函数(命名为GetxxxPinName(...)),在其中加入判断shadingmodel以及对应名称的语句。对于Specular、Roughness等没有定义函数的,可以自己添加函数,并在文件开头处调用。






3、修改HLSL预处理器定义:MaterialShared.cpp:FMaterial::SetupMaterialEnvironment(...),修改OutEnvironment变量。在GetShadingModel的switch语句中添加MSM_xxxx的case。(4.25版本在HLSLMaterialTranslator.cpp的void FHLSLMaterialTranslator::GetMaterialEnvironment(...)中)


------------------以上c++部分修改完成,可以重新编译,但不要打开编辑器-----------------
4、更新GBuffer的ShadingModelID

[*]在shader中定义关键字:DeferredShadingCommon.ush中添加关键字。并在GetShadingModelColor(...)中定义着色器颜色。




-------------------------------以下基于Deferred Shading-----------------------------------

[*]开启CustomData写入的关键字(如果你之前有添加customdata的引脚):在BasePassCommon.ush中为关键字WRITES_CUSTOMDATA_TO_GBUFFER增加一个条件。然后在ShadingModelMaterial.ush的SetGBufferForShadingModel(...)中添加预处理判断,在使用自定义的shadingmodel时,将CustomData 0 写入到GBuffer.CustomData.x中。






---------------------------------以下基于Mobile Shading-----------------------------------
5、打开MobileBasePassPixelShader.usf,大部分的操作都在这个地方完成。

[*]做卡渲需要做个ramp,如果在材质编辑器里只获取光照计算NdotL来采样Ramp贴图的话,无法将自阴影算进去。但mobile管线下,材质编辑器无法获取阴影的信息,因此我们只能想办法将自定义贴图采样这个步骤移到内部完成。

[*]方法1:在MaterialTemplate.ush中添加相关的模板函数,并在C++中完成自动生成HLSL的工作,相关链接:Unreal向shader传入材质贴图 - 知乎 (zhihu.com)。(这位大佬还有一篇给自定义shadingmodel增加自定义引脚的文章,感觉未来可能也会用到,上面这篇文章里有附链接)
[*]方法2:编译一次Shader,查看编译的HLSL文件,HLSL中会有采样贴图的步骤,寻找与需要的贴图相关的采样代码,复制到basepasspixelshader中使用。

例如:这里我想使用连在EmissiveColor上的贴图,那么我就在HLSL中找到给EmissiveColor赋值的代码行(因为通常我们在basepixelshader中使用的都是GetMaterialxxxx,而这个函数的实现就在MaterialTemplate.ush中,非常简单一般就是把http://PixelMaterialInputs.xxx给返回了,因此这里我们只要寻找http://PixelMaterialInputs.xxx就行);


然后我们发现EmissiveColor = Local1,我们往前寻找Local1的定义;


这里我们看到Local1来自Local0 + 另外一个东西(暂时不知道是干什么的,输出看了一下是个黑色的东西),而Local0就是在采样贴图,因此我们把采样贴图的这个函数Texure2DSampleBias连同底层给这张贴图赋予的ID(即Material.Texture2D_0)一起复制到basspasspixelshader中使用就行。


(我这里是为了用shadow*NoL来采样Ramp贴图。)
6、由于需要在材质编辑器中获得光照信息,但UE4 mobile端材质编辑器屏蔽使用灯光信息,无法获取光源的方向,颜色等数据。Mobile Lighting的信息是运行时生成的UniformBuffer,在MaterialTemplate.ush里包含"/Engine/Generated/UniformBuffers/MobileDirectionalLight.ush"可以解决Custom Expression对灯光声明引用的问题。(在custom节点里获取)


7、切换到移动端rendering level,像是上面获取光照方向的方法只能在Mobile shading中使用。在settings->Preview rendering level中切换。(注意创建工程时选移动平台)

xiaozongpeng 发表于 2022-2-27 14:02

欢呼

KaaPexei 发表于 2022-2-27 14:04

女王。

量子计算9 发表于 2022-2-27 14:06

xiaozongpeng 发表于 2022-2-27 14:16

大佬,我在RenderDoc这步给难住了。通过UE官网文档链家下载了Render Doc完事在UE中打开插件,一捕获Captures collected窗口什么都没有。。。

kirin77 发表于 2022-2-27 14:24

私信您了能解答一下吗[大哭]

zifa2003293 发表于 2022-2-27 14:33

大佬, 想问一下, 在hlsl转成metal shader时, 看上去有某种优化, 会导致同一段shader在不同的地方转换后的shader代码不一样, 是否有方法, 可以确保这种翻译保持一致?
页: [1]
查看完整版本: UE4 Rendering系列教程笔记