Okay so lets break these changes down in how I understand this entire thing works.
Adding a Material pin
Is straightforward. In MaerialExpressionsSubstrate.h UMaterialExpressionSubstrateSlabBSDF method, add your pin after GlintUV near the bottom
UPROPERTY()
FExpressionInput Custom0;
And then we have add a few more changes in MaterialExpressions.cpp on the method overrides for the UMaterialExpressionSubstrateSlabBSDF::GetInputType method we just add a new branch to the if that corresponds where the pin is on the node. Since we added the pin after GlintUV we add our branch in the same location. This Custom0 pin will be pin 18, so we just gotta make sure it’s type is what we want (float)
}
else if(InputIndex == 18)
{
return MCT_Float; //Custom 0
}
In the same file, we have to fix up UMaterialExpressionSubstrateSlabBSDF::GetInputType so the pin will have the correct label to whatever we want.
}
else if(InputIndex == 18)
{
return TEXT("Custom 0");
}
In the same file again. We have to add our material input pin and compile it to an int32 “Code Chunk”. This happens on line 24472ish in SlabBSDF::Compile. Adding it after the GlintUVCodeChunk…
int32 Custom0CodeChunk = CompileWithDefaultFloat1(Compiler, Custom0, 0.0f); //Custom 0
We need to pass this into the OutputCodeChunk when we compile the full slab a bit below in the Compiler->SubstrateSlabBSDF() call. So add your Custom0CodeChunk below the GintUVCodeChunk as a parameter there.
Substrate Slab BSDF
Now that we changed the SubstrateSlabBSDF() method signature, we have to go through and change all usages of this to allow for the extra parameter we added. This happens in a few places…
- HLSLMaterialTranslator.h line 1233
- HLSLMaterialTranslator.cpp line 12697
- MaterialCompiler.h line 1257, 1273, and 625ish
SubsrateLegacyConversion.ush line 104ish the return GetSubstrateSlab call needs default values for our new params too. Adding a 0.0f after the glintUV line.
With all those changes we should now have everywhere important so we can write out the substrate material…
This happens in HLSLMaterialTranslator.cpp line 12729ish there’s a big long string in the return AddCodeChunk line. We just need to add another “%s,” and then add a new
*SubstrateGetCastParameterCode(Custom0, MCT_Float),
Line after the GlintUV line again. This is getting the pin data and writing it to the compiled material successfully. Which, when called, will call the GetSubstrateSlabBSDF method we just modified with extra params.
We also have to do the same thing again line 12771ish.
And finally, have to full out some default values in DebugProbes.ush line 121ish, adding in 0.0f after the glint parameter.
Packing, Unpacking
All the substrate stuff happens in Substrate.ush. Here we are gunna create a macro for our data, so we can easily bitpack it, and unpack it. This is just how substrate works.
So first lets create a new float value on our FSubstrateBSDF line 184ish, under haziness
float Custom0;
Line 608ish, after the #endif for the SUBSTRATE_INLINE_SINGLELAYERWATER stuff we add a macro to access this for ease of use (and that’s how all the other packed values do it).
#define SLAB_CUSTOM0(X) X.Custom0
Note, that macro does not and should not have a semicolon at the end of it!!
We’re gunna add a new parameter to the method signature of InternalGetSubstateSlabBSDF on line 1346ish of our float Custom0 again always after glints.
Inside this method at line 1483ish, under the SLAB_ROUGHNESS call we are going to add
SLAB_CUSTOM0 (SubstrateData.InlinedBSDF) = Custom0;
To take the data from the function call and store it in our new float.
We need to add our float params to the method signature of GetSubstrateSlabBSDF on line 1529 and 1569ish, also, and fill in each return InternalGetSubstrateSlabBSDF call to pass through our added parameter also.
And finally add some default values to the call to GetSubstrateSlabBSDF on line 3710ish, adding 0.0 under the GlintUV param.
Now for packing. This happens in SubstrateExport.ush in the PackSubstrateOut method, On line 527 we are going to after the closing brace for the //Data1 block, we are going to add another code block that will take our float Custom0, pack it into a uint, and store it on the substrate header.
Here I am packing the same value 4 times, to show how you can pack multiple values. But you could just pack the single Custom0 once.
//Data2 (Custom0)
{
const uint PackedCustom0Data = PackR8(SLAB_CUSTOM0(BSDF));
const uint PackedCustom1Data = PackR8(SLAB_CUSTOM0(BSDF));
const uint PackedCustom2Data = PackR8(SLAB_CUSTOM0(BSDF));
const uint PackedCustom3Data = PackR8(SLAB_CUSTOM0(BSDF));
const uint ReplicatedData = PackedCustom0Data | (PackedCustom1Data << 8) | (PackedCustom2Data << 16) | (PackedCustom3Data << 24);
SUBSTRATE_STORE_UINT1(ReplicatedData);
}
Then to unpack it. This happens back in Substrate.ush in the UnpackFastPathSubstrateBSDFIn method. We need to do a few things.
First, add this line under the uint Data1 variable on line 3932. Since we added a new uint1 to our substrate, we need to get it out.
uint Data2 = SubstrateLoadUint1(SubstrateBuffer, SubstrateAddressing); //custom 0 x 4 channels
Then we need to unpack the floats from that buffer. So below the PackedDiffuse20Bits variable on line 3943 add the following line. Again showing how to pack 4 values, but since we packed the same value 4 times all these will have our pin data.
const uint PackedCustomData0_8Bits = 0xFF & (Data2); //custom 0
const uint PackedCustomData1_8Bits = 0xFF & (Data2 >> 8); //custom 1
const uint PackedCustomData2_8Bits = 0xFF & (Data2 >> 16); //custom 2
const uint PackedCustomData3_8Bits = 0xFF & (Data2 >> 24); //custom 3
And then a bit below on line 3953 under the SLAB_ANISOTROPY line, we are going to take our now unpacked data add assign it to our SLAB_CUSTOM0 macro.
SLAB_CUSTOM0(OutBSDF) = UnpackR8(PackedCustomData0_8Bits); // Custom0
We can now USE this SLAB_CUSTOM0 value in our actual shader code to do whatever we want with! Huzzah!
Using The Data
Going back to the first post in this thread. We can now just use this macro to control our rimlight or shadow sharpness.
In SubstrateEvaluation.ush line 392ish. Before the Sample.IntegratedDiffuseValue we can grab the lighting data, calculate a rimlight, or do whatever we want here.
Here I have two custom values, SLAB_CUSTOM0, and SLAB_CUSTOM1 which control both effects.
float ShadowHardness = SLAB_CUSTOM1(BSDFContext.BSDF); // value of 0 is hard shadows, 1 is normal lighting
float NoLStep = smoothstep(0.0, ShadowHardness , AreaLightContext.NoL); //calculate the stepped shadow
float rimlightScale = 1.0f; //We want to eventually hook this up to a custom pin float
rimlightScale = SLAB_CUSTOM0(BSDFContext.BSDF);
float3 rimlight = saturate(pow(((1.0 - saturate(NoV)) + 0.25), 50)) * ShadowTerms.SurfaceShadow * rimlightScale; //sharp fresnel rimlight, scaled by SurfaceShadow mask
float3 finalRimlight = rimlight*AreaLightContext.AreaLight.FalloffColor*AreaLightContext.Falloff; //scaled by light params so it works with all light types (?)
Sample.IntegratedDiffuseValue += (((ShadowTerms.SurfaceShadow) * (NoLStep) * AreaLightContext.Falloff) * Sample.DiffusePathValue * AreaLightContext.AreaLight.FalloffColor) + finalRimlight;
And vooolia! It works!
Source code is available in the link to the github fork of 5.4 in the original post. Will work to clean it up a little bit and make sure it actually works in all cases. But for now, very happy to have this working and can make some cool lighting tricks for my game, toon, half-lambert, stylized spec, whatever!