量子计算9 发表于 2023-1-11 09:40

虚幻引擎4渲染中如何添加新的着色模型?

添加新的着色模型

Unreal 支持多种开箱即用的常见着色模型,可满足大多数游戏的需求。Unreal 支持通用的微面镜面反射作为其默认光照模型,但也有支持高端头发和眼睛效果的光照模型。这些着色模型可能不适合您的游戏,您可能希望调整它们或添加全新的模型,尤其是对于高度风格化的游戏。


集成一个新的光照模型是非常少的代码,但需要一些耐心(因为它需要(几乎)完全编译引擎和所有着色器)。一旦您决定开始自己进行增量更改,请务必查看迭代部分,因为这有助于减少约 10 分钟的迭代时间,您会发现开箱即用。
这篇文章中的大部分代码都是基于 FelixK 在其博客系列中的优秀(但有些过时)信息,以及评论员对各种帖子的一些更正。非常鼓励您也阅读 FelixK 的博客,因为我浏览了一些着色器代码更改,以换取更多关于该过程以及我们这样做的原因。
我们需要修改引擎的三个不同区域以支持新的着色模型、材质编辑器、材质本身和现有的着色器代码。我们将一次一个地解决这些变化。
修改材质编辑器

我们的第一站是EngineTypes.hEMaterialShadingModel 内部的枚举。此枚举确定在材质编辑器内的着色模型下拉列表中显示的内容。我们将在之前的枚举中添加我们的新枚举条目MSM_StylizedShadow MSM_MAX
// 注意:如果更改,请检查 UMaterialInstance::Serialize!
UENUM()
enum EMaterialShadingModel
{
// ... 为简洁起见省略了先前的条目
MSM_Eye UMETA(DisplayName=”Eye”),
MSM_StylizedShadow UMETA(DisplayName=”Stylized Shadow”),
MSM_MAX,
};枚举似乎是按名称序列化的(如果存在?),但对于引擎的任何可能按整数值序列化它们的部分,无论如何都值得添加到列表的末尾。
Epic 在枚举上方留下了一条评论,EMaterialShadingModel警告开发人员在UMaterialInstance::Serialize我们更改枚举时检查函数。如果添加一个新的着色模型,看起来我们不需要更改任何内容,这样我们就可以忽略它并继续前进。(如果您对该函数的作用感到好奇,看起来它们确实在某一时刻改变了枚举值的顺序,因此该函数有一些代码可以根据正在加载的资产版本来修复它。)


完成此更改后,如果我们要编译它,新的着色模型将显示在材质编辑器内的着色模型下拉列表中,但它不会做任何事情!FelixK 使用该Custom Data 0引脚允许艺术家设置光衰减范围的大小。我们需要修改代码以使Custom Data 0引脚为我们的自定义着色模型启用。
打开Material.cpp(不要与 Lightmass 项目中的同名文件混淆)并查找UMaterial::IsPropertyActive函数。为材质上的每个可能的引脚调用此函数。如果您尝试修改材质域(例如贴花、后处理等),您需要特别注意此函数的第一部分,他们查看每个域并简单地指定应启用哪些引脚。如果您像我们一样修改着色模型,那么它会稍微复杂一些——如果给定其他属性,如果每个引脚应该处于活动状态,则有一个 switch 语句为每个引脚返回 true。
在我们的例子中,我们想要启用MP_CustomData0pin,所以我们向下滚动到部分MP_CustomData0并添加|| ShadingModel == MSM_StylizedShadow到它的末尾。当您将Shading Model更改为 Stylized Shadow 时,此引脚应启用,允许您将材质图连接到它。
switch (InProperty)
{
// 其他情况为简洁省略
case MP_CustomData0:
Active = ShadingModel == MSM_ClearCoat || ShadingModel == MSM_Hair || ShadingModel == MSM_Cloth || ShadingModel == MSM_Eye || ShadingModel == MSM_StylizedShadow;
休息;
}重要的是要了解此代码仅更改材质编辑器中的 UI,您仍然需要确保使用在着色器中提供给这些引脚的数据。
旁注:Custom Data 0和Custom Data 1是单通道浮点属性,对于您的自定义着色模型可能有足够的额外数据,也可能不够。Javad Kouchakzadeh向我指出,您可以创建全新的引脚,让您选择如何为它们生成 HLSL 代码。不幸的是,这超出了本教程的范围,但可能是未来教程的主题。如果您喜欢冒险,请查看MaterialShared.cpp的InitializeAttributeMap()功能!
修改 HLSL 预处理器定义

一旦我们修改了材质编辑器以便能够选择我们的新着色模型,我们需要确保我们的着色器知道它们何时被设置为使用我们的着色模型!
打开MaterialShared.cpp并寻找有点庞大的FMaterial::SetupMaterialEnvironment(EShaderPlatform Platform, const FUniformExpressionSet& InUniformExpressionSet, FShaderCompilerEnvironment& OutEnvironment) const功能。此功能可让您查看各种配置因素(例如材料的属性),然后OutEnvironment通过添加附加定义来修改变量。
在我们的特定案例中,我们将向下滚动到打开的部分GetShadingModel()并添加我们的MSM_StylizedShadow案例(来自EngineTypes.h)并按照现有模式为其指定一个字符串名称。
switch(GetShadingModel())
{
// 其他情况为简洁省略
case MSM_Eye:
OutEnvironment.SetDefine(TEXT(“MATERIAL_SHADINGMODEL_EYE”), TEXT(“1”)); 休息;
case MSM_StylizedShadow:
OutEnvironment.SetDefine(TEXT(“MATERIAL_SHADINGMODEL_STYLIZED_SHADOW”), TEXT(“1”)); 休息;
}现在,当材质的 Shading Model 设置为MSM_StylizedShadowHLSL 编译器将设置MATERIAL_SHADINGMODEL_STYLIZED_SHADOW为预处理器定义。这将允许我们稍后#if MATERIAL_SHADINGMODEL_STYLIZED_SHADOW在 HLSL 代码中制作仅适用于使用我们的着色模型的着色器排列的东西。
审查

这结束了对 C++ 代码所需的修改。我们已将着色模型添加到编辑器的下拉菜单中,我们更改了用户可以将数据插入的引脚,并且我们确保生成的着色器能够判断我们何时处于该模式。编译引擎,喝杯咖啡——修改EngineTypes.h将导致大部分 C++ 代码被重新编译。在对 .ush/.usf 文件进行更改之前,我们不想运行编辑器,因为修改它们会导致我们所有的着色器重新编译!
更新 GBuffer 着色模型 ID

现在可以知道我们何时正在构建使用我们的光照模型的着色器的排列(通过MATERIAL_SHADINGMODEL_STYLIZED_SHADOW我们可以开始对着色器进行更改。我们需要做的第一件事是将新的着色模型 ID 写入 GBuffer . 这允许DeferredLightPixelShader在运行光照计算时知道要尝试和使用的着色模型。
打开DeferredShadingCommon.ush,中间有一个以 . 开头的部分#define SHADINGMODELID_UNLIT。我们将在其末尾添加我们自己的着色模型 ID,然后更新SHADINGMODELID_NUM.
#define SHADINGMODELID_EYE 9
#define SHADINGMODELID_STYLIZED_SHADOW 10
#define SHADINGMODELID_NUM 11我们需要告诉着色器将此着色模型 ID 写入 GBuffer,但在我们离开此文件之前,我们应该更新Buffer Visualization > Shading Model颜色,以便您可以知道场景中的哪些像素是使用着色模型渲染的. 在文件的底部应该是float3 GetShadingModelColor(uint ShadingModelID).
我们将在这两个#if PS4_PROFILE部分以及switch(ShadingModelID)现有模式之后添加一个条目。我们选择紫色只是因为原始教程也是如此。
switch(ShadingModelID)
{
// 为了简洁省略
case SHADINGMODELID_EYE: return float3(0.3f, 1.0f, 1.0f);
case SHADINGMODELID_STYLIZED_SHADOW: return float3(0.4f, 0.0f, 0.8f); // 紫色
}

现在我们需要告诉BasePassPixelShader将正确的 ID 写入 Shading Model ID 纹理。打开ShadingModelsMaterial.ush并查看SetGBufferForShadingModel函数。此函数允许每个着色模型选择如何将各种 PBR 数据通道写入FGBufferData结构。您唯一需要做的就是确保分配了 GBuffer.ShadingModelID。如果我们希望使用Custom Data 0材质编辑器中的通道,您可以在此处查询值并将其写入 GBuffer。
#elif MATERIAL_SHADINGMODEL_EYE
GBuffer.ShadingModelID = SHADINGMODELID_EYE;
// 为简洁省略
#elif MATERIAL_SHADINGMODEL_STYLIZED_SHADOW
GBuffer.ShadingModelID = SHADINGMODELID_STYLIZED_SHADOW;
GBuffer.CustomData.x = GetMaterialCustomData0(MaterialParameters);
#else
// 缺少着色模型,编译器应该报告 ShadingModelID is not set
#endif我们Custom Data 0之前通过更改 C++ 代码启用了编辑器中的引脚。调用GetMaterialCustomData0(…)是实际获取值并将其存储在 GBuffer 中,以便稍后在我们的着色模型中读取。如果您正在使用 GBuffer 的CustomData部分,则需要打开BasePassCommon.ush并将您的添加MATERIAL_SHADINGMODEL_STYLIZED_SHADOW到该#define WRITES_CUSTOMDATA_TO_GBUFFER部分的末尾。这是一项优化,如果着色模型不使用自定义数据缓冲区,虚幻引擎可以省略写入或采样自定义数据缓冲区。
改变衰减计算

到目前为止,我们只专注于添加新的着色模型并处理添加它所需的各种样板代码。现在我们将研究在使用我们的着色模型时修改光衰减的计算方式。为此,我们将打开DeferredLightingCommon.ush并找到该GetDynamicLighting函数。
虚幻使用以下计算来确定最终的光照倍数:LightColor * (NoL * SurfaceAttenuation). NoL (N dot L) 产生平滑的渐变,这不是我们在这里想要的。我们将创建一个新的光衰减变量并根据我们的着色模型修改该值。然后我们将更新现有的函数调用以使用我们的新衰减变量以避免重复代码。通过函数的大部分方式应该是AreaLightSpecular在两次调用之前调用函数的部分LightAccumulator_Add(一次累积表面,一次累积地下)。我们将添加这段代码:
float3 AttenuationColor = 0.f;分支 if( ShadingModelID
== SHADINGMODELID_STYLIZED_SHADOW)
{
浮动范围 = GBuffer.CustomData.x * 0.5f;
AttenuationColor = LightColor * ((DistanceAttenuation * LightRadiusMask * SpotFalloff) * smoothstep(0.5f — Range, 0.5f + Range, SurfaceShadow) * 0.1f);
}
else
{
AttenuationColor = LightColor * (NoL * SurfaceAttenuation);
}然后我们需要替换调用以LightAccumulator_Add使用我们的新AttenuationColor变量。
// 累积表面
{
float3 SurfaceLighting = SurfaceShading(GBuffer, LobeRoughness, LobeEnergy, L, V, N, Random);
LightAccumulator_Add(LightAccumulator, SurfaceLighting, (1.0/PI), AttenuationColor, bNeedsSeparateSubsurfaceLightAccumulation);
}您会注意到这里我们必须使用动态分支(if 语句)而不是预处理器定义。DeferredLightingCommon.ush中的像素着色器针对每个灯光运行,这可能会影响具有多个着色模型的多个对象;这阻止了我们使用预处理器定义,因此我们不得不使用动态分支来检查 GBuffer 纹理通道的 ID。
更改表面着色

既然我们已经声明了一个新的光照模型,您还应该修改表面着色函数。如果您不声明新的表面着色模型,它将把像素视为黑色,因此您至少需要在 switch 函数中添加大小写以使用标准着色。
打开ShadingModels.ush并转到SurfaceShading底部的功能。我们将在开关中添加一个新条目,然后还声明要使用的函数。
float3 SurfaceShading(FGBufferData GBuffer, float3 LobeRoughness, float3 LobeEnergy, float3 L, float3 V, half3 N, uint2 Random)
{
switch( GBuffer.ShadingModelID )
{
case SHADINGMODELID_UNLIT:
return StandardShading( GBuffer.DiffuseColor, GBuffer.SpecularColor, LobeRoughness, LobeEnergy, L, V, N);
// 其他为简洁省略
case SHADINGMODELID_STYLIZED_SHADOW:
return StylizedShadowShading(GBuffer, LobeRoughness, L, V, N);
}
}然后我们声明我们的 StylizedShadowShading 函数:
float3 StylizedShadowShading(FGBufferData GBuffer, float3 Roughness, float3 L, float3 V, half3 N)
{
float Range = GBuffer.CustomData.x * 0.5f;
float3 H = 归一化(V+L);
浮动 NoH = 饱和(点(N,H));
return GBuffer.DiffuseColor + saturate(smoothstep(0.5f — Range, 0.5f + Range, D_GGX(Roughness.y, NoH)) * GBuffer.SpecularColor);
}支持光照半透明

如果我们希望我们的着色器适用于必须为其添加特定支持的半透明对象 - 打开BasePassPixelShader.usf并找到带有注释“// Volume lighting for lit translucency”的部分,并将您的着色模型添加到 #if 语句。
//光照半透明的体积光照
#if (MATERIAL_SHADINGMODEL_DEFAULT_LIT || MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_STYLIZED_SHADOW) && (MATERIALBLENDING_TRANSLUCENT || MATERIALBLENDING_ADDITIVE) && !SIMPLE_FORWARD_SHADING && !FORWARD_SHADING)颜色 += GetTranslucencyVolumeLighting(MaterialParameters, PixelMaterialInputs, BasePassInterpolants, GBuffer, IndirectIrradiance);
#万一此时重新启动编辑器并重新编译所有着色器是个好主意。如果您想从这里继续调整着色器,最好查看有关如何减少着色器重新编译时间的迭代文章!
审查

我们修改了 BasePassPixelShaders 以便它将正确的 ID 写入 GBuffer 的 ID 通道,然后我们修改 Light Attenuation 和 Surface Shading 以通过从 GBuffer 中采样 ID 来使用我们的新着色模型。
完整的修改代码可在 GitHub 上找到,但您需要先访问 Unreal Engine 的主存储库。
重要提示:

Unreal软件电脑配置的要求是比较高的,特别是实时渲染,前期的硬件成本是比较高的,这里有一个简单的节省硬件成本的方法,使用呆猫超高清设计师云工作站,即使本地普通的电脑也能运行Unreal软件,且普通电脑也能享受行业最高端的CPU和GPU,极大提高制作效率和使用体验,且使用方便快捷,全面支持3D应用软件插件运行,随时调用百余款软件插件,高效作业。

呆猫云桌面可以为UNREAL 用户提供云端制作输出方案,提高工作效率。为Unity用户提供灵活、高效、低成本的云端烘焙服务,享受游戏制作的乐趣。用户在全国各地通过呆猫直接连接服务器,共享一套资产, 可以直接在呆猫上制作 / 修改工程文件,减少数据传输成本,高性能云办公选呆猫!
页: [1]
查看完整版本: 虚幻引擎4渲染中如何添加新的着色模型?