I’ve been wanting to create a custom shader model for my project for a while now, since before Unreal 5 and resources on it are few and far between. This thread will be a working journal as I (try to) get this figured out, and including all the information so others can follow along. If you have successful done this, or are working on it, please let me know! I’d love to be able to collaborate on this (or at least share info, two heads are better than one and I have no idea what I’m doing!) Follow me on twitter @DMevile if you want to follow the struggles. When I have new info I will collect it here. And also in this github fork: https://github.com/DMeville/UnrealEngine-Custom-Substrate/tree/5.4
This is for substrate only. If you are not using substrate, there ARE some guides you can follow.
The current goal is to create a custom Substrate BSDF that has a few extra custom pins in order to:
- Do ramped (toon) diffuse lighting via a “Shadow Sharpness” float pin
- Do shadow-masked rimlighting via a “Rimlight Intensity” float pin
- Do ramped (toon) specular for hair highlights via a “Specular Sharpness” float pin
This will require using a source code build of Unreal Engine 5 from github (I am using 5.4). I am using Jetbrains Rider as my coding environment.
First step is to download the engine and compile it (takes a while) and get the editor up and running. Instructions are on the UE5 github page. Once you have the editor open, make a new project and create a new scene with some objects that are using a material with the Substrate Slab BSDF directly.
Order of things I’m going to try:
- First, how to hard code lighting changes on the default BSDF evaluation. This will show me where the final code changes need to happen.
- How to create a custom pin on the default BSDF material node
- How to take this custom pin data and use it to drive code lighting changes
- Take all these changes and make a custom BSDF node with all these options.
Hard Coding Lighting Change
Once you have a working source build and enable substrate. You can change the lighting calculation for the standard slab BSDF pretty easily. This happens in SubstrateEvaluation.ush, in the SubstrateEvaluateBSDFCommon method, Sample.IntegratedDiffuseValue is the final diffuse lighting. By modifying the AreaLightContext.NoL term we can affect how lighting affects the object, so we can just step this value to get toon-lighting.
Replace that line with this, using a ShadowHardness value of 0 to get fully sharp shadows, or 1.0 to get normal non-toon shadows.
Can also do wrap lighting or whatever other kind of lighting you want. But I am doing it like this so that once we have custom pin data, we can just use that data to drive the shadow hardness directly and give it up to the material to control it.
float ShadowHardness = 0.0f;
float NoLStep = smoothstep(0.0, ShadowHardness , AreaLightContext.NoL);
Sample.IntegratedDiffuseValue += (((ShadowTerms.SurfaceShadow) * (NoLStep) * AreaLightContext.Falloff) * Sample.DiffusePathValue * AreaLightContext.AreaLight.FalloffColor)
We can also add a shadow-masked rimlight here easily using a very sharp fresnel and use the shadowmask to scale it away for parts of the mesh in cast shadow.
float3 rimlight = saturate(pow(((1.0 - saturate(NoV)) + 0.25), 50)) * ShadowTerms.SurfaceShadow;
float3 finalRimlight = rimlight*AreaLightContext.AreaLight.FalloffColor*AreaLightContext.Falloff; //scale the rimlight by the lighting falloff and colour, makes it work with other (non directional) lights too
and then just append this finalRimlight at the end of Sample.IntegratedDiffuseValue line to have
Sample.IntegratedDiffuseValue += (((ShadowTerms.SurfaceShadow) * (NoLStep) * AreaLightContext.Falloff) * Sample.DiffusePathValue * AreaLightContext.AreaLight.FalloffColor) + finalRimlight;
Right click the UE5 project in the engine folder, click “Build Selected projects” and wait until it succeeds. Then right click it again and hit “Debug UE5” and it will launch the engine and the project and you should see all lighting is now rim-lit and with hard shadows like this.
For specular it’s just a bit farther down in the same method I think. Find the line “Sample.IntegratedSpecularValue”. I’m just replacing this line with and it seems to work okay for now, but need to look into it more as.
Sample.IntegratedSpecularValue = smoothstep(0.46, 0.5, Sample.SpecularPathValue);
You could just stop here this looks cool and works across all objects using the basic BSDF. But I want to be able to have more control, able to turn the rimlight on/off for different objects and such.
Custom Material Node Pins
Adding a custom pin on an existing BSDF node is actually easy. Happens in the MaterialExpressionSubstrate.h, just find the UMaterialExpressionSubstrateSlabBSDF and add (after the GlintUV property) this
UPROPERTY()
FExpressionInput Custom0;
Next is a case of following where this code goes, and understanding how substrate stores it and how I can access it over on the lighting calculation area to scale the rimlight etc.