从零开始的UE5卡通渲染0x01:自定义着色模型
简单写完了第一篇没啥营养的概述后,这一章我们来做一点真东西,通过修改源码添加一个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 | // My Toon Shading Model CustomPinNames |
代码片段:
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 | FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow) |
其实个人感觉高光部分应该是不太对的,不过先凑活用,重点在二分法上,也就是:
1 | half3 H = normalize(V + L); |
很传统的Blinn-Phong漫反射计算,只不过结果上通过与ShadowThreshold做step进行明暗二分,并在之后的计算中以此来决定ShadowColor和BrightColor,这就是最基础的卡通渲染。
SkyLightingDiffuseShared.ush
\Engine\Shaders\Private\SkyLightingDiffuseShared.ush
到这里还没完,如果我们此刻编译Shader并连好材质,固然可以得到一个不错的效果,但如果拉高天光强度——我们就会发现的卡渲着色好像是被一层Default Lit着色“盖住”了一样(这里我没截图),原因是天光的计算并不走IntegrateBxDF(),而是独立存在的:
如上图所示,在SkyLightDiffuse()函数中加上我们卡渲模型的分支,因为我们想要获得单纯的卡渲效果,所以把前面的代码稍作修改,只保留DiffuseColor,并直接返回,避免后续的额外计算:
1 | BRANCH |
好,大功告成!
开始编译Shader!
材质编辑器
最后就来到了喜闻乐见的连连看环节,Shading Model切换为我们的卡渲模型,然后简单连一下。我这里基本就是把对应的数据接了上去,顶多也就是额外加一个可调节的强度参数。
然后回到场景,把光源(定向光)的Cast Shadows选项取消(因为卡渲阴影我们已经在Shading Model中计算过了),就看到完整的效果啦。
这里我加了外描边不过文中没讲,相关内容很简单知乎上也多得很,可以找一找,比如这个:
关于UE4物体描边(outline)的一些实现方法 - 知乎 (zhihu.com)









