forked from nikita/muzika-gromche
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 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 record Palette(Color[] Colors)
|
||||||
{
|
{
|
||||||
public static Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]);
|
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)
|
public float CalculateBeat(AudioSource start, AudioSource loop)
|
||||||
{
|
{
|
||||||
// If popped, calculate which beat the music is currently at.
|
// 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 normalized = Mod.Positive(elapsed / LoadedLoop.length, 1f);
|
||||||
|
|
||||||
var beat = normalized * (float)Beats;
|
var beat = normalized * (float)Beats;
|
||||||
|
var offset = Mod.Positive(beat - BeatsOffset, (float)Beats);
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var color = ColorAtBeat(beat);
|
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,
|
Time.realtimeSinceStartup, Time.deltaTime,
|
||||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||||
normalized, beat, color);
|
normalized, beat, offset, color);
|
||||||
#endif
|
#endif
|
||||||
return beat;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Palette _Palette = Palette.DEFAULT;
|
public Palette _Palette = Palette.DEFAULT;
|
||||||
|
@ -246,11 +307,67 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color ColorAtBeat(float beat)
|
public Color ColorAtBeat(float beat)
|
||||||
|
{
|
||||||
|
// 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);
|
int beatIndex = Mod.Positive(Mathf.FloorToInt(beat), Beats);
|
||||||
return Mod.Index(Palette.Colors, beatIndex);
|
return Mod.Index(Palette.Colors, beatIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default C#/.NET remainder operator % returns negative result for negative input
|
// Default C#/.NET remainder operator % returns negative result for negative input
|
||||||
// which is unsuitable as an index for an array.
|
// which is unsuitable as an index for an array.
|
||||||
|
@ -376,14 +493,25 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
public class Config : SyncedConfig2<Config>
|
public class Config : SyncedConfig2<Config>
|
||||||
{
|
{
|
||||||
|
public static ConfigEntry<bool> EnableColorAnimations { get; private set; }
|
||||||
|
|
||||||
public static ConfigEntry<float> AudioOffset { get; private set; }
|
public static ConfigEntry<float> AudioOffset { get; private set; }
|
||||||
|
|
||||||
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
||||||
|
|
||||||
public static Palette PaletteOverride { get; private set; } = null;
|
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)
|
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(
|
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.",
|
"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)));
|
new AcceptableValueRange<float>(-0.5f, 0.5f)));
|
||||||
|
@ -392,6 +520,7 @@ namespace MuzikaGromche
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
SetupEntriesToSkipWinding(configFile);
|
SetupEntriesToSkipWinding(configFile);
|
||||||
SetupEntriesForPaletteOverride(configFile);
|
SetupEntriesForPaletteOverride(configFile);
|
||||||
|
SetupEntriesForTimingsOverride(configFile);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var chanceRange = new AcceptableValueRange<int>(0, 100);
|
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
|
// farAudio is during windup, Start overrides popGoesTheWeaselTheme
|
||||||
|
|
Loading…
Reference in New Issue