Add Custom Render Pass In UE5
Extending From Last Post
In my last post, I tried to write a plugin to use ush
file as a library file to write shader codes and include it in custom node in material editor:
Use HLSL in UE5 by C++ Plugin
This was a good start to set up the dependencies of modules and mapping the shader directory into the correct place.
With this structure then, I tried to extend it’s function and added an actual global shader as a custom render pass to Unreal’s render pipeline, more importantly, without editing the engine codes.
In this post I will note how I manage to do that.
How to
The Plugin Structure
Compared with last post, there are several files added.
- First group has Plugin cpp file
CTLib.cpp
and Plugin header fileCTLib.h
. In last post I’ve already got these two files,CTLib.cpp
is the one has shader directory mapping. - Second group has shader file
CTGlobalShader.usf
, shader header fileCTGlobalShader.h
and shader cpp fileCTGlobalShader.cpp
. - Third group has ViewExtension cpp file
CTSceneViewExtension.cpp
and ViewExtension header fileCTSceneViewExtension.h
.
Add Module to the Project
Compared with last post, there’re 4 project files needs to modified in addition in order to add a custom module into the project:
Shader Files
Shader Header File
CTGlobalShader.h
- In header file, it is a fixed format to setup shader type, shader, shader parameters in Unreal. I still use codes from
Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessBloomSetup.cpp
as reference to write my own.
Shader Cpp File
CTGlobalShader.cpp
- in cpp file, it implements the shader type by a macro
IMPLEMENT_SHADER_TYPE()
fromEngine\Source\Runtime\RenderCore\Public\Shader.h
.
Usf File
CTGlobalShader.usf
- I wrote a very simple shader as a start, it simply multiplies the scene color with an Input Color.
ApplyScreenTransform()
is a function from"/Engine/Private/ScreenPass.ush"
, to transform position to texture UV.FScreenTransform SvPositionToInputTextureUV
is a parameter needs to be transfer into when shader is used.- *1
ApplyScreenTransform()
:
This is used for transforming SV_POSITION
from (0 ~ bufferwith) x (0 ~ bufferheight) to 0 ~ 1 as a texture UV.
It will multiply SV_POSITION.xy
by a Scale and add a Bias.
FScreenTransform SvPositionToInputTextureUV
defined as a float4 composed with FVector2f Scale and FVector2f Bias, need to get this argument in my viewextension.cpp later
This way of transform coordinates is from most postprocessing shaders in Unreal, but I can also simply use:
float2 UV = float4(SvPosition.xy * (1 / ViewportUV.xy), 0.0f, 0.0f);
like below:
Where ViewportUV
is from FScreenPassTextureViewport(SceneColor).Rect.Width(), FScreenPassTextureViewport(SceneColor).Rect.Height()
later in the CTSceneViewExtension.cpp
file.
- SV_POSITION in Pixel Shader is the center position of a pixel, so that it has 0.5 offset for each pixel, when we scale SV_POSITION to 0 ~ 1 range, we need to translate pixel position back to (0,0), which is minus 0.5 firstly, then multiply its xy with an inverse scale factor
float4((SvPosition.xy - 0.5) * (1 / ViewportUV.xy), 0.0f, 0.0f)
.
Insert into the render pipeline
SceneViewExtension
There’s very few documentation about global shader in Unreal, one can be found is Adding Global Shaders to Unreal Engine, which is super outdated and need to modify the engine, have no idea how to implement it from this documentation :poop:
Well there is another interface provided by Unreal that can let us insert a pass in the post process passes: SceneViewExtensions
In its header file \Engine\Source\Runtime\Engine\Public\SceneViewExtension.h
, there’s surprisingly a large block of description:
So for this tool,
- it only supports deferred shading pipeline
- it can insert before or after different passes, in header file there’re functions end with _RenderThread:
Some documents saying it only works in post-processing stages and no mobile pipeline, but in the header file there’s after basepass option and mobile option, dk yet, need to give a try.
Anyway, I usePrePostProcessPass_RenderThread
that is the one called right before Post Processing rendering begins. - In its usage description above, the first step is to create a class that inherits from FSceneViewExtensionBase and first argument needs to be const FAutoRegister& AutoRegister.
- Second step is to inherit some virtual functions to set up FSceneView and FSceneViewFamily and type of RenderThread from ISceneViewExtension class.
- FSceneView is a class that manage projection from scene space into a 2D screen region, things like view matrix, actor being viewd, FOV, view frustum, near/far clipping plane and etc are also inside it. Check it out in
Engine\Source\Runtime\Engine\Public\SceneView.h
. - FSceneViewFamily is a set of SceneViews into a scene which only have different view transforms and owner actors.
- FSceneView is a class that manage projection from scene space into a 2D screen region, things like view matrix, actor being viewd, FOV, view frustum, near/far clipping plane and etc are also inside it. Check it out in
Then the third step is to register and initialize this class as using :
TSharedRef<FMyExtension,ESPMode::ThreadSafe> MyExtension;
MyExtension = FSceneViewExtensions::NewExtension(Param1, Param2);
Cpp File
*1 const FIntRect ViewPort = ...
Specify the viewport rect, it is retrieved from the FSceneView View object by casting it as an FViewInfo object and retrieving the view rect:
*2 RDG_EVENT_SCOPE(GraphBuilder, "CTRenderPass");
Use RDG_EVENT_SCOPE to add a GPU profile scope around passes. These are consumed by external profilers like RenderDoc, as well as RDG Insights.
*3 Create a Point sampler.
*4 Scene Color is updated incrementally through the post process pipeline: FPostProcessingInputs
If we dont have the FPostProcessingInputs argument, for example in PostRenderBasePassDeferred_RenderThread, can alternatively use const FSceneTextures& SceneTextures = FSceneTextures::Get(GraphBuilder);
to retrieve the scene textures.
*5 ChangeTextureBasisFromTo(TextureViewport, SrcBasis, DestBasis)
:
*6 The first argument:
FScreenPassTextureViewport
:
// 描述包含在纹理范围内的视图矩形。用于导出纹理坐标变换。
This will let us get TextureViewport.Extent
and TextureViewport.Rect
.
*7 The second argument:
ETextureBasis::TexelPosition
*8 The third argument:
ETextureBasis::ViewportUV
:point_up_2:
From 5 to 8, these are aming to get a proper scale and bias value to do the transform later in shader to get a proper scene texture uv, this part of code is from postprocess shaders in Unreal such as Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessBloomSetup.cpp
.
I tried to understand what ChangeTextureBasisFromTo
function does, it’s comparing the last two arguments SrcBasis and DestBasis, and both of them are from a enum class ETextureBasis, having 4 different texture coordinate basis:
ScreenPosition, ViewportUV, TexelPosition, TextureUV
These 4 texture coordinate basis have different range as in the comments above each of them (in the screenshot above), it looks like the four enums are arranged in a progressive order, then in ChangeTextureBasisFromTo
function, by comparing the source and destination enum, it makes a corresponding calculation that returns a vector4 value composed by FVector2f Scale and FVector2f Bias.
Take codes from PostProcessBloomSetup.cpp
as example:
The Output
is the render target where pixels will draw on, and SceneColor
has the viewportUV we want, the first ChangeTextureBasisFromTo turn from TexelPosition to ViewportUV, the second call turn from ViewportUV to TextureUV, multiply the two calls results together we get a scale and bias transfrom from TexelPosition to TextureUV. Then this vector4 can be used in shader.
In my cpp code above, I put SceneColor as FScreenPassTextureViewport argument in both calls, cuz my output render target is also SceneColor. But in my particular case I know what exact coordinates I need, so I just use another simply way to get screen texture UV, it is:
CTGlobalShaderParameters->ViewportUV = FVector2f(FScreenPassTextureViewport(SceneColor).Rect.Width(),FScreenPassTextureViewport(SceneColor).Rect.Height());
*9 FPixelShaderUtils::AddFullscreenPass
: Dispatch a pixel shader to render graph builder with its parameters.
Header File
MYMODULE_API
makes the entire class or functions to be callable outside of module dll. This header fille I took this file from a plugin named Color Correct Regions as a reference: Engine\Plugins\Experimental\ColorCorrectRegions\Source\ColorCorrectRegions\Public\ColorCorrectRegionsSceneViewExtension.h
Use Custom Module
I simply call the module in an Actor c++ class which built from the engine, then drag that actor into a scene and in play mode it will be called and run.
This is the result that shader applied on screen:
If you capture in RenderDoc, the pass is under PostProcessing pass:
What’s Next
So at this point I’ve managed to finish a frame of adding a custom render pass and custom shaders, then I can start to add more interesting stuff in my global shader to make more complex effect such as a post processing shader or a debug tool shader or etc. It will be a good start to know deeper about Unreal render pipeline and graphics API.