简单写完了第一篇没啥营养的概述后,这一章我们来做一点真东西,通过修改源码添加一个Custom Shading Model,并使其可以集成我们上一篇文章提到的BaseColor、ILM、SSS贴图进行卡通渲染。

这里我使用的引擎版本是UE5.0.2。

拉取源码版UE5并编译

想要对源码做修改,绝对逃不开的第一步自然就是拉源码和编译。关于这一步,知乎上有大量的资料,官方文档写得也非常详细,我这种懒狗肯定不会再写一遍,不过还是有几点需要注意,值得说一下:

  • 源码工程目录一定要放在固态硬盘里
  • 预留大约250G的硬盘空间
  • 建议整块好的CPU,框框越多越好,再多插几根内存条……不然的话等编译的空闲你还可以抽时间去学学Unity 😃

C++部分

C++部分大多都在进行Shading Model注册、开放和修改数据接口的工作,这里直接上截图。

EngineTypes.h

\Engine\Source\Runtime\Engine\Classes\Engine\EngineTypes.h

在枚举EMaterialShadingModel 中注册我们的Shading Model:

MaterialShader.cpp

\Engine\Source\Runtime\Engine\Private\Materials\MaterialShader.cpp

HLSLMaterialTranslator.cpp

\Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp

FHLSLMaterialTranslator::GetMaterialEnvironment() 下添加新的分支:

Material.cpp

\Engine\Source\Runtime\Engine\Private\Materials\Material.cpp

IsPropertyActive_Internal()函数中为我们的Shading Model开放接口,这里我把SubsurfaceColor和CustomData0、CustomData1全部开放了,而实际上在后面只用到了SubsurfaceColor和CustomData0。

MaterialShared.cpp

\Engine\Source\Runtime\Engine\Private\Materials\MaterialShared.cpp

接下来,需要在FMaterialAttributeDefinitionMap::GetAttributeOverrideForMaterial()中添加与修改我们自己的Shading Model的接口名称, 除BaseColor接口以及我们刚刚开放的SubsurfaceColor、CustomData0接口外,我们还需要用到Metallic、Specular和Roughness这pbr三巨头,具体对照关系如下:

1
2
3
4
5
6
7
8
9
// My Toon Shading Model CustomPinNames
/* -------------------------------------------
// Metallic -> Specular Range
// Specular -> Specular Intensity
// Roughness -> Shadow Threshold
// Subsurface Color -> SSS Color
// Custom Data 0 -> Inner Line
// Custom Data 1 -> Other (Unused)
------------------------------------------- */

代码片段:

MaterialShared.h

\Engine\Source\Runtime\Engine\Public\MaterialShared.h

在完成接口的修改后,还要在IsSubsurfaceShadingModel()中添加我们的ShadingModel判断条件,让SubsurfaceColor接口生效:

ShaderMaterial.h

\Engine\Source\Runtime\RenderCore\Public\ShaderMaterial.h

完成接口的开放后,还需要给我们的Shading Model的GBuffer启用CustomData,我们的需要CustomData来帮助我们传输数据。首先在FShaderMaterialPropertyDefines结构体中添加相关位域:

ShaderMaterialDerivedHelpers.cpp

\Engine\Source\Runtime\RenderCore\Private\ShaderMaterialDerivedHelpers.cpp

之后在CalculateDerivedMaterialParameters()函数中更改判断条件:

具体代码如下:(其实就是在最后多加了一个Mat.MATERIAL_SHADINGMODEL_TOON的判断)

1
Dst.WRITES_CUSTOMDATA_TO_GBUFFER = (Dst.USES_GBUFFER && (Mat.MATERIAL_SHADINGMODEL_SUBSURFACE || Mat.MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || Mat.MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || Mat.MATERIAL_SHADINGMODEL_CLEAR_COAT || Mat.MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || Mat.MATERIAL_SHADINGMODEL_HAIR || Mat.MATERIAL_SHADINGMODEL_CLOTH || Mat.MATERIAL_SHADINGMODEL_EYE || Mat.MATERIAL_SHADINGMODEL_TOON));

ShaderGenerationUtil.cpp

\Engine\Source\Runtime\Engine\Private\ShaderCompiler\ShaderGenerationUtil.cpp

然后这边的FShaderCompileUtilities::ApplyFetchEnvironment() 和DetermineUsedMaterialSlots()函数也改一下:(涉及Shader的程序化生成)

PixelInspectorResult.h && PixelInspectorResult.cpp && PixelInspectorDetailsCustomization.cpp

\Engine\Source\Editor\PixelInspector\Private\PixelInspectorResult.h

\Engine\Source\Editor\PixelInspector\Private\PixelInspectorResult.cpp

\Engine\Source\Editor\PixelInspector\Private\PixelInspectorDetailsCustomization.cpp

最后是Inspector相关,这里可以控制Inspector序列化的显示,比如隐藏某些内容之类的,有需要可以自行定义。

还是老一套,先在PixelInspectorResult.h和PixelInspectorResult.cpp中定义,然后就可以在PixelInspectorDetailsCustomization.cpp中切case自定义了。

就这样,简单粗暴的C++部分就此结束,检查无误后就可以重新编译引擎了——然后根据你硬件配置的不同可以决定是去吃个饭还是去睡一觉,亦或者是抽空学学Unity。(笑

编译成功后重新打开引擎,就可以在材质编辑器的Shading Model下拉框下看到我们定义好的新光照模型啦。

Shader部分

在经过前面一系列枯燥无味的C++前置工作后,我们终于来到了激动人心的Shader环节,想必你已经迫不及待去正经八百地写一个自己的Shading Model了。但,虽然我知道你很急,但你先别急——Shader端我们依然有很多需要做的前置工作。

ShadingCommon.ush

\Engine\Shaders\Private\ShadingCommon.ush

Definitions.usf

\Engine\Shaders\Private\Definitions.usf

依旧是没有什么营养的注册工作,但有一点值得注意,ShadingCommon.ush中SHADINGMODELID_TOON宏定义的位次必须与之前EngineTypes.h里EMaterialShadingModel枚举类中的枚举位次一致,我这里是顺位第14(从0开始计数为13) 。

当然,如果你想,还可以在ShadingCommon.ush里的GetShadingModelColor()中定义我们的Shading Model的debug颜色,这里我比较懒就没改。

BasePassCommon.ush

\Engine\Shaders\Private\BasePassCommon.ush

然后,与C++中类似,在Shader端同样需要为我们的Shading Model开启SubsurfaceColor和CustomData的相关写入权限,并手动进行数据传输。

首先开启GBuffer的CustomData写入权限:

具体代码如下,实际上也还是只在最后加了一个MATERIAL_SHADINGMODEL_TOON判断:

1
#define WRITES_CUSTOMDATA_TO_FBUFFER		(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 || MATERIAL_SHADINGMODEL_TOON))

BasePassPixelShader.usf

\Engine\Shaders\Private\BasePassPixelShader.usf

然后在FPixelShaderInOut_MainPS()中添加判断条件,为SubsurfaceColor写入数据。注意这里有个坑,这是一个两层的if,而且在if外面还套了#if的宏判断,我们要在内外两层四个判断处加上两个MATERIAL_SHADINGMODEL_TOON两个ShadingModel == SHADINGMODELID_TOON才行,我当初改的时候就是漏掉了外层的if,结果最后的结果没有SSS……找了一下午才找到问题所在。😭

DeferredShadingCommon.ush

\Engine\Shaders\Private\DeferredShadingCommon.ush

继续添加判断,为Subsurface和CustomData开启接口。

ShadingModelsMaterial.ush

\Engine\Shaders\Private\ShadingModelsMaterial.ush

所有前置工作结束后,把SubsurfaceColor和CustomData压入GBuffer.CustomData

BasePassPixelShader.usf & ReflectionEnvironmentPixelShader.usf

\Engine\Shaders\Private\BasePassPixelShader.usf

\Engine\Shaders\Private\ReflectionEnvironmentPixelShader.usf

由于我们用到Metallic和Specular来传数据,所以在我们的管线中他们实际上并没有起到原有的作用,为此,我们应该取消掉他们在管线中的影响。

修改BasePassPixelShader.usf里FPixelShaderInOut_MainPS()中的SpecularColor和DiffuseColor为0,因为他们是通过Metallic和Specular计算得出的,而我们的计算并不需要他们:

相同的操作在

修改ReflectionEnvironmentPixelShader.usf里ReflectionEnvironment()的返回结果:

ShadingModels.ush

\Engine\Shaders\Private\ShadingModels.ush

终于到了最激动人心的时刻!

我们在IntegrateBxDF()中添加case和我们自己的BxDF函数:

那么ToonBxDF()在哪呢?哦,原来我们还没定义。赶紧定义一个:

全部代码如下,这里我参考了尝试在UE4.22中实现罪恶装备Xrd的卡通渲染 - 知乎 (zhihu.com)中的代码,只在其基础上稍作修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
//并不是真正光源颜色
//真正的光源颜色在DeferredLightingCommon.ush中通过LightAccumulator_AddSplit()叠加
float3 LightColor = AreaLight.FalloffColor * Falloff;
//解GBuffer
float SpecularRange = GBuffer.Metallic;
float SpecularIntensity = GBuffer.Specular;
float ShadowThreshold = GBuffer.Roughness;
float InnerLine = GBuffer.CustomData.a;
float3 SSSColor = ExtractSubsurfaceColor(GBuffer); //解码SSS
//明暗颜色
float3 BrightColor = GBuffer.BaseColor;
float3 ShadowColor = GBuffer.BaseColor * SSSColor;
//加粗内描边
if (InnerLine < 0.8f)
{
InnerLine *= 0.5f;
}
float3 InnerLineColor = float3(InnerLine, InnerLine, InnerLine);

half3 H = normalize(V + L);
float NoH = saturate(dot(N, H));
//阴影区计算
float IsShadow = step(ShadowThreshold, NoL * Shadow.SurfaceShadow);
//光照计算
FDirectLighting Lighting;
Lighting.Diffuse = InnerLineColor * LightColor * Diffuse_Lambert(lerp(ShadowColor, BrightColor, IsShadow));
Lighting.Specular = LightColor * BrightColor * IsShadow * InnerLineColor * step(0.2f, SpecularRange * pow(NoH, SpecularIntensity));
Lighting.Transmission = 0;
return Lighting;
}

其实个人感觉高光部分应该是不太对的,不过先凑活用,重点在二分法上,也就是:

1
2
3
half3 H = normalize(V + L);
float NoH = saturate(dot(N, H));
float IsShadow = step(ShadowThreshold, NoL * Shadow.SurfaceShadow);

很传统的Blinn-Phong漫反射计算,只不过结果上通过与ShadowThreshold做step进行明暗二分,并在之后的计算中以此来决定ShadowColor和BrightColor,这就是最基础的卡通渲染。

SkyLightingDiffuseShared.ush

\Engine\Shaders\Private\SkyLightingDiffuseShared.ush

到这里还没完,如果我们此刻编译Shader并连好材质,固然可以得到一个不错的效果,但如果拉高天光强度——我们就会发现的卡渲着色好像是被一层Default Lit着色“盖住”了一样(这里我没截图),原因是天光的计算并不走IntegrateBxDF(),而是独立存在的:

如上图所示,在SkyLightDiffuse()函数中加上我们卡渲模型的分支,因为我们想要获得单纯的卡渲效果,所以把前面的代码稍作修改,只保留DiffuseColor,并直接返回,避免后续的额外计算:

1
2
3
4
5
6
7
8
9
10
11
12
BRANCH
if(GBuffer.ShadingModelID == SHADINGMODELID_TOON)
{
float InnerLine = GBuffer.CustomData.a;
if (InnerLine < 0.8f)
{
InnerLine *= 0.5f;
}
float3 InnerLineColor = float3(InnerLine, InnerLine, InnerLine);
Lighting = InnerLineColor * GBuffer.BaseColor * View.SkyLightColor.rgb * 0.05f; //削弱天光影响
return Lighting;
}

好,大功告成!

开始编译Shader!

材质编辑器

最后就来到了喜闻乐见的连连看环节,Shading Model切换为我们的卡渲模型,然后简单连一下。我这里基本就是把对应的数据接了上去,顶多也就是额外加一个可调节的强度参数。

然后回到场景,把光源(定向光)的Cast Shadows选项取消(因为卡渲阴影我们已经在Shading Model中计算过了),就看到完整的效果啦。

这里我加了外描边不过文中没讲,相关内容很简单知乎上也多得很,可以找一找,比如这个:

关于UE4物体描边(outline)的一些实现方法 - 知乎 (zhihu.com)

参考文献

  1. 尝试在UE4.22中实现罪恶装备Xrd的卡通渲染 - 知乎 (zhihu.com)
  2. UE5自定义着色模型 Unreal Engine 5 custom Shading Model - 知乎 (zhihu.com)
  3. 【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(1) - Trace0429 - 博客园 (cnblogs.com)
  4. 剖析虚幻渲染体系(05)- 光源和阴影 - 0向往0 - 博客园 (cnblogs.com)