Add support for interpolated color transitions for lights, with debug-only synced overrides
This commit is contained in:
parent
ad77530b6d
commit
a8761bf679
|
@ -139,6 +139,33 @@ namespace MuzikaGromche
|
|||
public static readonly Language RUSSIAN = new("RU", "Russian");
|
||||
}
|
||||
|
||||
public readonly record struct Easing(string Name, Func<float, float> Eval)
|
||||
{
|
||||
public static Easing Linear = new("Linear", static x => x);
|
||||
public static Easing OutCubic = new("OutCubic", static x => 1 - Mathf.Pow(1f - x, 3f));
|
||||
public static Easing InCubic = new("InCubic", static x => x * x * x);
|
||||
public static Easing InOutCubic = new("InOutCubic", static x => x < 0.5f ? 4f * x * x * x : 1 - Mathf.Pow(-2f * x + 2f, 3f) / 2f);
|
||||
public static Easing InExpo = new("InExpo", static x => x == 0f ? 0f : Mathf.Pow(2f, 10f * x - 10f));
|
||||
public static Easing OutExpo = new("OutExpo", static x => x == 1f ? 1f : 1f - Mathf.Pow(2f, -10f * x));
|
||||
public static Easing InOutExpo = new("InOutExpo", static x =>
|
||||
x == 0f
|
||||
? 0f
|
||||
: x == 1f
|
||||
? 1f
|
||||
: x < 0.5f
|
||||
? Mathf.Pow(2f, 20f * x - 10f) / 2f
|
||||
: (2f - Mathf.Pow(2f, -20f * x + 10f)) / 2f);
|
||||
|
||||
public static Easing[] All = [Linear, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo];
|
||||
|
||||
public static string[] AllNames => [.. All.Select(easing => easing.Name)];
|
||||
|
||||
public static Easing FindByName(string Name)
|
||||
{
|
||||
return All.Where(easing => easing.Name == Name).DefaultIfEmpty(Linear).First();
|
||||
}
|
||||
}
|
||||
|
||||
public record Palette(Color[] Colors)
|
||||
{
|
||||
public static Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]);
|
||||
|
@ -203,6 +230,39 @@ namespace MuzikaGromche
|
|||
_ => "",
|
||||
};
|
||||
|
||||
// Offset of beats. Bigger offset => colors will change later.
|
||||
public float _BeatsOffset = 0f;
|
||||
public float BeatsOffset
|
||||
{
|
||||
get => Config.BeatsOffsetOverride ?? _BeatsOffset;
|
||||
set => _BeatsOffset = value;
|
||||
}
|
||||
|
||||
// Duration of color transition, measured in beats.
|
||||
public float _ColorTransitionIn = 0.25f;
|
||||
public float ColorTransitionIn
|
||||
{
|
||||
get => Config.ColorTransitionInOverride ?? _ColorTransitionIn;
|
||||
set => _ColorTransitionIn = value;
|
||||
}
|
||||
|
||||
public float _ColorTransitionOut = 0.25f;
|
||||
public float ColorTransitionOut
|
||||
{
|
||||
get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut;
|
||||
set => _ColorTransitionOut = value;
|
||||
}
|
||||
|
||||
// Easing function for color transitions.
|
||||
public Easing _ColorTransitionEasing = Easing.OutExpo;
|
||||
public Easing ColorTransitionEasing
|
||||
{
|
||||
get => Config.ColorTransitionEasingOverride != null
|
||||
? Easing.FindByName(Config.ColorTransitionEasingOverride)
|
||||
: _ColorTransitionEasing;
|
||||
set => _ColorTransitionEasing = value;
|
||||
}
|
||||
|
||||
public float CalculateBeat(AudioSource start, AudioSource loop)
|
||||
{
|
||||
// If popped, calculate which beat the music is currently at.
|
||||
|
@ -227,15 +287,16 @@ namespace MuzikaGromche
|
|||
var normalized = Mod.Positive(elapsed / LoadedLoop.length, 1f);
|
||||
|
||||
var beat = normalized * (float)Beats;
|
||||
var offset = Mod.Positive(beat - BeatsOffset, (float)Beats);
|
||||
#if DEBUG
|
||||
var color = ColorAtBeat(beat);
|
||||
Debug.LogFormat("MuzikaGromche t={0,10:N4} d={1,7:N4} Start[{2}{3,8:N4} ==0f? {4}] Loop[{5}{6,8:N4}] norm={7,6:N4} beat={8,7:N4} color={9}",
|
||||
Debug.LogFormat("MuzikaGromche t={0,10:N4} d={1,7:N4} Start[{2}{3,8:N4} ==0f? {4}] Loop[{5}{6,8:N4}] norm={7,6:N4} beat={8,7:N4} {9,7:N4} color={10}",
|
||||
Time.realtimeSinceStartup, Time.deltaTime,
|
||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||
normalized, beat, color);
|
||||
normalized, beat, offset, color);
|
||||
#endif
|
||||
return beat;
|
||||
return offset;
|
||||
}
|
||||
|
||||
public Palette _Palette = Palette.DEFAULT;
|
||||
|
@ -247,8 +308,64 @@ namespace MuzikaGromche
|
|||
|
||||
public Color ColorAtBeat(float beat)
|
||||
{
|
||||
int beatIndex = Mod.Positive(Mathf.FloorToInt(beat), Beats);
|
||||
return Mod.Index(Palette.Colors, beatIndex);
|
||||
// Imagine the timeline as a sequence of clips without gaps where each clip is a whole beat long.
|
||||
// Transition is when two adjacent clips need to be combined with some blend function(t)
|
||||
// where t is a factor in range 0..1 expressed as (time - Transition.Start) / Transition.Length;
|
||||
//
|
||||
// How to find a transition at a given time?
|
||||
// First, we need to find the current clip's start and length.
|
||||
// - Length is always 1 beat, and
|
||||
// - start is just time rounded down.
|
||||
//
|
||||
// If time interval from the start of the clip is less than Transition.Out
|
||||
// then blend between previous and current clips.
|
||||
//
|
||||
// Else if time interval to the end of the clip is less than Transition.In
|
||||
// then blend between current and next clips.
|
||||
//
|
||||
// Otherwise there is no transition running at this time.
|
||||
const float currentClipLength = 1f;
|
||||
var currentClipStart = Mathf.Floor(beat);
|
||||
var currentClipEnd = currentClipStart + currentClipLength;
|
||||
|
||||
float transitionLength = ColorTransitionIn + ColorTransitionOut;
|
||||
|
||||
if (Config.EnableColorAnimations.Value)
|
||||
{
|
||||
if (transitionLength > /* epsilon */ 0.01)
|
||||
{
|
||||
if (beat - currentClipStart < ColorTransitionOut)
|
||||
{
|
||||
return ColorTransition(currentClipStart);
|
||||
}
|
||||
else if (currentClipEnd - beat < ColorTransitionIn)
|
||||
{
|
||||
return ColorTransition(currentClipEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
// default
|
||||
return ColorAtWholeBeat(beat);
|
||||
|
||||
Color ColorTransition(float clipsBoundary)
|
||||
{
|
||||
var transitionStart = clipsBoundary - ColorTransitionIn;
|
||||
var transitionEnd = clipsBoundary + ColorTransitionOut;
|
||||
var x = (beat - transitionStart) / transitionLength;
|
||||
var t = Mathf.Clamp(ColorTransitionEasing.Eval(x), 0f, 1f);
|
||||
if (ColorTransitionIn == 0.0f)
|
||||
{
|
||||
// Subtract an epsilon, so we don't use the same beat twice
|
||||
transitionStart -= 0.01f;
|
||||
}
|
||||
return Color.Lerp(ColorAtWholeBeat(transitionStart), ColorAtWholeBeat(transitionEnd), t);
|
||||
}
|
||||
|
||||
Color ColorAtWholeBeat(float beat)
|
||||
{
|
||||
int beatIndex = Mod.Positive(Mathf.FloorToInt(beat), Beats);
|
||||
return Mod.Index(Palette.Colors, beatIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,14 +493,25 @@ namespace MuzikaGromche
|
|||
|
||||
public class Config : SyncedConfig2<Config>
|
||||
{
|
||||
public static ConfigEntry<bool> EnableColorAnimations { get; private set; }
|
||||
|
||||
public static ConfigEntry<float> AudioOffset { get; private set; }
|
||||
|
||||
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
||||
|
||||
public static Palette PaletteOverride { get; private set; } = null;
|
||||
|
||||
public static float? BeatsOffsetOverride { get; private set; } = null;
|
||||
public static float? ColorTransitionInOverride { get; private set; } = null;
|
||||
public static float? ColorTransitionOutOverride { get; private set; } = null;
|
||||
public static string ColorTransitionEasingOverride { get; private set; } = null;
|
||||
|
||||
public Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID)
|
||||
{
|
||||
EnableColorAnimations = configFile.Bind("General", "Enable Color Animations", true,
|
||||
new ConfigDescription("Smooth light color transitions are known to cause performance issues on some setups.\n\nTurn them off if you experience lag spikes."));
|
||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(EnableColorAnimations, requiresRestart: false));
|
||||
|
||||
AudioOffset = configFile.Bind("General", "Audio Offset", 0f, new ConfigDescription(
|
||||
"Adjust audio offset (in seconds).\n\nIf you are playing with Bluetooth headphones and experiencing a visual desync, try setting this to about negative 0.2.\n\nIf your video output has high latency (like a long HDMI cable etc.), try positive values instead.",
|
||||
new AcceptableValueRange<float>(-0.5f, 0.5f)));
|
||||
|
@ -392,6 +520,7 @@ namespace MuzikaGromche
|
|||
#if DEBUG
|
||||
SetupEntriesToSkipWinding(configFile);
|
||||
SetupEntriesForPaletteOverride(configFile);
|
||||
SetupEntriesForTimingsOverride(configFile);
|
||||
#endif
|
||||
|
||||
var chanceRange = new AcceptableValueRange<int>(0, 100);
|
||||
|
@ -579,6 +708,110 @@ namespace MuzikaGromche
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupEntriesForTimingsOverride(ConfigFile configFile)
|
||||
{
|
||||
const string section = "Timings";
|
||||
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
|
||||
// Declare and initialize early to avoid "Use of unassigned local variable"
|
||||
SyncedEntry<bool> overrideTimingsSyncedEntry = null;
|
||||
SyncedEntry<float> beatsOffsetSyncedEntry = null;
|
||||
SyncedEntry<float> colorTransitionInSyncedEntry = null;
|
||||
SyncedEntry<float> colorTransitionOutSyncedEntry = null;
|
||||
SyncedEntry<string> colorTransitionEasingSyncedEntry = null;
|
||||
|
||||
var loadButton = new GenericButtonConfigItem(section, "Load Timings from the Current Track",
|
||||
"Override custom timings with the built-in timings of the current track.", "Load", load);
|
||||
loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost;
|
||||
LethalConfigManager.AddConfigItem(loadButton);
|
||||
|
||||
overrideTimingsSyncedEntry = configFile.BindSyncedEntry(section, "Override Timings", false,
|
||||
new ConfigDescription("If checked, custom timings override track's own built-in timings."));
|
||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(overrideTimingsSyncedEntry.Entry, new BoolCheckBoxOptions
|
||||
{
|
||||
RequiresRestart = false,
|
||||
CanModifyCallback = CanModifyIfHost,
|
||||
}));
|
||||
CSyncHackAddSyncedEntry(overrideTimingsSyncedEntry);
|
||||
overrideTimingsSyncedEntry.Changed += (sender, args) => apply();
|
||||
overrideTimingsSyncedEntry.SyncHostToLocal();
|
||||
|
||||
beatsOffsetSyncedEntry = configFile.BindSyncedEntry(section, "Beats Offset", 0f,
|
||||
new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f)));
|
||||
colorTransitionInSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition In", 0.25f,
|
||||
new ConfigDescription("Fraction of a beat *before* the whole beat when the color transition should start.", colorTransitionRange));
|
||||
colorTransitionOutSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition Out", 0.25f,
|
||||
new ConfigDescription("Fraction of a beat *after* the whole beat when the color transition should end.", colorTransitionRange));
|
||||
colorTransitionEasingSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition Easing", Easing.Linear.Name,
|
||||
new ConfigDescription("Interpolation/easing method to use for color transitions", new AcceptableValueList<string>(Easing.AllNames)));
|
||||
|
||||
var floatSliderOptions = new FloatSliderOptions
|
||||
{
|
||||
RequiresRestart = false,
|
||||
CanModifyCallback = CanModifyIfHost,
|
||||
};
|
||||
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetSyncedEntry.Entry, floatSliderOptions));
|
||||
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInSyncedEntry.Entry, floatSliderOptions));
|
||||
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutSyncedEntry.Entry, floatSliderOptions));
|
||||
LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingSyncedEntry.Entry, new TextDropDownOptions
|
||||
{
|
||||
RequiresRestart = false,
|
||||
CanModifyCallback = CanModifyIfHost,
|
||||
}));
|
||||
|
||||
CSyncHackAddSyncedEntry(beatsOffsetSyncedEntry);
|
||||
CSyncHackAddSyncedEntry(colorTransitionInSyncedEntry);
|
||||
CSyncHackAddSyncedEntry(colorTransitionOutSyncedEntry);
|
||||
CSyncHackAddSyncedEntry(colorTransitionEasingSyncedEntry);
|
||||
|
||||
beatsOffsetSyncedEntry.SyncHostToLocal();
|
||||
colorTransitionInSyncedEntry.SyncHostToLocal();
|
||||
colorTransitionOutSyncedEntry.SyncHostToLocal();
|
||||
colorTransitionEasingSyncedEntry.SyncHostToLocal();
|
||||
|
||||
beatsOffsetSyncedEntry.Changed += (sender, args) => apply();
|
||||
colorTransitionInSyncedEntry.Changed += (sender, args) => apply();
|
||||
colorTransitionOutSyncedEntry.Changed += (sender, args) => apply();
|
||||
colorTransitionEasingSyncedEntry.Changed += (sender, args) => apply();
|
||||
|
||||
void load()
|
||||
{
|
||||
// if track is null, set everything to defaults
|
||||
var track = Plugin.CurrentTrack;
|
||||
if (track == null)
|
||||
{
|
||||
beatsOffsetSyncedEntry.LocalValue = 0f;
|
||||
colorTransitionInSyncedEntry.LocalValue = 0f;
|
||||
colorTransitionOutSyncedEntry.LocalValue = 0f;
|
||||
colorTransitionEasingSyncedEntry.LocalValue = Easing.Linear.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
beatsOffsetSyncedEntry.LocalValue = track._BeatsOffset;
|
||||
colorTransitionInSyncedEntry.LocalValue = track._ColorTransitionIn;
|
||||
colorTransitionOutSyncedEntry.LocalValue = track._ColorTransitionOut;
|
||||
colorTransitionEasingSyncedEntry.LocalValue = track._ColorTransitionEasing.Name;
|
||||
}
|
||||
}
|
||||
|
||||
void apply()
|
||||
{
|
||||
if (!overrideTimingsSyncedEntry.Value)
|
||||
{
|
||||
BeatsOffsetOverride = null;
|
||||
ColorTransitionInOverride = null;
|
||||
ColorTransitionOutOverride = null;
|
||||
ColorTransitionEasingOverride = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
BeatsOffsetOverride = beatsOffsetSyncedEntry.Value;
|
||||
ColorTransitionInOverride = colorTransitionInSyncedEntry.Value;
|
||||
ColorTransitionOutOverride = colorTransitionOutSyncedEntry.Value;
|
||||
ColorTransitionEasingOverride = colorTransitionEasingSyncedEntry.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// farAudio is during windup, Start overrides popGoesTheWeaselTheme
|
||||
|
|
Loading…
Reference in New Issue