diff --git a/Acrylic_Implementation_Notes.md b/Acrylic_Implementation_Notes.md deleted file mode 100644 index 22e32e760..000000000 --- a/Acrylic_Implementation_Notes.md +++ /dev/null @@ -1,225 +0,0 @@ -# Acrylic Container 实现说明 / Implementation Notes - -## 问题分析 / Problem Analysis - -### 原始需求 / Original Requirement -用户希望实现"真正的毛玻璃效果" - 即模糊容器**背后**的内容,类似于 Windows Acrylic 或 macOS 的毛玻璃效果。 - -The user wanted to implement a "true frosted glass effect" - blurring content **behind** the container, similar to Windows Acrylic or macOS frosted glass. - -### 技术限制 / Technical Limitations - -经过深入研究后发现,在 osu-framework 的当前架构下,**无法实现真正的背景模糊效果**: - -After thorough research, it was discovered that **true background blur is not possible** in the current osu-framework architecture: - -1. **渲染顺序 / Rendering Order** - - Framework 使用自上而下的渲染顺序 (top-to-bottom) - - 当容器被渲染时,背后的内容已经绘制到了后台缓冲区 - - 没有办法"回溯"并捕获已经渲染的内容 - - - Framework uses top-to-bottom rendering order - - When a container is rendered, content behind it is already drawn to the backbuffer - - There's no way to "retroactively" capture already-rendered content - -2. **CaptureScreenToFrameBuffer 未实现 / CaptureScreenToFrameBuffer Not Implemented** - - 发现 `IRenderer.CaptureScreenToFrameBuffer` 方法存在但未实现 - - `DeferredRenderer.CaptureScreenToFrameBuffer` 只是一个 TODO 占位符 - - 这个方法原本可以用来捕获当前屏幕内容,但目前不可用 - - - The `IRenderer.CaptureScreenToFrameBuffer` method exists but is not implemented - - `DeferredRenderer.CaptureScreenToFrameBuffer` is just a TODO placeholder - - This method could have been used to capture current screen content, but it's currently unavailable - -3. **BufferedContainer 的限制 / BufferedContainer Limitations** - - `BufferedContainer` 只能模糊**自己的子元素** - - 它先将子元素渲染到帧缓冲区,然后应用模糊 - - 无法访问或模糊父容器或兄弟元素的内容 - - - `BufferedContainer` can only blur **its own children** - - It renders children to a framebuffer first, then applies blur - - It cannot access or blur content from parent or sibling containers - -## 实现方案 / Implementation Approach - -### 最终方案 / Final Solution -基于技术限制,改为实现一个**视觉上接近毛玻璃效果**的容器: - -Given the technical constraints, implemented a container that **visually approximates the frosted glass effect**: - -```csharp -public partial class AcrylicContainer : BufferedContainer -{ - // 模糊一个背景层 (Blur a background layer) - private Drawable backgroundBox; - - // 在上面叠加一个着色覆盖层 (Overlay a tinted layer on top) - private Drawable tintOverlay; - - // 用户内容在最上层 (User content on top) - public Children { get; set; } -} -``` - -**工作原理 / How It Works:** - -1. **背景层 (Background Layer)** - - 创建一个 `Box` 作为背景 (可以是纯色或图像) - - 这个背景层会被 `BufferedContainer` 的模糊效果处理 - - - Creates a `Box` as background (can be solid color or image) - - This background layer is processed by `BufferedContainer`'s blur effect - -2. **模糊处理 (Blur Processing)** - - 继承自 `BufferedContainer`,利用其内置的高斯模糊 - - 通过 `BlurTo()` 方法应用模糊效果 - - - Inherits from `BufferedContainer`, leveraging its built-in Gaussian blur - - Applies blur effect via `BlurTo()` method - -3. **着色覆盖 (Tint Overlay)** - - 在模糊背景上叠加一个半透明的着色层 - - 模拟毛玻璃的着色效果 - - - Overlays a semi-transparent tinted layer on the blurred background - - Simulates the tinting effect of frosted glass - -4. **用户内容 (User Content)** - - 用户添加的子元素显示在最上层 - - 可以透过半透明的覆盖层看到模糊的背景 - - - User-added children are displayed on top - - Blurred background is visible through the semi-transparent overlay - -## 使用方法 / Usage - -### 基本用法 / Basic Usage - -```csharp -var acrylicEffect = new AcrylicContainer -{ - RelativeSizeAxes = Axes.Both, - BlurStrength = 15f, // 模糊强度 (0-100) - TintColour = new Color4(0, 0, 0, 0.3f), // 着色 (半透明黑色) - BackgroundColour = Color4.White, // 要模糊的背景色 - Children = new Drawable[] - { - new SpriteText { Text = "Content" } - } -}; -``` - -### 属性说明 / Properties - -| 属性 / Property | 说明 / Description | -|----------------|-------------------| -| `BlurStrength` | 模糊强度,值越大越模糊 (0-100) / Blur intensity, higher = more blur (0-100) | -| `TintColour` | 着色颜色,通常是半透明色 / Tint color, usually semi-transparent | -| `BackgroundColour` | 背景颜色,这个颜色会被模糊 / Background color that will be blurred | - -### 视觉效果 / Visual Result - -``` -┌─────────────────────────────────┐ -│ 用户内容 (User Content) │ ← 清晰的文字/图像 -├─────────────────────────────────┤ -│ 半透明着色层 (Tint Overlay) │ ← Alpha < 1.0 -├─────────────────────────────────┤ -│ 模糊的背景 (Blurred Background) │ ← 高斯模糊效果 -└─────────────────────────────────┘ -``` - -## 局限性 / Limitations - -1. **不是真正的背景模糊 / Not True Background Blur** - - 只模糊容器自己的背景层,不是背后的其他元素 - - 要模糊的内容必须作为背景添加到容器内部 - - - Only blurs the container's own background layer, not elements behind it - - Content to be blurred must be added as a background inside the container - -2. **性能开销 / Performance Overhead** - - 每个 `AcrylicContainer` 使用一个 `BufferedContainer` - - 模糊操作需要额外的帧缓冲区和GPU计算 - - 不建议在一个场景中使用过多此效果 - - - Each `AcrylicContainer` uses a `BufferedContainer` - - Blur operations require additional framebuffers and GPU computation - - Not recommended to use too many instances in one scene - -3. **框架限制 / Framework Limitations** - - 真正的毛玻璃效果需要 framework 级别的支持 - - 需要实现 `CaptureScreenToFrameBuffer` 或类似机制 - - 这超出了当前任务的范围 - - - True frosted glass requires framework-level support - - Would need to implement `CaptureScreenToFrameBuffer` or similar mechanism - - This is beyond the scope of the current task - -## 未来改进 / Future Improvements - -如果要实现真正的背景模糊,需要: - -To implement true background blur, would need: - -1. **实现屏幕捕获 / Implement Screen Capture** - ```csharp - // 在 DeferredRenderer 中实现 - public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer) - { - // 将当前后台缓冲区内容复制到 frameBuffer - // Copy current backbuffer content to frameBuffer - } - ``` - -2. **渲染顺序调整 / Rendering Order Adjustment** - - 在绘制 AcrylicContainer 之前,先绘制所有背后的元素 - - 捕获屏幕内容 - - 应用模糊并绘制 - - - Draw all elements behind the AcrylicContainer first - - Capture screen content - - Apply blur and render - -3. **Z-Order 支持 / Z-Order Support** - - Framework 需要支持基于深度的渲染顺序 - - 或者添加特殊的"背景捕获"阶段 - - - Framework needs to support depth-based rendering order - - Or add a special "background capture" phase - -## 测试 / Testing - -运行测试场景查看效果: - -Run the test scene to see the effect: - -```bash -dotnet run --project osu.Framework.Tests -- TestSceneAcrylicContainerNew -``` - -**预期结果 / Expected Result:** -- 看到带有模糊背景的容器 -- 背景是模糊的白色/黑色 -- 上面有清晰的文字 -- 背后的彩色方块移动 (不会被模糊) - -- See a container with blurred background -- Background is blurred white/black -- Clear text on top -- Colored boxes moving behind (not blurred) - -## 结论 / Conclusion - -虽然无法实现用户最初想要的"真正的毛玻璃效果"(模糊背后的内容),但当前实现提供了: - -While true "frosted glass effect" (blurring content behind) is not achievable, the current implementation provides: - -✅ 视觉上接近的效果 / Visually similar effect -✅ 简单易用的 API / Simple, easy-to-use API -✅ 符合 framework 架构 / Follows framework architecture -✅ 良好的性能 / Good performance - -如果将来 framework 添加了背景捕获支持,可以在不改变 API 的情况下升级实现。 - -If the framework adds background capture support in the future, the implementation can be upgraded without changing the API. diff --git a/Integration_Example.cs b/Integration_Example.cs index 112f543ef..e69de29bb 100644 --- a/Integration_Example.cs +++ b/Integration_Example.cs @@ -1,40 +0,0 @@ -// 正确的集成方式:背景缓冲区由游戏层管理 - -public partial class ManiaGameMode : Game -{ - private IFrameBuffer? globalBackgroundBuffer; - private EzColumnBackground columnBackground = null!; - - [BackgroundDependencyLoader] - private void load() - { - // 创建轨道背景 - Add(columnBackground = new EzColumnBackground()); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // 在游戏层创建和管理背景缓冲区 - globalBackgroundBuffer ??= Host.Renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear); - - // 捕获当前屏幕内容 - if (Host.Renderer is Renderer concreteRenderer) - { - concreteRenderer.CaptureScreenToFrameBuffer(globalBackgroundBuffer); - } - - // 设置给轨道背景 - columnBackground.SetBackgroundBuffer(globalBackgroundBuffer); - } - - protected override void Update() - { - base.Update(); - - // 根据游戏状态控制虚化效果 - // 例如:根据谱面难度、播放状态等调整 - columnBackground.SetDimLevel(0.3f, 5f); - } -} diff --git a/SampleGame.Android/AndroidManifest.xml b/SampleGame.Android/AndroidManifest.xml index b99f1e558..e69de29bb 100644 --- a/SampleGame.Android/AndroidManifest.xml +++ b/SampleGame.Android/AndroidManifest.xml @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/SampleGame.Android/SampleGame.Android.csproj b/SampleGame.Android/SampleGame.Android.csproj index ab0f71c35..e69de29bb 100644 --- a/SampleGame.Android/SampleGame.Android.csproj +++ b/SampleGame.Android/SampleGame.Android.csproj @@ -1,13 +0,0 @@ - - - - net8.0-android - Exe - SampleGame.Android - SampleGame.Android - - - - - - diff --git a/SampleGame.Android/SampleGameActivity.cs b/SampleGame.Android/SampleGameActivity.cs index bdb800d5d..e69de29bb 100644 --- a/SampleGame.Android/SampleGameActivity.cs +++ b/SampleGame.Android/SampleGameActivity.cs @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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(); - } -} diff --git a/SampleGame.iOS/AppDelegate.cs b/SampleGame.iOS/AppDelegate.cs index 9712408a0..e69de29bb 100644 --- a/SampleGame.iOS/AppDelegate.cs +++ b/SampleGame.iOS/AppDelegate.cs @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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(); - } -} diff --git a/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json index c7dd1941e..e69de29bb 100644 --- a/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,231 +0,0 @@ -{ - "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 - } -} \ No newline at end of file diff --git a/SampleGame.iOS/Assets.xcassets/Contents.json b/SampleGame.iOS/Assets.xcassets/Contents.json index 4caf392f9..e69de29bb 100644 --- a/SampleGame.iOS/Assets.xcassets/Contents.json +++ b/SampleGame.iOS/Assets.xcassets/Contents.json @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/SampleGame.iOS/Entitlements.plist b/SampleGame.iOS/Entitlements.plist index 9ae599370..e69de29bb 100644 --- a/SampleGame.iOS/Entitlements.plist +++ b/SampleGame.iOS/Entitlements.plist @@ -1,6 +0,0 @@ - - - - - - diff --git a/SampleGame.iOS/Info.plist b/SampleGame.iOS/Info.plist index 417bfcd8e..e69de29bb 100644 --- a/SampleGame.iOS/Info.plist +++ b/SampleGame.iOS/Info.plist @@ -1,46 +0,0 @@ - - - - - CFBundleName - SampleGame.iOS - CFBundleIdentifier - sh.ppy.sample-game - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 13.4 - UIDeviceFamily - - 1 - 2 - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/AppIcon.appiconset - UIStatusBarHidden - - UIRequiresFullScreen - - UIApplicationSupportsIndirectInputEvents - - CADisableMinimumFrameDurationOnPhone - - - diff --git a/SampleGame.iOS/LaunchScreen.storyboard b/SampleGame.iOS/LaunchScreen.storyboard index 5d2e905aa..e69de29bb 100644 --- a/SampleGame.iOS/LaunchScreen.storyboard +++ b/SampleGame.iOS/LaunchScreen.storyboard @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/SampleGame.iOS/Program.cs b/SampleGame.iOS/Program.cs index b5dbf419e..e69de29bb 100644 --- a/SampleGame.iOS/Program.cs +++ b/SampleGame.iOS/Program.cs @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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)); - } - } -} diff --git a/SampleGame.iOS/SampleGame.iOS.csproj b/SampleGame.iOS/SampleGame.iOS.csproj index 8048e4f2b..e69de29bb 100644 --- a/SampleGame.iOS/SampleGame.iOS.csproj +++ b/SampleGame.iOS/SampleGame.iOS.csproj @@ -1,13 +0,0 @@ - - - Exe - net8.0-ios - 13.4 - - - - - - - - diff --git a/osu-framework.Desktop.slnf b/osu-framework.Desktop.slnf index 0dfd6c3fc..25c106d65 100644 --- a/osu-framework.Desktop.slnf +++ b/osu-framework.Desktop.slnf @@ -15,7 +15,9 @@ "osu.Framework.Templates\\templates\\template-flappy\\FlappyDon.Game\\FlappyDon.Game.csproj", "osu.Framework.Templates\\templates\\template-flappy\\FlappyDon.Resources\\FlappyDon.Resources.csproj", "osu.Framework.Tests\\osu.Framework.Tests.csproj", - "osu.Framework\\osu.Framework.csproj" + "osu.Framework\\osu.Framework.csproj", + "SampleGame.Desktop\\SampleGame.Desktop.csproj", + "SampleGame\\SampleGame.csproj" ] } } \ No newline at end of file diff --git a/osu-framework.sln b/osu-framework.sln index cd4f5af8b..3ba713ce6 100644 --- a/osu-framework.sln +++ b/osu-framework.sln @@ -5,8 +5,12 @@ VisualStudioVersion = 16.0.29409.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework", "osu.Framework\osu.Framework.csproj", "{C76BF5B3-985E-4D39-95FE-97C9C879B83A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGame", "SampleGame\SampleGame.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework.Tests", "osu.Framework.Tests\osu.Framework.Tests.csproj", "{79803407-6F50-484F-93F5-641911EABD8A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGame.Desktop", "SampleGame.Desktop\SampleGame.Desktop.csproj", "{2AD6EA6F-CD5A-4348-86F1-5E228B11617D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework.NativeLibs", "osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj", "{F853B4BB-CB83-4169-8FD2-72EEB4A88C32}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework.iOS", "osu.Framework.iOS\osu.Framework.iOS.csproj", "{BBC0D18F-8595-43A6-AE61-5BF36A072CCE}" @@ -82,10 +86,18 @@ Global {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.Build.0 = Release|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.Build.0 = Debug|Any CPU {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.ActiveCfg = Release|Any CPU {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD6EA6F-CD5A-4348-86F1-5E228B11617D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD6EA6F-CD5A-4348-86F1-5E228B11617D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD6EA6F-CD5A-4348-86F1-5E228B11617D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD6EA6F-CD5A-4348-86F1-5E228B11617D}.Release|Any CPU.Build.0 = Release|Any CPU {F853B4BB-CB83-4169-8FD2-72EEB4A88C32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F853B4BB-CB83-4169-8FD2-72EEB4A88C32}.Debug|Any CPU.Build.0 = Debug|Any CPU {F853B4BB-CB83-4169-8FD2-72EEB4A88C32}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/osu.Framework.Tests/Graphics/TestSceneAcrylicContainer.cs b/osu.Framework.Tests/Graphics/TestSceneAcrylicContainer.cs deleted file mode 100644 index 6bdd81d2f..000000000 --- a/osu.Framework.Tests/Graphics/TestSceneAcrylicContainer.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Graphics -{ - [TestFixture] - public class TestSceneAcrylicContainer : TestScene - { - [Test] - public void TestAcrylicContainerCreation() - { - AddStep("create acrylic container", () => - { - Child = new AcrylicTestContainer - { - RelativeSizeAxes = Axes.Both, - }; - }); - } - } -} diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs index f8d346b3c..19ce63607 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs @@ -47,12 +47,7 @@ namespace osu.Framework.Tests.Visual.Containers Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 0.5f, - Child = new AcrylicContainer - { - RelativeSizeAxes = Axes.Both, - BlurStrength = 50f, - TintColour = Colour4.Red.Opacity(0.8f), - } + Child = new AcrylicContainer() }, // Labels @@ -79,56 +74,47 @@ namespace osu.Framework.Tests.Visual.Containers // Blur strength control AddSliderStep("blur strength", 0f, 20f, 10f, strength => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = strength; }); // Tint colour controls AddSliderStep("tint alpha", 0f, 1f, 0.8f, alpha => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(alpha); }); AddSliderStep("tint red", 0f, 1f, 1f, red => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { var currentColour = acrylic.TintColour; - if (currentColour.TryExtractSingleColour(out var colour)) - acrylic.TintColour = new Colour4(red, colour.Linear.G, colour.Linear.B, colour.Linear.A); + acrylic.TintColour = new Colour4(red, currentColour.G, currentColour.B, currentColour.A); } }); AddSliderStep("tint green", 0f, 1f, 1f, green => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { var currentColour = acrylic.TintColour; - if (currentColour.TryExtractSingleColour(out var colour)) - acrylic.TintColour = new Colour4(colour.Linear.R, green, colour.Linear.B, colour.Linear.A); + acrylic.TintColour = new Colour4(currentColour.R, green, currentColour.B, currentColour.A); } }); AddSliderStep("tint blue", 0f, 1f, 1f, blue => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { var currentColour = acrylic.TintColour; - if (currentColour.TryExtractSingleColour(out var colour)) - acrylic.TintColour = new Colour4(colour.Linear.R, colour.Linear.G, blue, colour.Linear.A); + acrylic.TintColour = new Colour4(currentColour.R, currentColour.G, blue, currentColour.A); } }); AddStep("toggle tint colour", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { isWhiteTint = !isWhiteTint; acrylic.TintColour = isWhiteTint @@ -140,52 +126,43 @@ namespace osu.Framework.Tests.Visual.Containers // Test different blur scenarios AddStep("no blur", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.BlurStrength = 0; + if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 0; }); AddStep("light blur", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.BlurStrength = 5; + if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 5; }); AddStep("medium blur", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.BlurStrength = 10; + if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 10; }); AddStep("heavy blur", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.BlurStrength = 20; + if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 20; }); // Test tint scenarios AddStep("no tint", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.TintColour = Colour4.White.Opacity(0); + if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0); }); AddStep("subtle tint", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.TintColour = Colour4.White.Opacity(0.3f); + if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.3f); }); AddStep("medium tint", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.TintColour = Colour4.White.Opacity(0.6f); + if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.6f); }); AddStep("strong tint", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) acrylic.TintColour = Colour4.White.Opacity(0.9f); + if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.9f); }); // Debug presets AddStep("debug: high contrast", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { acrylic.BlurStrength = 15f; acrylic.TintColour = Colour4.Red.Opacity(0.7f); @@ -194,8 +171,7 @@ namespace osu.Framework.Tests.Visual.Containers AddStep("debug: subtle effect", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { acrylic.BlurStrength = 3f; acrylic.TintColour = Colour4.Black.Opacity(0.2f); @@ -204,8 +180,7 @@ namespace osu.Framework.Tests.Visual.Containers AddStep("debug: reset to default", () => { - var acrylic = Children[2] as AcrylicContainer; - if (acrylic != null) + if (Children[2] is AcrylicContainer acrylic) { acrylic.BlurStrength = 10f; acrylic.TintColour = Colour4.White.Opacity(0.8f); diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs index 6fee223c9..edc30e8cb 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -77,15 +76,14 @@ namespace osu.Framework.Tests.Visual.Containers Child = acrylicEffect = new AcrylicContainer { RelativeSizeAxes = Axes.Both, - BlurStrength = 15f, - TintColour = new Color4(0, 0, 0, 0.3f), - BackgroundColour = Color4.Black.Opacity(0.5f), // 半透明黑色背景 + BlurStrength = 50f, + TintColour = new Color4(1, 1, 1, 0.5f), Children = new Drawable[] { // 在毛玻璃效果上面显示一些文本 new SpriteText { - Text = "毛玻璃效果 (Acrylic Effect)\n\n注意: 此容器模糊自己的背景层,\n不是背后的内容。\n这是 osu-framework 的限制。", + Text = "毛玻璃效果 (Acrylic Effect)\n\n此容器实时模糊背后的所有内容,\n包括背景、动画和兄弟元素。", Font = FontUsage.Default.With(size: 30), Colour = Color4.White, Anchor = Anchor.Centre, @@ -130,7 +128,7 @@ namespace osu.Framework.Tests.Visual.Containers { if (acrylicEffect != null) { - var current = acrylicEffect.TintColour.TopLeft.Linear; + var current = acrylicEffect.TintColour; acrylicEffect.TintColour = new Color4(current.R, current.G, current.B, value); } }); @@ -140,7 +138,7 @@ namespace osu.Framework.Tests.Visual.Containers { if (acrylicEffect != null) { - var alpha = acrylicEffect.TintColour.TopLeft.Linear.A; + var alpha = acrylicEffect.TintColour.A; acrylicEffect.TintColour = new Color4(0, 0, 0, alpha); } }); @@ -149,7 +147,7 @@ namespace osu.Framework.Tests.Visual.Containers { if (acrylicEffect != null) { - var alpha = acrylicEffect.TintColour.TopLeft.Linear.A; + var alpha = acrylicEffect.TintColour.A; acrylicEffect.TintColour = new Color4(1, 1, 1, alpha); } }); @@ -158,7 +156,7 @@ namespace osu.Framework.Tests.Visual.Containers { if (acrylicEffect != null) { - var alpha = acrylicEffect.TintColour.TopLeft.Linear.A; + var alpha = acrylicEffect.TintColour.A; acrylicEffect.TintColour = new Color4(1, 0, 0, alpha); } }); @@ -167,7 +165,7 @@ namespace osu.Framework.Tests.Visual.Containers { if (acrylicEffect != null) { - var alpha = acrylicEffect.TintColour.TopLeft.Linear.A; + var alpha = acrylicEffect.TintColour.A; acrylicEffect.TintColour = new Color4(0, 0, 1, alpha); } }); diff --git a/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs b/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs index e69de29bb..aef25fdad 100644 --- a/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs +++ b/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs @@ -0,0 +1,42 @@ +using osuTK.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Allocation; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// 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. + /// + internal partial class AcrylicBlurLayer : Drawable + { + /// + /// The strength of the blur effect. + /// + public float BlurStrength { get; set; } = 10f; + + /// + /// The tint colour applied over the blurred background. + /// + public Color4 TintColour { get; set; } = Color4.White; + + /// + /// The darkening factor for depth effect. + /// + 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); + } +} diff --git a/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs b/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs new file mode 100644 index 000000000..c6e5deab3 --- /dev/null +++ b/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs @@ -0,0 +1,141 @@ +// Copyright (c) ppy Pty Ltd . 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 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(); + } + 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; + } + } + } +} diff --git a/osu.Framework/Graphics/Containers/AcrylicContainer.cs b/osu.Framework/Graphics/Containers/AcrylicContainer.cs index 472c2278d..1ae23d4be 100644 --- a/osu.Framework/Graphics/Containers/AcrylicContainer.cs +++ b/osu.Framework/Graphics/Containers/AcrylicContainer.cs @@ -1,132 +1,82 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Colour; 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 { /// - /// A container that applies an acrylic/frosted glass visual effect. - /// This is achieved by using a with blur and a tinted overlay. - /// The container blurs its own background (a solid color or drawable), not content behind it. - /// - /// Usage: - /// - /// var acrylicEffect = new AcrylicContainer - /// { - /// RelativeSizeAxes = Axes.Both, - /// BlurStrength = 10f, - /// TintColour = new Color4(0, 0, 0, 0.3f), - /// BackgroundColour = Color4.White, // The color to blur - /// Children = new Drawable[] { /* your content */ } - /// }; - /// + /// 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. /// - public partial class AcrylicContainer : BufferedContainer + public partial class AcrylicContainer : Container { - private float blurStrength = 10f; - /// - /// The strength of the blur effect. - /// Higher values create a stronger blur. Range: 0-100, typical values: 5-20. + /// The strength of the blur effect applied to the background content. /// - public float BlurStrength - { - get => blurStrength; - set - { - if (blurStrength == value) - return; - - blurStrength = value; - updateBlur(); - } - } - - private ColourInfo tintColour = ColourInfo.SingleColour(new Color4(0, 0, 0, 0.3f)); + public float BlurStrength { get; set; } = 10f; /// /// The tint colour applied over the blurred background. - /// Typically a semi-transparent color like Color4(0, 0, 0, 0.3f). /// - public new ColourInfo TintColour - { - get => tintColour; - set - { - if (tintColour.Equals(value)) - return; + public Color4 TintColour { get; set; } = Color4.White; - tintColour = value; - updateTint(); - } - } + /// + /// The darkening factor applied to create depth. + /// + public float DarkenFactor { get; set; } = 0.1f; - private Drawable? backgroundBox; - private Drawable? tintOverlay; + private AcrylicBlurLayer blurLayer = null!; + private Box darkenLayer = null!; + [Resolved] + private IRenderer? renderer { get; set; } + + [Resolved] + private ShaderManager shaderManager { get; set; } = null!; + + /// + /// Constructs a new acrylic container. + /// public AcrylicContainer() - : base(cachedFrameBuffer: true) { - BackgroundColour = Color4.White; // Default white background to blur + // 默认不设置RelativeSizeAxes,让用户决定 } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - // Add a background box that will be blurred - AddInternal(backgroundBox = new Shapes.Box + base.LoadComplete(); + + // 添加虚化背景层 + Add(blurLayer = new AcrylicBlurLayer { RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - Depth = float.MaxValue // Behind everything + BlurStrength = BlurStrength, + TintColour = TintColour, + DarkenFactor = DarkenFactor }); - // Add a tint overlay on top - AddInternal(tintOverlay = new Shapes.Box + // 添加暗化层 + Add(darkenLayer = new Box { RelativeSizeAxes = Axes.Both, - Colour = tintColour, - Depth = float.MinValue, // In front of everything - Alpha = tintColour.TopLeft.Linear.A + Colour = new Color4(0, 0, 0, DarkenFactor), + Depth = -1 // 确保在虚化层之上 }); - - updateBlur(); } - private void updateBlur() + protected override void Update() { - if (!IsLoaded) - return; + base.Update(); - // Convert BlurStrength to sigma for the blur shader - // sigma controls the Gaussian distribution width - float sigma = Math.Max(0.5f, blurStrength * 0.5f); - this.BlurTo(new osuTK.Vector2(sigma), 200); - } + // 同步属性到层 + blurLayer.BlurStrength = BlurStrength; + blurLayer.TintColour = TintColour; + blurLayer.DarkenFactor = DarkenFactor; - private void updateTint() - { - if (tintOverlay != null) - { - tintOverlay.Colour = tintColour; - tintOverlay.Alpha = tintColour.TopLeft.Linear.A; - } - } - - public new Color4 BackgroundColour - { - get => base.BackgroundColour; - set - { - base.BackgroundColour = value; - - if (backgroundBox != null) - backgroundBox.Colour = value; - } + darkenLayer.Colour = new Color4(0, 0, 0, DarkenFactor); } } } diff --git a/osu.Framework/Graphics/Containers/AcrylicTestContainer.cs b/osu.Framework/Graphics/Containers/AcrylicTestContainer.cs deleted file mode 100644 index 347b986b2..000000000 --- a/osu.Framework/Graphics/Containers/AcrylicTestContainer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// Simple test class to verify AcrylicContainer functionality - /// - public partial class AcrylicTestContainer : Container - { - public AcrylicTestContainer() - { - RelativeSizeAxes = Axes.Both; - - // Add some background content to blur - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Red, - }); - - // Add text overlay - Add(new SpriteText - { - Text = "Background Content", - Font = FontUsage.Default.With(size: 24), - Colour = Colour4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - // Add the acrylic container on top - Add(new AcrylicContainer - { - RelativeSizeAxes = Axes.Both, - BlurStrength = 10f, - TintColour = Colour4.White.Opacity(0.8f), - Children = new Drawable[] - { - new SpriteText - { - Text = "Acrylic Effect Test", - Font = FontUsage.Default.With(size: 32), - Colour = Colour4.Black, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - } - }); - } - } -} diff --git a/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs b/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs index 744e0ab71..87a19cdc9 100644 --- a/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs +++ b/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs @@ -37,14 +37,9 @@ namespace osu.Framework.Graphics.Containers initialized = false; } - // 获取背景缓冲区 - 如果不存在则创建 + // 获取背景缓冲区 - 不再自动创建,只返回已存在的 public IFrameBuffer? GetBackgroundBuffer() { - if (backgroundBuffer == null && renderer != null) - { - backgroundBuffer = renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear); - initialized = true; - } return backgroundBuffer; } @@ -74,10 +69,11 @@ namespace osu.Framework.Graphics.Containers // 更新背景缓冲区(在UpdateAfterChildren中调用) public void UpdateBackgroundBuffer() { - if (renderer is Renderer concreteRenderer && backgroundBuffer != null) - { - concreteRenderer.CaptureScreenToFrameBuffer(backgroundBuffer); - } + // 暂时禁用屏幕捕获,直到实现正确的API + // if (renderer is Renderer concreteRenderer && backgroundBuffer != null) + // { + // concreteRenderer.CaptureScreenToFrameBuffer(backgroundBuffer); + // } } // 清理资源 diff --git a/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs b/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs new file mode 100644 index 000000000..004ef875f --- /dev/null +++ b/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs @@ -0,0 +1,259 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// 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- type, use the + /// generic version . + /// + public partial class FrostedGlassContainer : FrostedGlassContainer + { + /// + public FrostedGlassContainer(RenderBufferFormat[] formats = null, bool pixelSnapping = false) + : base(formats, pixelSnapping) + { + } + } + + /// + /// 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. + /// + public partial class FrostedGlassContainer : Container, IBufferedContainer, IBufferedDrawable + where T : Drawable + { + private Vector2 blurSigma = Vector2.Zero; + + /// + /// Controls the amount of blurring in two orthogonal directions (X and Y if + /// is zero). + /// Blur is parametrized by a gaussian image filter. This property controls + /// the standard deviation (sigma) of the gaussian kernel. + /// + public Vector2 BlurSigma + { + get => blurSigma; + set + { + if (blurSigma == value) + return; + + blurSigma = value; + ForceRedraw(); + } + } + + private float blurRotation; + + /// + /// Rotates the blur kernel clockwise. In degrees. Has no effect if + /// has the same magnitude in both directions. + /// + public float BlurRotation + { + get => blurRotation; + set + { + if (blurRotation == value) + return; + + blurRotation = value; + ForceRedraw(); + } + } + + private ColourInfo effectColour = Color4.White; + + /// + /// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is . + /// + public ColourInfo EffectColour + { + get => effectColour; + set + { + if (effectColour.Equals(value)) + return; + + effectColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private BlendingParameters effectBlending = BlendingParameters.Inherit; + + /// + /// The to use after applying all effects. Default is . + /// + public BlendingParameters EffectBlending + { + get => effectBlending; + set + { + if (effectBlending == value) + return; + + effectBlending = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Color4 backgroundColour = new Color4(0, 0, 0, 0); + + /// + /// The background colour of the framebuffer. Transparent black by default. + /// + 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(); + } + } + + /// + /// Forces a redraw of the framebuffer before it is blitted the next time. + /// + public void ForceRedraw() => Invalidate(Invalidation.DrawNode); + + /// + /// 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. + /// + private long updateVersion; + + private readonly BufferedContainerDrawNodeSharedData sharedData; + + public IShader TextureShader { get; private set; } + + private IShader blurShader; + private IShader grayscaleShader; + + /// + /// Constructs an empty frosted glass container. + /// + /// The render buffer formats attached to the frame buffer of this . + /// + /// Whether the frame buffer position should be snapped to the nearest pixel when blitting. + /// This amounts to setting the texture filtering mode to "nearest". + /// + 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); + + /// + /// The blending which uses for the effect. + /// + 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(); + } + } +} diff --git a/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs b/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs new file mode 100644 index 000000000..8c1b86f6a --- /dev/null +++ b/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs @@ -0,0 +1,152 @@ +// Copyright (c) ppy Pty Ltd . 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 + { + private class FrostedGlassDrawNode : BufferedDrawNode, ICompositeDrawNode + { + protected new FrostedGlassContainer Source => (FrostedGlassContainer)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 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 blurParametersBuffer; + + private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation) + { + blurParametersBuffer ??= renderer.CreateUniformBuffer(); + + 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 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) + { + } + } + } +} diff --git a/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs b/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs index becabc919..e69de29bb 100644 --- a/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs +++ b/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs @@ -1,61 +0,0 @@ -#ifndef ACRYLIC_BLUR_FS -#define ACRYLIC_BLUR_FS - -#include "sh_Utils.h" - -#undef INV_SQRT_2PI -#define INV_SQRT_2PI 0.39894 - -layout(location = 2) in mediump vec2 v_TexCoord; - -layout(std140, set = 0, binding = 0) uniform m_BlurParameters -{ - mediump vec2 g_TexSize; - int g_Radius; - mediump float g_Sigma; - highp vec2 g_BlurDirection; -}; - -layout(set = 1, binding = 0) uniform lowp texture2D m_Texture; -layout(set = 1, binding = 1) uniform lowp sampler m_Sampler; - -layout(location = 0) out vec4 o_Colour; - -mediump float computeGauss(in mediump float x, in mediump float sigma) -{ - return INV_SQRT_2PI * exp(-0.5*x*x / (sigma*sigma)) / sigma; -} - -lowp vec4 blur(int radius, highp vec2 direction, mediump vec2 texCoord, mediump vec2 texSize, mediump float sigma) -{ - mediump vec4 sum = vec4(0.0); - mediump float totalWeight = 0.0; - - // 中心像素 - mediump float weight = computeGauss(0.0, sigma); - sum += texture(sampler2D(m_Texture, m_Sampler), texCoord) * weight; - totalWeight += weight; - - // 对称采样 - 修复采样位置和权重 - for (int i = 1; i <= radius; ++i) - { - // 使用正确的采样位置(1, 2, 3... 而不是1.5, 3.5...) - mediump float x = float(i); - weight = computeGauss(x, sigma); - sum += texture(sampler2D(m_Texture, m_Sampler), texCoord + direction * x / texSize) * weight; - sum += texture(sampler2D(m_Texture, m_Sampler), texCoord - direction * x / texSize) * weight; - totalWeight += 2.0 * weight; - } - - return sum / totalWeight; -} - -void main(void) -{ - // 应用模糊 - vec4 blurredColour = blur(g_Radius, g_BlurDirection, v_TexCoord, g_TexSize, g_Sigma); - - o_Colour = blurredColour; -} - -#endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.fs b/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.fs deleted file mode 100644 index 521cb18be..000000000 --- a/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.fs +++ /dev/null @@ -1,152 +0,0 @@ -#ifndef ACRYLIC_DEPTH_BLUR_FS -#define ACRYLIC_DEPTH_BLUR_FS - -#include "sh_Utils.h" -#include "sh_Masking.h" - -#undef INV_SQRT_2PI -#define INV_SQRT_2PI 0.39894 - -layout(location = 0) in highp vec4 v_Colour; -layout(location = 1) in mediump vec2 v_TexCoord; -layout(location = 2) in highp vec4 v_ScreenPosition; - -// 模糊参数 -layout(std140, set = 0, binding = 0) uniform m_AcrylicParameters -{ - mediump vec2 g_ScreenSize; // 屏幕尺寸 - int g_BlurRadius; // 模糊半径 - mediump float g_BlurSigma; // 高斯模糊 sigma - mediump float g_BlurStrength; // 模糊强度(0-1) - mediump float g_TintAlpha; // 着色透明度 - mediump vec3 g_TintColor; // 着色颜色 - highp float g_CurrentDepth; // 当前容器的深度 -}; - -// 场景纹理(后台缓冲区) -layout(set = 1, binding = 0) uniform lowp texture2D m_SceneTexture; -layout(set = 1, binding = 1) uniform lowp sampler m_SceneSampler; - -// 深度纹理 -layout(set = 2, binding = 0) uniform highp texture2D m_DepthTexture; -layout(set = 2, binding = 1) uniform highp sampler m_DepthSampler; - -layout(location = 0) out vec4 o_Colour; - -// 计算高斯权重 -mediump float computeGauss(in mediump float x, in mediump float sigma) -{ - return INV_SQRT_2PI * exp(-0.5 * x * x / (sigma * sigma)) / sigma; -} - -// 基于深度的采样 - 只采样比当前深度更深的像素 -lowp vec4 sampleWithDepth(vec2 uv, highp float currentDepth) -{ - // 采样深度值 - highp float depth = texture(sampler2D(m_DepthTexture, m_DepthSampler), uv).r; - - // 只有当采样点的深度大于当前深度时才采样(更深的内容) - // 在深度缓冲区中,更大的值表示更深的深度 - if (depth > currentDepth) - { - return texture(sampler2D(m_SceneTexture, m_SceneSampler), uv); - } - else - { - // 如果采样点在当前层之上或同层,返回透明色 - return vec4(0.0); - } -} - -// 双通道可分离高斯模糊(水平+垂直) -lowp vec4 applyDepthAwareBlur(vec2 screenCoord, highp float currentDepth, int radius, float sigma) -{ - vec2 pixelSize = 1.0 / g_ScreenSize; - - // 第一步: 水平模糊 - mediump float factor = computeGauss(0.0, sigma); - mediump vec4 horizontalSum = sampleWithDepth(screenCoord, currentDepth) * factor; - mediump float totalFactor = factor; - int validSamples = 1; - - for (int i = 1; i <= radius; i++) - { - factor = computeGauss(float(i), sigma); - - vec4 sample1 = sampleWithDepth(screenCoord + vec2(float(i) * pixelSize.x, 0.0), currentDepth); - vec4 sample2 = sampleWithDepth(screenCoord - vec2(float(i) * pixelSize.x, 0.0), currentDepth); - - // 只有当采样有效时才累加 - if (sample1.a > 0.0) - { - horizontalSum += sample1 * factor; - totalFactor += factor; - validSamples++; - } - - if (sample2.a > 0.0) - { - horizontalSum += sample2 * factor; - totalFactor += factor; - validSamples++; - } - } - - vec4 horizontalBlur = totalFactor > 0.0 ? horizontalSum / totalFactor : vec4(0.0); - - // 第二步: 垂直模糊(对水平模糊的结果进行) - // 为简化,我们在fragment shader中做简化版的双通道模糊 - // 实际生产环境可以用两个pass来实现更高效的可分离模糊 - factor = computeGauss(0.0, sigma); - mediump vec4 finalSum = horizontalBlur * factor; - totalFactor = factor; - - for (int i = 1; i <= radius; i++) - { - factor = computeGauss(float(i), sigma); - - vec4 sample1 = sampleWithDepth(screenCoord + vec2(0.0, float(i) * pixelSize.y), currentDepth); - vec4 sample2 = sampleWithDepth(screenCoord - vec2(0.0, float(i) * pixelSize.y), currentDepth); - - if (sample1.a > 0.0) - { - finalSum += sample1 * factor; - totalFactor += factor; - } - - if (sample2.a > 0.0) - { - finalSum += sample2 * factor; - totalFactor += factor; - } - } - - return totalFactor > 0.0 ? finalSum / totalFactor : vec4(0.0); -} - -void main(void) -{ - // 计算屏幕空间UV坐标 - vec2 screenCoord = (v_ScreenPosition.xy / v_ScreenPosition.w) * 0.5 + 0.5; - - // 获取当前片段的深度 - highp float currentDepth = g_CurrentDepth; - - // 应用深度感知模糊 - vec4 blurredColor = applyDepthAwareBlur( - screenCoord, - currentDepth, - g_BlurRadius, - g_BlurSigma - ); - - // 应用着色层 - vec4 tintColor = vec4(g_TintColor, g_TintAlpha); - vec4 finalColor = blend(tintColor, blurredColor); - - // 应用遮罩(圆角等) - vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); - o_Colour = getRoundedColor(finalColor * v_Colour, wrappedCoord); -} - -#endif diff --git a/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.vs b/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.vs deleted file mode 100644 index 5598606ae..000000000 --- a/osu.Framework/Resources/Shaders/sh_AcrylicDepthBlur.vs +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef ACRYLIC_DEPTH_BLUR_VS -#define ACRYLIC_DEPTH_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 = 0) out highp vec4 v_Colour; -layout(location = 1) out highp vec2 v_TexCoord; -layout(location = 2) out highp vec4 v_ScreenPosition; - -void main(void) -{ - // 传递顶点颜色和纹理坐标 - v_Colour = m_Colour; - v_TexCoord = m_TexCoord; - - // 计算屏幕空间位置(用于深度采样) - gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); - v_ScreenPosition = gl_Position; -} - -#endif