Implementing a Celshading model directly into UE5.1 source. This celshading use a linear color curve atlas to drive all the values.
Learn how to set your own shading model, add a render target to GBuffer, storing information in the View struct, creating a global node for material graph. Some tips and tricks
We will see restriction about mobile and raytracing.
https://dev.epicgames.com/community/learning/tutorials/2R5x/unreal-engine-new-shading-models-and-changing-the-gbuffer
Hello @One3y3.
Great post, havenât dig deep into gbuffer section first but the rest of it is just great.
So I want to point out something that bothers me:
AFAIK the only communication between base pass (gbuffer) and deferred pass(lighting) is through gbuffer shading model.
The problem is shading model encoded in gbuffer (4byte), sharing 4byte with selective output mask. (Check EncodeShadingModelIDAndSelectiveOutputMask).
Now the code in tutorial using up to 16 (with SHADINGMODEL_NUM assigned to 16).
It might be works in this case, if itâs not involving raytrace, but it might be harder to extend it beyond this. (For example Toon model for hair, etc).
Changing the number of bits allocated to store GBuffer shading model is not preferrable (due to the amount of engine files touched - integration nightmare).
So I wonder if anyone has better approach to extends this beyond shading model id.
From what I saw in the gbuffer generation, Shadingmodelid is a int value packed into a uint4 with Outputmask, which then is converted into a float in the alpha channel (because textures are only 0-1 range).
I donât have any problem when enabling raytracing shadow / raytracing skylight and hardware Lumen in my case.
GBuffer is the only way to communicate specific material properties to the deffered light rendering pass. There is some global struct like the view structure, but itâs common to the whole scene. The only way would be to add your own subpass in between basepass and light pass. But thatâs like re-coding the whole engine and raytracing implementation
Hi! great post and thank you for the tutorial
Hello.
Here is the comment taken from ShadingCommon.ush
// SHADINGMODELID_* occupy the 4 low bits of an 8bit channel and SKIP_* occupy the 4 high bits
so in essence, ShadingModelID uses 1 channel on GBuffer, and in this channel shared half of them with OutputMask.
So effectively only 4 bits are allowed, which is 15 only.
I havenât found any issue so far, it might be I havenât used OutputMask extensively yet.
I am still struggling to find the proper solution to extend this ShaderModelID beyond 15 without not much gbuffer encoding modification. Still have so many things to learn and explore
@0ne3y3 thanks for this awesome guideâŚ
@pemjahat I dont know much about this but is extending to uint32 not a good solution for your case?
Anything changing the gbuffer packaging is a red flag at this moment.
Currently, this packed ShadingModel and MaterialMask only have room for uint8 in gbuffer.
I am exploring another solution, like Strata too see if itâs feasible.
Thanks again for your guide⌠this is working for me (havent done shadows yet), but Iâm a bit stuck on the curve atlas usage. Does the curve atlas require a specific type of compression, such as Alpha or Grayscale? If I dont use one of those, I get a stair-step effect at what seems to be the 0 value, please see attached picture.
Its like, the light response is taking up all of the Curve information.
Here is the curve:
I think there is something to do with âDisable Adjustmentsâ and Compression options within the Curve Atlas settings, but I donât have enough understanding. All I know is that Disable Adjusments preserves negative values in curves, but not sure how this applies here since my curve goes from 0,0 to 1,1⌠does it mean that the full range of light needs to be accounted for (including negatives?) Anyway, here are some things I have experimentedâŚ
If I âDisable Adjustmentsâ and plug Specular with the same node as Base Color (with atlas compression still Default), I get slightly better result, but donât fully know why:
If I keep Adjustments disabled, but change compression to Grayscale (or Alpha), it seems to give the proper result, including the light response. Iâm guessing that when plugging the same node as Base Color into Specular, I am essentially overwriting the light response to be that of Base Color, which is why it disappears, but this raises a question - If I do want to disable the light response, is that the correct way to do it? Setting roughness to 1 also works, maybe that is the correct way. Or should I be handling that in the Color Curve?. At this stage though, Iâm still unsure how the light response and the rest of the shading are encompassed within a single curve like mine:
I guess Iâm just more confused than ever Any help to clarify would be greatly appreciated! I know I have more learning to do regarding lighting, shaders etcâŚ
I didnât touch any settings on my curve atlas, except disabling adjustments. It didnât change anything with or without.
After checking my curve atlas settings, it set the texture to HDR (RGBA16F) by default.
You definitely need, at least RGBA 8bits no sRGB, so VectorDisplacementMap.
The curve is encoded into only 1 pixel height, so with the texture compression, it will blend between the curve at the bottom and at the top. Which is full white in your case. Also, no sRGB because itâs linear âmathâ data and will also blend your curves together.
On another topic.
I made a small correction, if we donât use the new gbuffer rendertarget. We need to add the default celshading to HasCustomData() in the HLSL file, else it doesnât write the curve selection value to GBuffer.CustomData.a .
Hey this is a good tutorial, still going through it but small typo found.
Our atlas RHI texture, its sampler and a global uint for the texture height (used later to determine which curve to choose).
Now we need to initialize our values in the constructor. Itâs MANDATORY because the texture and values wonât be sent at the start of the engine and the Renderthread really doesnât like nullptr (the engine will just crash).
In the file SceneManagement.h, go to the constructor FViewUniformShaderParameters::FViewUniformShaderParameters() and add at the end
Should be SceneManagement.cpp instead of the header file.
Thank you for this in-depth tutorial. I really love seeing the deeper dives and concepts on the forums and hope to one day also be able to add my own content to advanced concepts. Thanks again!
Hi, Thank you for the tutorial.
However I get the following error in UE5 Material Editor after going through step 8 B Celshading calculation:
â[SM5] /Engine/Private/ShadingModelsMaterial.ush:211:35: error: use of undeclared identifier âGetCelShadingSelection0â
GBuffer.CustomData.a = saturate(GetCelShadingSelection0(MaterialParameters)/View_CelshadingTextureHeight);
^â
Am I missing something?
Thanks
I can see 2 things.
-
You didnât added the Celshading node in your material graph
-
The name of the function isnât good in the celshading node class
virtual FString GetFunctionName() const override { return TEXT("GetCelShadingSelection"); }
Thank you very much! it works:
I can now make the cel-shading as shown above.
However the shadow part is seems still smooth:
I have already followed âCelshading shadow cast by other objectâ section but the shadow part is still seems smooth.
Am I still missing something in the shadow part?
Thanks!
The chairâs shadow on the ground is because your ground isnât using the celshading model.
The âsmooth shadowâ on the object itself is Lumen GI / ambient occlusion kicking in
Thanks. After I disable the Lumen GI, I get below:
Which is not a smooth shadow finally.
So does it means that if I want non-smooth shadow in this cel shading model, I cant use Lumen GI?
Another question is that I find the lit part is too bright, comparing to the base color of the texture.
You can see the color (yellow color) comparison below:
How can I control the brightness of the lit part?
Thanks so much again for your guidance and help.
The goal of global illumination (GI) is to make the light bounce of the surface and illuminate more or less the surface, so yes you will get smooth shade with Lumen GI.
For the color, you can tone down the intensity of your light, use a darker color or instead of using 1 for the celshading curve, use 0.75. Itâs up to you in your asset creation pipeline how to tackle this.
Thank you for the tutorial! Itâs so detailed.
It works on 5.3. Now I can apply this in my projects. Some problems left⌠Iâd like to use vertex color in my codes (maybe in ShadingModels.ush? Iâm not sure).
Any help would be appreciated
Thanks again!
I think I have a new problem⌠Now color can be only written in Base Color when Blend Mode is Opaque. W hen I change Blend Mode into Translucent, the whole model turn into black.
Hi 0ne3y3. First off. Thank you for this very detailed guide. Very insightful!
Iâm trying to go the Non GBuffer route. While I am proficient in C++, this is my first deep dive into messing with unreal ush/usf shaders so Iâm trying to put together the inferred pieces, specifically how you detail out how to write the function: âCelshadingPreintegratedSkinBxDFâ
Is this what you meant by " I copied PreintegratedSkinBxDF() and celshade the dot(N, L) * .5 + .5 . "
FDirectLighting CelshadingPreintegratedSkinBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
half3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer);
half Opacity = GBuffer.CustomData.a;
float CurveIndex = 1.0f / View.CelshadingTextureHeight;
float DotValueCelShaded = Texture2DSampleLevel(View.CelshadingAtlas, View.CelshadingSampler, CurveIndex, 0).b;
float OldDotValue = (dot(N, L) * .5 + .5);
//half3 PreintegratedBRDF = Texture2DSampleLevel(View.PreIntegratedBRDF, View.PreIntegratedBRDFSampler, float2(saturate(OldDotValue), 1 - Opacity), 0).rgb;
half3 PreintegratedBRDF = Texture2DSampleLevel(View.PreIntegratedBRDF, View.PreIntegratedBRDFSampler, float2(saturate(DotValueCelShaded), 1 - Opacity), 0).rgb;
Lighting.Transmission = AreaLight.FalloffColor * Falloff * PreintegratedBRDF * SubsurfaceColor;
return Lighting;
}
Thanks!!!