马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本文是渲染干系系列博客中的一篇,该系列博客已按照逻辑序次编排,方便各人依次阅读。如您对渲染干系感爱好,可以通过以下链接访问整个系列:渲染干系系列博客导航
在 DirectX 利用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色 博客中和各人先容了最简方式创建了窗口和对接了 DirectX 层。在此根本上,各人也能看到此时创建的窗口是无法应用透明配景结果的
纵然强行设置 SwapChainDescription1.AlphaMode 为 AlphaMode.Premultiplied 也会在 IDXGIFactory2.CreateSwapChainForHwnd 报错
传统 Win32 应用可以通过 UpdateLayeredWindow 方法设置窗口透明,然而 UpdateLayeredWindow 是有比力大的性能代价的,具体请参阅 WPF 从最底层源代码相识 AllowsTransparency 性能差的缘故起因
性能较好的透明窗口实现可参阅 WPF 制作支持点击穿透的高性能的透明配景异形窗口
以上是在 WPF 框架内里帮助封装好的,现在咱只有纯控制台,须要本身手动干一些活
为了方便各人阅读,本文将重新从零控制台开始,先创建好 WS_EX_LAYERED 的窗口,再将 DirectX 对接上去。总代码控制在 500 行左右。额外,为了方便 Win32 方法调用,本文还请出了 CsWin32 库,具体利用方法请参阅
dotnet 利用 CsWin32 库简化 Win32 函数调用逻辑
准备工作
按照 .NET 惯例,先安装一些库。本文的 D2D 根本没有戏份,仅用于绘制一点用于辅助测试的内容,本身此技能就和 D2D 无关- <ItemGroup>
- <PackageReference Include="Vortice.Direct2D1" Version="3.8.2" />
- <PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
- <PackageReference Include="Vortice.DirectComposition" Version="3.8.2" />
- <PackageReference Include="Vortice.DXGI" Version="3.8.2" />
- <PackageReference Include="Vortice.Win32" Version="2.3.0" />
- <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.257">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- </ItemGroup>
复制代码 安装之后的 csproj 项目文件代码如下- Exe net10.0 enable true true <ItemGroup>
- <PackageReference Include="Vortice.Direct2D1" Version="3.8.2" />
- <PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
- <PackageReference Include="Vortice.DirectComposition" Version="3.8.2" />
- <PackageReference Include="Vortice.DXGI" Version="3.8.2" />
- <PackageReference Include="Vortice.Win32" Version="2.3.0" />
- <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.257">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- </ItemGroup>
复制代码 如以上代码所示,本文提供的代码也是 AOT 友爱的。在我的测试中,构建的 32 位的步伐只需 2.12 MB 的体积。如果不知道本文的项目是怎样构造的,可以在本文末端找到本文全部代码的下载方法,拉代替码相识更多细节
添加 NativeMethods.txt 文件,添加以下内容,让 CsWin32 辅助天生一些代码- EnumDisplayMonitors
- GetMonitorInfo
- MONITORINFOEXW
- EnumDisplaySettings
- GetDisplayConfigBufferSizes
- QueryDisplayConfig
- DisplayConfigGetDeviceInfo
- DISPLAYCONFIG_SOURCE_DEVICE_NAME
- DISPLAYCONFIG_TARGET_DEVICE_NAME
- RegisterClassEx
- GetModuleHandle
- LoadCursor
- IDC_ARROW
- WndProc
- CreateWindowEx
- CW_USEDEFAULT
- ShowWindow
- SW_SHOW
- GetMessage
- TranslateMessage
- DispatchMessage
- DefWindowProc
- GetClientRect
- WM
- WM_PAINT
- GetWindowLong
- SetWindowLong
- DwmIsCompositionEnabled
- UpdateLayeredWindow
- DwmExtendFrameIntoClientArea
- DCompositionCreateDevice
复制代码 以上提供的列表是凌驾本文所用范围的,多了也没有什么关系,一来这是测试项目,二来发布的时间 AOT 带裁剪
创建窗口
创建窗口的步调和 上一篇博客 提供的方法非常靠近,只是须要设置 WS_EX_LAYERED 样式,核心代码如下- WINDOW_EX_STYLE exStyle = WINDOW_EX_STYLE.WS_EX_OVERLAPPEDWINDOW
- | WINDOW_EX_STYLE.WS_EX_LAYERED; // Layered 是透明窗口的最关键
- var style = WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW;
- var defaultCursor = LoadCursor(
- new HINSTANCE(IntPtr.Zero), new PCWSTR(IDC_ARROW.Value));
- var className = $"lindexi-{Guid.NewGuid().ToString()}";
- var title = "The Title";
- fixed (char* pClassName = className)
- fixed (char* pTitle = title)
- {
- var wndClassEx = new WNDCLASSEXW
- {
- cbSize = (uint) Marshal.SizeOf<WNDCLASSEXW>(),
- style = style,
- lpfnWndProc = new WNDPROC(WndProc),
- hInstance = new HINSTANCE(GetModuleHandle(null).DangerousGetHandle()),
- hCursor = defaultCursor,
- hbrBackground = new HBRUSH(IntPtr.Zero),
- lpszClassName = new PCWSTR(pClassName)
- };
- ushort atom = RegisterClassEx(in wndClassEx);
- var dwStyle = WINDOW_STYLE.WS_OVERLAPPEDWINDOW;
- var windowHwnd = CreateWindowEx(
- exStyle,
- new PCWSTR((char*) atom),
- new PCWSTR(pTitle),
- dwStyle,
- 0, 0, 1900, 1000,
- HWND.Null, HMENU.Null, HINSTANCE.Null, null);
- return windowHwnd;
- }
复制代码 以上代码放在 CreateWindow 方法中。在开始之前,也先调用 DwmIsCompositionEnabled 方法,判断是否可用- DwmIsCompositionEnabled(out var compositionEnabled);
- if (!compositionEnabled)
- {
- Console.WriteLine($"无法启用透明窗口效果");
- }
复制代码 预期在 Win10 以上体系都是能利用的,除非体系被魔改
窗口的消息处置惩罚代码 WndProc 先不发急写,等候完成渲染部分的逻辑再一起写
完成窗口创建之后,即可将窗口体现出来,代码如下- var window = CreateWindow();
- ShowWindow(window, SHOW_WINDOW_CMD.SW_NORMAL);
复制代码 随后先开启独立的线程作为渲染线程,再跑起来消息循环
渲染线程干系逻辑,我封装到 RenderManager 范例内里,其代码如下- var renderManager = new RenderManager(window);
- renderManager.StartRenderThread();
复制代码 跑起来渲染线程之后,利用尺度的消息循环跑起来应用- while (true)
- {
- var msg = new MSG();
- var getMessageResult = GetMessage(&msg, HWND, 0,
- 0);
- if (!getMessageResult)
- {
- break;
- }
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
复制代码 在 RenderManager 内里也提供窗口尺寸变更的方法,可以在消息循环中调用。此时的消息循环的核心代码如下- private LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam)
- {
- switch ((WindowsMessage)message)
- {
- case WindowsMessage.WM_NCCALCSIZE:
- {
- return new LRESULT(0);
- }
- case WindowsMessage.WM_SIZE:
- {
- RenderManager?.ReSize();
- break;
- }
- }
- return DefWindowProc(hwnd, message, wParam, lParam);
- }
复制代码 以上的 WM_NCCALCSIZE 用于声明客户区,通过直接返回 0 告诉体系整个地区都是客户区
透明窗口的实现在窗口创建过程中,最关键点只有 WS_EX_LAYERED 和 WM_NCCALCSIZE 的逻辑
渲染线程
独立的渲染线程也是 WPF 等 UI 框架所采取的方式,只须要新建一个线程跑起来就可以了,如果故意的话,再设置线程为 STA 的就更好,代码如下- unsafe class RenderManager(HWND hwnd)
- {
- public HWND HWND => hwnd;
- private readonly Format _colorFormat = Format.B8G8R8A8_UNorm;
- public void StartRenderThread()
- {
- var thread = new Thread(() => { RenderCore(); })
- {
- IsBackground = true,
- Name = "Render"
- };
- thread.Priority = ThreadPriority.Highest;
- thread.Start();
- }
- }
复制代码 以上的 RenderCore 就是核心的渲染方法了
由于渲染线程是独立的,不能在 ReSize 方法直接修改渲染线程干系的逻辑。本文这里简单利用一个字段体现窗口尺寸变更,须要渲染线程修改互换链尺寸- unsafe class RenderManager(HWND hwnd)
- {
- public void ReSize()
- {
- _isReSize = true;
- }
- private bool _isReSize;
- }
复制代码 在 RenderCore 的一开始就是实行初始化逻辑,初始化为本文的关键,核心就是对接 DirectComposition 实现透明窗口结果
初始化渲染
先获取客户区,即窗口尺寸,此尺寸用于后续互换链的创建- RECT windowRect;
- GetClientRect(HWND, &windowRect);
- var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);
复制代码 按照 上一篇博客 提供的方法获取显卡信息,代码如下- var dxgiFactory2 = DXGI.CreateDXGIFactory1<IDXGIFactory2>();
- IDXGIAdapter1? hardwareAdapter = GetHardwareAdapter(dxgiFactory2)
- // 这里 ToList 只是想列出所有的 IDXGIAdapter1 在实际代码里,大部分都是获取第一个
- .ToList().FirstOrDefault();
- if (hardwareAdapter == null)
- {
- throw new InvalidOperationException("Cannot detect D3D11 adapter");
- }
- private static IEnumerable<IDXGIAdapter1> GetHardwareAdapter(IDXGIFactory2 factory)
- {
- using IDXGIFactory6? factory6 = factory.QueryInterfaceOrNull<IDXGIFactory6>();
- if (factory6 != null)
- {
- // 这个系统的 DX 支持 IDXGIFactory6 类型
- // 先告诉系统,要高性能的显卡
- for (uint adapterIndex = 0;
- factory6.EnumAdapterByGpuPreference(adapterIndex, GpuPreference.HighPerformance,
- out IDXGIAdapter1? adapter).Success;
- adapterIndex++)
- {
- if (adapter == null)
- {
- continue;
- }
- AdapterDescription1 desc = adapter.Description1;
- if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None)
- {
- // Don't select the Basic Render Driver adapter.
- adapter.Dispose();
- continue;
- }
- Console.WriteLine($"枚举到 {adapter.Description1.Description} 显卡");
- yield return adapter;
- }
- }
- else
- {
- // 不支持就不支持咯,用旧版本的方式获取显示适配器接口
- }
- // 如果枚举不到,那系统返回啥都可以
- for (uint adapterIndex = 0;
- factory.EnumAdapters1(adapterIndex, out IDXGIAdapter1? adapter).Success;
- adapterIndex++)
- {
- AdapterDescription1 desc = adapter.Description1;
- if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None)
- {
- // Don't select the Basic Render Driver adapter.
- adapter.Dispose();
- continue;
- }
- Console.WriteLine($"枚举到 {adapter.Description1.Description} 显卡");
- yield return adapter;
- }
- }
复制代码 实行创建 ID3D11Device 装备,代码如下- FeatureLevel[] featureLevels = new[]
- {
- FeatureLevel.Level_12_2,
- FeatureLevel.Level_12_1,
- FeatureLevel.Level_12_0,
- FeatureLevel.Level_11_1,
- FeatureLevel.Level_11_0,
- FeatureLevel.Level_10_1,
- FeatureLevel.Level_10_0,
- FeatureLevel.Level_9_3,
- FeatureLevel.Level_9_2,
- FeatureLevel.Level_9_1,
- };
- IDXGIAdapter1 adapter = hardwareAdapter;
- DeviceCreationFlags creationFlags = DeviceCreationFlags.BgraSupport;
- var result = D3D11.D3D11CreateDevice
- (
- adapter,
- DriverType.Unknown,
- creationFlags,
- featureLevels,
- out ID3D11Device d3D11Device, out FeatureLevel featureLevel,
- out ID3D11DeviceContext d3D11DeviceContext
- );
复制代码 本文以上的 FeatureLevel[] 中添加了 Level_12_2 等的不公道需求,如果创建失败了,则实行降级逻辑。按照技能原理,只需有 Level_11_1 即可- if (result.Failure)
- {
- // 降低等级试试
- featureLevels = new[]
- {
- //FeatureLevel.Level_12_2,
- //FeatureLevel.Level_12_1,
- //FeatureLevel.Level_12_0,
- FeatureLevel.Level_11_1,
- FeatureLevel.Level_11_0,
- FeatureLevel.Level_10_1,
- FeatureLevel.Level_10_0,
- FeatureLevel.Level_9_3,
- FeatureLevel.Level_9_2,
- FeatureLevel.Level_9_1,
- };
- result = D3D11.D3D11CreateDevice
- (
- adapter,
- DriverType.Unknown,
- creationFlags,
- featureLevels,
- out d3D11Device, out featureLevel,
- out d3D11DeviceContext
- );
- }
复制代码 将获取到的 ID3D11Device 当成 ID3D11Device1 装备来用,这一步根本不会碰到堕落的,代码如下- // 大部分情况下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型
- // 从 ID3D11Device 转换为 ID3D11Device1 类型
- ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
- var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<ID3D11DeviceContext1>();
- // 获取到了新的两个接口,就可以减少 `d3D11Device` 和 `d3D11DeviceContext` 的引用计数。调用 Dispose 不会释放掉刚才申请的 D3D 资源,只是减少引用计数
- d3D11Device.Dispose();
- d3D11DeviceContext.Dispose();
复制代码 准备互换链参数,代码如下- // 缓存的数量,包括前缓存。大部分应用来说,至少需要两个缓存,这个玩过游戏的伙伴都知道
- const int FrameCount = 2;
- SwapChainDescription1 swapChainDescription = new()
- {
- Width = (uint) clientSize.Width,
- Height = (uint) clientSize.Height,
- Format = _colorFormat,
- BufferCount = FrameCount,
- BufferUsage = Usage.RenderTargetOutput,
- SampleDescription = SampleDescription.Default,
- Scaling = Scaling.Stretch,
- SwapEffect = SwapEffect.FlipSequential, // 使用 FlipSequential 配合 Composition
- AlphaMode = AlphaMode.Premultiplied,
- Flags = SwapChainFlags.None,
- };
复制代码 互换链中有以下参数必须固定为此搭配:
- Scaling: Scaling.Stretch
- SwapEffect: SwapEffect.FlipDiscard 或 SwapEffect.FlipSequential ,正常来说都会采取 FlipSequential 共同 Composition 利用
- AlphaMode: AlphaMode.Premultiplied 。利用 AlphaMode.Ignore 和 AlphaMode.Unspecified 参数也是正当的,但是云云就丢失了窗口透明白,不是咱的需求。而 AlphaMode.Straight 参数则是不搭的
如果以上参数不搭配,则会在创建互换链时,返回 0x887A0001 错误
上文提到了 AlphaMode.Premultiplied 预乘,简单来说就是终极输出的值里的 RGB 分量都乘以透明度。更多细节请参阅 支持的像素格式和 Alpha 模式 - Win32 apps - Microsoft Learn
判断体系版本,决定能否利用 DirectComposition 功能,代码如下- // 使用 DirectComposition 才能支持透明窗口
- bool useDirectComposition = true;
- // 使用 DirectComposition 时有系统版本要求
- useDirectComposition = useDirectComposition & OperatingSystem.IsWindowsVersionAtLeast(8, 1);
复制代码 云云可以让代码走两个分支,利用 DirectComposition 的分支的代码如下- IDXGISwapChain1 swapChain;
- if (useDirectComposition)
- {
- // 使用 CreateSwapChainForComposition 创建支持预乘 Alpha 的 SwapChain
- swapChain =
- dxgiFactory2.CreateSwapChainForComposition(d3D11Device1, swapChainDescription);
- // 创建 DirectComposition 设备和目标
- IDXGIDevice dxgiDevice = d3D11Device1.QueryInterface<IDXGIDevice>();
- IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice<IDCompositionDevice>(dxgiDevice);
- compositionDevice.CreateTargetForHwnd(HWND, true, out IDCompositionTarget compositionTarget);
- // 创建视觉对象并设置 SwapChain 作为内容
- IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
- compositionVisual.SetContent(swapChain);
- compositionTarget.SetRoot(compositionVisual);
- compositionDevice.Commit();
- }
复制代码 从上面代码可见核心步调是先让 CreateSwapChainForComposition 创建出互换链对象。再将 ID3D11Device1 当成 IDXGIDevice 装备,用于调用 DComp.DCompositionCreateDevice 创建出 IDCompositionDevice 装备
调用 IDCompositionDevice 的 CreateTargetForHwnd 方法即可为当前的窗口挂上 IDCompositionTarget 对象。随后再调用 IDCompositionDevice 装备的 CreateVisual 创建 IDCompositionVisual 视觉对象。将刚才创建出来的互换链作为视觉对象的内容,云云即可完成互换链与内容的绑定- // 创建视觉对象并设置 SwapChain 作为内容
- IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
- compositionVisual.SetContent(swapChain);
复制代码 现在互换链所渲染的画面已经可以或许到 IDCompositionVisual 里了,再将 IDCompositionVisual 作为 IDCompositionTarget 的根,即可让 IDCompositionVisual 到场 DWM 合成- compositionTarget.SetRoot(compositionVisual);
- compositionDevice.Commit();
复制代码 如果没有 DirectComposition 可用,则依然利用上一篇博客先容的方法创建互换链,代码如下- IDXGISwapChain1 swapChain;
- if (useDirectComposition)
- {
- ...
- }
- else
- {
- var fullscreenDescription = new SwapChainFullscreenDescription()
- {
- Windowed = true,
- };
- swapChainDescription.AlphaMode = AlphaMode.Ignore;
- swapChain = dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, hwnd, swapChainDescription,
- fullscreenDescription);
- }
复制代码 以上代码的 DirectComposition 为本文的核心,只须要创建出输出带预乘的互换链,共同 WS_EX_LAYERED 窗口,即可渲染出透明窗口
接下来的逻辑就是和 D2D 对接,实行渲染透明的界面用于测试
对接渲染
由于 D2D 没有什么戏份,本文就只贴出核心代码- using D2D.ID2D1Factory1 d2DFactory = D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();
- var d3D11Texture2D = _renderContext.SwapChain.GetBuffer<ID3D11Texture2D>(0);
- var dxgiSurface = d3D11Texture2D.QueryInterface<IDXGISurface>();
- var renderTargetProperties = new D2D.RenderTargetProperties()
- {
- PixelFormat = new PixelFormat(D2DColorFormat, Vortice.DCommon.AlphaMode.Premultiplied),
- Type = D2D.RenderTargetType.Hardware,
- };
- D2D.ID2D1RenderTarget d2D1RenderTarget =
- d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
- while (!_isDisposed)
- {
- D2D.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
- renderTarget.BeginDraw();
- var color = new Color4(Random.Shared.NextSingle(), Random.Shared.NextSingle(),
- Random.Shared.NextSingle(), 0.1f);
- renderTarget.Clear(color);
- renderTarget.EndDraw();
- _renderContext.SwapChain.Present(1, PresentFlags.None);
- _renderContext.D3D11DeviceContext1.Flush();
- }
复制代码 如果准备处置惩罚窗口尺寸改变,则须要在循环内里判断 _isReSize 字段,调用互换链的 ResizeBuffers 方法,代码如下- if (_isReSize)
- {
- // 处理窗口大小变化
- _isReSize = false;
- GetClientRect(HWND, out var pClientRect);
- var clientSize = new SizeI(pClientRect.right - pClientRect.left, pClientRect.bottom - pClientRect.top);
- var swapChain = _renderContext.SwapChain;
- swapChain.ResizeBuffers(2,
- (uint) (clientSize.Width),
- (uint) (clientSize.Height),
- _colorFormat,
- SwapChainFlags.None
- );
- }
复制代码 实行运行代码,可见一个不停闪灼的配景透明的窗口
代码
本文代码放在 github 和 gitee 上,可以利用如下下令行拉代替码。我整个代码堆栈比力巨大,利用以下下令行可以举行部分拉取,拉取速率比力快
先创建一个空文件夹,接着利用下令行 cd 下令进入此空文件夹,在下令行内里输入以下代码,即可获取到本文的代码- git init
- git remote add origin https://gitee.com/lindexi/lindexi_gd.git
- git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
复制代码 以上利用的是国内的 gitee 的源,如果 gitee 不能访问,请更换为 github 的源。请在下令行继承输入以下代码,将 gitee 源换成 github 源举行拉代替码。如果依然拉取不到代码,可以发邮件向我要代码- git remote remove origin
- git remote add origin https://github.com/lindexi/lindexi_gd.git
- git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
复制代码 获代替码之后,进入 DirectX/D2D/FarjairyakaBurnefuwache 文件夹,即可获取到源代码
更多技能博客,请参阅 博客导航
更多博客
渲染部分,关于 SharpDx 和 Vortice 的利用方法,包罗入门级教程,请参阅:
更多关于我博客请参阅 博客导航
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |