Add light flicker component

This commit is contained in:
Steve Streeting 2023-11-06 16:59:07 +00:00
parent 322edbd185
commit d3c97fc992
6 changed files with 348 additions and 0 deletions

View File

@ -11,6 +11,8 @@ which makes a bunch of things better:
* Reliable notification when the player changes input method * Reliable notification when the player changes input method
* [Debug visualisation](https://www.stevestreeting.com/2021/09/14/ue4-editor-visualisation-helper/) * [Debug visualisation](https://www.stevestreeting.com/2021/09/14/ue4-editor-visualisation-helper/)
* [Better DataTable Row References](https://www.stevestreeting.com/2023/10/06/a-better-unreal-datatable-row-picker/) * [Better DataTable Row References](https://www.stevestreeting.com/2023/10/06/a-better-unreal-datatable-row-picker/)
* [Light Flicker](doc/LightFlicker.md)
* Halton Sequence based random stream
## Examples ## Examples

View File

@ -0,0 +1,178 @@
// Copyright Steve Streeting
// Licensed under the MIT License (see License.txt)
#include "StevesLightFlicker.h"
#include "Net/UnrealNetwork.h"
TMap<EStevesLightFlickerPattern, FRichCurve> UStevesLightFlickerHelper::Curves;
TMap<FString, FRichCurve> UStevesLightFlickerHelper::CustomCurves;
FCriticalSection UStevesLightFlickerHelper::CriticalSection;
// Quake lighting flicker functions
// https://github.com/id-Software/Quake/blob/bf4ac424ce754894ac8f1dae6a3981954bc9852d/qw-qc/world.qc#L328-L372
const TMap<EStevesLightFlickerPattern, FString> UStevesLightFlickerHelper::QuakeCurveSources {
{ EStevesLightFlickerPattern::Flicker1, TEXT("mmnmmommommnonmmonqnmmo") },
{ EStevesLightFlickerPattern::SlowStrongPulse, TEXT("abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba") },
{ EStevesLightFlickerPattern::Candle1, TEXT("mmmmmaaaaammmmmaaaaaabcdefgabcdefg") },
{ EStevesLightFlickerPattern::FastStrobe, TEXT("mamamamamama") },
{ EStevesLightFlickerPattern::GentlePulse1, TEXT("jklmnopqrstuvwxyzyxwvutsrqponmlkj") },
{ EStevesLightFlickerPattern::Flicker2, TEXT("nmonqnmomnmomomno") },
{ EStevesLightFlickerPattern::Candle2, TEXT("mmmaaaabcdefgmmmmaaaammmaamm") },
{ EStevesLightFlickerPattern::Candle3, TEXT("mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa") },
{ EStevesLightFlickerPattern::SlowStrobe, TEXT("aaaaaaaazzzzzzzz") },
{ EStevesLightFlickerPattern::FlourescentFlicker, TEXT("mmamammmmammamamaaamammma") },
{ EStevesLightFlickerPattern::SlowPulseNoBlack, TEXT("abcdefghijklmnopqrrqponmlkjihgfedcba") },
};
float UStevesLightFlickerHelper::EvaluateLightCurve(EStevesLightFlickerPattern CurveType, float Time)
{
return GetLightCurve(CurveType).Eval(Time);
}
const FRichCurve& UStevesLightFlickerHelper::GetLightCurve(EStevesLightFlickerPattern CurveType)
{
FScopeLock ScopeLock(&CriticalSection);
if (auto pCurve = Curves.Find(CurveType))
{
return *pCurve;
}
auto& Curve = Curves.Emplace(CurveType);
BuildCurve(CurveType, Curve);
return Curve;
}
const FRichCurve& UStevesLightFlickerHelper::GetLightCurve(const FString& CurveStr)
{
FScopeLock ScopeLock(&CriticalSection);
if (auto pCurve = CustomCurves.Find(CurveStr))
{
return *pCurve;
}
auto& Curve = CustomCurves.Emplace(CurveStr);
BuildCurve(CurveStr, Curve);
return Curve;
}
void UStevesLightFlickerHelper::BuildCurve(EStevesLightFlickerPattern CurveType, FRichCurve& OutCurve)
{
if (auto pTxt = QuakeCurveSources.Find(CurveType))
{
BuildCurve(*pTxt, OutCurve);
}
}
void UStevesLightFlickerHelper::BuildCurve(const FString& QuakeCurveChars, FRichCurve& OutCurve)
{
OutCurve.Reset();
for (int i = 0; i < QuakeCurveChars.Len(); ++i)
{
// We actually build the curve a..z = 0..1, and then use a default max value of 2 to restore the original behaviour.
// Actually the curve is 0..1.04 due to original behaviour that z is 2.08 not 2
const int CharIndex = QuakeCurveChars[i] - 'a';
const float Val = (float)CharIndex / 24.f; // to ensure m==1, z==2.08 (rescaled to half that so 0..1.04)
// Quake default was each character was 0.1s
OutCurve.AddKey(i * 0.1f, Val);
}
// To catch empty
if (QuakeCurveChars.IsEmpty())
{
OutCurve.AddKey(0, 1);
}
}
UStevesLightFlickerComponent::UStevesLightFlickerComponent(const FObjectInitializer& Initializer):
Super(Initializer),
TimePos(0),
CurrentValue(0),
Curve(nullptr)
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bTickEvenWhenPaused = false;
PrimaryComponentTick.bStartWithTickEnabled = false;
}
void UStevesLightFlickerComponent::BeginPlay()
{
Super::BeginPlay();
if (FlickerPattern == EStevesLightFlickerPattern::Custom)
{
Curve = &UStevesLightFlickerHelper::GetLightCurve(CustomFlickerPattern);
}
else
{
Curve = &UStevesLightFlickerHelper::GetLightCurve(FlickerPattern);
}
TimePos = 0;
if (bAutoPlay)
{
Play();
}
}
void UStevesLightFlickerComponent::ValueUpdate()
{
CurrentValue = FMath::Lerp(MinValue, MaxValue, Curve->Eval(TimePos));
OnLightFlickerUpdate.Broadcast(CurrentValue);
}
void UStevesLightFlickerComponent::Play(bool bResetTime)
{
if (GetOwnerRole() == ROLE_Authority || !GetIsReplicated())
{
if (bResetTime)
{
TimePos = 0;
}
ValueUpdate();
PrimaryComponentTick.SetTickFunctionEnable(true);
}
}
void UStevesLightFlickerComponent::Pause()
{
if (GetOwnerRole() == ROLE_Authority || !GetIsReplicated())
{
PrimaryComponentTick.SetTickFunctionEnable(false);
}
}
float UStevesLightFlickerComponent::GetCurrentValue() const
{
return CurrentValue;
}
void UStevesLightFlickerComponent::OnRep_TimePos()
{
ValueUpdate();
}
void UStevesLightFlickerComponent::TickComponent(float DeltaTime,
ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
TimePos += DeltaTime * Speed;
const float MaxTime = Curve->GetLastKey().Time;
while (TimePos > MaxTime)
{
TimePos -= MaxTime;
}
ValueUpdate();
}
void UStevesLightFlickerComponent::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UStevesLightFlickerComponent, TimePos);
}

View File

@ -0,0 +1,119 @@
// Copyright Steve Streeting
// Licensed under the MIT License (see License.txt)
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "StevesLightFlicker.generated.h"
UENUM(BlueprintType)
enum class EStevesLightFlickerPattern : uint8
{
Flicker1,
Flicker2,
SlowStrongPulse,
Candle1,
Candle2,
Candle3,
FastStrobe,
SlowStrobe,
GentlePulse1,
FlourescentFlicker,
SlowPulseNoBlack,
Custom
};
/**
* Helper class to get lighting flicker curves
*/
UCLASS()
class STEVESUEHELPERS_API UStevesLightFlickerHelper : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
protected:
static TMap<EStevesLightFlickerPattern, FRichCurve> Curves;
static TMap<FString, FRichCurve> CustomCurves;
static FCriticalSection CriticalSection;
static const TMap<EStevesLightFlickerPattern, FString> QuakeCurveSources;
static void BuildCurve(EStevesLightFlickerPattern CurveType, FRichCurve& OutCurve);
static void BuildCurve(const FString& QuakeCurveChars, FRichCurve& OutCurve);
public:
/**
* Directly evaluate a lighting curve. Alternatively, see ULightingCurveComponent.
* @param CurveType The type of curve
* @param Time The time index (0..1 period)
* @return Normalised value of the curve at this time
*/
UFUNCTION(BlueprintPure, Category="Lighting Curves")
static float EvaluateLightCurve(EStevesLightFlickerPattern CurveType, float Time);
static const FRichCurve& GetLightCurve(EStevesLightFlickerPattern CurveType);
static const FRichCurve& GetLightCurve(const FString& CurveStr);
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLightFlickerUpdate, float, LightValue);
/**
* This is like a generated version of TimelineComponent, providing a generated lighting curve.
*/
UCLASS(Blueprintable, ClassGroup=(Lights), meta=(BlueprintSpawnableComponent))
class UStevesLightFlickerComponent : public UActorComponent
{
GENERATED_UCLASS_BODY()
protected:
UPROPERTY(EditAnywhere, Category="Light Flicker")
EStevesLightFlickerPattern FlickerPattern = EStevesLightFlickerPattern::Candle1;
/// If using a custom pattern, provide your own Quake-style string of letters, a-z (a = 0, m = 1, z = 2)
UPROPERTY(EditAnywhere, Category="Light Flicker")
FString CustomFlickerPattern;
/// Max output intensity multiplier value. Defaults to 2 since that's what Quake used!
/// We can *very slightly* exceed this max with 'z' as per standard Quake where z was 2.08
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
float MaxValue = 2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
float MinValue = 0;
/// Playback speed
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light Flicker")
float Speed = 1;
/// Whether to auto-start
UPROPERTY(EditAnywhere, Category="Light Flicker")
bool bAutoPlay = true;
UPROPERTY(ReplicatedUsing=OnRep_TimePos)
float TimePos;
float CurrentValue;
const FRichCurve* Curve;
UFUNCTION()
void OnRep_TimePos();
void ValueUpdate();
public:
UPROPERTY(BlueprintAssignable)
FOnLightFlickerUpdate OnLightFlickerUpdate;
UFUNCTION(BlueprintCallable)
void Play(bool bResetTime = false);
UFUNCTION(BlueprintCallable)
void Pause();
UFUNCTION(BlueprintPure)
float GetCurrentValue() const;
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime,
ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
};

49
doc/LightFlicker.md Normal file
View File

@ -0,0 +1,49 @@
# Light Flicker Component
This component adds the ability to generate a light flicker value, which can be plugged into light intensities, or material parameters, or anything else you want.
It works on the same basis as Quake, Half Life: Alyx and countless other games; there is a "flicker string" made up of lower-case letters a-z, where a = 0, m = 1 and z ~= 2 (actually 2.08 in practice), meaning the default value range is from completely off, to double brightness.
You can use all the [Quake in-built flicker patterns](https://github.com/id-Software/Quake/blob/bf4ac424ce754894ac8f1dae6a3981954bc9852d/qw-qc/world.qc#L328-L372), or make your own.
## Adding the component
The flicker component is a non-scene component that can be added to any actor, just search for "Steves Light Flicker"
## Configure it
![](configure.png)
### Patterns and Min/Max
If you select "Custom" in the flicker pattern field, you need to supply your own string of a-z characters in "Custom Flicker Pattern". With the default min/max of 0-2 the character values are:
| Char |Value|
|-|--|
|a|0|
|m|1|
|z|2.08|
The max is slightly over 2 as you can see, that's because to make 'm' (char index 12) exactly 1 the divisor has to be 24, meaning 'z' as character 25 is slightly over ('y' is actually 2).
You can change the output range from the default of 0-2 if you want, just remember that 'z' will slightly exceed the max.
### Speed
By default as with Quake every character in the pattern applies for 0.1s. Change the speed multiplier if you want that to be different.
### AutoPlay
Whether to start playing the flicker immediately, or whether to await a call to `Play()`.
> When not playing, the component does not tick.
## Using the Output
The component will just output values when it's played on the OnLightFlicker event:
![](flickerupdate.png)
It's up to you to feed these into light intensity values, material parameters etc.

BIN
doc/configure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
doc/flickerupdate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB