Unreal Engine 4 – Custom Lens Flares

Sometimes you want to have more options for the lens flare effects, like halo or better glare. One way is turn on a convolution bloom (4.16+) to get a better glare result or add the particles with a certain texture and material or use UI Widgets and transfer the camera/view vectors from a certain blueprints (that case you can have a lot of problems with occlusion factors), etc.. There is another option – to have something custom in order to have better controls or options or additional effects using the existing bloomOutput. In this particular example I will add only glare and simple halo using user textures, but it’s easy to add own different/additional functionality after you will add a simple stuff.

Default

ue4-custom-lens-flares-01

Custom

ue4-custom-lens-flares-02
Let’s started! First lets make a controls and a switch on/off for our halo and glare, you can add your own controls in the same way.. To see your changes you need have a UE4 project generated for building your own custom Editor (I use VS2015).

..\Engine\Source\Runtime\Engine\Classes\Engine\Scene.h and add this near other structs:

UENUM()
 enum ECustomLensFlareSwitch
 {
 LENSFLARES_Default UMETA(DisplayName = "Default"),
 LENSFLARE_Custom UMETA(DisplayName = "Custom"),
 };

After in the same file add this lines in the FPostProcessSettings struct, we adding the bOverrides for future controls.

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMethod:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMainHalo:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMainHaloSize:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareHaloIntensity:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMainHaloOffset:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMainGlare:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareMainGlareSize:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareIntensity:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareTint:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareTints:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Overrides, meta = (PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_CustomLensFlareThreshold:1;

Next will be register the properties (good thing is when you write a commentary above the UPROPERTY everyone able to see them in the engine, in pop-up description window). You can change the Category to desired one btw..

// LensFlare Switch: Default (Will use the Lens Flares rollout) and Custom (will use th Custom Lens Flare rollout).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (editcondition = "bOverride_CustomLensFlareMethod", DisplayName = "Lens Flare Method"))
TEnumAsByte<enum ECustomLensFlareSwitch> CustomLensFlareSwitch;

// Define the Main Halo texture.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (editcondition = "bOverride_CustomLensFlareMainHalo", DisplayName = "Main Halo Texture"))
class UTexture* CustomLensFlareMainHalo;

// The Main Halo Texture scale.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "0.0", UIMax = "100.0", editcondition = "bOverride_CustomLensFlareMainHaloSize", DisplayName = "Custom Halo Size"))
float CustomLensFlareMainHaloSize;

// The Main Halo bright intensity.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "0.0", UIMax = "250.0", editcondition = "bOverride_CustomLensFlareHaloIntensity", DisplayName = "Custom Halo Intensity"))
float CustomLensFlareHaloIntensity;

// The Main Halo offset.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "-5.0", UIMax = "5.0", editcondition = "bOverride_CustomLensFlareMainHaloOffset", DisplayName = "Custom Halo Offset"))
float CustomLensFlareMainHaloOffset;

// Define the Main Glare texture.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (editcondition = "bOverride_CustomLensFlareMainGlare", DisplayName = "Main Glare Texture"))
class UTexture* CustomLensFlareMainGlare;

// The Main Glare Texture scale.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "0.0", UIMax = "100.0", editcondition = "bOverride_CustomLensFlareMainGlareSize", DisplayName = "Custom Glare Size"))
float CustomLensFlareMainGlareSize;
 
// The Main Glare bright intensity.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "0.0", UIMax = "250.0", editcondition = "bOverride_CustomLensFlareIntensity", DisplayName = "Custom Glare Intensity"))
float CustomLensFlareIntensity;

// Tint color for the Custom Lens Flares. Will aply for all elements.
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (editcondition = "bOverride_CustomLensFlareTint", DisplayName = "Custom Flare Tint", HideAlphaChannel))
FLinearColor CustomLensFlareTint;

// Define the color for each element. 0 - Halo, 1 - Glare.
UPROPERTY(EditAnywhere, Category = "Lens|Custom Lens Flares", meta = (editcondition = "bOverride_CustomLensFlareTints", DisplayName = "Halo and Glare Tints"))
FLinearColor CustomLensFlareTints[2];

// Custom Lens Flare Threshold
UPROPERTY(interp, BlueprintReadWrite, Category = "Lens|Custom Lens Flares", meta = (UIMin = "0.1", UIMax = "32.0", editcondition = "bOverride_CustomLensFlareThreshold", DisplayName = "Custom Threshold"))
float CustomsLensFlareThreshold;

And setup the default values for our just created parameters:

CustomLensFlareSwitch = LENSFLARES_Default; // default lens flares
CustomLensFlareMainHalo = 70.0f;
CustomLensFlareMainGlareSize = 40.0f;
CustomLensFlareIntensity = 5.0f;
CustomLensFlareHaloIntensity = 7.0f;
CustomLensFlareMainHaloOffset = 0.5f;
CustomLensFlareTints[0] = FLinearColor(1.1f, 0.91f, 0.44f, 0.6f);
CustomLensFlareTints[1] = FLinearColor(1.2f, 0.79f, 0.45f, 0.53f);
CustomLensFlareTint = FLinearColor(1.0f, 1.0f, 1.0f);
CustomLensFlareThreshold = 32.0f;

We finished with Scene.h now let’s move to the ..\Engine\Source\Runtime\Engine\Private\SceneView.cpp to the FSceneView::OverridePostProcessSettings:

LERP_PP(CustomLensFlareMainHalo);
LERP_PP(CustomLensFlareMainGlareSize);
LERP_PP(CustomLensFlareIntensity);
LERP_PP(CustomLensFlareHaloIntensity);
LERP_PP(CustomLensFlareMainHaloOffset);
LERP_PP(CustomLensFlareThreshold);
LERP_PP(CustomLensFlareTint);

IF_PP(CustomLensFlareMainHalo)
{
 Dest.CustomLensFlareMainHalo = Src.CustomLensFlareMainHalo;
}

IF_PP(CustomLensFlareMainGlare)
{
 Dest.CustomLensFlareMainGlare = Src.CustomLensFlareMainGlare;
}

if (Src.CustomLensFlareSwitch)
{
 Dest.CustomLensFlareSwitch = Src.CustomLensFlareSwitch;
}

if (Src.bOverride_CustomLensFlareTints)
{
 for (uint32 i = 0; i < 2; ++i)
 {
 Dest.CustomLensFlareTints[i] = FMath::Lerp(Dest.CustomLensFlareTints[i], Src.CustomLensFlareTints[i], Weight);
 }
}

Now you can build your UE4 solution and you will have something like this:

ue4-custom-lens-flares-03

It will do nothing yet 🙂 But this is a good step to understand how to add new feature controls and options to the PostProcess settings.

The next step will be adding a new files .cpp and .h, go to the ../Engine/Source/Runtime/Renderer/Private/PostProcess/ and create TWO new files:

PostProcessCustomLensFlares.cpp

PostProcessCustomLensFlares.h

You can do this directly in the Visual Studio, simple go to the specific folder in Solution Explorer and Add New Item.

ue4-custom-lens-flares-04

The PostProcessCustomLensFlares.h file (you could use own parameter names, I use the same as a default Lens Flares):

#include "CoreMinimal.h"
#include "RendererInterfCustom.h"
#include "PostProcess/RenderingCompositionGraph.h"

// ePId_Input0: Bloom
// ePId_Input1: Lensflare image input
// derives from TRenderingCompositePassBase<InputCount, OutputCount> 
class FRCPassPostProcessCustomLensFlares : public TRenderingCompositePassBase<2, 1>
{

public:
 // constructor
 FRCPassPostProcessCustomLensFlares(float InHaloSize, float InGlareSize, float InThreshold, bool bCompositBloomIn = true);

 // interfCustom FRenderingCompositePass ---------

 virtual void Process(FRenderingCompositePassContext& Context) override;
 virtual void Release() override { delete this; }
 virtual FPooledRenderTargetDesc ComputeOutputDesc(EPassOutputId InPassOutputId) const override;
 
private:
 bool bCompositeBloom;
 float InHaloSize;
 float InGlareSize;
 float Threshold;
};

The PostProcessCustomLensFlares.cpp file:

#include "PostProcessCustomLensFlares.h"
#include "StaticBoundShaderState.h"
#include "SceneUtils.h"
#include "PostProcess/SceneRenderTargets.h"
#include "PostProcess/SceneFilterRendering.h"
#include "PostProcess/PostProcessing.h"
#include "ClearQuad.h"
#include "PipelineStateCache.h"

/** Encapsulates a simple copy pixel shader. */
template <bool bClearRegion = false>
class FPostProcessCustomLensFlaresBasePS : public FGlobalShader
{
 DECLARE_SHADER_TYPE(FPostProcessCustomLensFlaresBasePS, Global);

static bool ShouldCache(EShaderPlatform Platform)
 {
 return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
 }

/** Default constructor. */
 FPostProcessCustomLensFlaresBasePS() {}

public:
 FPostProcessPassParameters PostprocessParameter;
 FShaderParameter CompositeBloomParameter;

/** Initialization constructor. */
 FPostProcessCustomLensFlaresBasePS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
 : FGlobalShader(Initializer)
 {
 PostprocessParameter.Bind(Initializer.ParameterMap);
 }

// FShader interfCustom.
 static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment)
 {

FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment);
 if (bClearRegion)
 {
 OutEnvironment.SetDefine(TEXT("CLEAR_REGION"), 1);
 }
 }

virtual bool Serialize(FArchive& Ar) override
 {
 bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
 Ar << PostprocessParameter;
 return bShaderHasOutdatedParameters;
 }

void SetParameters(const FRenderingCompositePassContext& Context)
 {
 const FPixelShaderRHIParamRef ShaderRHI = GetPixelShader();

FGlobalShader::SetParameters<FViewUniformShaderParameters>(Context.RHICmdList, ShaderRHI, Context.View.ViewUniformBuffer);

PostprocessParameter.SetPS(ShaderRHI, Context, TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI());
 }
};

#define IMPLEMENT_LENSE_FLARE_BASE(_bClearRegion) \
typedef FPostProcessCustomLensFlaresBasePS< _bClearRegion > FPostProcessCustomLensFlaresBasePS##_bClearRegion ;\
IMPLEMENT_SHADER_TYPE(template<>,FPostProcessCustomLensFlaresBasePS##_bClearRegion ,TEXT("PostProcessCustomLensflares"),TEXT("CopyPS"),SF_Pixel);

IMPLEMENT_LENSE_FLARE_BASE(true)
IMPLEMENT_LENSE_FLARE_BASE(false)

#undef IMPLEMENT_LENSE_FLARE_BASE

/** Encapsulates the post processing vertex shader with an option to choose what type we will be using for. */
class FPostProcessCustomLensFlaresVS : public FGlobalShader
{
 DECLARE_SHADER_TYPE(FPostProcessCustomLensFlaresVS, Global);

static bool ShouldCache(EShaderPlatform Platform)
 {
 return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
 }

/** Default constructor. */
 FPostProcessCustomLensFlaresVS() {}

public:
 FPostProcessPassParameters PostprocessParameter;
 FShaderParameter TileCountAndSize;
 FShaderParameter KernelSize;
 FShaderParameter ColorScale;
 FShaderParameter LensflareType;
 FShaderParameter CustomLensFlareMainHaloOffset;

/** Initialization constructor. */
 FPostProcessCustomLensFlaresVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
 : FGlobalShader(Initializer)
 {
 PostprocessParameter.Bind(Initializer.ParameterMap);
 TileCountAndSize.Bind(Initializer.ParameterMap, TEXT("TileCountAndSize"));
 KernelSize.Bind(Initializer.ParameterMap, TEXT("KernelSize"));
 ColorScale.Bind(Initializer.ParameterMap, TEXT("ColorScale"));
 LensflareType.Bind(Initializer.ParameterMap, TEXT("LensflareType"));
 CustomLensFlareMainHaloOffset.Bind(Initializer.ParameterMap, TEXT("CustomLensFlareMainHaloOffset"));
 }

// FShader interfCustom.
 virtual bool Serialize(FArchive& Ar) override
 {
 bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
 Ar << PostprocessParameter << TileCountAndSize << KernelSize << ColorScale << LensflareType << CustomLensFlareMainHaloOffset;
 return bShaderHasOutdatedParameters;
 }

/** to have a similar interfCustom as all other shaders */
 void SetParameters(const FRenderingCompositePassContext& Context, FIntPoint TileCountValue, uint32 TileSize, float PixelKernelSize, float Threshold, float inLensflareType)
 {
 const FVertexShaderRHIParamRef ShaderRHI = GetVertexShader();

FGlobalShader::SetParameters<FViewUniformShaderParameters>(Context.RHICmdList, ShaderRHI, Context.View.ViewUniformBuffer);

PostprocessParameter.SetVS(ShaderRHI, Context, TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI());

{
 FIntRect TileCountAndSizeValue(TileCountValue, FIntPoint(TileSize, TileSize));

SetShaderValue(Context.RHICmdList, ShaderRHI, TileCountAndSize, TileCountAndSizeValue);
 }

{
 // only approximate as the mip mapping doesn't produce accurate brightness scaling
 FVector4 ColorScaleValue(1.0f / FMath::Max(1.0f, PixelKernelSize * PixelKernelSize), Threshold, 0, 0);

SetShaderValue(Context.RHICmdList, ShaderRHI, ColorScale, ColorScaleValue);
 }

{
 FVector4 KernelSizeValue(PixelKernelSize, PixelKernelSize, 0, 0);

SetShaderValue(Context.RHICmdList, ShaderRHI, KernelSize, KernelSizeValue);
 SetShaderValue(Context.RHICmdList, ShaderRHI, LensflareType, inLensflareType);
 SetShaderValue(Context.RHICmdList, ShaderRHI, CustomLensFlareMainHaloOffset, Context.View.FinalPostProcessSettings.CustomLensFlareMainHaloOffset);
 }
 }
};

IMPLEMENT_SHADER_TYPE(, FPostProcessCustomLensFlaresVS, TEXT("PostProcessCustomLensflares"), TEXT("MainVS"), SF_Vertex);


/** Encapsulates the post processing lens flare halo shader. */
class FPostProcessCustomLensFlaresPS : public FGlobalShader
{
 DECLARE_SHADER_TYPE(FPostProcessCustomLensFlaresPS, Global);

static bool ShouldCache(EShaderPlatform Platform)
 {
 return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
 }

/** Default constructor. */
 FPostProcessCustomLensFlaresPS() {}

public:
 FPostProcessPassParameters PostprocessParameter;
 FShaderParameter Intensity;
 FShaderParameter FlareColor;
 FShaderParameter TexScale;
 FShaderResourceParameter MainHaloTexture;
 FShaderResourceParameter MainHaloTextureSampler;

/** Initialization constructor. */
 FPostProcessCustomLensFlaresPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
 : FGlobalShader(Initializer)
 {
 PostprocessParameter.Bind(Initializer.ParameterMap);
 Intensity.Bind(Initializer.ParameterMap, TEXT("Intensity"));
 FlareColor.Bind(Initializer.ParameterMap, TEXT("FlareColor"));
 TexScale.Bind(Initializer.ParameterMap, TEXT("TexScale"));
 MainHaloTexture.Bind(Initializer.ParameterMap, TEXT("MainHaloTexture"));
 MainHaloTextureSampler.Bind(Initializer.ParameterMap, TEXT("MainHaloTextureSampler"));
 }

// FShader interfCustom.
 virtual bool Serialize(FArchive& Ar) override
 {
 bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
 Ar << PostprocessParameter << Intensity << FlareColor << MainHaloTexture << MainHaloTextureSampler << TexScale;
 return bShaderHasOutdatedParameters;
 }

void SetParameters(const FRenderingCompositePassContext& Context, FVector2D TexScaleValue)
 {
 const FPixelShaderRHIParamRef ShaderRHI = GetPixelShader();

FTextureRHIParamRef TextureRHI = GWhiteTexture->TextureRHI;

// adding Main Halo texture
 if (Context.View.FinalPostProcessSettings.CustomLensFlareMainHalo)
 {
 FTextureResource* MainHaloTextureResource = Context.View.FinalPostProcessSettings.CustomLensFlareMainHalo->Resource;

if (MainHaloTextureResource && MainHaloTextureResource->TextureRHI)
 {
 TextureRHI = MainHaloTextureResource->TextureRHI;
 }
 }

FGlobalShader::SetParameters<FViewUniformShaderParameters>(Context.RHICmdList, ShaderRHI, Context.View.ViewUniformBuffer);
 PostprocessParameter.SetPS(ShaderRHI, Context, TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI());

SetTextureParameter(Context.RHICmdList, ShaderRHI, MainHaloTexture, MainHaloTextureSampler, TStaticSamplerState<SF_Bilinear, AM_Border, AM_Border, AM_Clamp>::GetRHI(), TextureRHI);

SetShaderValue(Context.RHICmdList, ShaderRHI, TexScale, TexScaleValue);
 }
};

IMPLEMENT_SHADER_TYPE(, FPostProcessCustomLensFlaresPS, TEXT("PostProcessCustomLensflares"), TEXT("MainPS"), SF_Pixel);


/** Encapsulates the post processing lens flare glare shader. */
class FPostProcessCustomLensFlaresGlarePS : public FGlobalShader
{
 DECLARE_SHADER_TYPE(FPostProcessCustomLensFlaresGlarePS, Global);

static bool ShouldCache(EShaderPlatform Platform)
 {
 return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
 }

/** Default constructor. */
 FPostProcessCustomLensFlaresGlarePS() {}

public:
 FPostProcessPassParameters PostprocessParameter;
 FShaderParameter Intensity;
 FShaderParameter FlareColor;
 FShaderParameter TexScale;
 FShaderResourceParameter MainGlareTexture;
 FShaderResourceParameter MainGlareTextureSampler;


 /** Initialization constructor. */
 FPostProcessCustomLensFlaresGlarePS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
 : FGlobalShader(Initializer)
 {
 PostprocessParameter.Bind(Initializer.ParameterMap);
 Intensity.Bind(Initializer.ParameterMap, TEXT("Intensity"));
 FlareColor.Bind(Initializer.ParameterMap, TEXT("FlareColor"));
 TexScale.Bind(Initializer.ParameterMap, TEXT("TexScale"));
 MainGlareTexture.Bind(Initializer.ParameterMap, TEXT("MainGlareTexture"));
 MainGlareTextureSampler.Bind(Initializer.ParameterMap, TEXT("MainGlareTextureSampler"));

}

// FShader interfCustom.
 virtual bool Serialize(FArchive& Ar) override
 {
 bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
 Ar << PostprocessParameter << Intensity << FlareColor << MainGlareTexture << MainGlareTextureSampler << TexScale;
 return bShaderHasOutdatedParameters;
 }

void SetParameters(const FRenderingCompositePassContext& Context, FVector2D TexScaleValue)
 {
 const FPixelShaderRHIParamRef ShaderRHI = GetPixelShader();

FTextureRHIParamRef TextureRHI = GWhiteTexture->TextureRHI;

// adding Main Halo texture
 if (Context.View.FinalPostProcessSettings.CustomLensFlareMainGlare)
 {
 FTextureResource* MainGlareTextureResource = Context.View.FinalPostProcessSettings.CustomLensFlareMainGlare->Resource;

if (MainGlareTextureResource && MainGlareTextureResource->TextureRHI)
 {
 TextureRHI = MainGlareTextureResource->TextureRHI;
 }
 }

FGlobalShader::SetParameters<FViewUniformShaderParameters>(Context.RHICmdList, ShaderRHI, Context.View.ViewUniformBuffer);
 PostprocessParameter.SetPS(ShaderRHI, Context, TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI());

SetTextureParameter(Context.RHICmdList, ShaderRHI, MainGlareTexture, MainGlareTextureSampler, TStaticSamplerState<SF_Bilinear, AM_Border, AM_Border, AM_Clamp>::GetRHI(), TextureRHI);

SetShaderValue(Context.RHICmdList, ShaderRHI, TexScale, TexScaleValue);
 }
};

IMPLEMENT_SHADER_TYPE(, FPostProcessCustomLensFlaresGlarePS, TEXT("PostProcessCustomLensflares"), TEXT("MainPS2"), SF_Pixel);

FRCPassPostProcessCustomLensFlares::FRCPassPostProcessCustomLensFlares(float InHaloSize, float InGlareSize, float InThreshold, bool InbCompositeBloom )
 : HaloSize(InHaloSize), GlareSize(InGlareSize), Threshold(InThreshold), bCompositeBloom(InbCompositeBloom)
{
}

void FRCPassPostProcessCustomLensFlares::Process(FRenderingCompositePassContext& Context)
{
 SCOPED_DRAW_EVENT(Context.RHICmdList, LensFlares);

const FPooledRenderTargetDesc* InputDesc1 = GetInputDesc(ePId_Input0);
 const FPooledRenderTargetDesc* InputDesc2 = GetInputDesc(ePId_Input1);

if (!InputDesc1 || !InputDesc2)
 {
 // input is not hooked up correctly
 return;
 }

const FSceneView& View = Context.View;
 const FSceneViewFamily& ViewFamily = *(View.Family);

FIntPoint TexSize1 = InputDesc1->Extent;
 FIntPoint TexSize2 = InputDesc2->Extent;

FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(Context.RHICmdList);
 uint32 ScaleToFullRes1 = SceneContext.GetBufferSizeXY().X / TexSize1.X;
 uint32 ScaleToFullRes2 = SceneContext.GetBufferSizeXY().X / TexSize2.X;

FIntRect ViewRect1 = FIntRect::DivideAndRoundUp(View.ViewRect, ScaleToFullRes1);
 FIntRect ViewRect2 = FIntRect::DivideAndRoundUp(View.ViewRect, ScaleToFullRes2);

FIntPoint ViewSize1 = ViewRect1.Size();
 FIntPoint ViewSize2 = ViewRect2.Size();


 const FSceneRenderTargetItem& DestRenderTarget = PassOutputs[0].RequestSurfCustom(Context);

// Set the view family's render target/viewport.
 SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef());

if (Context.HasHmdMesh() && View.StereoPass == eSSP_LEFT_EYE)
 {
 DrawClearQuad(Context.RHICmdList, Context.GetFeatureLevel(), true, FLinearColor::Black, false, 0, false, 0);
 }
 else
 {
 // is optimized away if possible (RT size=view size, )
 DrawClearQuad(Context.RHICmdList, Context.GetFeatureLevel(), true, FLinearColor::Black, false, 0, false, 0, PassOutputs[0].RenderTargetDesc.Extent, ViewRect1);
 }

Context.SetViewportAndCallRHI(ViewRect1);

FGraphicsPipelineStateInitializer GraphicsPSOInit;
 Context.RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
 GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
 GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
 GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();

// setup background (bloom), can be implemented to use additive blending to avoid the read here
 if (bCompositeBloom)
 {
 TShaderMapRef<FPostProcessVS> VertexShader(Context.GetShaderMap());
 TShaderMapRef<FPostProcessCustomLensFlaresBasePS<false>> PixelShader(Context.GetShaderMap());

GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
 GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
 GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
 GraphicsPSOInit.PrimitiveType = PT_TriangleList;

SetGraphicsPipelineState(Context.RHICmdList, GraphicsPSOInit);

VertexShader->SetParameters(Context);
 PixelShader->SetParameters(Context);

// Draw a quad mapping scene color to the view's render target
 DrawRectangle(
 Context.RHICmdList,
 0, 0,
 ViewSize1.X, ViewSize1.Y,
 ViewRect1.Min.X, ViewRect1.Min.Y,
 ViewSize1.X, ViewSize1.Y,
 ViewSize1,
 TexSize1,
 *VertexShader,
 EDRF_UseTriangleOptimization);
 }
 else
 {
 TShaderMapRef<FPostProcessVS> VertexShader(Context.GetShaderMap());
 TShaderMapRef<FPostProcessCustomLensFlaresBasePS<true>> PixelShader(Context.GetShaderMap());

GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
 GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
 GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
 GraphicsPSOInit.PrimitiveType = PT_TriangleList;

SetGraphicsPipelineState(Context.RHICmdList, GraphicsPSOInit);

VertexShader->SetParameters(Context);
 PixelShader->SetParameters(Context);

// Draw a quad mapping scene color to the view's render target
 DrawRectangle(
 Context.RHICmdList,
 0, 0,
 ViewSize1.X, ViewSize1.Y,
 ViewRect1.Min.X, ViewRect1.Min.Y,
 ViewSize1.X, ViewSize1.Y,
 ViewSize1,
 TexSize1,
 *VertexShader,
 EDRF_UseTriangleOptimization);

}

// additive blend
 GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One>::GetRHI();

// add halo on top of that
 { 
 TShaderMapRef<FPostProcessCustomLensFlaresVS> VertexShader(Context.GetShaderMap());
 TShaderMapRef<FPostProcessCustomLensFlaresPS> PixelShader(Context.GetShaderMap());

GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
 GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
 GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
 GraphicsPSOInit.PrimitiveType = PT_TriangleList;

SetGraphicsPipelineState(Context.RHICmdList, GraphicsPSOInit);

FVector2D TexScaleValue = FVector2D(TexSize2) / ViewSize2;
 uint32 TileSize = 1;
 FIntPoint TileCount = ViewSize1 / TileSize;
 float HaloSizeOut = HaloSize / 100.0f * ViewSize1.X;
 float Type = 0.0;
 VertexShader->SetParameters(Context, TileCount, TileSize, HaloSizeOut, Threshold, Type);
 PixelShader->SetParameters(Context, TexScaleValue);

FLinearColor LensFlareHDRColor = Context.View.FinalPostProcessSettings.CustomLensFlareTint * Context.View.FinalPostProcessSettings.CustomsLensFlareHaloIntensity;

// to get the same brightness with 4x more quads (TileSize=1 in LensBlur)
 LensFlareHDRColor.R *= 0.25f;
 LensFlareHDRColor.G *= 0.25f;
 LensFlareHDRColor.B *= 0.25f;

// Using the Lenflare first tint color
 FLinearColor FlareColor = Context.View.FinalPostProcessSettings.CustomLensFlareTints[0];
 float IntensityL = Context.View.FinalPostProcessSettings.CustomsLensFlareHaloIntensity;
 
 // set the individual flare color
 SetShaderValue(Context.RHICmdList, PixelShader->GetPixelShader(), PixelShader->FlareColor, FlareColor * LensFlareHDRColor);
 SetShaderValue(Context.RHICmdList, PixelShader->GetPixelShader(), PixelShader->Intensity, IntensityL);
 
 // needs to be the same on shader side (faster on NVIDIA and AMD)
 int32 QuadsPerInstance = 4;
 Context.RHICmdList.DrawPrimitive(PT_TriangleList, 0, 2, FMath::DivideAndRoundUp(TileCount.X * TileCount.Y, QuadsPerInstance));
 
 }
 
 // add glare on top of that
 {
 TShaderMapRef<FPostProcessCustomLensFlaresVS> VertexShader(Context.GetShaderMap());
 TShaderMapRef<FPostProcessCustomLensFlaresGlarePS> PixelShader(Context.GetShaderMap());

GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
 GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
 GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
 GraphicsPSOInit.PrimitiveType = PT_TriangleList;

SetGraphicsPipelineState(Context.RHICmdList, GraphicsPSOInit);

FVector2D TexScaleValue = FVector2D(TexSize2) / ViewSize2;
 uint32 TileSize = 1;
 FIntPoint TileCount = ViewSize1 / TileSize;
 float GlareSizeOut = GlareSize / 100.0f * ViewSize1.X;

float Type = 1.0;
 VertexShader->SetParameters(Context, TileCount, TileSize, GlareSizeOut, Threshold, Type);
 PixelShader->SetParameters(Context, TexScaleValue);

FLinearColor LensFlareHDRColor = Context.View.FinalPostProcessSettings.CustomLensFlareTint * Context.View.FinalPostProcessSettings.CustomsLensFlareIntensity;

// to get the same brightness with 4x more quads (TileSize=1 in LensBlur)
 LensFlareHDRColor.R *= 0.25f;
 LensFlareHDRColor.G *= 0.25f;
 LensFlareHDRColor.B *= 0.25f;

// Using the Lenflare second tint color
 FLinearColor FlareColor = Context.View.FinalPostProcessSettings.CustomLensFlareTints[1];
 float IntensityL = Context.View.FinalPostProcessSettings.CustomsLensFlareIntensity;

// set the individual flare color
 SetShaderValue(Context.RHICmdList, PixelShader->GetPixelShader(), PixelShader->FlareColor, FlareColor * LensFlareHDRColor);
 SetShaderValue(Context.RHICmdList, PixelShader->GetPixelShader(), PixelShader->Intensity, IntensityL);

// needs to be the same on shader side (faster on NVIDIA and AMD)
 int32 QuadsPerInstance = 4;
 Context.RHICmdList.DrawPrimitive(PT_TriangleList, 0, 2, FMath::DivideAndRoundUp(TileCount.X * TileCount.Y, QuadsPerInstance));
 }

Context.RHICmdList.CopyToResolveTarget(DestRenderTarget.TargetableTexture, DestRenderTarget.ShaderResourceTexture, false, FResolveParams());
}

FPooledRenderTargetDesc FRCPassPostProcessCustomLensFlares::ComputeOutputDesc(EPassOutputId InPassOutputId) const

We encapsulates the same, as a default lensflares, simple copy pixes, in case something wont be working. We have a vertex shader with a LensflareType to have a different settings for Halo and Glare, you will see this later in the actual shader. Also we have two different pixel shaders for each effect, halo and glare.

Now lets add the changes inside the PostProcessing.cpp in the same folder:

First thing is to include our new header in the top of file.

#include "PostProcess/PostProcessCustomLensFlares.h"

Next go to the  FRenderingCompositeOutputRef AddBloom and find the “FLinearColor LensFlareHDRColor = Context.View.FinalPostProcessSettings.LensFlareTint * Context.View.FinalPostProcessSettings.LensFlareIntensity;” line, above this line add this:

// Register custom lensflare pass and put into the bloomOutput
if (Context.View.FinalPostProcessSettings.CustomLensFlareSwitch == LENSFLARES_Custom)
{
 float HaloSize = Context.View.FinalPostProcessSettings.CustomLensFlareMainHaloSize;
 float GlareSize = Context.View.FinalPostProcessSettings.CustomLensFlareMainGlareSize;
 float Threshold = Context.View.FinalPostProcessSettings.CustomLensFlareThreshold;

FRenderingCompositePass* PostProcessCustomLensflares = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessCustomLensFlares(1.0f, HaloSize, GlareSize, Threshold));
 PostProcessCustomLensflares->SetInput(ePId_Input0, BloomOutput);
 PostProcessCustomLensflares->SetInput(ePId_Input1, PostProcessDownsamples[0]);
 BloomOutput = FRenderingCompositeOutputRef(PostProcessCustomLensflares);
 return BloomOutput;
}

In this case you can see, that we not adding the default lens flares, instead using our FRCPassPostProcessCustomLensFlares. Of course we can add an option for using ALSO the old lensflares, to do this register a new pass with default lensflares blending with our custom. You can add as much as you want if performance it’s not a problem for you 🙂

In the final we need to add an actual shader that we will use for our CustomLensFlares, go to the ../Engine/Shaders/ folder and create PostProcessCustomLensflares.usf:

 

#include "Common.usf"
#include "PostProcessCommon.usf"

float Intensity;
float4 FlareColor; 
float2 TexScale;
float LensflareType;
Texture2D MainGlareTexture;
SamplerState MainGlareTextureSampler;
Texture2D MainHaloTexture;
SamplerState MainHaloTextureSampler;
uint4 TileCountAndSize;
float4 KernelSize;
float4 ColorScale;

float2 PixelToScreenPos(float2 PixelPos)
{
 return (PixelPos - ScreenPosToPixel.zw) / ScreenPosToPixel.xy;
}

// vertex shader
void MainVS(
 uint VId : SV_VertexID,
 uint IId : SV_InstanceID,
 out noperspective float2 OutTexCoord : TEXCOORD0,
 out noperspective float4 OutColor : TEXCOORD1,
 out float4 OutPosition : SV_POSITION
 )
{
 uint2 TileCount = TileCountAndSize.xy;
 uint2 TileSize = TileCountAndSize.zw;

// needs to be the same on shader side (faster on NVIDIA and AMD)
 uint QuadsPerInstance = 4;
 // remap the indices to get vertexid to VId and quadid into IId
 IId = IId * QuadsPerInstance + (VId / 6);
 VId = VId % 6;

// triangle A: 0:left top, 1:right top, 2: left bottom
 // triangle B: 3:right bottom, 4:left bottom, 5: right top
 float2 LocalPos = float2(VId % 2, VId > 1 && VId < 5);
 float2 TilePos = float2(IId % TileCount.x, IId / TileCount.x) + ViewportRect.xy;
 
 OutPosition = float4(0, 0, 0, 1);
 OutTexCoord = LocalPos.xy; 
 OutPosition.xy = PixelToScreenPos(TilePos * TileSize)*2; //centered, no offset
 
 float2 InputTexCoord = PostprocessInput1Size.zw * TilePos * TileSize;
 OutColor = Texture2DSampleLevel(PostprocessInput1, PostprocessInput1Sampler, InputTexCoord, 0);

// scale to blur outside of the view
 OutPosition.xy *= 0.5f;

float LuminanceVal = dot(OutColor.rgb, 1);
 float Threshold = ColorScale.y;
 float2 ThisKernelSize = KernelSize.xy; // done to avoid modifying input constant directly (OpenGL doesn't like it)

if(LuminanceVal < Threshold)
 {
 // reject this quad as it's not bright enough (should happen to most of them for good performance)
 ThisKernelSize = 0;
 }

OutColor *= ColorScale.x;
 
 // adding halo mask
 if(LensflareType == 0)
 {
 OutColor.rgb *= DiscMask(OutPosition.xy);
 }
 //{
 //OutColor.rgb *= DiscMask(OutPosition.xy);
 //}


// offset the corners
 OutPosition.xy += 2 * ViewportSize.zw * (LocalPos - 0.5f) * ThisKernelSize;
}

// pixel shader
void CopyPS(noperspective float4 InUV : TEXCOORD0, out float4 OutColor : SV_Target0)
{
#ifdef CLEAR_REGION
 OutColor = float4(0.f, 0.f, 0.f, 0.f);
#else 
 OutColor = Texture2DSample(PostprocessInput0, PostprocessInput0Sampler, InUV.xy);
#endif 
}

// pixel halo shader
void MainPS(
 noperspective float2 TexCoord : TEXCOORD0,
 noperspective float4 InColor : TEXCOORD1,
 out float4 OutColor : SV_Target0
 )
{
 float2 UV = TexCoord.xy;
 UV = -UV + float2(1.0,1.0);
 float3 Kernel = Texture2DSample(MainHaloTexture, MainHaloTextureSampler, UV) * Intensity * FlareColor.rgb;
 OutColor = float4(InColor.rgb * Kernel.rgb, 1);
}

// pixel glare shader
void MainPS2(
 noperspective float2 TexCoord : TEXCOORD0,
 noperspective float4 InColor : TEXCOORD1,
 out float4 OutColor : SV_Target0
 )
{

float2 UV = TexCoord.xy;
 UV = -UV + float2(1.0,1.0);
 float3 Kernel = Texture2DSample(MainGlareTexture, MainGlareTextureSampler, UV) * Intensity * FlareColor.rgb;
 OutColor = float4(InColor.rgb * Kernel.rgb, 1);
}

We do the VS calculations for position, uv and others same way its done for LensBlur pass.

That’s it. Now you can use the custom lens flares in the postprocess lens settings.
You can use this textures as an example for Halo and Glare. Also should try to add your own things inside the custom flares, additional Halo as an example or Burst effect..

CustomLensFlare_Halo CustomLensFlare_SunGlare

Enjoy!

P.S. This is not an optimal example, everything made up to have an better idea how the postprocess pass working in UE4, for leaning purposes.

Leave a Reply

Your email address will not be published. Required fields are marked *