This commit is contained in:
LA
2025-11-10 23:17:32 +08:00
parent 9917cba56f
commit abd37a6327
28 changed files with 690 additions and 1139 deletions

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -1,5 +0,0 @@
<?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

@@ -1,13 +0,0 @@
<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

@@ -1,15 +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 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

@@ -1,15 +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 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

@@ -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
}
}

View File

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

View File

@@ -1,6 +0,0 @@
<?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

@@ -1,46 +0,0 @@
<?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

@@ -1,27 +0,0 @@
<?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

@@ -1,15 +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 UIKit;
namespace SampleGame.iOS
{
public static class Program
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@@ -1,13 +0,0 @@
<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

@@ -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"
]
}
}

View File

@@ -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

View File

@@ -1,26 +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 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,
};
});
}
}
}

View File

@@ -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);

View File

@@ -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);
}
});

View File

@@ -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
{
/// <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

@@ -0,0 +1,141 @@
// 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,132 +1,82 @@
// 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.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
{
/// <summary>
/// A container that applies an acrylic/frosted glass visual effect.
/// This is achieved by using a <see cref="BufferedContainer"/> with blur and a tinted overlay.
/// The container blurs its own background (a solid color or drawable), not content behind it.
///
/// Usage:
/// <code>
/// 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 */ }
/// };
/// </code>
/// 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 : BufferedContainer
public partial class AcrylicContainer : Container
{
private float blurStrength = 10f;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// The tint colour applied over the blurred background.
/// Typically a semi-transparent color like Color4(0, 0, 0, 0.3f).
/// </summary>
public new ColourInfo TintColour
{
get => tintColour;
set
{
if (tintColour.Equals(value))
return;
public Color4 TintColour { get; set; } = Color4.White;
tintColour = value;
updateTint();
}
}
/// <summary>
/// The darkening factor applied to create depth.
/// </summary>
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!;
/// <summary>
/// Constructs a new acrylic container.
/// </summary>
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);
}
}
}

View File

@@ -1,55 +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.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// Simple test class to verify AcrylicContainer functionality
/// </summary>
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,
}
}
});
}
}
}

View File

@@ -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);
// }
}
// 清理资源

View File

@@ -0,0 +1,259 @@
// 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

@@ -0,0 +1,152 @@
// 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

@@ -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

View File

@@ -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

View File

@@ -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