diff --git a/ReadMe.md b/ReadMe.md index bd8c7c1..f22b4c0 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -11,6 +11,8 @@ which makes a bunch of things better: * Reliable notification when the player changes input method * [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/) +* [Light Flicker](doc/LightFlicker.md) +* Halton Sequence based random stream ## Examples diff --git a/Source/StevesUEHelpers/Private/StevesLightFlicker.cpp b/Source/StevesUEHelpers/Private/StevesLightFlicker.cpp new file mode 100644 index 0000000..79765fd --- /dev/null +++ b/Source/StevesUEHelpers/Private/StevesLightFlicker.cpp @@ -0,0 +1,178 @@ +// Copyright Steve Streeting +// Licensed under the MIT License (see License.txt) +#include "StevesLightFlicker.h" + +#include "Net/UnrealNetwork.h" + +TMap UStevesLightFlickerHelper::Curves; +TMap 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 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); +} diff --git a/Source/StevesUEHelpers/Public/StevesLightFlicker.h b/Source/StevesUEHelpers/Public/StevesLightFlicker.h new file mode 100644 index 0000000..50fbec8 --- /dev/null +++ b/Source/StevesUEHelpers/Public/StevesLightFlicker.h @@ -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 Curves; + static TMap CustomCurves; + static FCriticalSection CriticalSection; + static const TMap 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; +}; \ No newline at end of file diff --git a/doc/LightFlicker.md b/doc/LightFlicker.md new file mode 100644 index 0000000..d3da81c --- /dev/null +++ b/doc/LightFlicker.md @@ -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. diff --git a/doc/configure.png b/doc/configure.png new file mode 100644 index 0000000..faf7ac3 Binary files /dev/null and b/doc/configure.png differ diff --git a/doc/flickerupdate.png b/doc/flickerupdate.png new file mode 100644 index 0000000..b536e7e Binary files /dev/null and b/doc/flickerupdate.png differ