diff --git a/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs b/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs index 1f1730db4c..29611e72f0 100644 --- a/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs +++ b/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Android.Graphics.Textures { @@ -17,7 +18,7 @@ public AndroidTextureLoaderStore(IResourceStore store) { } - protected override Image ImageFromStream(Stream stream) + protected override PremultipliedImage ImageFromStream(Stream stream) { using (var bitmap = BitmapFactory.DecodeStream(stream)) { @@ -34,7 +35,7 @@ protected override Image ImageFromStream(Stream stream) } bitmap.Recycle(); - return Image.LoadPixelData(result, bitmap.Width, bitmap.Height); + return PremultipliedImage.FromPremultiplied(Image.LoadPixelData(result, bitmap.Width, bitmap.Height)); } } } diff --git a/osu.Framework.Benchmarks/BenchmarkFontLoading.cs b/osu.Framework.Benchmarks/BenchmarkFontLoading.cs index 696be54804..d36a34eb0c 100644 --- a/osu.Framework.Benchmarks/BenchmarkFontLoading.cs +++ b/osu.Framework.Benchmarks/BenchmarkFontLoading.cs @@ -102,7 +102,7 @@ private void runFor(GlyphStore store) var icon = (IconUsage)propValue; using (var upload = store.Get(icon.Icon.ToString())) - Trace.Assert(upload.Data != null); + Trace.Assert(upload.PremultipliedData != null); if (remainingCount-- == 0) return; diff --git a/osu.Framework.Tests/Resources/Textures/figma-blending-additive.png b/osu.Framework.Tests/Resources/Textures/figma-blending-additive.png new file mode 100644 index 0000000000..308ebd3a3d Binary files /dev/null and b/osu.Framework.Tests/Resources/Textures/figma-blending-additive.png differ diff --git a/osu.Framework.Tests/Resources/Textures/figma-blending-mixture.png b/osu.Framework.Tests/Resources/Textures/figma-blending-mixture.png new file mode 100644 index 0000000000..21063d9065 Binary files /dev/null and b/osu.Framework.Tests/Resources/Textures/figma-blending-mixture.png differ diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs new file mode 100644 index 0000000000..2f97dc6235 --- /dev/null +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs @@ -0,0 +1,342 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Drawables +{ + public partial class TestSceneBlendingCorrectness : FrameworkTestScene + { + [Resolved] + private TextureStore textures { get; set; } = null!; + + [Test] + public void TestMixture() + { + AddStep("setup", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20f), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f + } + } + } + } + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f, + } + } + } + } + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Container", + X = -150 + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Buffered", + X = 150 + }, + } + }, + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("figma-blending-mixture") + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Figma design", + }, + } + }; + }); + } + + [Test] + public void TestAdditive() + { + AddStep("setup", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20f), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f + } + } + } + } + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f, + } + } + } + } + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Container", + X = -150 + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Buffered", + X = 150 + }, + } + }, + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("figma-blending-additive") + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Figma design", + }, + } + }; + }); + } + + /// + /// There is no reference source for this one. This can be used to test whether or is more correct + /// to use as the blending type for . + /// + [Test] + public void TestAdditiveAlpha() + { + AddStep("setup", () => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Blue, + Alpha = 0.5f, + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.25f, + } + } + } + } + }; + }); + } + } +} diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs index 883c59538c..31c17d3169 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs @@ -220,7 +220,7 @@ public TestSceneComplexBlending() colourEquation.Current.Value = foregroundContainer.Blending.RGBEquation; alphaEquation.Current.Value = foregroundContainer.Blending.AlphaEquation; - blendingSrcDropdown.Current.Value = BlendingType.SrcAlpha; + blendingSrcDropdown.Current.Value = BlendingType.One; blendingDestDropdown.Current.Value = BlendingType.OneMinusSrcAlpha; blendingAlphaSrcDropdown.Current.Value = BlendingType.One; blendingAlphaDestDropdown.Current.Value = BlendingType.One; diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 6d0b7693a3..4c1681a37c 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -7,12 +7,14 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; using osuTK; +using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Tests.Visual.Graphics @@ -116,7 +118,7 @@ protected override void Draw(IRenderer renderer) var rng = new Random(1337); for (int i = 0; i < colourBuffer.Size; i++) - colourBuffer[i] = new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }; + colourBuffer[i] = new ColourData { Colour = PremultipliedColour.FromStraight(new Color4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1)) }; } // Bind the custom shader and SSBO. @@ -212,7 +214,7 @@ protected override void Draw(IRenderer renderer) for (int i = 0; i < areas.Count; i++) { - int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); + int colourIndex = colourBuffer.Push(new ColourData { Colour = PremultipliedColour.FromStraight(new Color4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1)) }); // Bind the SSBO. This may change between iterations if a buffer transition happens via the above push. shader.BindUniformBlock("g_ColourBuffer", colourBuffer.CurrentBuffer); @@ -260,7 +262,7 @@ protected override void Dispose(bool isDisposing) [StructLayout(LayoutKind.Sequential, Pack = 1)] private record struct ColourData { - public UniformVector4 Colour; + public UniformColour Colour; } [StructLayout(LayoutKind.Sequential)] diff --git a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs index 174afbf555..af38b62dfb 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Rendering; @@ -15,8 +16,6 @@ using osu.Framework.Testing; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.Input { @@ -33,12 +32,12 @@ private void load(IRenderer renderer) { const int width = 2; Texture gradientTexture = renderer.CreateTexture(width, 1, true); - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); for (int i = 0; i < width; ++i) { byte brightnessByte = (byte)((float)i / (width - 1) * 255); - image[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte); + image[i, 0] = new Color4(brightnessByte, brightnessByte, brightnessByte, 255).ToPremultiplied(); } gradientTexture.SetData(new TextureUpload(image)); diff --git a/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs b/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs index 27ae435d53..67b4590233 100644 --- a/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs +++ b/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs @@ -93,7 +93,7 @@ private class ReusableTextureUpload : ITextureUpload { private readonly ITextureUpload upload; - public ReadOnlySpan Data => upload.Data; + public ReadOnlySpan PremultipliedData => upload.PremultipliedData; public int Level => upload.Level; diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs b/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs index 148a490f39..82bf74a932 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs @@ -73,7 +73,7 @@ public void TestImage() clipboardImage = image!.Clone(); var texture = renderer.CreateTexture(image.Width, image.Height); - texture.SetData(new TextureUpload(image)); + texture.SetData(new TextureUpload(PremultipliedImage.FromStraight(image))); Child = new Sprite { diff --git a/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs b/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs index c03a9c04d9..60e9b1899b 100644 --- a/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs +++ b/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs @@ -78,7 +78,7 @@ private void takeScreenshot() var image = t.GetResultSafely(); var tex = renderer.CreateTexture(image.Width, image.Height); - tex.SetData(new TextureUpload(image)); + tex.SetData(new TextureUpload(PremultipliedImage.FromStraight(image))); display.Texture = tex; background.Show(); diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs index b12ecb5c22..7e39e6229a 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -11,8 +12,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -32,31 +31,31 @@ private void load() { const int width = 128; - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); gradientTextureHorizontal = renderer.CreateTexture(width, 1, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureHorizontal.SetData(new TextureUpload(image)); - image = new Image(width, 1); + image = new PremultipliedImage(width, 1); gradientTextureVertical = renderer.CreateTexture(1, width, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureVertical.SetData(new TextureUpload(image)); - image = new Image(width, width); + image = new PremultipliedImage(width, width); gradientTextureBoth = renderer.CreateTexture(width, width, true); @@ -66,11 +65,11 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image[i, j] = new Rgba32( + image[i, j] = new Color4( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), - 255); + 255).ToPremultiplied(); } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs index 299b77fece..0503cfaa53 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -13,8 +14,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -38,31 +37,31 @@ private void load() { const int width = 128; - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); gradientTextureHorizontal = renderer.CreateTexture(width, 1, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureHorizontal.SetData(new TextureUpload(image)); - image = new Image(width, 1); + image = new PremultipliedImage(width, 1); gradientTextureVertical = renderer.CreateTexture(1, width, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureVertical.SetData(new TextureUpload(image)); - image = new Image(width, width); + image = new PremultipliedImage(width, width); gradientTextureBoth = renderer.CreateTexture(width, width, true); @@ -72,11 +71,11 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image[i, j] = new Rgba32( + image[i, j] = new Color4( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), - 255); + 255).ToPremultiplied(); } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs index 42c15e0ffe..a211ed9c7c 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; @@ -15,8 +16,6 @@ using osu.Framework.Utils; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -29,12 +28,12 @@ public partial class TestSceneDrawablePath : FrameworkTestScene [BackgroundDependencyLoader] private void load(IRenderer renderer) { - var image = new Image(texture_width, 1); + var image = new PremultipliedImage(texture_width, 1); for (int i = 0; i < texture_width; ++i) { byte brightnessByte = (byte)((float)i / (texture_width - 1) * 255); - image[i, 0] = new Rgba32(255, 255, 255, brightnessByte); + image[i, 0] = new Color4(255, 255, 255, brightnessByte).ToPremultiplied(); } gradientTexture = renderer.CreateTexture(texture_width, 1, true); diff --git a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs index def073c20a..843e81f118 100644 --- a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs +++ b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using UIKit; namespace osu.Framework.iOS.Graphics.Textures @@ -19,7 +20,7 @@ public IOSTextureLoaderStore(IResourceStore store) { } - protected override Image ImageFromStream(Stream stream) + protected override PremultipliedImage ImageFromStream(Stream stream) { using (var nativeData = NSData.FromStream(stream)) { @@ -39,9 +40,11 @@ protected override Image ImageFromStream(Stream stream) using (CGBitmapContext textureContext = new CGBitmapContext(data, width, height, 8, width * 4, CGColorSpace.CreateDeviceRGB(), CGImageAlphaInfo.PremultipliedLast)) textureContext.DrawImage(new CGRect(0, 0, width, height), uiImage.CGImage); - var image = Image.LoadPixelData(data, width, height); + var image = Image.LoadPixelData(data, width, height); - return image; + // UIImage/CGBitmapContext already produce images in premultiplied form, + // so we can directly wrap the image without performing any conversion. + return PremultipliedImage.FromPremultiplied(image); } } } diff --git a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs index 4c1c2d8382..0f83a6ccec 100644 --- a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs +++ b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs @@ -4,6 +4,7 @@ using osuTK.Graphics; using System; using System.Globalization; +using osu.Framework.Graphics.Colour; namespace osu.Framework.Extensions.Color4Extensions { @@ -288,5 +289,12 @@ public static (float h, float s, float v) ToHSV(this Color4 colour) return (h, s, v); } + + /// + /// Converts a to premultiplied alpha and returns a resultant . + /// The input colour is assumed to be in straight-alpha form. + /// + /// The colour as straight-alpha. + public static PremultipliedColour ToPremultiplied(this Color4 colour) => PremultipliedColour.FromStraight(colour); } } diff --git a/osu.Framework/Graphics/BlendingParameters.cs b/osu.Framework/Graphics/BlendingParameters.cs index 1721dab3ac..533d216ceb 100644 --- a/osu.Framework/Graphics/BlendingParameters.cs +++ b/osu.Framework/Graphics/BlendingParameters.cs @@ -72,17 +72,17 @@ public struct BlendingParameters : IEquatable public static BlendingParameters Mixture => new BlendingParameters { - Source = BlendingType.SrcAlpha, + Source = BlendingType.One, Destination = BlendingType.OneMinusSrcAlpha, SourceAlpha = BlendingType.One, - DestinationAlpha = BlendingType.One, + DestinationAlpha = BlendingType.OneMinusSrcAlpha, RGBEquation = BlendingEquation.Add, AlphaEquation = BlendingEquation.Add, }; public static BlendingParameters Additive => new BlendingParameters { - Source = BlendingType.SrcAlpha, + Source = BlendingType.One, Destination = BlendingType.One, SourceAlpha = BlendingType.One, DestinationAlpha = BlendingType.One, @@ -123,7 +123,7 @@ public void CopyFromParent(BlendingParameters parent) public void ApplyDefaultToInherited() { if (Source == BlendingType.Inherit) - Source = BlendingType.SrcAlpha; + Source = BlendingType.One; if (Destination == BlendingType.Inherit) Destination = BlendingType.OneMinusSrcAlpha; @@ -132,7 +132,7 @@ public void ApplyDefaultToInherited() SourceAlpha = BlendingType.One; if (DestinationAlpha == BlendingType.Inherit) - DestinationAlpha = BlendingType.One; + DestinationAlpha = BlendingType.OneMinusSrcAlpha; if (RGBEquation == BlendingEquation.Inherit) RGBEquation = BlendingEquation.Add; diff --git a/osu.Framework/Graphics/BufferedDrawNode.cs b/osu.Framework/Graphics/BufferedDrawNode.cs index f250b96411..7de61d5c41 100644 --- a/osu.Framework/Graphics/BufferedDrawNode.cs +++ b/osu.Framework/Graphics/BufferedDrawNode.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Statistics; @@ -99,7 +100,7 @@ protected sealed override void Draw(IRenderer renderer) // We need to draw children as if they were zero-based to the top-left of the texture. // We can do this by adding a translation component to our (orthogonal) projection matrix. renderer.PushOrtho(screenSpaceDrawRectangle); - renderer.Clear(new ClearInfo(backgroundColour)); + renderer.Clear(new ClearInfo(backgroundColour.ToPremultiplied())); DrawOther(Child, renderer); diff --git a/osu.Framework/Graphics/Colour/PremultipliedColour.cs b/osu.Framework/Graphics/Colour/PremultipliedColour.cs new file mode 100644 index 0000000000..02f8e87676 --- /dev/null +++ b/osu.Framework/Graphics/Colour/PremultipliedColour.cs @@ -0,0 +1,74 @@ +// 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; +using SixLabors.ImageSharp.PixelFormats; + +namespace osu.Framework.Graphics.Colour +{ + /// + /// Represents a structure for containing a "premultiplied colour". + /// + public readonly struct PremultipliedColour : IEquatable + { + /// + /// The red component of this colour multiplied by the value of . + /// + public readonly float PremultipliedR; + + /// + /// The green component of this colour multiplied by the value of . + /// + public readonly float PremultipliedG; + + /// + /// The blue component of this colour multiplied by the value of . + /// + public readonly float PremultipliedB; + + /// + /// The alpha component of this colour, often referred to as "occlusion" instead of "opacity" in the context of premultiplied colours. + /// + public readonly float Occlusion; + + public PremultipliedColour(float premultipliedR, float premultipliedG, float premultipliedB, float occlusion) + { + PremultipliedR = premultipliedR; + PremultipliedG = premultipliedG; + PremultipliedB = premultipliedB; + Occlusion = occlusion; + } + + /// + /// Creates a containing the premultiplied components of this colour. + /// + public Rgba32 ToPremultipliedRgba32() => new Rgba32(PremultipliedR, PremultipliedG, PremultipliedB, Occlusion); + + /// + /// Creates a from a straight-alpha colour. + /// + /// The straight-alpha colour. + public static PremultipliedColour FromStraight(Color4 colour) => new PremultipliedColour( + colour.R * colour.A, + colour.G * colour.A, + colour.B * colour.A, + colour.A); + + /// + /// Creates a from a premultiplied-alpha colour. + /// + /// The premultiplied-alpha colour. + public static PremultipliedColour FromPremultiplied(Rgba32 premultipliedColour) + { + var premultipliedVector = premultipliedColour.ToVector4(); + return new PremultipliedColour(premultipliedVector.X, premultipliedVector.Y, premultipliedVector.Z, premultipliedVector.W); + } + + public bool Equals(PremultipliedColour other) + => PremultipliedR == other.PremultipliedR && + PremultipliedG == other.PremultipliedG && + PremultipliedB == other.PremultipliedB && + Occlusion == other.Occlusion; + } +} diff --git a/osu.Framework/Graphics/Colour4.cs b/osu.Framework/Graphics/Colour4.cs index 8809dbe71e..3a29283f86 100644 --- a/osu.Framework/Graphics/Colour4.cs +++ b/osu.Framework/Graphics/Colour4.cs @@ -6,6 +6,7 @@ using System; using System.Globalization; using System.Numerics; +using osu.Framework.Graphics.Colour; using osuTK.Graphics; namespace osu.Framework.Graphics @@ -547,6 +548,12 @@ public Vector4 ToHSL() return new Vector4(hue, saturation, lightness, A); } + /// + /// Converts this to premultiplied alpha and returns a resultant . + /// This colour is assumed to be in straight-alpha form. + /// + public PremultipliedColour ToPremultiplied() => PremultipliedColour.FromStraight(this); + private const double gamma = 2.4; private static float toLinear(float color) diff --git a/osu.Framework/Graphics/Lines/Path_DrawNode.cs b/osu.Framework/Graphics/Lines/Path_DrawNode.cs index 3ff49ec497..814b7c1151 100644 --- a/osu.Framework/Graphics/Lines/Path_DrawNode.cs +++ b/osu.Framework/Graphics/Lines/Path_DrawNode.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using System.Diagnostics; +using osu.Framework.Extensions.Color4Extensions; namespace osu.Framework.Graphics.Lines { @@ -105,19 +106,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = new Vector3(segmentRight.EndPoint.X, segmentRight.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.EndPoint) + Colour = colourAt(segmentRight.EndPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentRight.StartPoint.X, segmentRight.StartPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.StartPoint) + Colour = colourAt(segmentRight.StartPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); // Outer quad, triangle 2 @@ -125,19 +126,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = secondMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour + Colour = secondMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentRight.EndPoint.X, segmentRight.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.EndPoint) + Colour = colourAt(segmentRight.EndPoint).ToPremultiplied() }); // Inner quad, triangle 1 @@ -145,19 +146,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = secondMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour + Colour = secondMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentLeft.EndPoint.X, segmentLeft.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.EndPoint) + Colour = colourAt(segmentLeft.EndPoint).ToPremultiplied() }); // Inner quad, triangle 2 @@ -165,19 +166,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = new Vector3(segmentLeft.EndPoint.X, segmentLeft.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.EndPoint) + Colour = colourAt(segmentLeft.EndPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentLeft.StartPoint.X, segmentLeft.StartPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.StartPoint) + Colour = colourAt(segmentLeft.StartPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); } @@ -214,7 +215,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(origin.X, origin.Y, 1), TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = originColour + Colour = originColour.ToPremultiplied() }); // First outer point @@ -222,7 +223,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(current.X, current.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour + Colour = currentColour.ToPremultiplied() }); current = i < stepCount ? origin + pointOnCircle(theta0 + i * thetaStep) * radius : end; @@ -233,7 +234,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(current.X, current.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour + Colour = currentColour.ToPremultiplied() }); } } diff --git a/osu.Framework/Graphics/Lines/SmoothPath.cs b/osu.Framework/Graphics/Lines/SmoothPath.cs index e012020326..a121d384b8 100644 --- a/osu.Framework/Graphics/Lines/SmoothPath.cs +++ b/osu.Framework/Graphics/Lines/SmoothPath.cs @@ -8,8 +8,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osuTK.Graphics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Lines { @@ -66,7 +64,7 @@ private void validateTexture() int textureWidth = (int)PathRadius * 2; //initialise background - var raw = new Image(textureWidth, 1); + var raw = new PremultipliedImage(textureWidth, 1); const float aa_portion = 0.02f; @@ -75,13 +73,11 @@ private void validateTexture() float progress = (float)i / (textureWidth - 1); var colour = ColourAt(progress); - raw[i, 0] = new Rgba32(colour.R, colour.G, colour.B, colour.A * Math.Min(progress / aa_portion, 1)); + raw[i, 0] = colour.Opacity(colour.A * Math.Min(progress / aa_portion, 1)).ToPremultiplied(); } if (Texture?.Width == textureWidth) - { Texture.SetData(new TextureUpload(raw)); - } else { var texture = new DisposableTexture(renderer.CreateTexture(textureWidth, 1, true)); diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 338cff27f3..8af0e879f9 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -238,8 +238,8 @@ protected override void DeleteFrameBufferImplementation(IFrameBuffer frameBuffer protected override void ClearImplementation(ClearInfo clearInfo) { - if (clearInfo.Colour != CurrentClearInfo.Colour) - GL.ClearColor(clearInfo.Colour); + if (!clearInfo.Colour.Equals(CurrentClearInfo.Colour)) + GL.ClearColor(new Color4(clearInfo.Colour.PremultipliedR, clearInfo.Colour.PremultipliedG, clearInfo.Colour.PremultipliedB, clearInfo.Colour.Occlusion)); if (clearInfo.Depth != CurrentClearInfo.Depth) { diff --git a/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs b/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs index a5ba2c5919..74863d80ac 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs @@ -213,7 +213,7 @@ public unsafe bool Upload() { using (upload) { - fixed (Rgba32* ptr = upload.Data) + fixed (Rgba32* ptr = upload.PremultipliedData) DoUpload(upload, (IntPtr)ptr); uploadedRegions.Add(upload.Bounds); diff --git a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs index f752c88c51..ad2f54b5aa 100644 --- a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs +++ b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs @@ -23,7 +23,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Platform; using osuTK; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using WindowState = osu.Framework.Platform.WindowState; @@ -255,14 +254,14 @@ private void load(IRenderer renderer) { //initialise background var columnUpload = new ArrayPoolTextureUpload(1, HEIGHT); - var fullBackground = new Image(WIDTH, HEIGHT); + var fullBackground = new PremultipliedImage(WIDTH, HEIGHT); addArea(null, null, HEIGHT, amount_ms_steps, columnUpload); for (int i = 0; i < HEIGHT; i++) { for (int k = 0; k < WIDTH; k++) - fullBackground[k, i] = columnUpload.RawData[i]; + fullBackground.SetPremultipliedRgba32(k, i, columnUpload.RawData[i]); } addArea(null, null, HEIGHT, amount_count_steps, columnUpload); @@ -506,7 +505,7 @@ private int addArea(FrameStatistics frame, PerformanceCollectionType? frameTimeT else if (acceptableRange) brightnessAdjust *= 0.8f; - columnUpload.RawData[i] = new Rgba32(col.R * brightnessAdjust, col.G * brightnessAdjust, col.B * brightnessAdjust, col.A); + columnUpload.RawData[i] = new Rgba32(col.R * col.A * brightnessAdjust, col.G * col.A * brightnessAdjust, col.B * col.A * brightnessAdjust, col.A); currentHeight--; } diff --git a/osu.Framework/Graphics/Rendering/ClearInfo.cs b/osu.Framework/Graphics/Rendering/ClearInfo.cs index c9f19ba327..c94f91ff7f 100644 --- a/osu.Framework/Graphics/Rendering/ClearInfo.cs +++ b/osu.Framework/Graphics/Rendering/ClearInfo.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK.Graphics; +using osu.Framework.Graphics.Colour; namespace osu.Framework.Graphics.Rendering { @@ -18,7 +18,7 @@ public readonly struct ClearInfo /// /// The colour to write to the frame buffer. /// - public readonly Color4 Colour; + public readonly PremultipliedColour Colour; /// /// The depth to write to the frame buffer. @@ -30,7 +30,7 @@ public readonly struct ClearInfo /// public readonly int Stencil; - public ClearInfo(Color4 colour = default, double depth = 1f, int stencil = 0) + public ClearInfo(PremultipliedColour colour = default, double depth = 1f, int stencil = 0) { Colour = colour; Depth = depth; diff --git a/osu.Framework/Graphics/Rendering/GlobalUniformData.cs b/osu.Framework/Graphics/Rendering/GlobalUniformData.cs index 51ba8b7e13..aa1ccec0ad 100644 --- a/osu.Framework/Graphics/Rendering/GlobalUniformData.cs +++ b/osu.Framework/Graphics/Rendering/GlobalUniformData.cs @@ -26,7 +26,7 @@ public record struct GlobalUniformData public UniformFloat BorderThickness; private readonly UniformPadding12 pad3; - public UniformMatrix4 BorderColour; + public UniformColourInfo BorderColour; public UniformFloat MaskingBlendRange; public UniformFloat AlphaExponent; public UniformVector2 EdgeOffset; diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 01a8a1f39a..98ce4ee9ad 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Runtime.InteropServices; using osu.Framework.Development; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering.Vertices; @@ -265,7 +266,7 @@ protected internal virtual void BeginFrame(Vector2 windowSize) PushDepthInfo(DepthInfo.Default); PushStencilInfo(StencilInfo.Default); - Clear(new ClearInfo(Color4.Black)); + Clear(new ClearInfo(Color4.Black.ToPremultiplied())); freeUnusedVertexBuffers(); vboInUse.Value = vertexBuffersInUse.Count; @@ -975,27 +976,7 @@ public void DrawVertices(PrimitiveTopology topology, int vertexStart, int vertic currentMaskingInfo.MaskingRect.Bottom), BorderThickness = currentMaskingInfo.BorderThickness / currentMaskingInfo.BlendRange, BorderColour = currentMaskingInfo.BorderThickness > 0 - ? new Matrix4( - // TopLeft - currentMaskingInfo.BorderColour.TopLeft.SRGB.R, - currentMaskingInfo.BorderColour.TopLeft.SRGB.G, - currentMaskingInfo.BorderColour.TopLeft.SRGB.B, - currentMaskingInfo.BorderColour.TopLeft.SRGB.A, - // BottomLeft - currentMaskingInfo.BorderColour.BottomLeft.SRGB.R, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.G, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.B, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.A, - // TopRight - currentMaskingInfo.BorderColour.TopRight.SRGB.R, - currentMaskingInfo.BorderColour.TopRight.SRGB.G, - currentMaskingInfo.BorderColour.TopRight.SRGB.B, - currentMaskingInfo.BorderColour.TopRight.SRGB.A, - // BottomRight - currentMaskingInfo.BorderColour.BottomRight.SRGB.R, - currentMaskingInfo.BorderColour.BottomRight.SRGB.G, - currentMaskingInfo.BorderColour.BottomRight.SRGB.B, - currentMaskingInfo.BorderColour.BottomRight.SRGB.A) + ? currentMaskingInfo.BorderColour : globalUniformBuffer.Data.BorderColour, MaskingBlendRange = currentMaskingInfo.BlendRange, AlphaExponent = currentMaskingInfo.AlphaExponent, @@ -1281,7 +1262,9 @@ private void validateUniformLayout() if (field.FieldType == typeof(UniformMatrix3) || field.FieldType == typeof(UniformMatrix4) || field.FieldType == typeof(UniformVector3) - || field.FieldType == typeof(UniformVector4)) + || field.FieldType == typeof(UniformVector4) + || field.FieldType == typeof(UniformColour) + || field.FieldType == typeof(UniformColourInfo)) { checkAlignment(field, offset, 16); } @@ -1310,8 +1293,10 @@ static void checkValidType(FieldInfo field) || field.FieldType == typeof(UniformPadding8) || field.FieldType == typeof(UniformPadding12) || field.FieldType == typeof(UniformVector2) + || field.FieldType == typeof(UniformVector3) || field.FieldType == typeof(UniformVector4) - || field.FieldType == typeof(UniformVector4)) + || field.FieldType == typeof(UniformColour) + || field.FieldType == typeof(UniformColourInfo)) { return; } diff --git a/osu.Framework/Graphics/Rendering/RendererExtensions.cs b/osu.Framework/Graphics/Rendering/RendererExtensions.cs index 848db8f702..e1c758202c 100644 --- a/osu.Framework/Graphics/Rendering/RendererExtensions.cs +++ b/osu.Framework/Graphics/Rendering/RendererExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Primitives; @@ -70,7 +71,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = topColour.SRGB, + Colour = topColour.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -78,7 +79,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = drawColour.BottomLeft.SRGB, + Colour = drawColour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -86,7 +87,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = bottomColour.SRGB, + Colour = bottomColour.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -94,7 +95,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = drawColour.BottomRight.SRGB, + Colour = drawColour.BottomRight.SRGB.ToPremultiplied(), }); long area = (long)vertexTriangle.Area; @@ -154,7 +155,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.BottomLeft.SRGB, + Colour = drawColour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -162,7 +163,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.BottomRight.SRGB, + Colour = drawColour.BottomRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -170,7 +171,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.TopRight.SRGB, + Colour = drawColour.TopRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -178,7 +179,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.TopLeft.SRGB, + Colour = drawColour.TopLeft.SRGB.ToPremultiplied(), }); long area = (long)vertexQuad.Area; diff --git a/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs index c352a2b949..7d2021eaeb 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct ParticleVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs index 808d46314b..6a98358af7 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TexturedVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs index 34bcbac314..9834bbc9d9 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TexturedVertex3D : IEquatable, IVertex public Vector3 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs index 2ce3663f7f..851759f19f 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TimedTexturedVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs index d530fb2b47..8e9d95eb6c 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct Vertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; public readonly bool Equals(Vertex2D other) => Position.Equals(other.Position) && Colour.Equals(other.Colour); } diff --git a/osu.Framework/Graphics/Shaders/Types/UniformColour.cs b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs new file mode 100644 index 0000000000..f666bfed2b --- /dev/null +++ b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; + +namespace osu.Framework.Graphics.Shaders.Types +{ + /// + /// Must be aligned to a 16-byte boundary. Enforces colours to be in premultiplied-alpha form. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)] + public record struct UniformColour + { + public UniformVector4 Value; + + public static implicit operator PremultipliedColour(UniformColour value) + => new PremultipliedColour(value.Value.X, value.Value.Y, value.Value.Z, value.Value.W); + + public static implicit operator UniformColour(PremultipliedColour value) => new UniformColour + { + Value = + { + X = value.PremultipliedR, + Y = value.PremultipliedG, + Z = value.PremultipliedB, + W = value.Occlusion, + } + }; + } +} diff --git a/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs b/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs new file mode 100644 index 0000000000..27a639b9f8 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Colour; + +namespace osu.Framework.Graphics.Shaders.Types +{ + /// + /// Must be aligned to a 16-byte boundary. Wraps in a structure + /// with the rows containing top-left, bottom-left, top-right, bottom-right respectively. Alpha pre-multiplication is applied. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 64)] + public record struct UniformColourInfo + { + public UniformMatrix4 Value; + + public static implicit operator UniformColourInfo(ColourInfo value) => new UniformColourInfo + { + Value = + { + Row0 = ((UniformColour)value.TopLeft.SRGB.ToPremultiplied()).Value, + Row1 = ((UniformColour)value.BottomLeft.SRGB.ToPremultiplied()).Value, + Row2 = ((UniformColour)value.TopRight.SRGB.ToPremultiplied()).Value, + Row3 = ((UniformColour)value.BottomRight.SRGB.ToPremultiplied()).Value, + } + }; + } +} diff --git a/osu.Framework/Graphics/Shapes/FastCircle.cs b/osu.Framework/Graphics/Shapes/FastCircle.cs index 660239ad45..ed976a30a5 100644 --- a/osu.Framework/Graphics/Shapes/FastCircle.cs +++ b/osu.Framework/Graphics/Shapes/FastCircle.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; @@ -97,7 +98,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(0, drawRectangle.W), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -105,7 +106,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(drawRectangle.Z, drawRectangle.W), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.BottomRight.SRGB, + Colour = DrawColourInfo.Colour.BottomRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -113,7 +114,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(drawRectangle.Z, 0), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.TopRight.SRGB, + Colour = DrawColourInfo.Colour.TopRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -121,7 +122,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = Vector2.Zero, TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.TopLeft.SRGB, + Colour = DrawColourInfo.Colour.TopLeft.SRGB.ToPremultiplied(), }); shader.Unbind(); diff --git a/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs b/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs index 683cbfc256..163581e816 100644 --- a/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs +++ b/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs @@ -36,7 +36,7 @@ public void Dispose() public Span RawData => data; - public ReadOnlySpan Data => data; + public ReadOnlySpan PremultipliedData => data; public int Level { get; set; } diff --git a/osu.Framework/Graphics/Textures/ITextureUpload.cs b/osu.Framework/Graphics/Textures/ITextureUpload.cs index 2f78284710..3180367373 100644 --- a/osu.Framework/Graphics/Textures/ITextureUpload.cs +++ b/osu.Framework/Graphics/Textures/ITextureUpload.cs @@ -13,7 +13,7 @@ public interface ITextureUpload : IDisposable /// /// The raw data to be uploaded. /// - ReadOnlySpan Data { get; } + ReadOnlySpan PremultipliedData { get; } /// /// The target mipmap level to upload into. diff --git a/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs b/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs index f0a9560c53..43b4173619 100644 --- a/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs +++ b/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs @@ -16,7 +16,7 @@ public class MemoryAllocatorTextureUpload : ITextureUpload { public Span RawData => memoryOwner.Memory.Span; - public ReadOnlySpan Data => RawData; + public ReadOnlySpan PremultipliedData => RawData; private readonly IMemoryOwner memoryOwner; diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs new file mode 100644 index 0000000000..794cf94339 --- /dev/null +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -0,0 +1,119 @@ +// 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 osu.Framework.Extensions.ImageExtensions; +using osu.Framework.Graphics.Colour; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace osu.Framework.Graphics.Textures +{ + public class PremultipliedImage : IDisposable + { + /// + /// Gets the image width in px units. + /// + public int Width => premultipliedImage.Width; + + /// + /// Gets the image height in px units. + /// + public int Height => premultipliedImage.Height; + + private readonly Image premultipliedImage; + + private PremultipliedImage(Image premultipliedImage) + { + this.premultipliedImage = premultipliedImage; + } + + public PremultipliedImage(int width, int height) + : this(new Image(width, height)) + { + } + + public PremultipliedImage(int width, int height, PremultipliedColour colour) + : this(new Image(width, height, colour.ToPremultipliedRgba32())) + { + } + + /// + /// Gets or sets the pixel at the specified position. + /// + /// The x-coordinate of the pixel. + /// The y-coordinate of the pixel. + public PremultipliedColour this[int x, int y] + { + get => PremultipliedColour.FromPremultiplied(premultipliedImage[x, y]); + set => premultipliedImage[x, y] = value.ToPremultipliedRgba32(); + } + + /// + /// Creates a contiguous and read-only memory from the pixels of a . + /// Useful for retrieving unmanaged pointers to the entire pixel data of the for marshalling. + /// + /// + /// The returned must be disposed when usage is finished. + /// + /// The . + public ReadOnlyPixelMemory CreateReadOnlyPremultipliedPixelMemory() + => premultipliedImage.CreateReadOnlyPixelMemory(); + + /// + /// Gets the representation of the premultiplied pixels as of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The row. + /// The + public Memory DangerousGetPremultipliedPixelRowMemory(int rowIndex) + => premultipliedImage.DangerousGetPixelRowMemory(rowIndex); + + /// + /// Sets a pixel at the specified position with a premultiplied colour. + /// + /// The x-coordinate of the pixel. + /// The y-coordinate of the pixel. + /// The premultiplied colour. + public void SetPremultipliedRgba32(int x, int y, Rgba32 rgba32) => premultipliedImage[x, y] = rgba32; + + /// + /// Clones the current image. + /// + /// Returns a new image with all the same metadata as the original. + public PremultipliedImage Clone() => FromPremultiplied(premultipliedImage.Clone()); + + public void Dispose() + { + premultipliedImage.Dispose(); + } + + /// + /// Creates a from a non-premultiplied source by converting colours to premultiplied. + /// + /// The non-premultiplied source image. + public static PremultipliedImage FromStraight(Image image) + { + var premultiplied = image.Clone(); + premultiplied.Mutate(p => p.ProcessPixelRowsAsVector4(r => + { + foreach (ref var pixel in r) + { + pixel.X *= pixel.W; + pixel.Y *= pixel.W; + pixel.Z *= pixel.W; + } + })); + + return new PremultipliedImage(premultiplied); + } + + /// + /// Creates a from a premultiplied source . No conversion is done in this method. + /// + /// The premultiplied source image. + public static PremultipliedImage FromPremultiplied(Image image) => new PremultipliedImage(image); + } +} diff --git a/osu.Framework/Graphics/Textures/Texture.cs b/osu.Framework/Graphics/Textures/Texture.cs index fb93dcf0ce..1283997108 100644 --- a/osu.Framework/Graphics/Textures/Texture.cs +++ b/osu.Framework/Graphics/Textures/Texture.cs @@ -202,14 +202,14 @@ internal virtual void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMod if (upload.Bounds.Width > NativeTexture.MaxSize || upload.Bounds.Height > NativeTexture.MaxSize) throw new TextureTooLargeForGLException(); - if (upload.Bounds.IsEmpty && upload.Data.Length > 0) + if (upload.Bounds.IsEmpty && upload.PremultipliedData.Length > 0) { upload.Bounds = new RectangleI(0, 0, Width, Height); - if (upload.Bounds.Width * upload.Bounds.Height > upload.Data.Length) + if (upload.Bounds.Width * upload.Bounds.Height > upload.PremultipliedData.Length) { throw new InvalidOperationException( - $"Size of texture upload ({upload.Bounds.Width}x{upload.Bounds.Height}) does not contain enough data ({upload.Data.Length} < {upload.Bounds.Width * upload.Bounds.Height})"); + $"Size of texture upload ({upload.Bounds.Width}x{upload.Bounds.Height}) does not contain enough data ({upload.PremultipliedData.Length} < {upload.Bounds.Width * upload.Bounds.Height})"); } } diff --git a/osu.Framework/Graphics/Textures/TextureAtlas.cs b/osu.Framework/Graphics/Textures/TextureAtlas.cs index a379dc577e..149bd7fc4f 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas.cs @@ -4,13 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Numerics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Logging; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Textures { @@ -84,7 +81,7 @@ public void Reset() using (var whiteTex = new TextureRegion(atlasTexture, bounds, WrapMode.Repeat, WrapMode.Repeat)) // Generate white padding as if the white texture was wrapped, even though it isn't - whiteTex.SetData(new TextureUpload(new Image(SixLabors.ImageSharp.Configuration.Default, whiteTex.Width, whiteTex.Height, new Rgba32(Vector4.One)))); + whiteTex.SetData(new TextureUpload(new PremultipliedImage(whiteTex.Width, whiteTex.Height, Colour4.White.ToPremultiplied()))); currentPosition = new Vector2I(PADDING + WHITE_PIXEL_SIZE, PADDING); } diff --git a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs index 67e80627ec..a33dfbebc4 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs @@ -51,7 +51,7 @@ internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMo // Can only perform padding when the bounds are a sub-part of the texture RectangleI middleBounds = upload.Bounds; - if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.Data.Length) + if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.PremultipliedData.Length) { // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. @@ -61,7 +61,7 @@ internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMo int actualPadding = padding / (1 << upload.Level); - var data = upload.Data; + var data = upload.PremultipliedData; uploadCornerPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None && wrapModeT != WrapMode.None); uploadHorizontalPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None); @@ -215,10 +215,10 @@ private void uploadCornerPadding(ReadOnlySpan upload, RectangleI middleB [MethodImpl(MethodImplOptions.AggressiveInlining)] private void transferBorderPixel(ref Rgba32 dest, Rgba32 source, bool fillOpaque) { - dest.R = source.R; - dest.G = source.G; - dest.B = source.B; dest.A = fillOpaque ? source.A : (byte)0; + dest.R = (byte)(source.R * (dest.A / 255.0)); + dest.G = (byte)(source.G * (dest.A / 255.0)); + dest.B = (byte)(source.B * (dest.A / 255.0)); } /// diff --git a/osu.Framework/Graphics/Textures/TextureLoaderStore.cs b/osu.Framework/Graphics/Textures/TextureLoaderStore.cs index d719402d5b..53d38647cc 100644 --- a/osu.Framework/Graphics/Textures/TextureLoaderStore.cs +++ b/osu.Framework/Graphics/Textures/TextureLoaderStore.cs @@ -9,8 +9,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.IO.Stores; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Textures { @@ -38,7 +36,7 @@ public TextureUpload Get(string name) using (var stream = store.GetStream(name)) { if (stream != null) - return new TextureUpload(ImageFromStream(stream)); + return new TextureUpload(ImageFromStream(stream)); } } catch @@ -50,8 +48,7 @@ public TextureUpload Get(string name) public Stream GetStream(string name) => store.GetStream(name); - protected virtual Image ImageFromStream(Stream stream) where TPixel : unmanaged, IPixel - => TextureUpload.LoadFromStream(stream); + protected virtual PremultipliedImage ImageFromStream(Stream stream) => TextureUpload.LoadFromStream(stream); public IEnumerable GetAvailableResources() => store.GetAvailableResources(); diff --git a/osu.Framework/Graphics/Textures/TextureUpload.cs b/osu.Framework/Graphics/Textures/TextureUpload.cs index 096e7ed783..0c0b4af451 100644 --- a/osu.Framework/Graphics/Textures/TextureUpload.cs +++ b/osu.Framework/Graphics/Textures/TextureUpload.cs @@ -38,7 +38,10 @@ public class TextureUpload : ITextureUpload /// public RectangleI Bounds { get; set; } - public ReadOnlySpan Data => pixelMemory.Span; + /// + /// Overview of the texture upload's pixels content. The pixel colours are premultiplied. + /// + public ReadOnlySpan PremultipliedData => premultipliedPixelMemory.Span; public int Width => image?.Width ?? 0; @@ -47,18 +50,18 @@ public class TextureUpload : ITextureUpload /// /// The backing texture. A handle is kept to avoid early GC. /// - private readonly Image image; + private readonly PremultipliedImage image; - private ReadOnlyPixelMemory pixelMemory; + private ReadOnlyPixelMemory premultipliedPixelMemory; /// /// Create an upload from a . This is the preferred method. /// /// The texture to upload. - public TextureUpload(Image image) + public TextureUpload(PremultipliedImage image) { this.image = image; - pixelMemory = image.CreateReadOnlyPixelMemory(); + premultipliedPixelMemory = image.CreateReadOnlyPremultipliedPixelMemory(); } /// @@ -68,16 +71,16 @@ public TextureUpload(Image image) /// /// The image content. public TextureUpload(Stream stream) - : this(LoadFromStream(stream)) + : this(LoadFromStream(stream)) { } private static bool stbiNotFound; - internal static Image LoadFromStream(Stream stream) where TPixel : unmanaged, IPixel + internal static PremultipliedImage LoadFromStream(Stream stream) { if (stbiNotFound) - return Image.Load(stream); + return PremultipliedImage.FromStraight(Image.Load(stream)); long initialPos = stream.Position; @@ -88,7 +91,7 @@ internal static Image LoadFromStream(Stream stream) where TPixel stream.ReadExactly(buffer.Memory.Span); using (var stbiImage = Stbi.LoadFromMemory(buffer.Memory.Span, 4)) - return Image.LoadPixelData(MemoryMarshal.Cast(stbiImage.Data), stbiImage.Width, stbiImage.Height); + return PremultipliedImage.FromStraight(Image.LoadPixelData(MemoryMarshal.Cast(stbiImage.Data), stbiImage.Width, stbiImage.Height)); } } catch (Exception e) @@ -98,7 +101,7 @@ internal static Image LoadFromStream(Stream stream) where TPixel Logger.Log($"Texture could not be loaded via STB; falling back to ImageSharp: {e.Message}"); stream.Position = initialPos; - return Image.Load(stream); + return PremultipliedImage.FromStraight(Image.Load(stream)); } } @@ -125,7 +128,7 @@ protected virtual void Dispose(bool isDisposing) return; image?.Dispose(); - pixelMemory.Dispose(); + premultipliedPixelMemory.Dispose(); disposed = true; } diff --git a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs index 76ff428298..fafc082667 100644 --- a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs +++ b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs @@ -63,7 +63,7 @@ public override void Begin() /// The clearing parameters. public void Clear(ClearInfo clearInfo) { - Commands.ClearColorTarget(0, clearInfo.Colour.ToRgbaFloat()); + Commands.ClearColorTarget(0, clearInfo.Colour.ToPremultipliedRgbaFloat()); var framebuffer = currentFrameBuffer?.Framebuffer ?? Device.SwapchainFramebuffer; if (framebuffer.DepthTarget != null) diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs index 8e50c8f5cb..e245db72e2 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs @@ -470,7 +470,7 @@ protected virtual void DoUpload(ITextureUpload upload) int lastMaximumUploadedLod = maximumUploadedLod; - if (!upload.Data.IsEmpty) + if (!upload.PremultipliedData.IsEmpty) { // ensure all mip levels up to the target level are initialised. // generally we always upload at level 0, so this won't run. @@ -483,7 +483,7 @@ protected virtual void DoUpload(ITextureUpload upload) } Renderer.UpdateTexture(texture, upload.Bounds.X >> upload.Level, upload.Bounds.Y >> upload.Level, upload.Bounds.Width >> upload.Level, upload.Bounds.Height >> upload.Level, - upload.Level, upload.Data); + upload.Level, upload.PremultipliedData); } if (sampler == null || maximumUploadedLod > lastMaximumUploadedLod) diff --git a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs index 272452a14d..ec572ed281 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs @@ -7,10 +7,10 @@ using System.Runtime.InteropServices; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; -using osuTK.Graphics; using SharpGen.Runtime; using Veldrid; using Veldrid.MetalBindings; @@ -27,7 +27,8 @@ namespace osu.Framework.Graphics.Veldrid { internal static class VeldridExtensions { - public static RgbaFloat ToRgbaFloat(this Color4 colour) => new RgbaFloat(colour.R, colour.G, colour.B, colour.A); + public static RgbaFloat ToPremultipliedRgbaFloat(this PremultipliedColour colour) + => new RgbaFloat(colour.PremultipliedR, colour.PremultipliedG, colour.PremultipliedB, colour.Occlusion); public static BlendFactor ToBlendFactor(this BlendingType type) { diff --git a/osu.Framework/Graphics/Video/VideoTextureUpload.cs b/osu.Framework/Graphics/Video/VideoTextureUpload.cs index ee6442cca5..209d8df520 100644 --- a/osu.Framework/Graphics/Video/VideoTextureUpload.cs +++ b/osu.Framework/Graphics/Video/VideoTextureUpload.cs @@ -24,7 +24,7 @@ public int GetPlaneHeight(uint plane) return (plane == 0) ? Frame->height : (Frame->height + 1) / 2; } - public ReadOnlySpan Data => ReadOnlySpan.Empty; + public ReadOnlySpan PremultipliedData => ReadOnlySpan.Empty; public int Level => 0; diff --git a/osu.Framework/IO/Stores/GlyphStore.cs b/osu.Framework/IO/Stores/GlyphStore.cs index 3688f8b52c..459dd99f1d 100644 --- a/osu.Framework/IO/Stores/GlyphStore.cs +++ b/osu.Framework/IO/Stores/GlyphStore.cs @@ -17,8 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Text; using SharpFNT; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.IO.Stores @@ -166,8 +164,8 @@ protected virtual TextureUpload LoadCharacter(Character character) var page = GetPageImage(character.Page); LoadedGlyphCount++; - var image = new Image(SixLabors.ImageSharp.Configuration.Default, character.Width, character.Height); - var source = page.Data; + var image = new PremultipliedImage(character.Width, character.Height); + var source = page.PremultipliedData; // the spritesheet may have unused pixels trimmed int readableHeight = Math.Min(character.Height, page.Height - character.Y); @@ -175,11 +173,11 @@ protected virtual TextureUpload LoadCharacter(Character character) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.DangerousGetPixelRowMemory(y); + var premultipliedPixelRowMemory = image.DangerousGetPremultipliedPixelRowMemory(y); int readOffset = (character.Y + y) * page.Width + character.X; for (int x = 0; x < character.Width; x++) - pixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(255, 255, 255, 0); + premultipliedPixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(0, 0, 0, 0); } return new TextureUpload(image); diff --git a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs index c8a1bdaa85..a6324cbe2d 100644 --- a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs +++ b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs @@ -14,7 +14,6 @@ using osu.Framework.Platform; using SharpFNT; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.IO.Stores @@ -107,7 +106,7 @@ private PageInfo createCachedPageInfo(int page) using (var buffer = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate(convert.Width * convert.Height)) { var output = buffer.Memory.Span; - var source = convert.Data; + var source = convert.PremultipliedData; for (int i = 0; i < output.Length; i++) output[i] = source[i].A; @@ -141,7 +140,7 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) try { - var image = new Image(SixLabors.ImageSharp.Configuration.Default, character.Width, character.Height); + var image = new PremultipliedImage(character.Width, character.Height); if (!pageStreamHandles.TryGetValue(page.Filename, out var source)) source = pageStreamHandles[page.Filename] = CacheStorage.GetStream(page.Filename); @@ -156,13 +155,14 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.DangerousGetPixelRowMemory(y); - var span = pixelRowMemory.Span; + var premultipliedPixelRowMemory = image.DangerousGetPremultipliedPixelRowMemory(y); + var span = premultipliedPixelRowMemory.Span; int readOffset = y * pageWidth + character.X; for (int x = 0; x < character.Width; x++) { - span[x] = new Rgba32(255, 255, 255, x < readableWidth && y < readableHeight ? readBuffer[readOffset + x] : (byte)0); + byte val = x < readableWidth && y < readableHeight ? readBuffer[readOffset + x] : (byte)0; + span[x] = new Rgba32(val, val, val, val); } } diff --git a/osu.Framework/Resources/Shaders/sh_CircularBlob.fs b/osu.Framework/Resources/Shaders/sh_CircularBlob.fs index fa5f027085..a4899f4c96 100644 --- a/osu.Framework/Resources/Shaders/sh_CircularBlob.fs +++ b/osu.Framework/Resources/Shaders/sh_CircularBlob.fs @@ -33,7 +33,7 @@ void main(void) highp vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); lowp vec4 textureColour = getRoundedColor(wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9), wrappedCoord); - o_Colour = vec4(textureColour.rgb, textureColour.a * blobAlphaAt(pixelPos, innerRadius, texelSize, frequency, amplitude, noisePosition)); + o_Colour = textureColour.rgba * blobAlphaAt(pixelPos, innerRadius, texelSize, frequency, amplitude, noisePosition); } #endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_CircularProgress.fs b/osu.Framework/Resources/Shaders/sh_CircularProgress.fs index 559a064831..bf5f4b8a10 100644 --- a/osu.Framework/Resources/Shaders/sh_CircularProgress.fs +++ b/osu.Framework/Resources/Shaders/sh_CircularProgress.fs @@ -32,7 +32,7 @@ void main(void) highp vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); lowp vec4 textureColour = getRoundedColor(wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9), wrappedCoord); - o_Colour = vec4(textureColour.rgb, textureColour.a * progressAlphaAt(pixelPos, progress, innerRadius, roundedCaps, texelSize)); + o_Colour = textureColour.rgba * progressAlphaAt(pixelPos, progress, innerRadius, roundedCaps, texelSize); } #endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_FastCircle.fs b/osu.Framework/Resources/Shaders/sh_FastCircle.fs index 307f00a74c..be60600f41 100644 --- a/osu.Framework/Resources/Shaders/sh_FastCircle.fs +++ b/osu.Framework/Resources/Shaders/sh_FastCircle.fs @@ -20,7 +20,7 @@ void main(void) highp float alpha = v_BlendRange.x == 0.0 ? float(dst < radius) : (clamp(radius - dst, 0.0, v_BlendRange.x) / v_BlendRange.x); - o_Colour = getRoundedColor(vec4(vec3(1.0), alpha), vec2(0.0)); + o_Colour = getRoundedColor(vec4(alpha), vec2(0.0)); } #endif diff --git a/osu.Framework/Resources/Shaders/sh_Masking.h b/osu.Framework/Resources/Shaders/sh_Masking.h index 3fdc6c8846..e9e0223052 100644 --- a/osu.Framework/Resources/Shaders/sh_Masking.h +++ b/osu.Framework/Resources/Shaders/sh_Masking.h @@ -116,15 +116,15 @@ lowp vec4 getRoundedColor(lowp vec4 texel, mediump vec2 texCoord) lowp vec4 contentColour = v_Colour * texel; if (colourWeight == 1.0) - return vec4(contentColour.rgb, contentColour.a * alphaFactor); + return contentColour.rgba * alphaFactor; lowp vec4 borderColour = getBorderColour(); if (colourWeight <= 0.0) - return vec4(borderColour.rgb, borderColour.a * alphaFactor); + return borderColour.rgba * alphaFactor; - contentColour.a *= alphaFactor; - borderColour.a *= 1.0 - colourWeight; + contentColour.rgba *= alphaFactor; + borderColour.rgba *= 1.0 - colourWeight; return blend(borderColour, contentColour); } diff --git a/osu.Framework/Resources/Shaders/sh_Particle.vs b/osu.Framework/Resources/Shaders/sh_Particle.vs index 8da3d9ed64..9a8781f19e 100644 --- a/osu.Framework/Resources/Shaders/sh_Particle.vs +++ b/osu.Framework/Resources/Shaders/sh_Particle.vs @@ -23,7 +23,7 @@ void main(void) vec2(0.0, g_Gravity * g_FadeClock * g_FadeClock / 1000000.0); gl_Position = g_ProjMatrix * vec4(targetPosition, 1.0, 1.0); - v_Colour = vec4(1.0, 1.0, 1.0, 1.0 - clamp(g_FadeClock / m_Time, 0.0, 1.0)); + v_Colour = vec4(1.0 - clamp(g_FadeClock / m_Time, 0.0, 1.0)); v_TexCoord = m_TexCoord; } diff --git a/osu.Framework/Resources/Shaders/sh_Utils.h b/osu.Framework/Resources/Shaders/sh_Utils.h index 6e2e010c25..ffeb972298 100644 --- a/osu.Framework/Resources/Shaders/sh_Utils.h +++ b/osu.Framework/Resources/Shaders/sh_Utils.h @@ -8,15 +8,7 @@ // see http://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha.html lowp vec4 blend(lowp vec4 src, lowp vec4 dst) { - lowp float finalAlpha = src.a + dst.a * (1.0 - src.a); - - if (finalAlpha == 0.0) - return vec4(0); - - return vec4( - (src.rgb * src.a + dst.rgb * dst.a * (1.0 - src.a)) / finalAlpha, - finalAlpha - ); + return src.rgba + dst.rgba * (1.0 - src.a); } // http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl