Fixing Unreal Engine 4 Lens-Flare Settings

They are broken. Nobody can use them. Until today.

April 28, 2021



If you are a user of UE4, especially recent versions, you may have stumbled upon an issue regarding its lens-flare post-process: you cannot tweak the flare colors properly and they have no effect.

This is actually a UI bug which has been here for a while (at least since version 4.18). It is is currently logged in the UE4 bug database and is marked as backlogged without any updates since 2019.

As you can see, tweaking the parameters make no sense and even if you were able to change the value as desired they would have no effect. So let's see a way to fix that problem.

This article was written based on Unreal Engine version 4.25.


Isolating the Issue

The issue seems to be coming from the engine system that handles the parameter override (the little checkbox that enable the setting for you to edit). The actual data saved in the level/asset seems to be fine, but the engine doesn't register properly that a parameter is overridden and therefore gets ignored. This is why tweaking the color has no visual effect.

The first step was to look at the engine code for these settings to figure how much different there were from other parameters (which are working fine).

Let's take a look at Source/Runtime/Engine/Classes/Engine/Scene.h:

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTint:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints:1;

So there are two override variables here: one for the global tint color and one for each individual colors. These variables basically store the states of the checkoxes. Nothing special here. So let's continue further in the same file:

    /** Brightness scale of the image cased lens flares (linear) */
    UPROPERTY(interp, BlueprintReadWrite, Category="Lens|Lens Flares", meta=(UIMin = "0.0", UIMax = "16.0", editcondition = "bOverride_LensFlareIntensity", DisplayName = "Intensity"))
    float LensFlareIntensity;

[...]

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", meta=(editcondition = "bOverride_LensFlareTints", DisplayName = "Tints"))
    FLinearColor LensFlareTints[8];

Ah ! Here are the actual value of the parameters. Notice the different between the general tint and the individual tints: one is a single FLinearColor while the other one is an array.

So while I don't know why, I suppose the issue here is with the fact Tints are a C-style array. So I replaced the LensFlareTints[8] by individual values instead. Let's see that in details in the next section.


The Fix

My solution to fixing the array issue is to simply flatten it as individual parameters. It's a pretty easy replacement to do. The most important point is to make sure to not forget any places to update, because UE4 post-process settings are actually very big to manage in term of code in the engine.

So how does that translate in term of changes ?

Scene.H

First we need to replace the original override setting and declare 8 individual ones instead in Source/Runtime/Engine/Classes/Engine/Scene.h.

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints:1;

becomes:

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_1:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_2:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_3:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_4:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_5:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_6:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_7:1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
    uint8 bOverride_LensFlareTints_8:1;

Then we can replace the array by the actual individual values (still in Scene.h).

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", meta=(editcondition = "bOverride_LensFlareTints", DisplayName = "Tints"))
    FLinearColor LensFlareTints[8];

becomes:

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_1", DisplayName = "Flare Tint 1"))
    FLinearColor LensFlareTints_1;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_2", DisplayName = "Flare Tint 2"))
    FLinearColor LensFlareTints_2;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_3", DisplayName = "Flare Tint 3"))
    FLinearColor LensFlareTints_3;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_4", DisplayName = "Flare Tint 4"))
    FLinearColor LensFlareTints_4;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_5", DisplayName = "Flare Tint 5"))
    FLinearColor LensFlareTints_5;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_6", DisplayName = "Flare Tint 6"))
    FLinearColor LensFlareTints_6;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_7", DisplayName = "Flare Tint 7"))
    FLinearColor LensFlareTints_7;

    /** RGB defines the lens flare color, A it's position. This is a temporary solution. */
    UPROPERTY(EditAnywhere, Category="Lens|Lens Flares", AdvancedDisplay, meta=(editcondition = "bOverride_LensFlareTints_8", DisplayName = "Flare Tint 8"))
    FLinearColor LensFlareTints_8;

Because the array is now a set of individual colors, I added the tag AdvancedDisplay so that the parameters are inside an collapsed group, otherwise they would be always visible which might be annoying compared to the original UI behavior.

You can notice that the Flare tints actually not only control the color of the flares but also their size ! This is not documented outside of the code as far I know. So RGB = color, A = size (which is a scale in relative screen space).
You can also appreciate the "this is a temporary solution" comment made by an Epic Games developer here.


Now that the parameters exist, we need to initialize them. This is where I personally find UE4 post-process settings annoying and convoluted (blame C++ maybe ?). You will see why in a second.

So let's open Source/Runtime/Engine/Private/Scene.cpp and find:

    // 200 should be enough even for extreme aspect ratios to give the default no effect
    DepthOfFieldVignetteSize = 200.0f;
    LensFlareTints[0] = FLinearColor(1.0f, 0.8f, 0.4f, 0.6f);
    LensFlareTints[1] = FLinearColor(1.0f, 1.0f, 0.6f, 0.53f);
    LensFlareTints[2] = FLinearColor(0.8f, 0.8f, 1.0f, 0.46f);
    LensFlareTints[3] = FLinearColor(0.5f, 1.0f, 0.4f, 0.39f);
    LensFlareTints[4] = FLinearColor(0.5f, 0.8f, 1.0f, 0.31f);
    LensFlareTints[5] = FLinearColor(0.9f, 1.0f, 0.8f, 0.27f);
    LensFlareTints[6] = FLinearColor(1.0f, 0.8f, 0.4f, 0.22f);
    LensFlareTints[7] = FLinearColor(0.9f, 0.7f, 0.7f, 0.15f);

and replace the array assignations by our individual variables:

    // 200 should be enough even for extreme aspect ratios to give the default no effect
    DepthOfFieldVignetteSize = 200.0f;
    LensFlareTints_1 = FLinearColor(1.0f, 0.8f, 0.4f, 0.6f);
    LensFlareTints_2 = FLinearColor(1.0f, 1.0f, 0.6f, 0.53f);
    LensFlareTints_3 = FLinearColor(0.8f, 0.8f, 1.0f, 0.46f);
    LensFlareTints_4 = FLinearColor(0.5f, 1.0f, 0.4f, 0.39f);
    LensFlareTints_5 = FLinearColor(0.5f, 0.8f, 1.0f, 0.31f);
    LensFlareTints_6 = FLinearColor(0.9f, 1.0f, 0.8f, 0.27f);
    LensFlareTints_7 = FLinearColor(1.0f, 0.8f, 0.4f, 0.22f);
    LensFlareTints_8 = FLinearColor(0.9f, 0.7f, 0.7f, 0.15f);

Now we need to update the C++ initializers for the override:

    , bOverride_LensFlareTint(Settings.bOverride_LensFlareTint)
    , bOverride_LensFlareTints(Settings.bOverride_LensFlareTints)

becomes:

    , bOverride_LensFlareTint(Settings.bOverride_LensFlareTint)
    , bOverride_LensFlareTints_1(Settings.bOverride_LensFlareTints_1)
    , bOverride_LensFlareTints_2(Settings.bOverride_LensFlareTints_2)
    , bOverride_LensFlareTints_3(Settings.bOverride_LensFlareTints_3)
    , bOverride_LensFlareTints_4(Settings.bOverride_LensFlareTints_4)
    , bOverride_LensFlareTints_5(Settings.bOverride_LensFlareTints_5)
    , bOverride_LensFlareTints_6(Settings.bOverride_LensFlareTints_6)
    , bOverride_LensFlareTints_7(Settings.bOverride_LensFlareTints_7)
    , bOverride_LensFlareTints_8(Settings.bOverride_LensFlareTints_8)

Last thing in this file is to change:

{
    for (int32 i = 0; i < UE_ARRAY_COUNT(LensFlareTints); i++)
        LensFlareTints[i] = Settings.LensFlareTints[i];
}

into:

{
    /*
    for (int32 i = 0; i < UE_ARRAY_COUNT(LensFlareTints); i++)
        LensFlareTints[i] = Settings.LensFlareTints[i];
    */
}

Basically we disable this part of the code. This is because we don't have an array anymore. Instead we can initialize the values individually like the other, so replace:

    , LensFlareBokehShape(Settings.LensFlareBokehShape)
    , VignetteIntensity(Settings.VignetteIntensity)

with:

    , LensFlareBokehShape(Settings.LensFlareBokehShape)
    , LensFlareTints_1(Settings.LensFlareTints_1)
    , LensFlareTints_2(Settings.LensFlareTints_2)
    , LensFlareTints_3(Settings.LensFlareTints_3)
    , LensFlareTints_4(Settings.LensFlareTints_4)
    , LensFlareTints_5(Settings.LensFlareTints_5)
    , LensFlareTints_6(Settings.LensFlareTints_6)
    , LensFlareTints_7(Settings.LensFlareTints_7)
    , LensFlareTints_8(Settings.LensFlareTints_8)
    , VignetteIntensity(Settings.VignetteIntensity)

SceneView.Cpp

Next we need to go into Source/Runtime/Engine/Private/SceneView.cpp to add the Tint values to make sure they can be blended. So replace:

        LERP_PP(LensFlareTint);
        LERP_PP(LensFlareBokehSize);

with:

        LERP_PP(LensFlareTint);
        LERP_PP(LensFlareTints_1);
        LERP_PP(LensFlareTints_2);
        LERP_PP(LensFlareTints_3);
        LERP_PP(LensFlareTints_4);
        LERP_PP(LensFlareTints_5);
        LERP_PP(LensFlareTints_6);
        LERP_PP(LensFlareTints_7);
        LERP_PP(LensFlareTints_8);
        LERP_PP(LensFlareBokehSize);

Still in the same file, look for:

        if (Src.bOverride_LensFlareTints)
        {
            for (uint32 i = 0; i < 8; ++i)
            {
                Dest.LensFlareTints[i] = FMath::Lerp(Dest.LensFlareTints[i], Src.LensFlareTints[i], Weight);
            }
        }

and disable it as well:

        /*
        if (Src.bOverride_LensFlareTints)
        {
            for (uint32 i = 0; i < 8; ++i)
            {
                Dest.LensFlareTints[i] = FMath::Lerp(Dest.LensFlareTints[i], Src.LensFlareTints[i], Weight);
            }
        }
        */

Once again, because we don't use an array this bit is not needed anymore.

PostProcessLensFlares.Cpp

Finally, here is the last place to edit. Open Source/Runtime/Renderer/Private/PostProcess/PostProcessLensFlares.cpp and find:

LensFlareInputs.TintColorsPerFlare = Settings.LensFlareTints;

and replace it with:

        FLinearColor Tints[8] = {
            Settings.LensFlareTints_1,
            Settings.LensFlareTints_2,
            Settings.LensFlareTints_3,
            Settings.LensFlareTints_4,
            Settings.LensFlareTints_5,
            Settings.LensFlareTints_6,
            Settings.LensFlareTints_7,
            Settings.LensFlareTints_8
        };

        LensFlareInputs.TintColorsPerFlare = Tints;

Because we don't have an array anymore in the post-process settings, we have to build one. This is the simplest change I could find to avoid editing too much of the lens-flare postprocess code for future maintenance.

You can now compile the changes and go take a break because it's gonna take a while.


You should now be able to tweak the lens-flare tint settings as much as you like:


Flattening the Array vs Using a TArray()

An alternative solution I have seen on UE4 answerhub is to replace the original Array by a TArray (UE4 dynamic sized array). I had considered this solution initially but I wasn't satisfied with it for several reasons:

If you are still curious, you can check out the answerhub page over here.