IN THIS ARTICLE
术语表
- 渲染管道 (Render Pipeline): 渲染通道的序列,在例如
MainRenderPipeline.azasset和MainPipeline.pass,或LowEndRenderPipeline.azasset和LowEndPipeline.pass中声明 - 材质类型 (Materialtype): 渲染使用该材质的网格所需的材质属性、材质函数和光栅通道着色器的列表,例如
AutoBrick.materialtype或basepbr_generated.materialtype - 抽象材质类型 (Abstract Materialtype): 材质属性和材质函数的列表以及多个着色器函数,但没有实际的着色器。还指定材质管道所需的光照模型。例如
BasePBR.materialtype - 材质 (Material): 具有特定属性值的材质类型的实例,分配给网格并进行渲染。
- 材质实例 (Material Instance): 此术语有时在编辑器中使用,但功能上与材质相同,即材质类型的一组属性值。
- 材质属性 (Material Property): 材质的通用属性值,由着色器直接使用(例如
baseColor、baseColorMap和baseColorFactor),作为着色器选项(例如enableShadows或enableIBL),或作为光栅化器设置(例如doubleSided) - 材质着色器参数 (Material Shader Parameters): 着色器在渲染期间使用的特定属性值集(例如纹理或颜色)。
- 材质函数 (Material Functors): 在修改材质属性时执行的通用 Lua 脚本或 C++ 函数,例如
UseTexture在选择纹理时设置着色器选项和纹理贴图索引。 - 材质管道 (Material Pipeline): 与渲染管道兼容并使用抽象材质类型的材质着色器代码的着色器模板列表。材质管道生成非抽象材质类型和着色器,以便抽象材质可以使用特定渲染管道进行渲染,例如(
MainPipeline.materialpipeline或LowEndPipeline.materialpipelin)。 - 材质管道脚本 (Material Pipeline Script): 一种 Lua 脚本,根据抽象材质类型的光照模型过滤材质管道的着色器模板,例如
MainPipelineScript.lua或LowEndPipelineScript.lua - 材质管道函数 (Material Pipeline Functors): 类似于材质函数,但在材质管道中指定:在修改材质属性时执行的通用 Lua 脚本:例如
ShaderEnable.lua根据材质的isTransparent属性启用或禁用透明度着色器。 - 材质画布 (Material Canvas): 一种基于节点的图形编辑器,用于为材质类型创建自定义着色器代码。图形被转换为抽象材质类型的着色器代码,后者又使用材质管道为特定渲染管道创建着色器。
MaterialPipeline 基础
在最基本的层面上,O3DE 中的(非抽象)材质提供了一组着色器,这些着色器由匹配的光栅通道为每个使用该材质的网格执行。这意味着材质着色器和渲染管道必须紧密关联:为了在 Forward+ 管道中渲染网格,材质必须至少提供深度预通道着色器和不透明着色器,并且所有着色器必须使用与相应光栅通道相同的渲染附件。此外,Forward+ 管道的不透明着色器需要基于视图的瓦片光照分配和用于方向光的级联阴影贴图,这因此也强制渲染管道提供这些内容。
在 O3DE 中,渲染管道非常可配置。设置延迟渲染管道或完全光线追踪渲染管道相当容易。但由于材质着色器与渲染管道紧密关联,它们无法在不同配置之间重用。例如,在延迟渲染管道中,不透明通道作为每个材质类型的全屏着色器执行,而不是作为每个网格执行的光栅通道。作为另一个不同的例子,默认不透明着色器需要方向光的级联阴影贴图,但该阴影贴图仅在相机的视锥内有效。对于光线追踪反射等功能,这会成为问题,因为视锥外的网格仍然需要有效的阴影值。
虽然材质类型理论上可以为 Forward+ 和延迟渲染管道提供着色器,但这样做需要在每次修改管道或引入新管道时调整着色器。这意味着对渲染管道的每次更改(例如在 MainPipeline 的不透明通道中添加或删除渲染目标)都需要更新每个项目中每种材质的每个不透明着色器,包括用户生成的材质,否则现有项目将无法正常工作。
为了解决这个问题,O3DE 使用材质管道(例如 MainPipeline.materialpipeline)和抽象材质类型在一定程度上将材质着色器与渲染管道分离。材质管道与渲染管道严格相关,并定义了与渲染管道的渲染通道兼容的模板着色器列表。这些着色器中的每个着色器都在特定位置调用特定的材质函数,而抽象材质必须提供这些函数。这允许 O3DE 生成使用任何渲染管道渲染网格所需的实际材质着色器。
注意: 材质管道没有明确指定材质函数或着色器结构,对着色器和抽象材质的正式要求也非常少:抽象材质类型在 materialShaderDefines 和 materialShaderCode 字段中指定两个 .azsli 文件。这些文件分别通过 MATERIAL_TYPE_DEFINES_AZSLI_FILE_PATH 和 MATERIAL_TYPE_AZSLI_FILE_PATH 提供给着色器模板。有关其他背景信息: MATERIAL_PARAMETERS_AZSLI_FILE_PATH 中引用的定义文件包含 struct MaterialParameters,该结构从材质着色器参数生成。
尽管如此,引擎提供的抽象材质类型(即 BasePBR、StandardPBR 等)和材质管道的着色器模板(即 MainPipeline、LowEndPipeline、MobilePipeline 等)确实遵循相当严格的接口,这在下面描述。为了将自定义材质类型集成到现有渲染管道中,或创建使用现有材质的新渲染管道,建议坚持使用与此非常接近的内容。
示例:
MainPipeline.materialpipeline定义着色器模板ForwardPass_BaseLighting和pipelineScriptMainPipelineScript.luaBasePBR.materialtype定义lightingModel:Base、materialShaderDefines:BasePBR_Defines.azsli和materialShaderCode:BasePBR.azsli。- 这意味着
MainPipelineScript.lua脚本选择ForwardPass_BaseLighting着色器模板,并通过将ForwardPass_BaseLighting.azsli与BasePBR_Defines.azsli和BasePBR.azsli组合来实例化实际的着色器。- 实例化的着色器可以在
<project>/Cache/Intermediate Assets/materials/types/basepbr_mainpipeline_forwardpass_baselighting.azsl中找到,以及basepbr_generated.materialtype。
- 实例化的着色器可以在
材质接口基础
VsOutput EvaluateVertexGeometry(VsInput IN, VsSystemValues SV, const MaterialParameters params);- 通常在顶点着色器中调用,应该提供归一化设备坐标中的顶点。
- 结构体
VsInput和VsOutput由着色器模板定义,但可以通过各种定义控制,例如MATERIAL_USES_VERTEX_POSITION或MATERIAL_USES_VERTEX_NORMAL。 - 结构体
MaterialParameters由材质定义。 - 结构体
VsSystemValues由着色器模板定义,材质应将其视为不透明的。
PixelGeometryData EvaluatePixelGeometry(VsOutput IN, VsSystemValues SV, bool isFrontFace, const MaterialParameters params);- 通常在像素着色器中调用,从
VsOutput结构体转发值,并构建每个像素的切线帧。 - 可用于程序化生成几何体,或为更通用的
EvaluateSurface准备输入。 - 结构体
PixelGeometryData由材质定义,对着色器模板是不透明的。
- 通常在像素着色器中调用,从
Surface EvaluateSurface(VsOutput IN, VsSystemValues SV, PixelGeometryData geoData);- 通常在像素着色器中调用。它主要采样纹理。
- 结构体
Surface由材质定义。它需要提供一些固定成员,例如position。
LightingData EvaluateLighting(Surface surface, IN.position, ViewSrg::m_worldPosition.xyz);- 通常在像素着色器中调用,应该计算最终的着色。
- 结构体
LightingData由材质定义。它需要提供一些固定成员,例如diffuseLighting。 - 由着色器模板定义。它迭代分配的光照并评估阴影。
- 使用多个材质定义的函数:
void InitializeLightingData(Surface surface, float3 viewPosition, inout LightingData lightingData);- 期望每种光源类型都有一个
<LightType>Util类,其中包含多个特定于每种光源类型的函数,但主要归结为:static <LightType>Util Init(<LightType> light, Surface surface, float3 cameraPositionWS);real GetFalloff();- 衰减值用于在执行可能昂贵的阴影评估之前跳过光源。
void Apply(<LightType> light, Surface surface, real litRatio, inout LightingData lightingData);
void FinalizeLightingData(Surface surface, inout LightingData lightingData);
补充:HLSL 中的继承
HLSL 不支持继承。有时,使用一些"定义魔法"来达到类似的效果,例如 BasePBR_VertexEval.azsli 中的以下内容:
#ifndef EvaluateVertexGeometry
#define EvaluateVertexGeometry EvaluateVertexGeometry_BasePBR
#endif
VsOutput EvaluateVertexGeometry_BasePBR()
...
这样,可以在 StandardPBR_VertexEval.azsli 中在包含 BasePBR 文件之前使用 #define EvaluateVertexGeometry EvaluateVertexGeometry_StandardPBR。
这允许从 EvaluateVertexGeometry_StandardPBR() 函数内部调用 EvaluateVertexGeometry_BasePBR(),除了我们自己的着色器代码之外。
在其余的着色器代码中,可以以不可知的方式调用 EvaluateVertexGeometry(),而不管这些位置实际调用的是哪个函数。
ForwardPass_BaseLighting.azsli 作为材质接口的具体示例
Gems/Atom/Feature/Common/Assets/Materials/Pipelines/MainPipeline/ForwardPass_BaseLighting.azsli 与 BasePBR_Defines.azsli 和 BasePBR.azsli。
#pragma once
#include <Atom/Features/SrgSemantics.azsli>
// 以下定义为材质着色器代码提供有关当前着色器功能的信息。
// 例如,深度着色器(不带自定义 z)没有像素着色器阶段,因此材质着色器只需要定义 'EvaluatePixelGeometry()' 和相关结构体。
#define MATERIALPIPELINE_SHADER_HAS_PIXEL_STAGE 1
#define OUTPUT_DEPTH 0
#define FORCE_OPAQUE 1
#define OUTPUT_SUBSURFACE 0
#define ENABLE_TRANSMISSION 0
#define ENABLE_SHADER_DEBUGGING 1
#define ENABLE_CLEAR_COAT 0
//////////////////////////////////////////////////////////////////////////////////////////////////
#ifdef MATERIAL_TYPE_DEFINES_AZSLI_FILE_PATH
#include MATERIAL_TYPE_DEFINES_AZSLI_FILE_PATH
// -------------------------------------------------------------------------------------------------
// 这是在文件 'BasePBR.materialtype' 中声明的---特别是在字段 "materialShaderDefines" 中,在此示例中其值为 "BasePBR_Defines.azsli"。
//
// 材质需要在此文件中定义以下 SRG 和结构体:
// #if PIPELINE_HAS_OBJECT_SRG
// ShaderResourceGroup ObjectSrg : SRG_PerObject {};
// #endif /* PIPELINE_HAS_OBJECT_SRG */
//
// #if PIPELINE_HAS_DRAW_SRG
// ShaderResourceGroup DrawSrg : SRG_PerDraw {};
// #endif /* PIPELINE_HAS_DRAW_SRG */
//
// 通常通过包含 DefaultDrawSrg.azsli 和 DefaultObjectSrg.azsli 来声明这些。然而,使用自定义 SRG 时,至少应该存在相同的字段,
// 因为它们在创建绘制项时由特征处理器填充。
// 注意:延迟管道或光线追踪管道不能有 ObjectSRG 或 DrawSRG,这意味着相关定义将设置为 0,并且信息必须以不同的方式提供。
//
// struct MaterialParameters{};
// 材质的参数输入。这通常由材质管道生成,并通过使用 '#include MATERIAL_PARAMETERS_AZSLI_FILE_PATH' 工作。
// 当手动定义此结构体时,不能使用 SceneMaterialSrg,因为结构体的布局在 C++ 中未知,并且参数无法存储在结构化缓冲区中。
//
// ShaderResourceGroup MaterialSrg : SRG_PerMaterial {};
// MaterialSrg 的定义相当严格,通常最简单的方法是使用 '#include <Atom/Features/Materials/MaterialSrg.azsli>',它使用 MaterialParameters 结构体。
// 虽然可以使用自定义 MaterialSrg,但适用与上面的 MaterialParameters 结构体相同的限制:不能使用 SceneMaterialSrg。
//
// SceneMaterialSrg 是一次访问多组材质参数所必需的,例如在延迟管道中。
//
// 这些通常不是由材质完全定义的,而是由多个定义控制:参见 'BasePBR_VertexData.azsli' 与 'ForwardPassVertexData.azsli' 结合使用。
// struct VsInput{}; // 顶点着色器的输入
// struct VsOutput{}; // 顶点着色器的输出,像素着色器的输入
// struct VsSystemValues{}; // 渲染管道特有的值(例如 Forward+ 的 `instanceId`),通常被视为传递给各种实用函数的不透明结构体,并且从不由材质直接声明。
// 所有内容如何组合在一起可能会变得复杂。例如,InstanceId 需要从顶点着色器传递到像素着色器,因此,仅在 Forward+ 管道中需要成为 VsOutput 结构体的一部分。
// struct PixelGeometryData{}; // 将 VsOutput 转换为表面数据的输入;
// 理想情况下,这会处理视差和 Alpha 裁剪,以便自定义 z 通道可以在此之后停止。
// 实际上,Alpha 裁剪当前发生在表面上。
// See 'BasePBR_PixelGeometryData.azsli'
// struct SurfaceData{}; // 包含评估像素光照所需的所有表面数据。
// 通常在创建此结构体时采样纹理。
// See 'BasePBR_SurfaceData.azsli'
// struct LightingData{}; // 包含实际光照评估的输入和输出。
// 光照评估函数在评估不同光源类型时使用此结构体。
// 着色器将其转换为像素着色器的实际输出(取决于渲染管道)。
// 这意味着结构体需要至少具有这些函数和着色器所期望的成员。
// See 'BasePBR_LightingData.azsli'
// -------------------------------------------------------------------------------------------------
#endif
//////////////////////////////////////////////////////////////////////////////////////////////////
#include <viewsrg_all.srgi> // 收集 'partial ShaderResourceGroup ViewSrg {};'
#include <scenesrg_all.srgi> // 收集 'partial ShaderResourceGroup SceneSrg {};'
#include <Atom/Features/Pipeline/Forward/ForwardPassSrg.azsli> // 定义 'ShaderResourceGroup PassSrg : SRG_PerPass {};' 为主管道中的前向通道提供支持。
#include <Atom/Features/Pipeline/Forward/ForwardPassVertexData.azsli> // 使用材质创建的定义,并实际声明结构体 VsInput、VsOutput 和 VsSystemValues。
#include <Atom/Features/Pipeline/Forward/ForwardPassPipelineCallbacks.azsli> // 访问 DrawSrg/ObjectSrg/MaterialSrg 等的包装函数。
// 当前,定义了以下函数:
// 'uint GetLightingChannelMask(VsSystemValues SV);'
// 'float4x4 GetObjectToWorldMatrix(VsSystemValues SV);'
// 'float3x3 GetObjectToWorldMatrixInverseTranspose(VsSystemValues SV);'
// 'float4x4 GetViewProjectionMatrix(VsSystemValues SV);'
// 'bool HasGoboTexture(int index);'
// 'Texture2D<float> GetGoboTexture(int index);'
// 当定义了 MATERIALPIPELINE_USES_PREV_VERTEX_POSITION 时:
// 'float4x4 GetObjectToWorldMatrixPrev(VsSystemValues SV);'
// 'float4x4 GetViewProjectionPrevMatrix(VsSystemValues SV);'
//////////////////////////////////////////////////////////////////////////////////////////////////
#include MATERIAL_TYPE_AZSLI_FILE_PATH
// -------------------------------------------------------------------------------------------------
// 这是在文件 'BasePBR.materialtype' 中声明的,特别是在字段 "materialShaderCode" 中,在此示例中其值为 "BasePBR.azsli"
//
// 材质至少需要定义以下函数:
//
// - 'VsOutput EvaluateVertexGeometry(VsInput IN, VsSystemValues SV, const MaterialParameters params);'
// 在顶点着色器中调用(至少在 Forward+ 管道中),并将 VsInput 转换为 VsOutput,即执行视图投影变换。
// 如果材质不需要自定义 z 着色器,则只会为深度通道调用此函数。
//
// - 'PixelGeometryData EvaluatePixelGeometry(VsOutput IN, VsSystemValues SV, bool isFrontFace, const MaterialParameters params);'
// 在像素着色器中调用,并将 VsOutput IN 转换为 PixelGeometryData。
//
// - 'Surface EvaluateSurface(VsOutput IN, PixelGeometryData geoData, const MaterialParameters params);'
// 在像素着色器中调用。它将 PixelGeometryData 转换为表面数据。
// 大多数纹理采样在此函数中进行,材质通常在此评估大多数材质参数(和着色器选项)。
// 为了方便,材质输入也应该在此处使用,例如 'BaseColorInput.azsli' 提供 'GetBaseColorInput()' 和 'BlendBaseColor()',可以与使用 'COMMON_OPTIONS_BASE_COLOR' 定义的着色器选项一起使用,并且它使用在 'BaseColorPropertyGroup.json' 中定义的材质参数。
// 注意: 'COMMON_SRG_INPUTS_BASE_COLOR' 已过时;它们用于在结构体 MaterialParameters 存在之前手动创建 MaterialSRG。
//
// - 'real3 GetDiffuseLighting(Surface surface, LightingData lightingData, real3 lightIntensity, real3 dirToLight);'
// - 'real3 GetSpecularLighting(Surface surface, LightingData lightingData, const real3 lightIntensity, const real3 dirToLight);'
// 在(默认)光照评估期间调用。它允许材质提供自己的 BRDF:
//
// - 'void InitializeLightingData(inout Surface surface, float3 viewPosition, inout LightingData lightingData);'
// - 'void FinalizeLightingData(Surface surface, inout LightingData lightingData);'
// 在光照评估之前和之后调用。
//
// - class CapsuleLightUtil
// {
// static bool CheckLightChannel(CapsuleLight light, LightingData lightingData);
// static CapsuleLightUtil Init(CapsuleLight light, Surface surface, float3 cameraPositionWS);
// real GetFalloff();
// void Apply(CapsuleLight light, Surface surface, [real litRatio], inout LightingData lightingData);
// static void ApplySampled(CapsuleLight light, Surface surface, inout LightingData lightingData, const uint sampleCount); // 可选
// };
// 在光照评估期间用于每种光源类型(capsule、directional、disk、point、polygon、quad、simple point light、simple spot light)。
// 如果要在自定义着色器中使用默认 PBR 实现,必须包含 "Atom/Features/PBR/Lights/Lights.azsli"。它为每个支持的光源类型提供默认的 <Type>LightUtil 类。
//
// -------------------------------------------------------------------------------------------------
//////////////////////////////////////////////////////////////////////////////////////////////////
// 来自前向通道的光照评估函数,使用材质定义的函数和类
#include <Atom/Features/Pipeline/Forward/ForwardPassEvaluateLighting.azsli>
// - 'EvaluateLighting(Surface surface, float4 screenPosition, float3 viewPosition);'
// 此函数特定于 Forward+ 管道。它使用 PassSrg 中的 TileLightingIterator 迭代每个分配的光源,
// 评估阴影(如果适用),并使用 '<Type>LightUtil' 类将光源应用于表面(或跳过它)。
// 实际的顶点和像素着色器代码:
#include "ForwardPassVertexAndPixel.azsli"
// 顶点着色器调用:
// 'VsOutput OUT = EvaluateVertexGeometry(VsInput IN, VsSystemValues SV, MaterialParameters params);'
// 像素着色器随后调用:
// 'PixelGeometryData geoData = EvaluatePixelGeometry(VsOutput IN, VsSystemValues SV, bool isFrontFace, MaterialParameters params);'
// 'Surface surface = EvaluateSurface(VsOutput IN, PixelGeometryData geoData, MaterialParameters paramn);'
// 'LightingData lightingData = EvaluateLighting(Surface surface, float4 IN.position, float3 ViewSrg::m_worldPosition.xyz);'
// 然后将 'lightingData' 转换为当前通道所需的任何输出。