Prism is a lens flare editor plugin that allows the user to create customized lens-flares inside of Unreal Engine 5. The plugin improves the existing Lens-Flare rendering post-process and allows for more visual and creative freedom. In this blog I will go over some of the systems and things I have learned throughout the creation of this plugin.
Screenshot of the plugin in action

The lens-flares are rendered per instance, reason being that the only shape a lens-flare bokeh can have is a rectangle. Since we only have to render quads at different locations we don’t have to mess around with vertex buffers and can hard code the vertices for a quad inside of the vertex shader itself, after which we can do N number of draw-calls and use the instance index to transform the vertices into the right position.
The only thing the GPU has to know about is the per-bokeh struct that contains information about said lens-flare bokeh.
This is a highly stripped down version of the rendering code inside PrismRenderer.cpp.
// Create the SRV used to pass the bokeh data to the shader.
FRDGBufferSRVRef Srv = GraphBuilder.CreateSRV(CreateStructuredBuffer(GraphBuilder, TEXT("PrismRendererBokehs"), BokehGPUData));
// Set up vertex shader parameters.
FPrismRendererVS::FParameters VSParameters;
VSParameters.BokehDataBuffer = Srv;
VSParameters.NumBokehData = BokehGPUData.Num();
FPrismRendererPS::FParameters PSParameters;
PSParameters.RenderTargets[0] = FRenderTargetBinding(SceneColor.Texture, ERenderTargetLoadAction::ELoad);
PSParameters.BokehDataBuffer = Srv;
PSParameters.NumBokehData = BokehGPUData.Num();
// Conbine pixel and vertex shader parameters.
FPRismRendererShaderParameters* Parameters = GraphBuilder.AllocParameters<FPRismRendererShaderParameters>();
Parameters->VS = VSParameters;
Parameters->PS = PSParameters;
// Add the lens flare pass to the render graph.
GraphBuilder.AddPass(
RDG_EVENT_NAME("PrismLensFlarePass"),
Parameters,
ERDGPassFlags::Raster,
[this, PSParameters, Parameters, SceneColor](FRHICommandListImmediate& RHICmdList)
{
// Set the shader parameters.
SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), Parameters->VS);
SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), Parameters->PS);
// Submit instanced draw call based on how many bokehs there are.
RHICmdList.SetStreamSource(0, nullptr, 0);
RHICmdList.DrawPrimitive(0, 2, Parameters->VS.NumBokehData);
});
FSceneViewExtentionBase is the class used by Unreal Engine to hook into the post-processing pipeline. In this plugin it is used to hook in the pipeline right before the bloom pass. This is where Unreal Engine itself renders its built in lens-flares hence we do the same.
By overriding the SubscribeToPostProcessingPass function we can bind a delegate that will be ran before the bloom pass.
SubscribeToPostProcessingPass function override in PrismViewExtention.cpp
void FPrismViewExtention::SubscribeToPostProcessingPass(EPostProcessingPass Pass, const FSceneView& InView, FPostProcessingPassDelegateArray& InOutPassCallbacks, bool bIsPassEnabled)
{
// MotionBlur pass is marked as BL_SceneColorBeforeBloom. So it runs before the Bloom pass.
// The bloom pass contains the existing lens-flare pass hence we try to get the delegates for the MotionBlur pass.
if (Pass == EPostProcessingPass::MotionBlur)
{
InOutPassCallbacks.Add(FPostProcessingPassDelegate::CreateRaw(this, &FPrismViewExtention::RenderBeforeBloom_RenderThread));
}
}
The RenderBeforeBloom_RenderThread function will be ran every time before the bloom pass. And will get the sun light data from the active frame by getting it from the FPrismLightCollector.
We want to get the light’s position relative to the camera every frame. We also want to know if anything is obscuring the camera. For this we use a line-trace however, this was more difficult than expected, because we now have to 1: query the line-trace on the game thread and 2: make sure that the render function is querying from the right frame otherwise we get flickering and other graphical bugs.
I did this by first calling a line-trace query on the game thread for that frame, saving that query in a TMap<> where we use the FSceneView::GetViewKey() as the key for the query. After which the render thread can fetch this whenever it wants.
SetupView function inside of PrismViewExtention.cpp
void FPrismViewExtention::SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView)
{
UPrismRendererSubSystem* SubSystem = GEngine->GetEngineSubsystem<UPrismRendererSubSystem>();
if (SubSystem)
{
LightCollector.EnqueLightDataRequest_GameThread(InView);
}
}
RenderBeforeBloom_RenderThread function inside of PrismViewExtention.cpp
FScreenPassTexture FPrismViewExtention::RenderBeforeBloom_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessMaterialInputs& InOutInputs)
{
check(IsInRenderingThread());
FScreenPassTexture SceneColor = InOutInputs.ReturnUntouchedSceneColorForPostProcessing(GraphBuilder);
const TArray<FPrismScreenLight> Lights = LightCollector.GetLightDataRequest_RenderThread(View);
Renderer.Render_RenderThread(GraphBuilder, SceneColor, Lights);
return SceneColor;
}
Prism uses a custom UPrismLensFlareAsset asset that contains all the data necessary to render the lens-flare bokehs.
To allow for this we use several classes, including a: FAssetTypeActions class, PrismAssetFactory class and the actual UPrismLensFlareAsset class.
Custom asset creation classes.
/// <summary>
/// FAssetTypeActions_PrismAsset is used to register the PrismLensFlareAsset type and make it available in the editor when right clicking to create a new asset.
/// </summary>
class PRISMEDITOR_API FAssetTypeActions_PrismAsset : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override { return NSLOCTEXT("Prism LensFlare Asset", "AssetTypeActions_Prism", "Prism LensFlare Asset"); }
virtual FColor GetTypeColor() const override { return FColor(255, 200, 50); }
virtual UClass* GetSupportedClass() const override { return UPrismLensFlareAsset::StaticClass(); }
virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; }
virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
};
/// <summary>
/// PrismAssetfactory is required to create a new PrismLensFlareAsset in the editor.
/// </summary>
UCLASS()
class PRISMEDITOR_API UPrismAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UPrismAssetFactory();
virtual UObject* FactoryCreateNew(UClass* Class, UObject* Parent, FName Name, EObjectFlags Flags, UObject* Object, FFeedbackContext* FeedbackContext) override;
};
DECLARE_MULTICAST_DELEGATE(FOnAssetChanged);
DECLARE_MULTICAST_DELEGATE(FOnDeleting);
DECLARE_MULTICAST_DELEGATE(FOnAssetRenamed);
/// <summary>
/// UPrismLensFlareAsset is used to store an array of lens flare bokeh data as an asset.
/// </summary>
UCLASS(BlueprintType)
class PRISM_API UPrismLensFlareAsset : public UObject
{
GENERATED_BODY()
public:
#if WITH_EDITOR
/// <summary>
/// Broadcasts the OnAssetChanged event.
/// </summary>
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
/// <summary>
/// Broadcasts the OnAssetBeingDeleted event.
/// </summary>
virtual void BeginDestroy() override;
/// <summary>
/// Called when asset is renamed.
/// </summary>
virtual void PostRename(UObject* OldOuter, const FName OldName) override;
#endif
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PrismAsset")
TArray<FPrismLensFlareAssetData> Bokehs;
FOnAssetRenamed OnAssetRenamed;
FOnAssetChanged OnAssetChanged;
FOnDeleting OnAssetBeingDeleted;
};
Along with the custom asset there is also a PrismEditor module that has the code to allow the user to set the global lens-flare used for rendering during editing. This will also save the asset to a custom config file.
Menu entry code for custom lens-flare asset inside of AssetTypeActions_PrismAsset.cpp
void FAssetTypeActions_PrismAsset::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
TArray<TWeakObjectPtr<UPrismLensFlareAsset>> Assets = GetTypedWeakObjectPtrs<UPrismLensFlareAsset>(InObjects);
// Early out if the asset array is empty.
if(Assets.IsEmpty())
{
return;
}
// Get the frist element.
TWeakObjectPtr<UPrismLensFlareAsset> FirstAsset = Assets[0];
// Add Menu Entry button when right-clicking the asset.
MenuBuilder.AddMenuEntry(
NSLOCTEXT("AssetTypeActions", "Prism_DoSomething", "Use this Asset"),
NSLOCTEXT("AssetTypeActions", "Prism_DoSomethingTooltip", "Sets the currently selected Prism asset to the one used."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Play"),
FUIAction(
FExecuteAction::CreateLambda([FirstAsset]()
{
UPrismAssetSubSystem* SubSystem = GEngine->GetEngineSubsystem<UPrismAssetSubSystem>();
if (SubSystem)
{
SubSystem->SetGlobalPrismLensFlareAssetAndSaveToConfig(FirstAsset.Get());
}
})));
}