Language:
Page Info
Engine Version:
Share
此中文页面内容对应的英文页面有后续更新,如需浏览最新文档可切换至英文页面浏览。

着色器开发

快速入门

处理着色器时,请务必将 r.ShaderDevelopmentMode 设置为 1,以将其启用。最简单的方法是编辑 ConsoleVariables.ini,以便每次加载时都进行启用。这将启用“出错时重试”以及与着色器开发相关的日志和警告。

将更改保存到 .usf 文件之后,使用 Ctrl+Shift+. 可重新编译已更改的着色器。 如果您更改包括在许多着色器中的文件(例如,common.usf),那么此操作可能需要花费一些时间。如果您想对某个材质进行迭代,那么可通过对材质进行小幅更改(例如移动节点)并使用材质编辑器中的“应用”(Apply)来触发材质重新编译。

着色器与材质

全局着色器

全局着色器是对固定几何体(例如全屏幕四边形)执行操作并且不需要与材质进行交互的着色器。示例包括阴影过滤或者后处理。在内存中,对于任何给定的全局着色器类型,只有一个着色器。

材质与网格类型

材质由一组用于控制材质渲染方式(混合模式以及双面,等等)的状态以及一组用于控制材质与各种渲染过程的交互方式的材质输入(底色、粗糙度和法线,等等)控制。

顶点工厂

材质必须支持应用于不同的网格类型,而这是通过顶点工厂来实现的。FVertexFactoryType 代表唯一的网格类型,而 FVertexFactory 实例存储每个实例的数据以支持该唯一网格类型。例如,FGPUSkinVertexFactory 存储皮肤处理所需的骨基质,以及对 GPU 皮肤顶点工厂着色器代码需要用作输入的各种顶点缓冲区的引用。顶点工厂着色器代码是一个隐式接口,由各种过程着色器用于抽取网格类型差异。顶点工厂主要由顶点着色器代码组成,但也包含一些像素着色器代码。顶点工厂着色器代码的一些重要组成部分如下:

函数

说明

FVertexFactoryInput

定义顶点工厂所需的顶点着色器输入。这些输入必须与 C++ 端的 FVertexFactory 中的顶点声明匹配。例如,LocalVertexFactory 的 FVertexFactoryInput 具有 float4 Position:POSITION;,对应于 FStaticMeshLODResources::SetupVertexFactory 中的位置流声明。

FVertexFactoryIntermediates

用于存储高速缓存的中间数据,该数据将在多个顶点工厂函数中使用。一个常用的示例是 TangentToLocal 矩阵,该矩阵必须根据未打包的顶点输入进行计算。

FVertexFactoryInterpolantsVSToPS

要从顶点着色器传递到像素着色器的顶点工厂数据。

VertexFactoryGetWorldPosition

此函数从顶点着色器中调用,用于获取全局空间顶点位置。对于静态网格,此函数只是使用 LocalToWorld 矩阵将局部空间位置从顶点缓冲区转换到全局空间。对于由 GPU 处理皮肤的网格,将首先处理此位置的皮肤,然后再转换到全局空间。

VertexFactoryGetInterpolantsVSToPS

将 FVertexFactoryInput 转换为 FVertexFactoryInterpolants,后者将由图形硬件进行插值,然后再传递到像素着色器。

GetMaterialPixelParameters

此函数在像素着色器中调用,并将特定于顶点工厂的插值 (FVertexFactoryInterpolants) 转换为 FMaterialPixelParameters 结构,该结构由过程像素着色器使用。

材质着色器

使用 FMaterialShaderType 的着色器是特定于过程的着色器,它们需要访问材质的某些属性,因此必须针对每个材质进行编译,但不需要访问任何网格属性。光函数过程着色器是 FMaterialShaderType 的示例。

使用 FMeshMaterialShaderType 的着色器是特定于过程的着色器,它们依赖于材质的属性和网格类型,因此必须针对每个材质/顶点工厂组合进行编译。例如,TBasePassVS / TBasePassPS 需要对正向渲染过程中的所有材质输入进行评估。

材质的必需着色器集合包含在 FMaterialShaderMap 中。其类似于:

FMaterialShaderMap 
    FLightFunctionPixelShader - FMaterialShaderType
    FLocalVertexFactory - FVertexFactoryType
        TDepthOnlyPS - FMeshMaterialShaderType
        TDepthOnlyVS - FMeshMaterialShaderType
        TBasePassPS - FMeshMaterialShaderType
        TBasePassVS - FMeshMaterialShaderType
        等等
    FGPUSkinVertexFactory - FVertexFactoryType
        等等

顶点工厂根据其 ShouldCache 函数包括在此矩阵中,该函数依赖于材质的使用。例如,bUsedWithSkeletalMesh 值为 true 表示包括 GPU 皮肤顶点工厂。FMeshMaterialShaderType 根据其 ShouldCache 函数包括在此矩阵中,该函数依赖于材质及顶点工厂属性。这是一种对着色器进行高速缓存的稀疏矩阵方法,这种方法会导致着色器数目迅速增加,从而占用内存并增加编译时间。相对于存储实际需要的着色器列表,主要优点是不必生成任何列表,因此在控制台上运行之前,所需的着色器始终已编译完成。UE4 通过压缩着色器来缓解着色器内存问题,并通过多内核着色器编译来缓解编译时间问题。

创建材质着色器

材质着色器类型是使用 DECLARE_SHADER_TYPE 宏来创建的:

class FLightFunctionPixelShader : public FShader { DECLARE_SHADER_TYPE(FLightFunctionPixelShader,Material);

这个宏为材质着色器类型声明必要的元数据和函数。材质着色器类型将使用 IMPLEMENT_MATERIAL_SHADER_TYPE 进行实例化:

IMPLEMENT_MATERIAL_SHADER_TYPE(,FLightFunctionPixelShader,TEXT("LightFunctionPixelShader")

这将生成材质着色器类型的全局元数据,这些元数据允许我们在运行时执行各种操作,例如使用给定的着色器类型对所有着色器进行迭代。

典型的材质像素着色器类型将先通过调用 GetMaterialPixelParameters 顶点工厂函数来创建 FMaterialPixelParameters 构造。GetMaterialPixelParameters 将特定于顶点工厂的输入转换为任何过程可能想访问的属性,例如 WorldPosition 和 TangentNormal 等等。然后,材质着色器将调用 CalcMaterialParameters,后者将写出 FMaterialPixelParameters 的其余成员,之后 FMaterialPixelParameters 完全初始化。然后,材质着色器将通过 MaterialTemplate.usf 中的函数来访问该材质的某些输入(例如,通过 GetMaterialEmissive 访问材质的自发光输入),执行一些明暗处理,然后输出该过程的最终颜色。

特殊引擎材质

UMaterial 具有一项名为 bUsedAsSpecialEngineMaterial 的设置,该设置允许将材质与任何顶点工厂类型配合使用。这意味着所有顶点工厂都随该材质一起编译,而这将是一个非常大的集合。bUsedAsSpecialEngineMaterial 用于:

  • 仅与渲染视图模式(例如照明)配合使用的材质。

  • 在发生编译错误时用作后备的材质(DefaultDecalMaterial 和 DefaultMaterial,等等)。

  • 在渲染其他材质时使用其着色器,以减少必须高速缓存的着色器数目的材质。例如,某个不透明材质的“仅深度”着色器将生成与 DefaultMaterial 相同的深度输出,因此将改为使用 DefaultMaterial 的着色器,而该不透明材质将跳过对该“仅深度”着色器的高速缓存。

着色器编译

UE4 使用流式系统以异步方式编译着色器。编译请求在没有高速缓存的着色器贴图的材质加载时排入队列,编译结果将在它们变为可用时应用,而不会阻塞引擎。这可在装入时间和编译吞吐量方面实现最佳结果,但这意味着实际平台着色器编译与请求编译的材质之间存在相当多的层。

实际编译工作在称为“着色器编译工作程序”的辅助进程中完成,这是因为平台着色器编译函数 (D3DCompile) 中通常包含不可分割区块,这些区块导致无法在单个进程中进行多内核比例调整。

调试着色器编译器

有一些设置可控制完成编译的方式,这可以简化着色器编译器的调试。您可在 BaseEngine.ini 的 [DevOptions.Shaders] 一节中找到这些设置。

设置

说明

bAllowCompilingThroughWorkers

是否启动 SCW 以调用编译器 DLL,或者 UE4 是否应直接调用编译器 DLL。如果禁用此设置,那么将以单内核方式执行编译。

bAllowAsynchronousShaderCompiling

是否应通过 UE4 中的另一个线程来执行编译。

如果您想直接从 UE4 中单步跳入着色器编译器 DLL(例如 CompileD3D11Shader),那么应将这两者都设置为 false。但是,编译将花费较长时间,因此请确保已对所有其他着色器进行高速缓存。

发生编译错误时重试

启用 r.ShaderDevelopmentMode 之后,您将有机会在发生着色器编译错误时重试。这对于全局着色器而言特别重要,因为无法成功编译即表示发生致命错误。

在连接调试器之后进行调试时,您将遇到断点,而编译错误会显示在 Visual Studio 输出窗口中。然后,您可 双击 错误日志,以直接转到存在问题的行。

CompilerErrorDebug.png

否则,系统将显示 Yes/No 对话框

CompileError.png

着色器高速缓存与准备

着色器编译之后,它们将存储在“派生的数据高速缓存”中。在它们的键中,包含所有编译输入(包括着色器源文件)的散列。这意味着,您每次重新启动引擎或执行 'recompileshaders changed' 时,都会自动应用对着色器源文件所作的更改。

当您修改 FShader 序列化函数时,不需要处理向后兼容性,而只需在该着色器所包括的着色器文件中添加一个空格。

准备资产时,材质着色器将直接插入到材质的包中,而全局着色器单独存储在全局着色器文件中,这使其可以在引擎启动过程中的早期加载。

调试

调试着色器的主要方法是修改着色器以输出中间结果,然后使用适当的 VisualizeTexture 命令将该结果可视化。这样就可以快速执行迭代,因为您可以迅速完成编译,而不必重新启动引擎。例如,您可以使用类似于以下的代码来验证 WorldPosition 是否正确:

OutColor = frac(WorldPosition / 1000);

然后,验证比例正确,并且结果与视图无关。但是,对于那些构建数据结构的较复杂着色器,此方法无法很好地调整比例。

转储调试信息

您还可以使用 r.DumpShaderDebugInfo=1,以将编译的所有着色器的文件保存到磁盘。就像 r.ShaderDevelopmentMode 一样,在 ConsoleVariables.ini 中进行此设置可能非常有用。文件将保存到 GameName/Saved/ShaderDebugInfo,其中包括

  • 源文件,并包括

  • 着色器的预处理版本

  • 一个批处理文件,用于使用与已使用的编译器等效的命令行选项来编译预处理版本

    如果保持此设置开启,那么会在硬盘上生成许多非常小的文件和文件夹。

迭代最佳实践

如果您正在处理全局着色器,那么 recompileshaders changed 或 Ctrl+Shift+. 是最快的迭代方法。如果着色器要花费较长时间才能完成编译,您可考虑在着色器的 ModifyCompilationEnvironment 中指定 CFLAG_StandardOptimization 作为编译标志。

如果您正在处理材质着色器,例如 BasePassPixelShader.usf,那么对单个材质执行迭代将会快得多。您每次在材质编辑器中单击“应用”按钮时,都会从磁盘重新读取着色器文件,并且仅重新编译该材质。

交叉编译器

HLSL 交叉编译器 用来将 HLSL 自动转换为用于 OpenGL 平台的 GLSL,从而使您只需针对所有平台编写一次着色器。它在离线着色器编译期间运行,并对代码执行 OpenGL 驱动程序经常遗漏的各种优化。

AsyncCompute

AsyncCompute 是某些使用特定 GPU 的 API 中提供的一项硬件功能。 它使交错能够更好更有效率地利用 GPU 中的硬件单元。