diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 1767d25e77..bb28b2b217 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Tests { config.BindWith(OsuSetting.BeatmapSkins, BeatmapSkins); config.BindWith(OsuSetting.BeatmapColours, BeatmapColours); + + config.SetValue(OsuSetting.ComboColourNormalisationAmount, 0f); } [TestCase(true, true)] diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index cccf640e27..255d6400c8 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; @@ -25,10 +27,20 @@ namespace osu.Game.Tests.Gameplay [HeadlessTest] public class TestSceneHitObjectAccentColour : OsuTestScene { + [Resolved] + private OsuConfigManager config { get; set; } + private Container skinContainer; [SetUp] - public void Setup() => Schedule(() => Child = skinContainer = new SkinProvidingContainer(new TestSkin())); + public void Setup() + { + Schedule(() => + { + config.SetValue(OsuSetting.ComboColourNormalisationAmount, 0f); + Child = skinContainer = new SkinProvidingContainer(new TestSkin()); + }); + } [Test] public void TestChangeComboIndexBeforeLoad() diff --git a/osu.Game/Configuration/IGameplaySettings.cs b/osu.Game/Configuration/IGameplaySettings.cs new file mode 100644 index 0000000000..a35bdd20d0 --- /dev/null +++ b/osu.Game/Configuration/IGameplaySettings.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; + +namespace osu.Game.Configuration +{ + /// + /// A settings provider which generally sources from (global user settings) + /// but can allow overriding settings by caching more locally. For instance, in the editor. + /// + /// + /// More settings can be moved into this interface as required. + /// + [Cached] + public interface IGameplaySettings + { + IBindable ComboColourNormalisationAmount { get; } + + IBindable PositionalHitsoundsLevel { get; } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 3348f3326c..6cbb677a64 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; +using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; @@ -26,7 +27,7 @@ using osu.Game.Skinning; namespace osu.Game.Configuration { [ExcludeFromDynamicCompile] - public class OsuConfigManager : IniConfigManager + public class OsuConfigManager : IniConfigManager, IGameplaySettings { public OsuConfigManager(Storage storage) : base(storage) @@ -175,6 +176,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.LastProcessedMetadataId, -1); + + SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -275,6 +278,9 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } = _ => @"unknown"; public Func LookupKeyBindings { get; set; } = _ => @"unknown"; + + IBindable IGameplaySettings.ComboColourNormalisationAmount => GetOriginalBindable(OsuSetting.ComboColourNormalisationAmount); + IBindable IGameplaySettings.PositionalHitsoundsLevel => GetOriginalBindable(OsuSetting.PositionalHitsoundsLevel); } // IMPORTANT: These are used in user configuration files. @@ -362,11 +368,12 @@ namespace osu.Game.Configuration GameplayDisableWinKey, SeasonalBackgroundMode, EditorWaveformOpacity, + EditorShowHitMarkers, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, ShowOnlineExplicitContent, LastProcessedMetadataId, SafeAreaConsiderations, - EditorShowHitMarkers + ComboColourNormalisationAmount, } } diff --git a/osu.Game/Graphics/HSPAColour.cs b/osu.Game/Graphics/HSPAColour.cs new file mode 100644 index 0000000000..18f4c6bca4 --- /dev/null +++ b/osu.Game/Graphics/HSPAColour.cs @@ -0,0 +1,201 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osuTK.Graphics; + +namespace osu.Game.Graphics +{ + public struct HSPAColour + { + private const float p_r = 0.299f; + private const float p_g = 0.587f; + private const float p_b = 0.114f; + + /// + /// The hue. + /// + public float H; + + /// + /// The saturation. + /// + public float S; + + /// + /// The perceived brightness of this colour. + /// + public float P; + + /// + /// The alpha. + /// + public float A; + + public HSPAColour(float h, float s, float p, float a) + { + H = h; + S = s; + P = p; + A = a; + } + + public HSPAColour(Color4 colour) + { + H = 0; + S = 0; + P = MathF.Sqrt(colour.R * colour.R * p_r + colour.G * colour.G * p_g + colour.B + colour.B * p_b); + A = colour.A; + + if (colour.R == colour.G && colour.R == colour.B) + return; + + if (colour.R >= colour.G && colour.R >= colour.B) + { + if (colour.B >= colour.G) + { + H = 6f / 6f - 1f / 6f * (colour.B - colour.G) / (colour.R - colour.G); + S = 1f - colour.G / colour.R; + } + else + { + H = 0f / 6f + 1f / 6f * (colour.G - colour.B) / (colour.R - colour.B); + S = 1f - colour.B / colour.R; + } + } + else if (colour.G >= colour.R && colour.G >= colour.B) + { + if (colour.R >= colour.B) + { + H = 2f / 6f - 1f / 6f * (colour.R - colour.B) / (colour.G - colour.B); + S = 1f - colour.B / colour.G; + } + else + { + H = 2f / 6f + 1f / 6f * (colour.B - colour.R) / (colour.G - colour.R); + S = 1f - colour.R / colour.G; + } + } + else + { + if (colour.G >= colour.R) + { + H = 4f / 6f - 1f / 6f * (colour.G - colour.R) / (colour.B - colour.R); + S = 1f - colour.R / colour.B; + } + else + { + H = 4f / 6f + 1f / 6f * (colour.R - colour.G) / (colour.B - colour.G); + S = 1f - colour.G / colour.B; + } + } + } + + public Color4 ToColor4() + { + float minOverMax = 1f - S; + + Color4 result = new Color4 { A = A }; + float h = H; + + if (minOverMax > 0f) + { + if (h < 1f / 6f) + { + h = 6f * (h - 0f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.B = P / MathF.Sqrt(p_r / minOverMax / minOverMax + p_g * part * part + p_b); + result.R = result.B / minOverMax; + result.G = result.B + h * (result.R - result.B); + } + else if (h < 2f / 6f) + { + h = 6f * (-h + 2f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.B = P / MathF.Sqrt(p_g / minOverMax / minOverMax + p_r * part * part + p_b); + result.G = result.B / minOverMax; + result.R = result.B + h * (result.G - result.B); + } + else if (h < 3f / 6f) + { + h = 6f * (h - 2f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.R = P / MathF.Sqrt(p_g / minOverMax / minOverMax + p_b * part * part + p_r); + result.G = result.R / minOverMax; + result.B = result.R + h * (result.G - result.R); + } + else if (h < 4f / 6f) + { + h = 6f * (-h + 4f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.R = P / MathF.Sqrt(p_b / minOverMax / minOverMax + p_g * part * part + p_r); + result.B = result.R / minOverMax; + result.G = result.R + h * (result.B - result.R); + } + else if (h < 5f / 6f) + { + h = 6f * (h - 4f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.G = P / MathF.Sqrt(p_b / minOverMax / minOverMax + p_r * part * part + p_g); + result.B = result.G / minOverMax; + result.R = result.G + h * (result.B - result.G); + } + else + { + h = 6f * (-h + 6f / 6f); + float part = 1f + h * (1f / minOverMax - 1f); + result.G = P / MathF.Sqrt(p_r / minOverMax / minOverMax + p_b * part * part + p_g); + result.R = result.G / minOverMax; + result.B = result.G + h * (result.R - result.G); + } + } + else + { + if (h < 1f / 6f) + { + h = 6f * (h - 0f / 6f); + result.R = MathF.Sqrt(P * P / (p_r + p_g * h * h)); + result.G = result.R * h; + result.B = 0f; + } + else if (h < 2f / 6f) + { + h = 6f * (-h + 2f / 6f); + result.G = MathF.Sqrt(P * P / (p_g + p_r * h * h)); + result.R = result.G * h; + result.B = 0f; + } + else if (h < 3f / 6f) + { + h = 6f * (h - 2f / 6f); + result.G = MathF.Sqrt(P * P / (p_g + p_b * h * h)); + result.B = result.G * h; + result.R = 0f; + } + else if (h < 4f / 6f) + { + h = 6f * (-h + 4f / 6f); + result.B = MathF.Sqrt(P * P / (p_b + p_g * h * h)); + result.G = result.B * h; + result.R = 0f; + } + else if (h < 5f / 6f) + { + h = 6f * (h - 4f / 6f); + result.B = MathF.Sqrt(P * P / (p_b + p_r * h * h)); + result.R = result.B * h; + result.G = 0f; + } + else + { + h = 6f * (-h + 6f / 6f); + result.R = MathF.Sqrt(P * P / (p_r + p_b * h * h)); + result.B = result.R * h; + result.G = 0f; + } + } + + return result; + } + } +} diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 2ab9d9de87..6e05929d81 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -99,6 +99,11 @@ namespace osu.Game.Localisation /// public static LocalisableString StoryboardVideo => new TranslatableString(getKey(@"storyboard_video"), @"Storyboard / video"); + /// + /// "Combo colour normalisation" + /// + public static LocalisableString ComboColourNormalisation => new TranslatableString(getKey(@"combo_colour_normalisation"), @"Combo colour normalisation"); + /// /// "Hit lighting" /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 511f492b8a..1d5f5a75e5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -262,6 +262,7 @@ namespace osu.Game dependencies.Cache(largeStore); dependencies.CacheAs(LocalConfig); + dependencies.CacheAs(LocalConfig); InitialiseFonts(); diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs index becb7aa80f..14ff6ac2b5 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -15,9 +16,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { protected override LocalisableString Header => GameplaySettingsStrings.BeatmapHeader; + private readonly BindableFloat comboColourNormalisation = new BindableFloat(); + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + config.BindWith(OsuSetting.ComboColourNormalisationAmount, comboColourNormalisation); + Children = new Drawable[] { new SettingsCheckbox @@ -40,6 +45,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GraphicsSettingsStrings.StoryboardVideo, Current = config.GetBindable(OsuSetting.ShowStoryboard) }, + new SettingsSlider + { + LabelText = GraphicsSettingsStrings.ComboColourNormalisation, + Current = comboColourNormalisation, + DisplayAsPercentage = true, + } }; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5062cbc3e6..60792445cd 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,8 +15,10 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Types; @@ -127,7 +129,8 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly BindableList samplesBindable = new BindableList(); private readonly Bindable comboIndexBindable = new Bindable(); - private readonly Bindable positionalHitsoundsLevel = new Bindable(); + private readonly IBindable positionalHitsoundsLevel = new Bindable(); + private readonly IBindable comboColourBrightness = new Bindable(); private readonly Bindable comboIndexWithOffsetsBindable = new Bindable(); protected override bool RequiresChildrenUpdate => true; @@ -168,9 +171,10 @@ namespace osu.Game.Rulesets.Objects.Drawables } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, ISkinSource skinSource) + private void load(IGameplaySettings gameplaySettings, ISkinSource skinSource) { - config.BindWith(OsuSetting.PositionalHitsoundsLevel, positionalHitsoundsLevel); + positionalHitsoundsLevel.BindTo(gameplaySettings.PositionalHitsoundsLevel); + comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount); // Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal. base.AddInternal(Samples = new PausableSkinnableSound()); @@ -192,6 +196,8 @@ namespace osu.Game.Rulesets.Objects.Drawables comboIndexBindable.BindValueChanged(_ => UpdateComboColour()); comboIndexWithOffsetsBindable.BindValueChanged(_ => UpdateComboColour(), true); + comboColourBrightness.BindValueChanged(_ => UpdateComboColour()); + // Apply transforms updateState(State.Value, true); } @@ -511,7 +517,15 @@ namespace osu.Game.Rulesets.Objects.Drawables { if (!(HitObject is IHasComboInformation combo)) return; - AccentColour.Value = combo.GetComboColour(CurrentSkin); + Color4 colour = combo.GetComboColour(CurrentSkin); + + // Normalise the combo colour to the given brightness level. + if (comboColourBrightness.Value != 0) + { + colour = Interpolation.ValueAt(Math.Abs(comboColourBrightness.Value), colour, new HSPAColour(colour) { P = 0.6f }.ToColor4(), 0, 1); + } + + AccentColour.Value = colour; } /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 4c44117581..bb390dfbf3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -58,7 +58,8 @@ namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] [Cached] - public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider + public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider, + IGameplaySettings { public override float BackgroundParallaxAmount => 0.1f; @@ -98,6 +99,9 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved] + private IGameplaySettings globalGameplaySettings { get; set; } + public readonly Bindable Mode = new Bindable(); public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; @@ -1041,5 +1045,11 @@ namespace osu.Game.Screens.Edit { } } + + // Combo colour normalisation should not be applied in the editor. + IBindable IGameplaySettings.ComboColourNormalisationAmount => new Bindable(); + + // Arguable. + IBindable IGameplaySettings.PositionalHitsoundsLevel => globalGameplaySettings.PositionalHitsoundsLevel; } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index bb3360acec..6a7eabc6a2 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -12,6 +12,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; + private readonly PlayerSliderBar comboColourNormalisationSliderBar; private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapColorsToggle; @@ -34,6 +35,11 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle = new PlayerCheckbox { LabelText = GraphicsSettingsStrings.StoryboardVideo }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins }, beatmapColorsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapColours }, + comboColourNormalisationSliderBar = new PlayerSliderBar + { + LabelText = GraphicsSettingsStrings.ComboColourNormalisation, + DisplayAsPercentage = true, + }, }; } @@ -45,6 +51,7 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapColorsToggle.Current = config.GetBindable(OsuSetting.BeatmapColours); + comboColourNormalisationSliderBar.Current = config.GetBindable(OsuSetting.ComboColourNormalisationAmount); } } } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 44df495929..154ad0fe8c 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -334,6 +334,7 @@ GL GLSL HID + HSPA HSV HTML HUD