Unreal Engine 4 渲染 (2) 着色器与顶点数据 Shaders and Vertex Data

https://medium.com/@lordned/unreal-engine-4-rendering-part-2-shaders-and-vertex-data-80317e1ae5f3

翻译:yarswang 转载请保留

 

着色器与顶点工厂

 

    本章我们来关注着色器与顶点工厂。Unreal使用一些魔法来绑定C++的着色器表现与等价HLSL类,使用顶点工厂(Vertex Factory)来将顶点着色器的控制数据上传到GPU。从本章起,我们将使用Unreal的类名来讨论问题,让它们更容易被你自己查找。

    我们将专注于核心的着色器/顶点工厂涉及类。有很多处理内务的结构/方法如胶水一般地黏合了整个系统。我们不太可能去修改这些胶水,所以我们不会去谈他们来让事情变得更复杂。

 

Unreal Engine 4 渲染 (2) 着色器与顶点数据 Shaders and Vertex Data_第1张图片

FLocalVertexFactory的顶点输出

 

着色器

    Unreal中所有着色器都派生自一个基类 FShader.  Unreal有两个主要的着色器分类,FGlobalShader用于在只应存在一个实例的情况,以及FMaterialShader使用在关联材质的着色器上。FShader和FShaderResouce配对,后者跟踪与特定着色器相关联的GPU上的资源。如果FShader的已编译输出与已存在的输出相匹配,则可以在多个FShaders之间共享FShaderResource。

FGlobalShader

    这是个很简单,并且有限(但是有效!)的用法。 当着色器类从FGlobalShader派生时,它将它们标记为Global重新编译组的一部分(这似乎意味着它们在引擎打开时不会重新编译!)。 只有一个全局着色器实例存在,这意味着你不能拥有每个实例的参数。 但是,您可以拥有全局参数。 示例:FLensDistortionUVGenerationShader,FBaseGPUSkinCacheCS(用于计算网格蒙皮的计算着色器)和FSimpleElementVS/ FSimpleElementPS。

 Unreal Engine 4 渲染 (2) 着色器与顶点数据 Shaders and Vertex Data_第2张图片 

FMaterialShader和FMeshMaterialShader

    FMaterialShader和FMeshMaterialShader更加复杂。这两个类都允许多个实例,每个实例都与其自己的GPU资源副本相关联。 FMaterialShader添加了一个SetParameters函数,它允许着色器的C ++代码更改绑定HLSL参数的值。参数绑定是通过FShaderParameter / FShaderResourceParameter类完成的,并且可以在着色器的构造函数中完成,比如说FSimpleElementPS。在使用该着色器渲染某些内容之前调用SetParameters函数,并传递相当一部分信息(包括材质),这会为您提供大量信息来作为您计算要更改的参数的一部分。

    现在我们知道如何设置着色器范围的参数,我们可以看看FMeshMaterialShader。这增加了我们在绘制每个网格之前在着色器中设置参数的功能。大量着色器从该类派生,因为它是需要材质和顶点工厂参数的所有着色器的基类(根据文件中留下的注释)。这只需添加一个SetMesh函数,在绘制每个网格之前调用该函数,从而允许您修改GPU上的参数以适应特定的网格。示例:TDepthOnlyVS,TBasePassVS,TBasePassPS。

 

将C++绑定到HLSL

        因此,现在我们知道FShaders是CPU上着色器的C++表示,我们需要知道如何将给定的FShader与其相应的HLSL代码相关联。这是我们第一个C++宏的用途:IMPLEMENT_MATERIAL_SHADER_TYPE(TemplatePrefix,ShaderClass,SourceFilename,FunctionName,Frequency)。在解释每个参数之前,让我们看一下DepthRendering.cpp中的一个例子:

 

IMPLEMENT_MATERIAL_SHADER_TYPE(,FDepthOnlyPS,TEXT("/Engine/Private/DepthOnlyPixelShader.usf"),TEXT("Main"),SF_Pixel);

 

    该宏将C++类FDepthOnlyPS绑定到位于/Engine/Private/DepthOnlyPixelShader.usf中的HLSL代码。具体而言,它与入口点“Main”以及SF_Pixel的频率相关联。我们现在拥有一个存在于(DepthOnlyPixelShader.usf)中HLSL文件的C++代码(FDepthOnlyPS)和该HLSL代码中要调用(Main)的函数之间的关联。Unreal使用术语“频率”来指定它的着色器类型 - 顶点,船体,域,几何,像素或计算。

    你会注意到这个实现忽略了第一个参数。因为这个特定的例子不是模板化的函数。在某些情况下,宏专门化了一个模板类,其中的模板类由另一个宏实例化,以创建特定的实现。例如为每种可能的照明类型创建一个变体。如果您好奇的看到了BasePassRendering.cpp的顶部的宏IMPLEMENT_BASEPASS_LIGHTMAPPED_SHADER_TYPE......我们将在Base Pass文章中深入介绍它!

 

综述

FShader的实现是着色管道中的特定阶段,可以在使用之前修改其HLSL代码中的参数。Unreal使用宏将C++代码绑定到HLSL代码。从头开始实现着色非常简单,但将其集成到现有的延迟基本通道/阴影中将会复杂的多。

 

缓存和编译环境

在我们继续之前,有两个重要概念要介绍。在你修改材质后,Unreal会自动编译许多可能的着色器队列。这是一件好事,但会产生过多的无用着色器。这里介绍一下ShouldCache函数。

Unreal Engine 4 渲染 (2) 着色器与顶点数据 Shaders and Vertex Data_第3张图片

    如果“着色器”,“材质”和“顶点工厂”都同意应该缓存某队列,Unreal才会会创建着色器的这个特定队列。如果其中任何一个不同意,那么Unreal就会跳过创建该队列,这意味着在此队列可被绑定的情况下,你永远等不到结束。比如说,你不想去缓存一个需要SM5支持的着色器。如果您正在构建不支持它的平台,则没有理由编译和缓存它。

    ShouldCache函数是一个静态函数,可以在FShader,FMaterial或FVertexFactory类中实现。检查现有的使用情况将为您提供一个关于如何以及何时可以实现它的想法。

    第二个重要概念是在编译之前更改HLSL代码中的预处理器定义的能力。 FShader使用ModifyCompilationEnvironment(通过宏实现的静态函数),FMaterial使用SetupMaterialEnvironment,而FVertexFactory使用ModifyCompilationEnvironment。这些函数在着色器编译之前调用,并允许您修改HLSL预处理器定义。 FMaterial广泛使用该设置来根据材料内的设置设置着色模型相关的定义,以优化掉任何不必要的代码。

 

FVertexFactory

    现在我们知道如何在渲染着色器之前修改着色器,首先需要知道如何将数据提取到GPU中!顶点工厂封装顶点数据源并可以链接到顶点着色器。如果你已经写出了自己的渲染代码,那么你可能会试图创建一个包含顶点可能需要的所有可能数据的类。 Unreal使用顶点工厂,它只允许您将实际需要的数据上传到顶点缓冲区。

    要了解顶点工厂,我们应该了解两个具体的例子。 FLocalVertexFactory和FGPUBaseSkinVertexFactory。FLocalVertexFactory用于许多地方,因为它提供了一种简单的方法来将显式顶点属性从本地变换到世界空间。静态网格使用这个,缆绳和过程网格也是如此。骨骼网格(需要更多数据)另一方面使用FGPUBaseSkinVertexFactory。再下来,我们看看与这两个顶点工厂匹配的着色器数据如何具有不同的数据。

 

FPrimitiveSceneProxy

    那么Unreal如何知道哪个顶点工厂用于网格?通过FPrimitiveSceneProxy类! FPrimitiveSceneProxy是UPrimitiveComponent的呈现线程版本。它的目的是子类UPrimitiveComponent和FPrimitiveSceneProxy并创建特定的实现。

 

Unreal Engine 4 渲染 (2) 着色器与顶点数据 Shaders and Vertex Data_第4张图片

FCableSceneProxy aFPrimitiveSceneProxy for the dynamic cable component.

    后退一下 - 虚幻有一个游戏线程和一个渲染线程,两者不应该触及属于另一个线程的数据(除非通过几个特定的​​同步宏)。为了解决这个问题,Unreal使用UPrimitiveComponent作为游戏线程,并通过重写CreateSceneProxy()函数来决定创建哪个FPrimitiveSceneProxy类。然后,FPrimitiveSceneProxy可以在适当的时候查询游戏线程,从游戏线程获取数据到渲染线程中,以便将其处理并放置在GPU上。

    这两个类通常成对出现,这里有两个很好的例子:UCableComponent / FCableSceneProxyUImagePlateFrustrumComponent /FImagePlateFrustrumSceneProxy。在FCableSceneProxy中,渲染线程查看UCableComponent中的数据并构建一个新的网格(计算位置,颜色等),然后与之前的FLocalVertexFactory相关联。 UImagePlateFrustrumComponent是整洁的,因为它根本没有顶点工厂!它只是使用渲染线程的回调来计算一些数据,然后使用该数据来绘制线条。没有着色器或顶点工厂与它关联,它只是使用GPU回调来调用一些即时模式样式的渲染函数。

 

Binding C++ to HLSL

    目前为止,我们已经介绍了不同类型的顶点数据以及场景中的组件如何创建并存储这些数据(通过具有顶点工厂的场景代理)。现在我们需要知道如何在GPU上使用唯一的顶点数据,尤其是考虑到basepass只有一个顶点函数需要处理所有不同类型的传入数据!如果你猜“另一个C++宏”,答对了!

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

 

    这个宏允许我们将顶点工厂的C ++表示绑定到特定的HLSL文件。例:

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

    

    现在您会注意到一些有趣的内容,没有指定入口点(根本没有该入口)!我认为实际工作的方式非常精彩(虽然会让人感到困惑):Unreal会根据所使用的顶点工厂改变数据结构和函数调用的内容,同时重用相同的名称,以便通用代码工作。

    我们将看一个例子:BasePass顶点着色器将FVertexFactoryInput作为输入。 这个数据结构在LocalVertexFactory.ush中定义,具有特定的含义。 但是,GpuSkinVertexFactory.ush也定义了这个结构! 然后,根据包含哪个头,提供给顶点着色器的数据将发生变化。这种模式在其他领域中重复使用,并将在“Shader Architecture 着色器体系结构”文章中进行更深入的介绍。

// Entry point for the base pass vertexshader. We can see that it takes a generic FVertexFactoryInput struct andoutputs a generic FBasePassVSOutput.

void Main(FVertexFactoryInput Input, outFBasePassVSOutput Output)

{

   // This is where the Vertex Shader would calculate things based on theInput and store them in the Output.

}

// LocalVertexFactory.ush implements theFVertexFactoryInput struct

struct FVertexFactoryInput

{

   float4 Position : ATTRIBUTE0;

   float3 TangentX : ATTRIBUTE1;

   float4 TangentZ : ATTRIBUTE2;

   

   float4 Color : ATTRIBUTE3;

   // etc…

}

// GpuSkinVertexFactory.ush alsoimplements the FVertexFactoryInput struct

struct FVertexFactoryInput

{

   float4 Position : ATTRIBUTE0;

   half3 TangentX : ATTRIBUTE1;

   half4 TangentZ : ATTRIBUTE2;

   uint4 BlendIndices : ATTRIBUTE3;

   uint4 BlendIndicesExtra : ATTRIBUTE14;

   // etc…

}

 

总结

    IMPLEMENT_MATERIAL_SHADER_TYPE宏定义了着色器的入口点,但顶点工厂确定了传入该顶点着色器的数据。着色器使用非特定变量名称(例如FVertexFactoryInput),这些名称对不同的顶点工厂有不同的含义。 UPrimitiveComponent / FPrimitiveSceneProxy一起工作,通过特定的数据布局从您的场景中获取数据并将其传输到GPU上。

着色器管线的一个脚注

    Unreal具有“着色器管线”的概念,它在一个管线中一起处理多个着色器(顶点,像素),以便查看输入/输出并优化它们。它们在引擎中的三个地方使用:DepthRendering,MobileTranslucentRendering和VelocityRendering。 我不能很好地理解它们,但是如果您正在使用这三种系统中的任何一种,并且在阶段之间优化语义的问题,请调查IMPLEMENT_SHADERPIPELINE_TYPE_ *。

啊,是的,不常用的VSHSDSGSPS 类型

 

 

下章

    在下一篇文章中,我们将着眼于绘图策略是什么,绘图策略工厂是什么,以及Unreal如何实际告诉GPU绘制网格。

 

 

你可能感兴趣的:(Unreal,Engine,4,Rending)