mirror of
https://github.com/sinbad/StevesUEHelpers.git
synced 2025-02-23 17:45:23 +00:00
Add light flicker component
This commit is contained in:
parent
322edbd185
commit
d3c97fc992
@ -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
|
||||
|
||||
|
178
Source/StevesUEHelpers/Private/StevesLightFlicker.cpp
Normal file
178
Source/StevesUEHelpers/Private/StevesLightFlicker.cpp
Normal 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);
|
||||
}
|
119
Source/StevesUEHelpers/Public/StevesLightFlicker.h
Normal file
119
Source/StevesUEHelpers/Public/StevesLightFlicker.h
Normal 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
49
doc/LightFlicker.md
Normal 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
|
||||
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
It's up to you to feed these into light intensity values, material parameters etc.
|
BIN
doc/configure.png
Normal file
BIN
doc/configure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
doc/flickerupdate.png
Normal file
BIN
doc/flickerupdate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Loading…
x
Reference in New Issue
Block a user