同步更新

This commit is contained in:
LA
2025-12-06 17:22:05 +08:00
parent abd37a6327
commit a2ef94d6e5
70 changed files with 1648 additions and 1595 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="SampleGame.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
<application />
</manifest>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.Framework.Android.props" />
<PropertyGroup>
<TargetFramework>net8.0-android</TargetFramework>
<OutputType>Exe</OutputType>
<RootNamespace>SampleGame.Android</RootNamespace>
<AssemblyName>SampleGame.Android</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Framework.Android\osu.Framework.Android.csproj" />
<ProjectReference Include="..\SampleGame\SampleGame.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.App;
using osu.Framework;
using osu.Framework.Android;
namespace SampleGame.Android
{
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
public class SampleGameActivity : AndroidGameActivity
{
protected override Game CreateGame() => new SampleGameGame();
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework;
using osu.Framework.Platform;
namespace SampleGame.Desktop
{
public static class Program
{
[STAThread]
public static void Main(string[] args)
{
using (GameHost host = Host.GetSuitableDesktopHost(@"sample-game"))
using (Game game = new SampleGameGame())
host.Run(game);
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>net8.0</TargetFramework>
<OutputType>WinExe</OutputType>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
<ProjectReference Include="..\SampleGame\SampleGame.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Foundation;
using osu.Framework;
using osu.Framework.iOS;
namespace SampleGame.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Game CreateGame() => new SampleGameGame();
}
}

View File

@@ -0,0 +1,231 @@
{
"images": [
{
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
},
{
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
},
{
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
},
{
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
},
{
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
},
{
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
},
{
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
},
{
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
},
{
"idiom": "ipad",
"scale": "1x",
"size": "20x20"
},
{
"idiom": "ipad",
"scale": "2x",
"size": "20x20"
},
{
"idiom": "ipad",
"scale": "1x",
"size": "29x29"
},
{
"idiom": "ipad",
"scale": "2x",
"size": "29x29"
},
{
"idiom": "ipad",
"scale": "1x",
"size": "40x40"
},
{
"idiom": "ipad",
"scale": "2x",
"size": "40x40"
},
{
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
},
{
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
},
{
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
},
{
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
},
{
"idiom": "car",
"scale": "2x",
"size": "60x60"
},
{
"idiom": "car",
"scale": "3x",
"size": "60x60"
},
{
"idiom": "watch",
"role": "notificationCenter",
"scale": "2x",
"size": "24x24",
"subtype": "38mm"
},
{
"idiom": "watch",
"role": "notificationCenter",
"scale": "2x",
"size": "27.5x27.5",
"subtype": "42mm"
},
{
"idiom": "watch",
"role": "companionSettings",
"scale": "2x",
"size": "29x29"
},
{
"idiom": "watch",
"role": "companionSettings",
"scale": "3x",
"size": "29x29"
},
{
"idiom": "watch",
"role": "appLauncher",
"scale": "2x",
"size": "40x40",
"subtype": "38mm"
},
{
"idiom": "watch",
"role": "appLauncher",
"scale": "2x",
"size": "44x44",
"subtype": "40mm"
},
{
"idiom": "watch",
"role": "appLauncher",
"scale": "2x",
"size": "50x50",
"subtype": "44mm"
},
{
"idiom": "watch",
"role": "quickLook",
"scale": "2x",
"size": "86x86",
"subtype": "38mm"
},
{
"idiom": "watch",
"role": "quickLook",
"scale": "2x",
"size": "98x98",
"subtype": "42mm"
},
{
"idiom": "watch",
"role": "quickLook",
"scale": "2x",
"size": "108x108",
"subtype": "44mm"
},
{
"idiom": "watch-marketing",
"scale": "1x",
"size": "1024x1024"
},
{
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>SampleGame.iOS</string>
<key>CFBundleIdentifier</key>
<string>sh.ppy.sample-game</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>13.4</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIStatusBarHidden</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS" />
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530" />
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb" />
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok" />
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder" />
</objects>
<point key="canvasLocation" x="53" y="375" />
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using UIKit;
namespace SampleGame.iOS
{
public static class Program
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-ios</TargetFramework>
<SupportedOSPlatformVersion>13.4</SupportedOSPlatformVersion>
</PropertyGroup>
<Import Project="..\osu.Framework.iOS.props" />
<ItemGroup>
<ProjectReference Include="..\osu.Framework.iOS\osu.Framework.iOS.csproj" />
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
<ProjectReference Include="..\SampleGame\SampleGame.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Graphics;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Allocation;
namespace SampleGame
{
public partial class SampleGameGame : Game
{
private Box box = null!;
[BackgroundDependencyLoader]
private void load()
{
Add(box = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(150, 150),
Colour = Color4.Tomato
});
}
protected override void Update()
{
base.Update();
box.Rotation += (float)Time.Elapsed / 10;
}
}
}

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Xunit;
using VerifyCS = osu.Framework.SourceGeneration.Tests.Verifiers.CSharpSourceGeneratorVerifier<osu.Framework.SourceGeneration.Generators.Dependencies.DependencyInjectionSourceGenerator>;
@@ -47,5 +48,19 @@ namespace osu.Framework.SourceGeneration.Tests.Dependencies
return VerifyCS.VerifyAsync(commonSourceFiles, sourceFiles, commonGeneratedFiles, generatedFiles);
}
[Theory]
[InlineData("EmptyFile")]
public Task CheckDebugNoOutput(string name)
{
GetTestSources(name,
out (string filename, string content)[] commonSourceFiles,
out (string filename, string content)[] sourceFiles,
out _,
out _
);
return VerifyCS.VerifyAsync(commonSourceFiles, sourceFiles, [], [], OptimizationLevel.Debug);
}
}
}

View File

@@ -4,6 +4,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using osu.Framework.SourceGeneration.Generators;
@@ -16,9 +17,10 @@ namespace osu.Framework.SourceGeneration.Tests.Verifiers
(string filename, string content)[] commonSources,
(string filename, string content)[] sources,
(string filename, string content)[] commonGenerated,
(string filename, string content)[] generated)
(string filename, string content)[] generated,
OptimizationLevel optimizationLevel = OptimizationLevel.Release)
{
var test = new Test();
var test = new Test(optimizationLevel);
foreach (var s in commonSources)
test.TestState.Sources.Add((s.filename, SourceText.From(s.content, Encoding.UTF8)));

View File

@@ -16,13 +16,20 @@ namespace osu.Framework.SourceGeneration.Tests.Verifiers
{
public class Test : CSharpSourceGeneratorTest<EmptySourceGeneratorProvider, DefaultVerifier>
{
private readonly OptimizationLevel optimizationLevel;
public Test(OptimizationLevel optimizationLevel = OptimizationLevel.Release)
{
this.optimizationLevel = optimizationLevel;
}
public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default;
protected override IEnumerable<Type> GetSourceGenerators() => [typeof(TSourceGenerator)];
protected override CompilationOptions CreateCompilationOptions()
{
return base.CreateCompilationOptions().WithOptimizationLevel(OptimizationLevel.Release);
return base.CreateCompilationOptions().WithOptimizationLevel(optimizationLevel);
}
protected override ParseOptions CreateParseOptions()

View File

@@ -20,11 +20,15 @@ namespace osu.Framework.SourceGeneration.Generators
IncrementalValuesProvider<IncrementalSyntaxTarget> syntaxTargets =
context.SyntaxProvider.CreateSyntaxProvider(
(n, _) => isSyntaxTarget(n),
(ctx, _) => returnWithEvent(new IncrementalSyntaxTarget((ClassDeclarationSyntax)ctx.Node, ctx.SemanticModel), EventDriver.OnSyntaxTargetCreated))
.Select((t, _) => t.WithName())
.Combine(context.CompilationProvider)
.Where(c => c.Right.Options.OptimizationLevel == OptimizationLevel.Release)
.Select((t, _) => t.Item1)
(ctx, _) =>
{
if (ctx.SemanticModel.Compilation.Options.OptimizationLevel == OptimizationLevel.Debug)
return null;
return returnWithEvent(new IncrementalSyntaxTarget((ClassDeclarationSyntax)ctx.Node, ctx.SemanticModel), EventDriver.OnSyntaxTargetCreated);
})
.Where(t => t != null)
.Select((t, _) => t!.WithName())
.Select((t, _) => returnWithEvent(t.WithSemanticTarget(CreateSemanticTarget), EventDriver.OnSemanticTargetCreated));
// Stage 2: Separate out the old and new syntax targets for the same class object.

View File

@@ -1,191 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osuTK;
namespace osu.Framework.Tests.Visual.Containers
{
public partial class TestSceneAcrylicContainer : TestScene
{
private bool isWhiteTint = true;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Children = new Drawable[]
{
// Background texture (full screen)
new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get("sample-texture")
},
// Left side: Semi-transparent overlay to show original texture
new Container
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White.Opacity(0.1f) // Very subtle overlay
}
},
// Right side: AcrylicContainer with blur effect (buffered container)
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 0.5f,
Child = new AcrylicContainer()
},
// Labels
new SpriteText
{
Text = "Original Texture (No Blur)",
Font = FontUsage.Default.With(size: 24),
Colour = Colour4.White,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(-200, 20),
},
new SpriteText
{
Text = "AcrylicContainer (Buffered Blur)",
Font = FontUsage.Default.With(size: 24),
Colour = Colour4.White,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(200, 20),
}
};
// Blur strength control
AddSliderStep("blur strength", 0f, 20f, 10f, strength =>
{
if (Children[2] is AcrylicContainer acrylic)
acrylic.BlurStrength = strength;
});
// Tint colour controls
AddSliderStep("tint alpha", 0f, 1f, 0.8f, alpha =>
{
if (Children[2] is AcrylicContainer acrylic)
acrylic.TintColour = Colour4.White.Opacity(alpha);
});
AddSliderStep("tint red", 0f, 1f, 1f, red =>
{
if (Children[2] is AcrylicContainer acrylic)
{
var currentColour = acrylic.TintColour;
acrylic.TintColour = new Colour4(red, currentColour.G, currentColour.B, currentColour.A);
}
});
AddSliderStep("tint green", 0f, 1f, 1f, green =>
{
if (Children[2] is AcrylicContainer acrylic)
{
var currentColour = acrylic.TintColour;
acrylic.TintColour = new Colour4(currentColour.R, green, currentColour.B, currentColour.A);
}
});
AddSliderStep("tint blue", 0f, 1f, 1f, blue =>
{
if (Children[2] is AcrylicContainer acrylic)
{
var currentColour = acrylic.TintColour;
acrylic.TintColour = new Colour4(currentColour.R, currentColour.G, blue, currentColour.A);
}
});
AddStep("toggle tint colour", () =>
{
if (Children[2] is AcrylicContainer acrylic)
{
isWhiteTint = !isWhiteTint;
acrylic.TintColour = isWhiteTint
? Colour4.White.Opacity(0.8f)
: Colour4.Blue.Opacity(0.8f);
}
});
// Test different blur scenarios
AddStep("no blur", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 0;
});
AddStep("light blur", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 5;
});
AddStep("medium blur", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 10;
});
AddStep("heavy blur", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 20;
});
// Test tint scenarios
AddStep("no tint", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0);
});
AddStep("subtle tint", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.3f);
});
AddStep("medium tint", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.6f);
});
AddStep("strong tint", () =>
{
if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.9f);
});
// Debug presets
AddStep("debug: high contrast", () =>
{
if (Children[2] is AcrylicContainer acrylic)
{
acrylic.BlurStrength = 15f;
acrylic.TintColour = Colour4.Red.Opacity(0.7f);
}
});
AddStep("debug: subtle effect", () =>
{
if (Children[2] is AcrylicContainer acrylic)
{
acrylic.BlurStrength = 3f;
acrylic.TintColour = Colour4.Black.Opacity(0.2f);
}
});
AddStep("debug: reset to default", () =>
{
if (Children[2] is AcrylicContainer acrylic)
{
acrylic.BlurStrength = 10f;
acrylic.TintColour = Colour4.White.Opacity(0.8f);
}
});
}
}
}

View File

@@ -1,188 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
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.Testing;
using osu.Framework.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Framework.Tests.Visual.Containers
{
/// <summary>
/// 测试真正的毛玻璃效果 - 使用新重构的AcrylicContainer
/// 这个容器可以对下层的任何内容进行实时模糊,包括视频、动画等
/// </summary>
public partial class TestSceneAcrylicContainerNew : TestScene
{
private AcrylicContainer? acrylicEffect;
private Box? animatedBox;
private readonly List<Box> movingBoxes = new List<Box>();
[BackgroundDependencyLoader]
private void load()
{
// 创建一个彩色渐变背景层
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Blue, Color4.Purple)
});
// 添加一些移动的彩色方块作为背景内容
for (int i = 0; i < 5; i++)
{
var box = new Box
{
Size = new Vector2(100),
Colour = new Color4(
(float)RNG.NextDouble(),
(float)RNG.NextDouble(),
(float)RNG.NextDouble(),
1f
),
Position = new Vector2(
(float)(RNG.NextDouble() * 800),
(float)(RNG.NextDouble() * 600)
)
};
Add(box);
movingBoxes.Add(box);
}
// 添加一个持续旋转的大方块
animatedBox = new Box
{
Size = new Vector2(200),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Yellow
};
Add(animatedBox);
// 在上面添加毛玻璃效果容器
Add(new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(100),
Child = acrylicEffect = new AcrylicContainer
{
RelativeSizeAxes = Axes.Both,
BlurStrength = 50f,
TintColour = new Color4(1, 1, 1, 0.5f),
Children = new Drawable[]
{
// 在毛玻璃效果上面显示一些文本
new SpriteText
{
Text = "毛玻璃效果 (Acrylic Effect)\n\n此容器实时模糊背后的所有内容,\n包括背景、动画和兄弟元素。",
Font = FontUsage.Default.With(size: 30),
Colour = Color4.White,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = true
}
}
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
// 在LoadComplete后启动动画
// 让方块循环移动
foreach (var box in movingBoxes)
{
box.MoveTo(new Vector2(
(float)(RNG.NextDouble() * 800),
(float)(RNG.NextDouble() * 600)
), 3000).Then().MoveTo(new Vector2(
(float)(RNG.NextDouble() * 800),
(float)(RNG.NextDouble() * 600)
), 3000).Loop();
}
// 让黄色方块持续旋转
animatedBox?.RotateTo(360, 4000).Then().RotateTo(0, 0).Loop();
// 添加控制UI
AddLabel("模糊强度 (Blur Strength)");
AddSliderStep("blur", 0f, 50f, 15f, value =>
{
if (acrylicEffect != null)
acrylicEffect.BlurStrength = value;
});
AddLabel("着色透明度 (Tint Alpha)");
AddSliderStep("alpha", 0f, 1f, 0.3f, value =>
{
if (acrylicEffect != null)
{
var current = acrylicEffect.TintColour;
acrylicEffect.TintColour = new Color4(current.R, current.G, current.B, value);
}
});
AddLabel("着色颜色 (Tint Color)");
AddStep("黑色 (Black)", () =>
{
if (acrylicEffect != null)
{
var alpha = acrylicEffect.TintColour.A;
acrylicEffect.TintColour = new Color4(0, 0, 0, alpha);
}
});
AddStep("白色 (White)", () =>
{
if (acrylicEffect != null)
{
var alpha = acrylicEffect.TintColour.A;
acrylicEffect.TintColour = new Color4(1, 1, 1, alpha);
}
});
AddStep("红色 (Red)", () =>
{
if (acrylicEffect != null)
{
var alpha = acrylicEffect.TintColour.A;
acrylicEffect.TintColour = new Color4(1, 0, 0, alpha);
}
});
AddStep("蓝色 (Blue)", () =>
{
if (acrylicEffect != null)
{
var alpha = acrylicEffect.TintColour.A;
acrylicEffect.TintColour = new Color4(0, 0, 1, alpha);
}
});
AddLabel("效果演示 (Demos)");
AddStep("显示/隐藏毛玻璃", () =>
{
if (acrylicEffect != null)
acrylicEffect.Alpha = acrylicEffect.Alpha > 0 ? 0 : 1;
});
AddStep("脉冲动画", () =>
{
acrylicEffect?.ScaleTo(1.1f, 500, Easing.OutQuint)
.Then()
.ScaleTo(1f, 500, Easing.InQuint);
});
}
}
}

View File

@@ -0,0 +1,329 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osuTK;
namespace osu.Framework.Tests.Visual.Drawables
{
public partial class TestSceneCubicBezierEasing : TestScene
{
private SpriteText easingText = null!;
private EasingEditor easingEditor = null!;
[Resolved]
private Clipboard clipboard { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 100 },
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new Drawable[]
{
easingEditor = new EasingEditor
{
Size = new Vector2(200)
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30),
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(4),
Children = new Drawable[]
{
easingText = new SpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 420,
},
new BasicButton
{
AutoSizeAxes = Axes.Y,
Width = 50,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = "Copy",
FlashColour = FrameworkColour.BlueGreen.Lighten(0.5f),
FlashDuration = 1000,
Action = () => clipboard.SetText(easingText.Text.ToString()),
}
}
},
new EasingPreview(easingEditor.EasingFunction.GetBoundCopy())
}
}
}
};
easingEditor.EasingFunction.BindValueChanged(e =>
{
var easing = e.NewValue;
easingText.Text = FormattableString.Invariant($"new {nameof(CubicBezierEasingFunction)}({easing.X1:0.##}, {easing.Y1:0.##}, {easing.X2:0.##}, {easing.Y2:0.##})");
});
}
private partial class EasingEditor : CompositeDrawable
{
private readonly Bindable<Vector2> p1 = new Bindable<Vector2>(new Vector2(0.5f, 0f));
private readonly Bindable<Vector2> p2 = new Bindable<Vector2>(new Vector2(0.5f, 1f));
private readonly SmoothPath path;
private readonly Box line1, line2;
public readonly Bindable<CubicBezierEasingFunction> EasingFunction = new Bindable<CubicBezierEasingFunction>();
public EasingEditor()
{
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
},
path = new SmoothPath
{
Position = new Vector2(1, -1),
PathRadius = 1f,
Anchor = Anchor.BottomLeft,
},
line1 = new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft,
EdgeSmoothness = new Vector2(1),
Alpha = 0.1f,
},
line2 = new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,
Anchor = Anchor.TopRight,
Origin = Anchor.CentreRight,
EdgeSmoothness = new Vector2(1),
Alpha = 0.1f,
},
new ControlPointHandle
{
ControlPoint = { BindTarget = p1 },
},
new ControlPointHandle
{
ControlPoint = { BindTarget = p2 },
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
p1.BindValueChanged(_ => Scheduler.AddOnce(easingChanged));
p2.BindValueChanged(_ => Scheduler.AddOnce(easingChanged));
easingChanged();
}
private void easingChanged()
{
path.ClearVertices();
var easing = EasingFunction.Value = new CubicBezierEasingFunction(p1.Value.X, p1.Value.Y, p2.Value.X, p2.Value.Y);
for (double d = 0; d < 1; d += 0.01)
{
double value = easing.ApplyEasing(d);
path.AddVertex(new Vector2((float)d * DrawWidth, 1 - (float)value * DrawHeight));
}
path.AddVertex(new Vector2(DrawWidth, 1 - (float)easing.ApplyEasing(1) * DrawHeight));
path.OriginPosition = path.PositionInBoundingBox(new Vector2());
line1.Width = p1.Value.Length;
line1.Rotation = -MathHelper.RadiansToDegrees(MathF.Atan2(p1.Value.Y, p1.Value.X));
line2.Width = Vector2.Distance(p2.Value, Vector2.One);
line2.Rotation = -MathHelper.RadiansToDegrees(MathF.Atan2(1 - p2.Value.Y, 1 - p2.Value.X));
}
}
private partial class ControlPointHandle : CompositeDrawable
{
public readonly Bindable<Vector2> ControlPoint = new Bindable<Vector2>();
public ControlPointHandle()
{
RelativePositionAxes = Axes.Both;
Size = new Vector2(20);
Origin = Anchor.Centre;
InternalChild = new Circle
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ControlPoint.BindValueChanged(p => Position = new Vector2(p.NewValue.X, 1 - p.NewValue.Y), true);
}
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
var position = Vector2.Divide(Parent!.ToLocalSpace(e.ScreenSpaceMousePosition), Parent.ChildSize);
ControlPoint.Value = new Vector2(
float.Round(float.Clamp(position.X, 0, 1), 2),
float.Round(1f - position.Y, 2)
);
}
protected override bool OnHover(HoverEvent e)
{
this.ScaleTo(1.35f, 50);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.ScaleTo(1f, 50);
}
}
private partial class EasingPreview : CompositeDrawable
{
private readonly Box box;
private readonly SpriteText durationText;
private bool flipped;
private readonly IBindable<CubicBezierEasingFunction> easingFunction;
private readonly BindableDouble duration = new BindableDouble
{
Value = 1000,
MinValue = 100,
MaxValue = 5000,
};
public EasingPreview(IBindable<CubicBezierEasingFunction> easingFunction)
{
this.easingFunction = easingFunction;
Width = 400;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
Height = 25,
RowDimensions = [new Dimension(GridSizeMode.AutoSize)],
ColumnDimensions = [new Dimension(GridSizeMode.Absolute, 150), new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize)],
Content = new Drawable?[][]
{
[
durationText = new SpriteText
{
Text = "Duration: 1000"
},
new BasicSliderBar<double>
{
Current = duration,
RelativeSizeAxes = Axes.X,
Height = 20,
},
null,
new BasicButton
{
RelativeSizeAxes = Axes.Y,
Width = 50,
Text = "Play",
FlashColour = FrameworkColour.BlueGreen.Lighten(0.5f),
Action = play,
}
]
}
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 50 },
Child = box = new Box
{
RelativePositionAxes = Axes.X,
Size = new Vector2(50),
},
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
duration.BindValueChanged(e => durationText.Text = FormattableString.Invariant($"Duration: {e.NewValue:N0}"), true);
}
private void play()
{
box.MoveToX(flipped ? 0 : 1, duration.Value, easingFunction.Value);
flipped = !flipped;
}
}
}
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Framework.Tests.Visual.Graphics
{
public partial class TestSceneFrostedGlass : FrameworkTestScene
{
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Container backgroundCircles;
Children = new Drawable[]
{
// Background with animated circles
backgroundCircles = new Container
{
RelativeSizeAxes = Axes.Both,
},
// Frosted glass container overlaying part of the screen
// new FrostedGlassContainer
// {
// RelativeSizeAxes = Axes.Both,
// Width = 0.5f,
// BlurSigma = new Vector2(10),
// Children = new Drawable[]
// {
// new Box
// {
// RelativeSizeAxes = Axes.Both,
// Colour = new Color4(1, 1, 1, 0.5f),
// },
// new SpriteText
// {
// Text = "Frosted Glass Effect",
// Anchor = Anchor.Centre,
// Origin = Anchor.Centre,
// Colour = Color4.Black,
// }
// }
// },
new Label("Background"),
new Label("FrostedGlassContainer")
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
}
};
const float circle_radius = 0.05f;
const float spacing = 0.01f;
for (float xPos = 0; xPos < 1; xPos += circle_radius + spacing)
{
for (float yPos = 0; yPos < 1; yPos += circle_radius + spacing)
{
backgroundCircles.Add(new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(circle_radius),
RelativePositionAxes = Axes.Both,
Position = new Vector2(xPos, yPos),
Progress = 1,
Colour = Color4.HotPink,
});
}
}
}
private partial class Label : Container
{
public Label(string text)
{
AutoSizeAxes = Axes.Both;
Margin = new MarginPadding(10);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
new SpriteText
{
Text = text,
Margin = new MarginPadding(10)
}
};
}
}
}
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
namespace osu.Framework.Tests.Visual.Platform
{
public partial class TestSceneDisplayBoundsWindowBorder : FrameworkTestScene
{
[Test]
public void TestDisplayUsableBounds()
{
AddStep("Set up scene", () => Child = new WindowDisplaysPreview(true)
{
RelativeSizeAxes = Axes.Both,
});
}
[Test]
public void TestWindowBorder()
{
AddStep("Set up scene", () => Child = new WindowDisplaysPreview(false, true)
{
RelativeSizeAxes = Axes.Both,
});
}
[Test]
public void TestBoth()
{
AddStep("Set up scene", () => Child = new WindowDisplaysPreview(true, true)
{
RelativeSizeAxes = Axes.Both,
});
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -27,6 +28,7 @@ namespace osu.Framework.Tests.Visual.Platform
private readonly Container paddedContainer;
private readonly Container screenContainer;
private readonly Container windowContainer;
private readonly Container? borderContainer;
private Vector2 screenContainerOffset;
private static readonly Color4 active_fill = new Color4(255, 138, 104, 255);
@@ -35,13 +37,18 @@ namespace osu.Framework.Tests.Visual.Platform
private static readonly Color4 screen_stroke = new Color4(244, 137, 25, 255);
private static readonly Color4 window_fill = new Color4(95, 113, 197, 255);
private static readonly Color4 window_stroke = new Color4(36, 59, 166, 255);
private static readonly Color4 window_border_fill = new Color4(85, 207, 89, 200);
private static readonly Color4 window_border_stroke = new Color4(50, 122, 53, 255);
private IWindow? window;
private readonly Bindable<WindowMode> windowMode = new Bindable<WindowMode>();
private readonly Bindable<Display> currentDisplay = new Bindable<Display>();
public WindowDisplaysPreview()
private readonly bool showDisplayUsableBounds;
public WindowDisplaysPreview(bool showDisplayUsableBounds = false, bool showWindowBorder = false)
{
this.showDisplayUsableBounds = showDisplayUsableBounds;
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -85,6 +92,22 @@ namespace osu.Framework.Tests.Visual.Platform
}
}
};
if (showWindowBorder)
{
screenContainer.Add(borderContainer = new Container
{
BorderColour = window_border_stroke,
BorderThickness = 20,
Masking = true,
Depth = -5,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = window_border_fill
}
});
}
}
[BackgroundDependencyLoader]
@@ -110,13 +133,13 @@ namespace osu.Framework.Tests.Visual.Platform
private void refreshScreens(IEnumerable<Display> displays)
{
screenContainer.RemoveAll(d => d != windowContainer, false);
screenContainer.RemoveAll(d => d != windowContainer && d != borderContainer, false);
var bounds = new RectangleI();
foreach (var display in displays)
{
screenContainer.Add(createScreen(display, window.AsNonNull().CurrentDisplayBindable.Value.Index));
screenContainer.AddRange(createScreen(display, window.AsNonNull().CurrentDisplayBindable.Value.Index));
bounds = RectangleI.Union(bounds, new RectangleI(display.Bounds.X, display.Bounds.Y, display.Bounds.Width, display.Bounds.Height));
}
@@ -130,11 +153,11 @@ namespace osu.Framework.Tests.Visual.Platform
screenContainer.Size = bounds.Size;
}
private Container createScreen(Display display, int activeDisplayIndex)
private IEnumerable<Container> createScreen(Display display, int activeDisplayIndex)
{
bool isActive = display.Index == activeDisplayIndex;
return new Container
yield return new Container
{
X = display.Bounds.X,
Y = display.Bounds.Y,
@@ -166,6 +189,30 @@ namespace osu.Framework.Tests.Visual.Platform
}
}
};
if (showDisplayUsableBounds)
{
yield return new Container
{
X = display.UsableBounds.X,
Y = display.UsableBounds.Y,
Width = display.UsableBounds.Width,
Height = display.UsableBounds.Height,
BorderColour = isActive ? Color4.MediumPurple : Color4.Green,
BorderThickness = 20,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = (isActive ? Color4.MediumPurple : Color4.Green).Opacity(0.2f)
},
}
};
}
}
private string modeName(DisplayMode mode) => $"{mode.Size.Width}x{mode.Size.Height}@{mode.RefreshRate}";
@@ -183,6 +230,17 @@ namespace osu.Framework.Tests.Visual.Platform
windowContainer.Height = fullscreen ? currentBounds.Height : window.Size.Height;
windowContainer.Position -= screenContainerOffset;
windowCaption.Text = $"{windowMode}\nSize: {window.Size.Width}x{window.Size.Height}\nClient: {window.ClientSize.Width}x{window.ClientSize.Height}";
if (borderContainer != null)
{
var borderSize = window.BorderSize.Value;
borderContainer.X = window.Position.X - borderSize.Left;
borderContainer.Y = window.Position.Y - borderSize.Top;
borderContainer.Width = windowContainer.Width + borderSize.TotalHorizontal;
borderContainer.Height = windowContainer.Height + borderSize.TotalVertical;
borderContainer.Position -= screenContainerOffset;
}
}
protected override void Update()

View File

@@ -38,5 +38,43 @@ namespace osu.Framework.Extensions
return false;
}
}
/// <summary>
/// If <paramref name="key"/> is a <see cref="IsPhysical">physical</see> key which is covered by another <see cref="IsVirtual">virtual</see> key, returns that virtual key.
/// Otherwise, returns <see langword="null"/>.
/// </summary>
/// <example>
/// <code>
/// &gt; InputKey.LShift.GetVirtualKey()
/// Shift
/// &gt; InputKey.RSuper.GetVirtualKey()
/// Super
/// &gt; InputKey.A.GetVirtualKey()
/// null
/// </code>
/// </example>
public static InputKey? GetVirtualKey(this InputKey key)
{
switch (key)
{
case InputKey.LShift:
case InputKey.RShift:
return InputKey.Shift;
case InputKey.LControl:
case InputKey.RControl:
return InputKey.Control;
case InputKey.LAlt:
case InputKey.RAlt:
return InputKey.Alt;
case InputKey.LSuper:
case InputKey.RSuper:
return InputKey.Super;
}
return null;
}
}
}

View File

@@ -1,42 +0,0 @@
using osuTK.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Allocation;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// A specialized layer for acrylic blur effects that fills the entire area and applies blur to background content.
/// This layer handles the actual blurring logic using a custom shader and manages background capture automatically.
/// </summary>
internal partial class AcrylicBlurLayer : Drawable
{
/// <summary>
/// The strength of the blur effect.
/// </summary>
public float BlurStrength { get; set; } = 10f;
/// <summary>
/// The tint colour applied over the blurred background.
/// </summary>
public Color4 TintColour { get; set; } = Color4.White;
/// <summary>
/// The darkening factor for depth effect.
/// </summary>
public float DarkenFactor { get; set; } = 0.1f;
[Resolved]
private IRenderer? renderer { get; set; }
[Resolved]
private ShaderManager shaderManager { get; set; } = null!;
public AcrylicBlurLayer()
{
RelativeSizeAxes = Axes.Both;
}
protected override DrawNode CreateDrawNode() => new AcrylicBlurLayerDrawNode(this);
}
}

View File

@@ -1,141 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osuTK;
using osuTK.Graphics;
using System.Runtime.InteropServices;
using osu.Framework.Graphics.Textures;
namespace osu.Framework.Graphics.Containers
{
internal partial class AcrylicBlurLayer
{
private class AcrylicBlurLayerDrawNode : DrawNode
{
protected new AcrylicBlurLayer Source => (AcrylicBlurLayer)base.Source;
private RectangleF drawRectangle;
private IShader acrylicShader;
private IFrameBuffer backgroundBuffer;
private IUniformBuffer<AcrylicParameters> acrylicParametersBuffer;
public AcrylicBlurLayerDrawNode(AcrylicBlurLayer source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
drawRectangle = Source.ScreenSpaceDrawQuad.AABBFloat;
}
protected override void Draw(IRenderer renderer)
{
// 检查依赖是否可用
if (Source.renderer == null || Source.shaderManager == null)
{
// 如果依赖不可用,跳过绘制
return;
}
// 获取或创建背景缓冲区(延迟到绘制线程)
if (backgroundBuffer == null)
{
try
{
backgroundBuffer = renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear);
}
catch
{
// 如果创建失败,跳过绘制
return;
}
}
// 捕获当前屏幕到缓冲区
try
{
renderer.CaptureScreenToFrameBuffer(backgroundBuffer);
}
catch
{
// 如果捕获失败,使用固定的背景色
renderer.Clear(new ClearInfo(Color4.Gray));
return;
}
// 尝试加载毛玻璃着色器
if (acrylicShader == null)
{
try
{
acrylicShader = Source.shaderManager.Load("AcrylicBlur", "Texture");
acrylicParametersBuffer = renderer.CreateUniformBuffer<AcrylicParameters>();
}
catch (Exception ex)
{
// 如果加载失败,使用备用方案:直接绘制背景
Console.WriteLine($"Failed to load acrylic shader: {ex.Message}");
renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(Source.TintColour));
return;
}
}
// 使用着色器绘制
if (acrylicShader != null && acrylicParametersBuffer != null)
{
acrylicParametersBuffer.Data = acrylicParametersBuffer.Data with
{
TexSize = backgroundBuffer.Size,
Radius = (int)Source.BlurStrength,
Sigma = Source.BlurStrength / 2f,
BlurDirection = Vector2.One,
TintColour = new Vector4(Source.TintColour.R, Source.TintColour.G, Source.TintColour.B, Source.TintColour.A),
DarkenFactor = Source.DarkenFactor
};
acrylicShader.BindUniformBlock("m_AcrylicParameters", acrylicParametersBuffer);
acrylicShader.Bind();
// 绘制背景到当前区域
renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(new Color4(1, 1, 1, 1)));
acrylicShader.Unbind();
}
else
{
// 备用方案:直接绘制背景
renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(Source.TintColour));
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
acrylicParametersBuffer?.Dispose();
backgroundBuffer?.Dispose();
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct AcrylicParameters
{
public UniformVector2 TexSize;
public UniformInt Radius;
public UniformFloat Sigma;
public UniformVector2 BlurDirection;
public UniformVector4 TintColour;
public UniformFloat DarkenFactor;
private readonly UniformPadding12 pad1;
}
}
}
}

View File

@@ -1,82 +0,0 @@
using osuTK.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// A container that applies a true acrylic/mica effect by blurring the content behind it.
/// This implementation uses a layered approach with a blur background layer and a darkening overlay.
/// The effect is applied regardless of drawing order and adapts to background changes in real-time.
/// </summary>
public partial class AcrylicContainer : Container
{
/// <summary>
/// The strength of the blur effect applied to the background content.
/// </summary>
public float BlurStrength { get; set; } = 10f;
/// <summary>
/// The tint colour applied over the blurred background.
/// </summary>
public Color4 TintColour { get; set; } = Color4.White;
/// <summary>
/// The darkening factor applied to create depth.
/// </summary>
public float DarkenFactor { get; set; } = 0.1f;
private AcrylicBlurLayer blurLayer = null!;
private Box darkenLayer = null!;
[Resolved]
private IRenderer? renderer { get; set; }
[Resolved]
private ShaderManager shaderManager { get; set; } = null!;
/// <summary>
/// Constructs a new acrylic container.
/// </summary>
public AcrylicContainer()
{
// 默认不设置RelativeSizeAxes让用户决定
}
protected override void LoadComplete()
{
base.LoadComplete();
// 添加虚化背景层
Add(blurLayer = new AcrylicBlurLayer
{
RelativeSizeAxes = Axes.Both,
BlurStrength = BlurStrength,
TintColour = TintColour,
DarkenFactor = DarkenFactor
});
// 添加暗化层
Add(darkenLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(0, 0, 0, DarkenFactor),
Depth = -1 // 确保在虚化层之上
});
}
protected override void Update()
{
base.Update();
// 同步属性到层
blurLayer.BlurStrength = BlurStrength;
blurLayer.TintColour = TintColour;
blurLayer.DarkenFactor = DarkenFactor;
darkenLayer.Colour = new Color4(0, 0, 0, DarkenFactor);
}
}
}

View File

@@ -1,40 +0,0 @@
// 自动背景捕获组件 - 在游戏中自动管理背景缓冲区更新
using osu.Framework.Graphics.Rendering;
using osu.Framework.Allocation;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// A component that automatically captures the screen background for acrylic effects.
/// Add this to your game once to enable automatic background capture for all AcrylicContainer instances.
/// </summary>
public partial class AutoBackgroundCapture : Drawable
{
[Resolved]
private IRenderer? renderer { get; set; }
private bool initialized;
protected override void LoadComplete()
{
base.LoadComplete();
// 初始化延迟到Update方法中进行
}
protected override void Update()
{
base.Update();
// 延迟初始化,确保在正确的线程中
if (!initialized && renderer != null)
{
BackgroundBufferManager.Instance.Initialize(renderer);
initialized = true;
}
// 自动更新背景缓冲区
BackgroundBufferManager.Instance.UpdateBackgroundBuffer();
}
}
}

View File

@@ -1,87 +0,0 @@
// 全局背景缓冲区管理器 - 自动管理背景捕获
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
namespace osu.Framework.Graphics.Containers
{
public class BackgroundBufferManager
{
private static BackgroundBufferManager? instance;
private static readonly object lockObject = new object();
public static BackgroundBufferManager Instance
{
get
{
if (instance == null)
{
lock (lockObject)
{
instance ??= new BackgroundBufferManager();
}
}
return instance;
}
}
private IFrameBuffer? backgroundBuffer;
private IRenderer? renderer;
private bool initialized;
// 初始化管理器(在游戏启动时调用一次)
public void Initialize(IRenderer renderer)
{
this.renderer = renderer;
initialized = false;
}
// 获取背景缓冲区 - 不再自动创建,只返回已存在的
public IFrameBuffer? GetBackgroundBuffer()
{
return backgroundBuffer;
}
// 获取或创建背景缓冲区(在绘制线程调用 - 延迟创建模式)
public IFrameBuffer GetOrCreateBackgroundBuffer(IRenderer renderer)
{
if (backgroundBuffer == null)
{
this.renderer = renderer;
backgroundBuffer = renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear);
initialized = true;
}
return backgroundBuffer;
}
// 确保背景缓冲区已创建(在绘制线程安全的地方调用)
public void EnsureBackgroundBuffer()
{
if (!initialized && renderer != null)
{
backgroundBuffer ??= renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear);
initialized = true;
}
}
// 更新背景缓冲区在UpdateAfterChildren中调用
public void UpdateBackgroundBuffer()
{
// 暂时禁用屏幕捕获直到实现正确的API
// if (renderer is Renderer concreteRenderer && backgroundBuffer != null)
// {
// concreteRenderer.CaptureScreenToFrameBuffer(backgroundBuffer);
// }
}
// 清理资源
public void Dispose()
{
backgroundBuffer?.Dispose();
backgroundBuffer = null;
renderer = null;
}
}
}

View File

@@ -1,259 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Utils;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Layout;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// A container that renders the background (from the screen) to an internal framebuffer, applies blur, and then
/// blits the framebuffer to the screen, allowing for frosted glass effects on the background.
/// If all children are of a specific non-<see cref="Drawable"/> type, use the
/// generic version <see cref="FrostedGlassContainer{T}"/>.
/// </summary>
public partial class FrostedGlassContainer : FrostedGlassContainer<Drawable>
{
/// <inheritdoc />
public FrostedGlassContainer(RenderBufferFormat[] formats = null, bool pixelSnapping = false)
: base(formats, pixelSnapping)
{
}
}
/// <summary>
/// A container that renders the background (from the screen) to an internal framebuffer, applies blur, and then
/// blits the framebuffer to the screen, allowing for frosted glass effects on the background.
/// </summary>
public partial class FrostedGlassContainer<T> : Container<T>, IBufferedContainer, IBufferedDrawable
where T : Drawable
{
private Vector2 blurSigma = Vector2.Zero;
/// <summary>
/// Controls the amount of blurring in two orthogonal directions (X and Y if
/// <see cref="BlurRotation"/> is zero).
/// Blur is parametrized by a gaussian image filter. This property controls
/// the standard deviation (sigma) of the gaussian kernel.
/// </summary>
public Vector2 BlurSigma
{
get => blurSigma;
set
{
if (blurSigma == value)
return;
blurSigma = value;
ForceRedraw();
}
}
private float blurRotation;
/// <summary>
/// Rotates the blur kernel clockwise. In degrees. Has no effect if
/// <see cref="BlurSigma"/> has the same magnitude in both directions.
/// </summary>
public float BlurRotation
{
get => blurRotation;
set
{
if (blurRotation == value)
return;
blurRotation = value;
ForceRedraw();
}
}
private ColourInfo effectColour = Color4.White;
/// <summary>
/// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is <see cref="Color4.White"/>.
/// </summary>
public ColourInfo EffectColour
{
get => effectColour;
set
{
if (effectColour.Equals(value))
return;
effectColour = value;
Invalidate(Invalidation.DrawNode);
}
}
private BlendingParameters effectBlending = BlendingParameters.Inherit;
/// <summary>
/// The <see cref="BlendingParameters"/> to use after applying all effects. Default is <see cref="BlendingType.Inherit"/>.
/// </summary>
public BlendingParameters EffectBlending
{
get => effectBlending;
set
{
if (effectBlending == value)
return;
effectBlending = value;
Invalidate(Invalidation.DrawNode);
}
}
private Color4 backgroundColour = new Color4(0, 0, 0, 0);
/// <summary>
/// The background colour of the framebuffer. Transparent black by default.
/// </summary>
public Color4 BackgroundColour
{
get => backgroundColour;
set
{
if (backgroundColour == value)
return;
backgroundColour = value;
ForceRedraw();
}
}
private Vector2 frameBufferScale = Vector2.One;
public Vector2 FrameBufferScale
{
get => frameBufferScale;
set
{
if (frameBufferScale == value)
return;
frameBufferScale = value;
ForceRedraw();
}
}
private float grayscaleStrength;
public float GrayscaleStrength
{
get => grayscaleStrength;
set
{
if (grayscaleStrength == value)
return;
grayscaleStrength = value;
ForceRedraw();
}
}
/// <summary>
/// Forces a redraw of the framebuffer before it is blitted the next time.
/// </summary>
public void ForceRedraw() => Invalidate(Invalidation.DrawNode);
/// <summary>
/// In order to signal the draw thread to re-draw the frosted glass container we version it.
/// Our own version (update) keeps track of which version we are on, whereas the
/// drawVersion keeps track of the version the draw thread is on.
/// When forcing a redraw we increment updateVersion, pass it into each new drawnode
/// and the draw thread will realize its drawVersion is lagging behind, thus redrawing.
/// </summary>
private long updateVersion;
private readonly BufferedContainerDrawNodeSharedData sharedData;
public IShader TextureShader { get; private set; }
private IShader blurShader;
private IShader grayscaleShader;
/// <summary>
/// Constructs an empty frosted glass container.
/// </summary>
/// <param name="formats">The render buffer formats attached to the frame buffer of this <see cref="FrostedGlassContainer"/>.</param>
/// <param name="pixelSnapping">
/// Whether the frame buffer position should be snapped to the nearest pixel when blitting.
/// This amounts to setting the texture filtering mode to "nearest".
/// </param>
public FrostedGlassContainer(RenderBufferFormat[] formats = null, bool pixelSnapping = false)
{
sharedData = new BufferedContainerDrawNodeSharedData(formats, pixelSnapping, false);
}
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR);
grayscaleShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.GRAYSCALE);
}
protected override DrawNode CreateDrawNode() => new FrostedGlassDrawNode(this, sharedData);
/// <summary>
/// The blending which <see cref="FrostedGlassDrawNode"/> uses for the effect.
/// </summary>
public BlendingParameters DrawEffectBlending
{
get
{
BlendingParameters blending = EffectBlending;
blending.CopyFromParent(Blending);
blending.ApplyDefaultToInherited();
return blending;
}
}
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
bool result = base.OnInvalidate(invalidation, source);
if ((invalidation & Invalidation.DrawNode) > 0)
{
++updateVersion;
result = true;
}
return result;
}
public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo;
// Children should not receive the true colour to avoid colour doubling when the frame-buffers are rendered to the back-buffer.
public override DrawColourInfo DrawColourInfo
{
get
{
var blending = Blending;
blending.ApplyDefaultToInherited();
return new DrawColourInfo(Color4.White, blending);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
sharedData.Dispose();
}
}
}

View File

@@ -1,152 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shaders;
using System;
using System.Runtime.InteropServices;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Utils;
using osu.Framework.Statistics;
using osu.Framework.Allocation;
namespace osu.Framework.Graphics.Containers
{
public partial class FrostedGlassContainer<T>
{
private class FrostedGlassDrawNode : BufferedDrawNode, ICompositeDrawNode
{
protected new FrostedGlassContainer<T> Source => (FrostedGlassContainer<T>)base.Source;
protected new CompositeDrawableDrawNode Child => (CompositeDrawableDrawNode)base.Child;
private ColourInfo effectColour;
private BlendingParameters effectBlending;
private Vector2 blurSigma;
private Vector2I blurRadius;
private float blurRotation;
private float grayscaleStrength;
private long updateVersion;
private IShader blurShader;
private IShader grayscaleShader;
public FrostedGlassDrawNode(FrostedGlassContainer<T> source, BufferedContainerDrawNodeSharedData sharedData)
: base(source, new CompositeDrawableDrawNode(source), sharedData)
{
}
public override void ApplyState()
{
base.ApplyState();
updateVersion = Source.updateVersion;
effectColour = Source.EffectColour;
effectBlending = Source.DrawEffectBlending;
blurSigma = Source.BlurSigma;
blurRadius = new Vector2I(Blur.KernelSize(blurSigma.X), Blur.KernelSize(blurSigma.Y));
blurRotation = Source.BlurRotation;
grayscaleStrength = Source.GrayscaleStrength;
blurShader = Source.blurShader;
grayscaleShader = Source.grayscaleShader;
}
protected override long GetDrawVersion() => updateVersion;
protected override void PopulateContents(IRenderer renderer)
{
// Capture the screen to the main buffer
renderer.CaptureScreenToFrameBuffer(SharedData.MainBuffer);
// Then apply effects
base.PopulateContents(renderer);
}
protected override void DrawContents(IRenderer renderer)
{
renderer.SetBlend(effectBlending);
ColourInfo finalEffectColour = DrawColourInfo.Colour;
finalEffectColour.ApplyChild(effectColour);
renderer.DrawFrameBuffer(SharedData.CurrentEffectBuffer, DrawRectangle, finalEffectColour);
// Draw children on top
DrawOther(Child, renderer);
}
private IUniformBuffer<BlurParameters> blurParametersBuffer;
private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation)
{
blurParametersBuffer ??= renderer.CreateUniformBuffer<BlurParameters>();
IFrameBuffer current = SharedData.CurrentEffectBuffer;
IFrameBuffer target = SharedData.GetNextEffectBuffer();
renderer.SetBlend(BlendingParameters.None);
using (BindFrameBuffer(target))
{
float radians = float.DegreesToRadians(blurRotation);
blurParametersBuffer.Data = blurParametersBuffer.Data with
{
Radius = kernelRadius,
Sigma = sigma,
TexSize = current.Size,
Direction = new Vector2(MathF.Cos(radians), MathF.Sin(radians))
};
blurShader.BindUniformBlock("m_BlurParameters", blurParametersBuffer);
blurShader.Bind();
renderer.DrawFrameBuffer(current, new RectangleF(0, 0, current.Texture.Width, current.Texture.Height), ColourInfo.SingleColour(Color4.White));
blurShader.Unbind();
}
}
public List<DrawNode> Children
{
get => Child.Children;
set => Child.Children = value;
}
public bool AddChildDrawNodes => RequiresRedraw;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
blurParametersBuffer?.Dispose();
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct BlurParameters
{
public UniformVector2 TexSize;
public UniformInt Radius;
public UniformFloat Sigma;
public UniformVector2 Direction;
private readonly UniformPadding8 pad1;
}
}
private class BufferedContainerDrawNodeSharedData : BufferedDrawNodeSharedData
{
public BufferedContainerDrawNodeSharedData(RenderBufferFormat[] mainBufferFormats, bool pixelSnapping, bool clipToRootNode)
: base(2, mainBufferFormats, pixelSnapping, clipToRootNode)
{
}
}
}
}

View File

@@ -535,9 +535,9 @@ namespace osu.Framework.Graphics.Containers
// Things will be accounted for accurately later.
// All calls to `spacingFactor()` in the original code thus reduce to returning (0,0).
if (c.RelativeAnchorPosition != Vector2.Zero)
throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {RelativeAnchorPosition}s. Only (0,0) is supported.");
throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {nameof(RelativeAnchorPosition)}s. Only (0,0) is supported.");
if (c.RelativeOriginPosition != Vector2.Zero)
throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {RelativeOriginPosition}s. Only (0,0) is supported.");
throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {nameof(RelativeOriginPosition)}s. Only (0,0) is supported.");
// Populate running variables with sane initial values.
if (i == 0)

View File

@@ -10,9 +10,11 @@ using osuTK;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Allocation;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Layout;
using osuTK.Graphics;
@@ -34,8 +36,8 @@ namespace osu.Framework.Graphics.Lines
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
pathShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE);
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Path");
pathShader = shaders.Load("PathPrepass", "PathPrepass");
}
private readonly List<Vector2> vertices = new List<Vector2>();
@@ -291,20 +293,12 @@ namespace osu.Framework.Graphics.Lines
// The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer.
public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
private Color4 backgroundColour = new Color4(0, 0, 0, 0);
private static readonly Color4 background_colour = new Color4(0, 0, 0, 0);
/// <summary>
/// The background colour to be used for the frame buffer this path is rendered to.
/// </summary>
public virtual Color4 BackgroundColour
{
get => backgroundColour;
set
{
backgroundColour = value;
Invalidate(Invalidation.DrawNode);
}
}
public Color4 BackgroundColour => background_colour;
public long PathInvalidationID { get; private set; }
@@ -321,7 +315,7 @@ namespace osu.Framework.Graphics.Lines
return result;
}
private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderBufferFormat.D16 }, clipToRootNode: true);
private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(clipToRootNode: true);
protected override DrawNode CreateDrawNode() => new PathBufferedDrawNode(this, new PathDrawNode(this), sharedData);
@@ -335,11 +329,38 @@ namespace osu.Framework.Graphics.Lines
}
private long pathInvalidationID = -1;
private Texture texture;
private Vector4 textureRect;
private IUniformBuffer<PathTextureParameters> parametersBuffer;
public override void ApplyState()
{
base.ApplyState();
pathInvalidationID = Source.PathInvalidationID;
texture = Source.Texture;
var rect = texture.GetTextureRect();
textureRect = new Vector4(rect.Left, rect.Top, rect.Width, rect.Height);
}
protected override void BindUniformResources(IShader shader, IRenderer renderer)
{
base.BindUniformResources(shader, renderer);
parametersBuffer ??= renderer.CreateUniformBuffer<PathTextureParameters>();
parametersBuffer.Data = new PathTextureParameters
{
TexRect1 = textureRect,
};
shader.BindUniformBlock("m_PathTextureParameters", parametersBuffer);
texture?.Bind(1);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct PathTextureParameters
{
public UniformVector4 TexRect1;
}
protected override long GetDrawVersion() => pathInvalidationID;

View File

@@ -2,17 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Textures;
using osuTK;
using System;
using System.Collections.Generic;
using osuTK.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Utils;
using osuTK.Graphics.ES30;
namespace osu.Framework.Graphics.Lines
{
@@ -26,12 +25,10 @@ namespace osu.Framework.Graphics.Lines
private readonly List<Line> segments = new List<Line>();
private Texture? texture;
private Vector2 drawSize;
private float radius;
private IShader? pathShader;
private IVertexBatch<TexturedVertex3D>? triangleBatch;
private IVertexBatch<PathVertex>? triangleBatch;
public PathDrawNode(Path source)
: base(source)
@@ -45,8 +42,6 @@ namespace osu.Framework.Graphics.Lines
segments.Clear();
segments.AddRange(Source.segments);
texture = Source.Texture;
drawSize = Source.DrawSize;
radius = Source.PathRadius;
pathShader = Source.pathShader;
}
@@ -55,136 +50,65 @@ namespace osu.Framework.Graphics.Lines
{
base.Draw(renderer);
if (texture?.Available != true || segments.Count == 0 || pathShader == null)
if (segments.Count == 0 || pathShader == null || radius == 0f)
return;
// We multiply the size args by 3 such that the amount of vertices is a multiple of the amount of vertices
// per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong
// grouping of vertices into primitives.
triangleBatch ??= renderer.CreateLinearBatch<TexturedVertex3D>(max_res * 200 * 3, 10, PrimitiveTopology.Triangles);
triangleBatch ??= renderer.CreateLinearBatch<PathVertex>(max_res * 200 * 3, 10, PrimitiveTopology.Triangles);
renderer.PushLocalMatrix(DrawInfo.Matrix);
renderer.PushDepthInfo(DepthInfo.Default);
// Blending is removed to allow for correct blending between the wedges of the path.
renderer.SetBlend(BlendingParameters.None);
renderer.SetBlend(new BlendingParameters
{
Source = BlendingType.One,
Destination = BlendingType.One,
SourceAlpha = BlendingType.One,
DestinationAlpha = BlendingType.One,
RGBEquation = BlendingEquation.Max,
AlphaEquation = BlendingEquation.Max,
});
pathShader.Bind();
texture.Bind();
updateVertexBuffer();
pathShader.Unbind();
renderer.PopDepthInfo();
renderer.PopLocalMatrix();
}
private Vector2 pointOnCircle(float angle) => new Vector2(MathF.Cos(angle), MathF.Sin(angle));
private Vector2 relativePosition(Vector2 localPos) => Vector2.Divide(localPos, drawSize);
private Color4 colourAt(Vector2 localPos) => DrawColourInfo.Colour.TryExtractSingleColour(out SRGBColour colour)
? colour.SRGB
: DrawColourInfo.Colour.Interpolate(relativePosition(localPos)).SRGB;
private void addSegmentQuads(SegmentWithThickness segment, RectangleF texRect)
private void addCap(Line cap)
{
Debug.Assert(triangleBatch != null);
// The provided line is perpendicular to the end/start of a segment.
// To get the remaining quad positions we are expanding said segment by the path radius.
Vector2 ortho = cap.OrthogonalDirection;
if (float.IsNaN(ortho.X) || float.IsNaN(ortho.Y))
ortho = Vector2.UnitY;
// Each segment of the path is actually rendered as 2 quads, being split in half along the approximating line.
// On this line the depth is 1 instead of 0, which is done in order to properly handle self-overlap using the depth buffer.
Vector3 firstMiddlePoint = new Vector3(segment.Guide.StartPoint.X, segment.Guide.StartPoint.Y, 1);
Vector3 secondMiddlePoint = new Vector3(segment.Guide.EndPoint.X, segment.Guide.EndPoint.Y, 1);
Color4 firstMiddleColour = colourAt(segment.Guide.StartPoint);
Color4 secondMiddleColour = colourAt(segment.Guide.EndPoint);
Vector2 v2 = cap.StartPoint + ortho * radius;
Vector2 v3 = cap.EndPoint + ortho * radius;
// Each of the quads (mentioned above) is rendered as 2 triangles:
// Outer quad, triangle 1
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeRight.EndPoint.X, segment.EdgeRight.EndPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeRight.EndPoint)
});
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeRight.StartPoint.X, segment.EdgeRight.StartPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeRight.StartPoint)
});
triangleBatch.Add(new TexturedVertex3D
{
Position = firstMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = firstMiddleColour
});
// Outer quad, triangle 2
triangleBatch.Add(new TexturedVertex3D
{
Position = firstMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = firstMiddleColour
});
triangleBatch.Add(new TexturedVertex3D
{
Position = secondMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = secondMiddleColour
});
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeRight.EndPoint.X, segment.EdgeRight.EndPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeRight.EndPoint)
});
// Inner quad, triangle 1
triangleBatch.Add(new TexturedVertex3D
{
Position = firstMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = firstMiddleColour
});
triangleBatch.Add(new TexturedVertex3D
{
Position = secondMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = secondMiddleColour
});
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeLeft.EndPoint.X, segment.EdgeLeft.EndPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeLeft.EndPoint)
});
// Inner quad, triangle 2
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeLeft.EndPoint.X, segment.EdgeLeft.EndPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeLeft.EndPoint)
});
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(segment.EdgeLeft.StartPoint.X, segment.EdgeLeft.StartPoint.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = colourAt(segment.EdgeLeft.StartPoint)
});
triangleBatch.Add(new TexturedVertex3D
{
Position = firstMiddlePoint,
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = firstMiddleColour
});
drawQuad
(
new Quad(cap.StartPoint, v2, cap.EndPoint, v3),
new Quad(new Vector2(0, -1), new Vector2(1, -1), new Vector2(0, 1), Vector2.One)
);
}
private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight, Line prevSegmentLeft, Line prevSegmentRight, RectangleF texRect)
private void addSegmentQuad(DrawableSegment segment)
{
Debug.Assert(triangleBatch != null);
drawQuad
(
segment.DrawQuad,
new Quad(new Vector2(0, -1), new Vector2(0, -1), new Vector2(0, 1), new Vector2(0, 1))
);
}
private void addConnectionBetween(DrawableSegment segment, DrawableSegment prevSegment)
{
float thetaDiff = segment.Guide.Theta - prevSegment.Guide.Theta;
if (Math.Abs(thetaDiff) > MathF.PI)
thetaDiff = -Math.Sign(thetaDiff) * 2 * MathF.PI + thetaDiff;
@@ -192,83 +116,129 @@ namespace osu.Framework.Graphics.Lines
if (thetaDiff == 0f)
return;
Vector2 origin = (segmentLeft.StartPoint + segmentRight.StartPoint) / 2;
// Use segment end points instead of calculating start/end via theta to guarantee
// that the vertices have the exact same position as the quads, which prevents
// possible pixel gaps during rasterization.
Vector2 current = thetaDiff > 0f ? prevSegmentRight.EndPoint : prevSegmentLeft.EndPoint;
Vector2 end = thetaDiff > 0f ? segmentRight.StartPoint : segmentLeft.StartPoint;
Line start = thetaDiff > 0f ? new Line(prevSegmentLeft.EndPoint, prevSegmentRight.EndPoint) : new Line(prevSegmentRight.EndPoint, prevSegmentLeft.EndPoint);
float theta0 = start.Theta;
float thetaStep = Math.Sign(thetaDiff) * MathF.PI / max_res;
int stepCount = (int)MathF.Ceiling(thetaDiff / thetaStep);
Color4 originColour = colourAt(origin);
Color4 currentColour = colourAt(current);
for (int i = 1; i <= stepCount; i++)
// more than 90 degrees - add end cap to the previous segment
if (Math.Abs(thetaDiff) > Math.PI * 0.5)
{
// Center point
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(origin.X, origin.Y, 1),
TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y),
Colour = originColour
});
// First outer point
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(current.X, current.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = currentColour
});
current = i < stepCount ? origin + pointOnCircle(theta0 + i * thetaStep) * radius : end;
currentColour = colourAt(current);
// Second outer point
triangleBatch.Add(new TexturedVertex3D
{
Position = new Vector3(current.X, current.Y, 0),
TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y),
Colour = currentColour
});
addEndCap(prevSegment);
return;
}
Vector2 origin = segment.Guide.StartPoint;
Line end = thetaDiff > 0f ? new Line(segment.BottomLeft, segment.TopLeft) : new Line(segment.TopLeft, segment.BottomLeft);
Line start = thetaDiff > 0f ? new Line(prevSegment.TopRight, prevSegment.BottomRight) : new Line(prevSegment.BottomRight, prevSegment.TopRight);
// position of a vertex which is located slightly below segments intersection
Vector2 innerVertex = Vector2.Lerp(start.StartPoint, end.EndPoint, 0.5f);
// at this small angle curvature of the connection isn't noticeable, we can get away with a single triangle
if (Math.Abs(thetaDiff) < Math.PI / max_res)
{
drawTriangle(new Triangle(start.EndPoint, innerVertex, end.StartPoint), origin);
return;
}
// 2 triangles for the remaining cases
Vector2 middle1 = Vector2.Lerp(start.EndPoint, end.StartPoint, 0.5f);
Vector2 outerVertex = Vector2.Lerp(origin, middle1, radius / (float)Math.Cos(Math.Abs(thetaDiff) * 0.5) / Vector2.Distance(origin, middle1));
drawQuad(new Quad(start.EndPoint, outerVertex, innerVertex, end.StartPoint), origin);
}
private void drawTriangle(Triangle triangle, Vector2 origin)
{
drawTriangle
(
triangle,
new Triangle
(
Vector2.Divide(triangle.P0 - origin, radius),
Vector2.Divide(triangle.P1 - origin, radius),
Vector2.Divide(triangle.P2 - origin, radius)
)
);
}
private void drawTriangle(Triangle triangle, Triangle relativePos)
{
Debug.Assert(triangleBatch != null);
triangleBatch.Add(new PathVertex
{
Position = triangle.P0,
RelativePos = relativePos.P0
});
triangleBatch.Add(new PathVertex
{
Position = triangle.P1,
RelativePos = relativePos.P1
});
triangleBatch.Add(new PathVertex
{
Position = triangle.P2,
RelativePos = relativePos.P2
});
}
private void drawQuad(Quad quad, Vector2 origin)
{
drawQuad
(
quad,
new Quad
(
Vector2.Divide(quad.TopLeft - origin, radius),
Vector2.Divide(quad.TopRight - origin, radius),
Vector2.Divide(quad.BottomLeft - origin, radius),
Vector2.Divide(quad.BottomRight - origin, radius)
)
);
}
private void drawQuad(Quad quad, Quad relativePos)
{
Debug.Assert(triangleBatch != null);
triangleBatch.Add(new PathVertex
{
Position = quad.TopLeft,
RelativePos = relativePos.TopLeft
});
triangleBatch.Add(new PathVertex
{
Position = quad.TopRight,
RelativePos = relativePos.TopRight
});
triangleBatch.Add(new PathVertex
{
Position = quad.BottomLeft,
RelativePos = relativePos.BottomLeft
});
triangleBatch.Add(new PathVertex
{
Position = quad.BottomLeft,
RelativePos = relativePos.BottomLeft
});
triangleBatch.Add(new PathVertex
{
Position = quad.TopRight,
RelativePos = relativePos.TopRight
});
triangleBatch.Add(new PathVertex
{
Position = quad.BottomRight,
RelativePos = relativePos.BottomRight
});
}
private void updateVertexBuffer()
{
// Explanation of the terms "left" and "right":
// "Left" and "right" are used here in terms of a typical (Cartesian) coordinate system.
// So "left" corresponds to positive angles (anti-clockwise), and "right" corresponds
// to negative angles (clockwise).
//
// Note that this is not the same as the actually used coordinate system, in which the
// y-axis is flipped. In this system, "left" corresponds to negative angles (clockwise)
// and "right" corresponds to positive angles (anti-clockwise).
//
// Using a Cartesian system makes the calculations more consistent with typical math,
// such as in angle<->coordinate conversions and ortho vectors. For example, the x-unit
// vector (1, 0) has the orthogonal y-unit vector (0, 1). This would be "left" in the
// Cartesian system. But in the actual system, it's "right" and clockwise. Where
// this becomes confusing is during debugging, because OpenGL uses a Cartesian system.
// So to make debugging a bit easier (i.e. w/ RenderDoc or Nsight), this code uses terms
// that make sense in the realm of OpenGL, rather than terms which are technically
// accurate in the actually used "flipped" system.
Debug.Assert(texture != null);
Debug.Assert(segments.Count > 0);
RectangleF texRect = texture.GetTextureRect(new RectangleF(0.5f, 0.5f, texture.Width - 1, texture.Height - 1));
Line? segmentToDraw = null;
SegmentStartLocation location = SegmentStartLocation.Outside;
SegmentStartLocation modifiedLocation = SegmentStartLocation.Outside;
SegmentStartLocation nextLocation = SegmentStartLocation.End;
SegmentWithThickness? lastDrawnSegment = null;
DrawableSegment? lastDrawnSegment = null;
for (int i = 0; i < segments.Count; i++)
{
@@ -309,9 +279,9 @@ namespace osu.Framework.Graphics.Lines
}
else // Otherwise draw the expanded segment
{
SegmentWithThickness s = new SegmentWithThickness(segmentToDraw.Value, radius, location, modifiedLocation);
addSegmentQuads(s, texRect);
connect(s, lastDrawnSegment, texRect);
DrawableSegment s = new DrawableSegment(segmentToDraw.Value, radius, location, modifiedLocation);
addSegmentQuad(s);
connect(s, lastDrawnSegment);
lastDrawnSegment = s;
segmentToDraw = segments[i];
@@ -328,22 +298,22 @@ namespace osu.Framework.Graphics.Lines
// Finish drawing last segment (if exists)
if (segmentToDraw.HasValue)
{
SegmentWithThickness s = new SegmentWithThickness(segmentToDraw.Value, radius, location, modifiedLocation);
addSegmentQuads(s, texRect);
connect(s, lastDrawnSegment, texRect);
addEndCap(s, texRect);
DrawableSegment s = new DrawableSegment(segmentToDraw.Value, radius, location, modifiedLocation);
addSegmentQuad(s);
connect(s, lastDrawnSegment);
addEndCap(s);
}
}
/// <summary>
/// Connects the start of the segment to the end of a previous one.
/// </summary>
private void connect(SegmentWithThickness segment, SegmentWithThickness? prevSegment, RectangleF texRect)
private void connect(DrawableSegment segment, DrawableSegment? prevSegment)
{
if (!prevSegment.HasValue)
{
// Nothing to connect to - add start cap
addStartCap(segment, texRect);
addStartCap(segment);
return;
}
@@ -352,13 +322,13 @@ namespace osu.Framework.Graphics.Lines
default:
case SegmentStartLocation.End:
// Segment starts at the end of the previous one
addConnectionBetween(segment, prevSegment.Value, texRect);
addConnectionBetween(segment, prevSegment.Value);
break;
case SegmentStartLocation.Start:
case SegmentStartLocation.Middle:
// Segment starts at the start or the middle of the previous one - add end cap to the previous segment
addEndCap(prevSegment.Value, texRect);
addEndCap(prevSegment.Value);
break;
case SegmentStartLocation.Outside:
@@ -372,39 +342,19 @@ namespace osu.Framework.Graphics.Lines
// line since horizontal one will pass through it. However, that wouldn't be the case if horizontal line was located at
// the middle and so end cap would be required.
if (segment.StartLocation != SegmentStartLocation.End)
addEndCap(prevSegment.Value, texRect);
addEndCap(prevSegment.Value);
// add start cap to the current one
addStartCap(segment, texRect);
addStartCap(segment);
break;
}
}
private void addConnectionBetween(SegmentWithThickness segment, SegmentWithThickness prevSegment, RectangleF texRect)
{
float thetaDiff = segment.Guide.Theta - prevSegment.Guide.Theta;
addSegmentCaps(thetaDiff, segment.EdgeLeft, segment.EdgeRight, prevSegment.EdgeLeft, prevSegment.EdgeRight, texRect);
}
private void addEndCap(DrawableSegment segment) =>
addCap(new Line(segment.TopRight, segment.BottomRight));
private void addEndCap(SegmentWithThickness segment, RectangleF texRect)
{
// Explanation of semi-circle caps:
// Semi-circles are essentially 180 degree caps. So to create these caps, we
// can simply "fake" a segment that's 180 degrees flipped. This works because
// we are taking advantage of the fact that a path which makes a 180 degree
// bend would have a semi-circle cap.
Line flippedLeft = new Line(segment.EdgeRight.EndPoint, segment.EdgeRight.StartPoint);
Line flippedRight = new Line(segment.EdgeLeft.EndPoint, segment.EdgeLeft.StartPoint);
addSegmentCaps(MathF.PI, flippedLeft, flippedRight, segment.EdgeLeft, segment.EdgeRight, texRect);
}
private void addStartCap(SegmentWithThickness segment, RectangleF texRect)
{
Line flippedLeft = new Line(segment.EdgeRight.EndPoint, segment.EdgeRight.StartPoint);
Line flippedRight = new Line(segment.EdgeLeft.EndPoint, segment.EdgeLeft.StartPoint);
addSegmentCaps(MathF.PI, segment.EdgeLeft, segment.EdgeRight, flippedLeft, flippedRight, texRect);
}
private void addStartCap(DrawableSegment segment) =>
addCap(new Line(segment.BottomLeft, segment.TopLeft));
private static float progressFor(Line line, float length, Vector2 point)
{
@@ -427,38 +377,53 @@ namespace osu.Framework.Graphics.Lines
Outside
}
private readonly struct SegmentWithThickness
private readonly struct DrawableSegment
{
/// <summary>
/// The line defining this <see cref="SegmentWithThickness"/>.
/// The line defining this <see cref="DrawableSegment"/>.
/// </summary>
public Line Guide { get; }
/// <summary>
/// The line parallel to <see cref="Guide"/> and located on the left side of it.
/// The draw quad of this <see cref="DrawableSegment"/>.
/// </summary>
public Line EdgeLeft { get; }
public Quad DrawQuad { get; }
/// <summary>
/// The line parallel to <see cref="Guide"/> and located on the right side of it.
/// The top-left position of the <see cref="DrawQuad"/> of this <see cref="DrawableSegment"/>.
/// </summary>
public Line EdgeRight { get; }
public Vector2 TopLeft => DrawQuad.TopLeft;
/// <summary>
/// Position of this <see cref="SegmentWithThickness"/> relative to the previous one.
/// The top-right position of the <see cref="DrawQuad"/> of this <see cref="DrawableSegment"/>.
/// </summary>
public Vector2 TopRight => DrawQuad.TopRight;
/// <summary>
/// The bottom-left position of the <see cref="DrawQuad"/> of this <see cref="DrawableSegment"/>.
/// </summary>
public Vector2 BottomLeft => DrawQuad.BottomLeft;
/// <summary>
/// The bottom-right position of the <see cref="DrawQuad"/> of this <see cref="DrawableSegment"/>.
/// </summary>
public Vector2 BottomRight => DrawQuad.BottomRight;
/// <summary>
/// Position of this <see cref="DrawableSegment"/> relative to the previous one.
/// </summary>
public SegmentStartLocation StartLocation { get; }
/// <summary>
/// Position of this modified <see cref="SegmentWithThickness"/> relative to the previous one.
/// Position of this modified <see cref="DrawableSegment"/> relative to the previous one.
/// </summary>
public SegmentStartLocation ModifiedStartLocation { get; }
/// <param name="guide">The line defining this <see cref="SegmentWithThickness"/>.</param>
/// <param name="distance">The distance at which <see cref="EdgeLeft"/> and <see cref="EdgeRight"/> will be located from the <see cref="Guide"/>.</param>
/// <param name="startLocation">Position of this <see cref="SegmentWithThickness"/> relative to the previous one.</param>
/// <param name="modifiedStartLocation">Position of this modified <see cref="SegmentWithThickness"/> relative to the previous one.</param>
public SegmentWithThickness(Line guide, float distance, SegmentStartLocation startLocation, SegmentStartLocation modifiedStartLocation)
/// <param name="guide">The line defining this <see cref="DrawableSegment"/>.</param>
/// <param name="radius">The path radius.</param>
/// <param name="startLocation">Position of this <see cref="DrawableSegment"/> relative to the previous one.</param>
/// <param name="modifiedStartLocation">Position of this modified <see cref="DrawableSegment"/> relative to the previous one.</param>
public DrawableSegment(Line guide, float radius, SegmentStartLocation startLocation, SegmentStartLocation modifiedStartLocation)
{
Guide = guide;
StartLocation = startLocation;
@@ -468,10 +433,34 @@ namespace osu.Framework.Graphics.Lines
if (float.IsNaN(ortho.X) || float.IsNaN(ortho.Y))
ortho = Vector2.UnitY;
EdgeLeft = new Line(Guide.StartPoint + ortho * distance, Guide.EndPoint + ortho * distance);
EdgeRight = new Line(Guide.StartPoint - ortho * distance, Guide.EndPoint - ortho * distance);
DrawQuad = new Quad
(
Guide.StartPoint + ortho * radius,
Guide.EndPoint + ortho * radius,
Guide.StartPoint - ortho * radius,
Guide.EndPoint - ortho * radius
);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct PathVertex : IEquatable<PathVertex>, IVertex
{
[VertexMember(2, VertexAttribPointerType.Float)]
public Vector2 Position;
/// <summary>
/// A position of a vertex, where distance from that position to (0,0) defines it's colour.
/// Distance 0 means white and 1 means black.
/// This position is being interpolated between vertices and final colour is being applied in the fragment shader.
/// </summary>
[VertexMember(2, VertexAttribPointerType.Float)]
public Vector2 RelativePos;
public readonly bool Equals(PathVertex other) =>
Position.Equals(other.Position)
&& RelativePos.Equals(other.RelativePos);
}
}
}
}

View File

@@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osuTK.Graphics;
@@ -38,18 +37,6 @@ namespace osu.Framework.Graphics.Lines
}
}
private Color4? customBackgroundColour;
/// <summary>
/// The background colour to be used for the frame buffer this path is rendered to.
/// For <see cref="SmoothPath"/>, this automatically defaults to the colour at 0 (the outermost colour of the path) to avoid aliasing issues.
/// </summary>
public override Color4 BackgroundColour
{
get => customBackgroundColour ?? base.BackgroundColour;
set => customBackgroundColour = base.BackgroundColour = value;
}
private readonly Cached textureCache = new Cached();
protected void InvalidateTexture()
@@ -63,7 +50,7 @@ namespace osu.Framework.Graphics.Lines
if (textureCache.IsValid)
return;
int textureWidth = (int)PathRadius * 2;
int textureWidth = (int)Math.Max(PathRadius, 1) * 2;
//initialise background
var raw = new Image<Rgba32>(textureWidth, 1);
@@ -89,9 +76,6 @@ namespace osu.Framework.Graphics.Lines
Texture = texture;
}
if (customBackgroundColour == null)
base.BackgroundColour = ColourAt(0).Opacity(0);
textureCache.Validate();
}

View File

@@ -375,13 +375,6 @@ namespace osu.Framework.Graphics.OpenGL
return image;
}
public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer)
{
frameBuffer.Bind();
GL.CopyTexSubImage2D(All.Texture2D, 0, 0, 0, 0, 0, frameBuffer.Texture.Width, frameBuffer.Texture.Height);
frameBuffer.Unbind();
}
protected internal override Image<Rgba32> ExtractFrameBufferData(IFrameBuffer frameBuffer)
{
int width = frameBuffer.Texture.Width;

View File

@@ -191,12 +191,6 @@ namespace osu.Framework.Graphics.Rendering.Deferred
protected internal override Image<Rgba32> TakeScreenshot()
=> VeldridDevice.TakeScreenshot();
public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer)
{
// TODO: Implement screen capture for DeferredRenderer
// This is a placeholder implementation
}
void IRenderer.EnterDrawNode(DrawNode node)
{
drawNodeStack.Push(node);

View File

@@ -25,11 +25,6 @@ namespace osu.Framework.Graphics.Rendering.Dummy
protected internal override Image<Rgba32> TakeScreenshot()
=> new Image<Rgba32>(1, 1);
public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer)
{
// Dummy implementation - do nothing
}
protected override IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType)
=> new DummyShaderPart();

View File

@@ -350,12 +350,6 @@ namespace osu.Framework.Graphics.Rendering
/// </summary>
protected internal Image<Rgba32> TakeScreenshot();
/// <summary>
/// Captures the current screen content to a framebuffer for post-processing effects.
/// </summary>
/// <param name="frameBuffer">The framebuffer to capture to.</param>
void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer);
/// <summary>
/// Returns an image containing the content of a framebuffer.
/// </summary>
@@ -398,13 +392,8 @@ namespace osu.Framework.Graphics.Rendering
/// <param name="wrapModeS">The texture's horizontal wrap mode.</param>
/// <param name="wrapModeT">The texture's vertex wrap mode.</param>
/// <returns>The <see cref="Texture"/>.</returns>
Texture CreateTexture(int width,
int height,
bool manualMipmaps = false,
TextureFilteringMode filteringMode = TextureFilteringMode.Linear,
WrapMode wrapModeS = WrapMode.None,
WrapMode wrapModeT = WrapMode.None,
Color4? initialisationColour = null);
Texture CreateTexture(int width, int height, bool manualMipmaps = false, TextureFilteringMode filteringMode = TextureFilteringMode.Linear, WrapMode wrapModeS = WrapMode.None,
WrapMode wrapModeT = WrapMode.None, Color4? initialisationColour = null);
/// <summary>
/// Creates a new video texture.
@@ -471,7 +460,7 @@ namespace osu.Framework.Graphics.Rendering
{
}
#region TextureVisualiser Support
#region TextureVisualiser Support
/// <summary>
/// An event which is invoked every time a <see cref="Texture"/> is created.
@@ -483,6 +472,6 @@ namespace osu.Framework.Graphics.Rendering
/// </summary>
internal Texture[] GetAllTextures();
#endregion
#endregion
}
}

View File

@@ -328,10 +328,8 @@ namespace osu.Framework.Graphics.Rendering
protected internal abstract Image<Rgba32> TakeScreenshot();
/// <summary>
/// Captures the current screen content to a framebuffer for post-processing effects.
/// Returns an image containing the content of a framebuffer.
/// </summary>
/// <param name="frameBuffer">The framebuffer to capture to.</param>
public abstract void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer);
protected internal virtual Image<Rgba32>? ExtractFrameBufferData(IFrameBuffer frameBuffer) => null;
/// <summary>

View File

@@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Utils;
using osuTK;
namespace osu.Framework.Graphics.Transforms
{
/// <summary>
/// An easing function that creates a smooth transition using a cubic <see href="https://developer.mozilla.org/en-US/docs/Glossary/Bezier_curve">bezier curve</see>.
/// </summary>
public readonly struct CubicBezierEasingFunction : IEasingFunction
{
public readonly double X1;
public readonly double Y1;
public readonly double X2;
public readonly double Y2;
/// <param name="x1">x position of the first control point. Must be in 0-1 range</param>
/// <param name="y1">y position of the first control point</param>
/// <param name="x2">x position of the second control point. Must be in 0-1 range</param>
/// <param name="y2">y position of the second control point</param>
public CubicBezierEasingFunction(double x1, double y1, double x2, double y2)
{
if (Precision.DefinitelyBigger(0, x1) || Precision.DefinitelyBigger(x1, 1))
throw new ArgumentOutOfRangeException(nameof(x1), "Must be within [0, 1] range.");
if (Precision.DefinitelyBigger(0, x2) || Precision.DefinitelyBigger(x2, 1))
throw new ArgumentOutOfRangeException(nameof(x2), "Must be within [0, 1] range.");
X1 = x1;
Y1 = y1;
X2 = x2;
Y2 = y2;
}
/// <param name="p1">position of the first control point.</param>
/// <param name="p2">position of the second control point.</param>
/// <remarks>
/// The x psoition of both control points must be in 0-1 range.
/// </remarks>
public CubicBezierEasingFunction(Vector2 p1, Vector2 p2)
: this(p1.X, p1.Y, p2.X, p2.Y)
{
}
/// <summary>
/// Constructs an easing function with initializes <see cref="Y1"/> to 0 and <see cref="Y2"/> to 1, which will perfectly flatten out the curve on both ends.
/// </summary>
/// <param name="easeIn">ease-in strength, in 0-1 range</param>
/// <param name="easeOut">ease-out strength, in 0-1 range</param>
public CubicBezierEasingFunction(double easeIn, double easeOut)
: this(easeIn, 0, 1 - easeOut, 1)
{
}
private static double evaluateBezier(double t, double a1, double a2) => (((1 - 3 * a2 + 3 * a1) * t + (3 * a2 - 6 * a1)) * t + 3 * a1) * t;
private static double findTForX(double time, double x1, double x2)
{
double left = 0.0, right = 1.0, currentT = 0;
for (int i = 0; i < 100; i++)
{
currentT = left + (right - left) / 2;
double currentX = evaluateBezier(currentT, x1, x2) - time;
if (currentX > 0)
right = currentT;
else
left = currentT;
if (Math.Abs(currentX) <= 0.0000001)
break;
}
return currentT;
}
public double ApplyEasing(double time) => evaluateBezier(findTForX(time, X1, X2), Y1, Y2);
}
}

View File

@@ -103,20 +103,22 @@ namespace osu.Framework.Graphics.UserInterface
// discard control/special characters.
return false;
var currentNumberFormat = CultureInfo.CurrentCulture.NumberFormat;
switch (InputProperties.Type)
if (InputProperties.Type.IsNumerical())
{
case TextInputType.Decimal:
return char.IsAsciiDigit(character) || currentNumberFormat.NumberDecimalSeparator.Contains(character);
bool validNumericalCharacter = false;
case TextInputType.Number:
case TextInputType.NumericalPassword:
return char.IsAsciiDigit(character);
var currentNumberFormat = CultureInfo.CurrentCulture.NumberFormat;
default:
return true;
validNumericalCharacter |= char.IsAsciiDigit(character);
validNumericalCharacter |= selectionLeft == 0 && currentNumberFormat.NegativeSign.Contains(character);
if (InputProperties.Type == TextInputType.Decimal)
validNumericalCharacter |= currentNumberFormat.NumberDecimalSeparator.Contains(character);
return validNumericalCharacter;
}
return true;
}
private bool readOnly;

View File

@@ -223,12 +223,6 @@ namespace osu.Framework.Graphics.Veldrid
protected internal override Image<Rgba32> TakeScreenshot()
=> veldridDevice.TakeScreenshot();
public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer)
{
// TODO: Implement screen capture for Veldrid
// This is a placeholder implementation
}
protected internal override Image<Rgba32>? ExtractFrameBufferData(IFrameBuffer frameBuffer)
=> ExtractTexture((VeldridTexture)frameBuffer.Texture.NativeTexture);

View File

@@ -328,8 +328,8 @@ namespace osu.Framework.Input.Bindings
}
}
if (handled != null)
Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug);
// if (handled != null)
// Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug);
return handled;
}

View File

@@ -108,30 +108,6 @@ namespace osu.Framework.Input.Bindings
return ContainsAll(Keys, pressedKeys.Keys, matchingMode);
}
private static InputKey? getVirtualKey(InputKey key)
{
switch (key)
{
case InputKey.LShift:
case InputKey.RShift:
return InputKey.Shift;
case InputKey.LControl:
case InputKey.RControl:
return InputKey.Control;
case InputKey.LAlt:
case InputKey.RAlt:
return InputKey.Alt;
case InputKey.LSuper:
case InputKey.RSuper:
return InputKey.Super;
}
return null;
}
/// <summary>
/// Check whether the provided set of pressed keys matches the candidate binding.
/// </summary>
@@ -192,7 +168,7 @@ namespace osu.Framework.Input.Bindings
internal static bool KeyBindingContains(ImmutableArray<InputKey> candidateKeyBinding, InputKey physicalKey)
{
return candidateKeyBinding.Contains(physicalKey) ||
(getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey));
(physicalKey.GetVirtualKey() is InputKey vKey && candidateKeyBinding.Contains(vKey));
}
/// <summary>
@@ -210,7 +186,7 @@ namespace osu.Framework.Input.Bindings
foreach (var pk in pressedPhysicalKeys)
{
if (getVirtualKey(pk) == candidateKey)
if (pk.GetVirtualKey() == candidateKey)
return true;
}

View File

@@ -20,6 +20,6 @@ namespace osu.Framework.Input.Events
Repeat = repeat;
}
public override string ToString() => $"{GetType().ReadableName()}({Key}, {Repeat})";
// public override string ToString() => $"{GetType().ReadableName()}({Key}, {Repeat})";
}
}

View File

@@ -45,24 +45,20 @@ namespace osu.Framework.Input.Handlers.Pen
return true;
}
// Pen input is not necessarily direct on mobile platforms (specifically Android, where external tablets are supported),
// but until users experience issues with this, consider it "direct" for now.
private static readonly TabletPenDeviceType device_type = RuntimeInfo.IsMobile ? TabletPenDeviceType.Direct : TabletPenDeviceType.Unknown;
private void handlePenMove(Vector2 position, bool pressed)
private void handlePenMove(TabletPenDeviceType deviceType, Vector2 position, bool pressed)
{
if (pressed && device_type == TabletPenDeviceType.Direct)
if (pressed && deviceType == TabletPenDeviceType.Direct)
enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), true));
else
enqueueInput(new MousePositionAbsoluteInputFromPen { DeviceType = device_type, Position = position });
enqueueInput(new MousePositionAbsoluteInputFromPen { DeviceType = deviceType, Position = position });
}
private void handlePenTouch(bool pressed, Vector2 position)
private void handlePenTouch(TabletPenDeviceType deviceType, bool pressed, Vector2 position)
{
if (device_type == TabletPenDeviceType.Direct)
if (deviceType == TabletPenDeviceType.Direct)
enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), pressed));
else
enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = device_type });
enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = deviceType });
}
private void handlePenButton(TabletPenButton button, bool pressed)

View File

@@ -66,6 +66,20 @@ namespace osu.Framework.Input
}
}
public static bool IsNumerical(this TextInputType type)
{
switch (type)
{
case TextInputType.Number:
case TextInputType.NumericalPassword:
case TextInputType.Decimal:
return true;
default:
return false;
}
}
public static bool SupportsIme(this TextInputType type) => type == TextInputType.Name || type == TextInputType.Text;
}
}

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
@@ -24,6 +25,12 @@ namespace osu.Framework.Platform
/// </summary>
public Rectangle Bounds { get; }
/// <summary>
/// The current usable bounds of the display in screen space.
/// This is smaller and contained within <see cref="Bounds"/>.
/// </summary>
public Rectangle UsableBounds { get; }
/// <summary>
/// The available <see cref="DisplayMode"/>s on this display, or empty if the display mode cannot be configured (e.g. mobile displays).
/// </summary>
@@ -34,11 +41,13 @@ namespace osu.Framework.Platform
/// </summary>
public int Index { get; }
public Display(int index, string? name, Rectangle bounds, DisplayMode[] displayModes)
public Display(int index, string? name, Rectangle bounds, Rectangle usableBounds, DisplayMode[] displayModes)
{
Debug.Assert(bounds.Contains(usableBounds));
Index = index;
Name = name;
Bounds = bounds;
UsableBounds = usableBounds;
DisplayModes = displayModes;
}

View File

@@ -8,6 +8,7 @@ using System.Drawing;
using System.IO;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
namespace osu.Framework.Platform
@@ -146,6 +147,15 @@ namespace osu.Framework.Platform
/// </summary>
BindableSafeArea SafeAreaPadding { get; }
/// <summary>
/// The size of the window decoration and border, relative to <see cref="Size"/>.
/// </summary>
/// <remarks>
/// This may include the invisible resize border, even when maximised.
/// Usually 0 when in borderless or fullscreen.
/// </remarks>
IBindable<MarginPadding> BorderSize { get; }
/// <summary>
/// The <see cref="WindowMode"/>s supported by this <see cref="IWindow"/> implementation.
/// </summary>

View File

@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Extensions.ImageExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Threading;
using SixLabors.ImageSharp;
@@ -38,6 +39,10 @@ namespace osu.Framework.Platform.SDL2
public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea();
protected readonly Bindable<MarginPadding> BorderSize = new Bindable<MarginPadding>();
IBindable<MarginPadding> IWindow.BorderSize => BorderSize;
public virtual Point PointToClient(Point point) => point;
public virtual Point PointToScreen(Point point) => point;

View File

@@ -10,6 +10,7 @@ using System.Drawing;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osuTK;
using static SDL2.SDL;
@@ -354,6 +355,12 @@ namespace osu.Framework.Platform.SDL2
return false;
}
if (SDL_GetDisplayUsableBounds(displayIndex, out var usableBounds) < 0)
{
Logger.Log($"Failed to get usable display bounds for display at index ({displayIndex}). Assuming whole display is usable. SDL Error: {SDL_GetError()}");
usableBounds = rect;
}
DisplayMode[] displayModes = Array.Empty<DisplayMode>();
if (RuntimeInfo.IsDesktop)
@@ -379,7 +386,11 @@ namespace osu.Framework.Platform.SDL2
.ToArray();
}
display = new Display(displayIndex, SDL_GetDisplayName(displayIndex), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes);
display = new Display(displayIndex,
SDL_GetDisplayName(displayIndex),
new Rectangle(rect.x, rect.y, rect.w, rect.h),
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
displayModes);
return true;
}
@@ -590,6 +601,27 @@ namespace osu.Framework.Platform.SDL2
currentDisplay = Displays.ElementAtOrDefault(displayIndex) ?? PrimaryDisplay;
CurrentDisplayBindable.Value = currentDisplay;
}
if (tryGetBorderSize(out var borderSize))
BorderSize.Value = borderSize;
}
private bool tryGetBorderSize(out MarginPadding borderSize)
{
if (SDL_GetWindowBordersSize(SDLWindowHandle, out int top, out int left, out int bottom, out int right) < 0)
{
borderSize = default;
return false;
}
borderSize = new MarginPadding
{
Top = top,
Left = left,
Bottom = bottom,
Right = right
};
return true;
}
/// <summary>

View File

@@ -6,6 +6,7 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.StateChanges;
using osuTK.Input;
using SDL;
using static SDL.SDL3;
@@ -1048,6 +1049,21 @@ namespace osu.Framework.Platform.SDL3
return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex);
}
public static TabletPenDeviceType ToTabletPenDeviceType(this SDL_PenDeviceType type)
{
switch (type)
{
case SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_DIRECT:
return TabletPenDeviceType.Direct;
case SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_INDIRECT:
return TabletPenDeviceType.Indirect;
default:
return TabletPenDeviceType.Unknown;
}
}
public static string ReadableName(this SDL_LogCategory category)
{
switch (category)

View File

@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Extensions.ImageExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Threading;
using SDL;
@@ -40,6 +41,10 @@ namespace osu.Framework.Platform.SDL3
public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea();
protected readonly Bindable<MarginPadding> BorderSize = new Bindable<MarginPadding>();
IBindable<MarginPadding> IWindow.BorderSize => BorderSize;
public virtual Point PointToClient(Point point) => point;
public virtual Point PointToScreen(Point point) => point;

View File

@@ -11,6 +11,7 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.States;
using osu.Framework.Logging;
using osuTK;
@@ -530,14 +531,16 @@ namespace osu.Framework.Platform.SDL3
private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();
private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ToTabletPenDeviceType();
private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
{
PenMove?.Invoke(new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN));
PenMove?.Invoke(getPenType(evtPenMotion.which), new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN));
}
private void handlePenTouchEvent(SDL_PenTouchEvent evtPenTouch)
{
PenTouch?.Invoke(evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale);
PenTouch?.Invoke(getPenType(evtPenTouch.which), evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale);
}
/// <summary>
@@ -745,13 +748,13 @@ namespace osu.Framework.Platform.SDL3
/// <summary>
/// Invoked when a pen moves. Passes pen position and whether the pen is touching the tablet surface.
/// </summary>
public event Action<Vector2, bool>? PenMove;
public event Action<TabletPenDeviceType, Vector2, bool>? PenMove;
/// <summary>
/// Invoked when a pen touches (<c>true</c>) or lifts (<c>false</c>) from the tablet surface.
/// Also passes the current position of the pen.
/// </summary>
public event Action<bool, Vector2>? PenTouch;
public event Action<TabletPenDeviceType, bool, Vector2>? PenTouch;
/// <summary>
/// Invoked when a <see cref="TabletPenButton">pen button</see> is pressed (<c>true</c>) or released (<c>false</c>).

View File

@@ -10,6 +10,7 @@ using System.Drawing;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osuTK;
using SDL;
@@ -360,6 +361,14 @@ namespace osu.Framework.Platform.SDL3
return false;
}
SDL_Rect usableBounds;
if (!SDL_GetDisplayUsableBounds(displayID, &usableBounds))
{
Logger.Log($"Failed to get usable display bounds for display at index ({displayIndex}). Assuming whole display is usable. SDL Error: {SDL_GetError()}");
usableBounds = rect;
}
DisplayMode[] displayModes = Array.Empty<DisplayMode>();
if (RuntimeInfo.IsDesktop)
@@ -382,7 +391,11 @@ namespace osu.Framework.Platform.SDL3
displayModes[i] = modes[i].ToDisplayMode(displayIndex);
}
display = new Display(displayIndex, SDL_GetDisplayName(displayID), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes);
display = new Display(displayIndex,
SDL_GetDisplayName(displayID),
new Rectangle(rect.x, rect.y, rect.w, rect.h),
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
displayModes);
return true;
}
@@ -605,6 +618,29 @@ namespace osu.Framework.Platform.SDL3
CurrentDisplayBindable.Value = currentDisplay;
}
if (tryGetBorderSize(out var borderSize))
BorderSize.Value = borderSize;
}
private unsafe bool tryGetBorderSize(out MarginPadding borderSize)
{
int top, left, bottom, right;
if (!SDL_GetWindowBordersSize(SDLWindowHandle, &top, &left, &bottom, &right))
{
borderSize = default;
return false;
}
borderSize = new MarginPadding
{
Top = top,
Left = left,
Bottom = bottom,
Right = right
};
return true;
}
private static bool tryGetDisplayIndex(SDL_DisplayID id, out int index)

View File

@@ -1,36 +0,0 @@
#ifndef ACRYLIC_BLUR_VS
#define ACRYLIC_BLUR_VS
#include "sh_Utils.h"
layout(location = 0) in highp vec2 m_Position;
layout(location = 1) in lowp vec4 m_Colour;
layout(location = 2) in highp vec2 m_TexCoord;
layout(location = 3) in highp vec4 m_TexRect;
layout(location = 4) in mediump vec2 m_BlendRange;
layout(location = 5) in highp float m_BackbufferDrawDepth;
layout(location = 0) out highp vec2 v_MaskingPosition;
layout(location = 1) out lowp vec4 v_Colour;
layout(location = 2) out highp vec2 v_TexCoord;
layout(location = 3) out highp vec4 v_TexRect;
layout(location = 4) out mediump vec2 v_BlendRange;
void main(void)
{
// Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour;
v_TexCoord = m_TexCoord;
v_TexRect = m_TexRect;
v_BlendRange = m_BlendRange;
gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
if (g_BackbufferDraw)
gl_Position.z = m_BackbufferDrawDepth;
}
#endif

View File

@@ -0,0 +1,37 @@
#ifndef PATH_FS
#define PATH_FS
#include "sh_Utils.h"
#include "sh_Masking.h"
#include "sh_TextureWrapping.h"
layout(location = 2) in mediump vec2 v_TexCoord;
// FrameBuffer texture
layout(set = 0, binding = 0) uniform lowp texture2D m_Texture;
layout(set = 0, binding = 1) uniform lowp sampler m_Sampler;
// Path texture
layout(set = 1, binding = 0) uniform lowp texture2D m_Texture1;
layout(set = 1, binding = 1) uniform lowp sampler m_Sampler1;
layout(std140, set = 2, binding = 0) uniform m_PathTextureParameters
{
highp vec4 TexRect1;
};
layout(location = 0) out vec4 o_Colour;
void main(void)
{
lowp vec4 frameBuffer = texture(sampler2D(m_Texture, m_Sampler), v_TexCoord, -0.9);
if (frameBuffer.r == 0.0)
{
o_Colour = vec4(0.0);
return;
}
lowp vec4 pathCol = texture(sampler2D(m_Texture1, m_Sampler1), TexRect1.xy + vec2(frameBuffer.r, 0.0) * TexRect1.zw, -0.9);
o_Colour = getRoundedColor(vec4(pathCol.rgb, pathCol.a * frameBuffer.a), v_TexCoord);
}
#endif

View File

@@ -0,0 +1,14 @@
#ifndef PATH_PREPASS_FS
#define PATH_PREPASS_FS
layout(location = 0) in highp vec2 v_RelativePos;
layout(location = 0) out vec4 o_Colour;
void main(void)
{
highp float dst = clamp(distance(v_RelativePos, vec2(0.0)), 0.0, 1.0);
o_Colour = vec4(vec3(1.0 - dst), float(dst < 1.0));
}
#endif

View File

@@ -0,0 +1,17 @@
#ifndef PATH_PREPASS_VS
#define PATH_PREPASS_VS
#include "sh_Utils.h"
layout(location = 0) in highp vec2 m_Position;
layout(location = 1) in highp vec2 m_RelativePos;
layout(location = 0) out highp vec2 v_RelativePos;
void main(void)
{
v_RelativePos = m_RelativePos;
gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
}
#endif

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
@@ -38,8 +38,8 @@
<PackageReference Include="ppy.osuTK.NS20" Version="1.0.211" />
<PackageReference Include="StbiSharp" Version="1.1.0" />
<PackageReference Include="ppy.SDL2-CS" Version="1.0.741-alpha" />
<PackageReference Include="ppy.SDL3-CS" Version="2025.920.0" />
<PackageReference Include="ppy.osu.Framework.SourceGeneration" Version="2024.1128.0" />
<PackageReference Include="ppy.SDL3-CS" Version="2025.1205.0" />
<PackageReference Include="ppy.osu.Framework.SourceGeneration" Version="2025.1121.1" />
<!-- DO NOT use ProjectReference for native packaging project.
See https://github.com/NuGet/Home/issues/4514 and https://github.com/dotnet/sdk/issues/765 . -->