行业资讯
Unity桌面实时捕获插件:基于Windows Graphics Capture API的免安装方案
1. 项目概述一个即插即用的桌面捕获方案最近在做一个Unity项目需要把Windows桌面上的任意窗口画面甚至是整个屏幕实时地“搬”到Unity的3D场景里。你可能马上会想到用OBS之类的软件录屏再导入视频但这延迟太高交互性为零。或者去研究Windows的GDI、DirectX抓图API再写一堆C插件和Unity交互光是环境配置和编译就能劝退一大半人。这正是我当初遇到的困境需要一个轻量、稳定、延迟低并且能无缝集成到Unity编辑器和工作流中的桌面画面捕获工具。于是就有了这个“Unity Windows桌面画面实时捕获插件”的开发想法。它的核心目标非常明确免安装即拖即用。你不需要在目标机器上安装任何额外的运行时库比如某些框架的Redistributable也不需要复杂的编译设置。就像导入一个普通的Unity资源包.unitypackage一样拖进项目简单配置一下就能立刻在Game视图里看到另一个程序窗口的动态画面延迟可以控制在几十毫秒以内完全满足AR/VR叠加演示、远程协助预览、游戏内嵌直播或录屏工具开发等场景的需求。这个插件解决的痛点很直接降低实时桌面捕获的技术门槛和集成成本。无论是做演示软件、教育应用还是需要将外部软件界面作为纹理进行二次渲染的创意项目它都能让你快速搭建起核心功能把精力集中在业务逻辑和创新上而不是和底层API搏斗。2. 核心原理与技术选型解析要实现“免安装即拖即用”意味着插件必须是一个纯托管Managed的解决方案或者将原生Native代码的依赖完全内嵌避免对外部DLL或系统组件的额外要求。同时“实时捕获”对性能有苛刻要求。2.1 为什么选择Windows Graphics Capture API在Windows平台上捕获桌面画面的历史方法有很多各有优劣GDI (BitBlt): 最古老的方法通过BitBlt函数抓取屏幕或窗口的位图。优点是兼容性极好从XP到Win11都能用。但致命缺点是效率低下并且无法捕获硬件加速如DirectX、OpenGL渲染的内容对于现代游戏或视频播放器窗口抓出来是一片黑。DirectX Desktop Duplication API: 从Windows 8开始引入是效率最高的方法之一能直接访问桌面纹理。但它复杂度很高需要处理DXGI设备、纹理共享等并且对系统版本和显卡驱动有要求不符合“免安装”的简易性目标。Windows Graphics Capture API: 这是Windows 101809版本及以上引入的现代API。它被设计用于取代旧的BitBlt和部分DirectX Duplication的场景特别是为屏幕录制、截图共享等UWP应用提供支持。其最大优势在于系统级支持作为Windows Runtime (WinRT) API的一部分只要系统版本满足其运行时环境是内置的无需额外分发。高性能与兼容性能够捕获包括硬件加速窗口在内的几乎所有内容并且微软对其进行了深度优化。安全与隐私会明确显示捕获提示黄色边框并需要用户授权对于捕获其他窗口符合现代系统规范。综合来看Windows Graphics Capture API在性能、兼容性和易用性之间取得了最佳平衡是实现我们插件核心功能的理想选择。2.2 Unity与原生代码的交互桥梁Unity本身是用C#开发的而Windows Graphics Capture API是C/WinRT接口。要让两者对话我们需要一个桥梁。这里有两种主流方案P/Invoke (平台调用)直接在C#中声明外部DLL的函数签名进行调用。这对于纯C API很有效但对于复杂的C/WinRT对象模型来说极其繁琐且容易出错不适合。C/CLI 包装器这是更优雅的方案。我们可以创建一个C/CLI项目它既能理解标准的C从而调用WinRT API又能生成.NET程序集供C#调用。这个包装器DLL可以随插件一起分发。本插件的核心架构正是一个用C/CLI编写的托管DLL内部封装了所有对Windows.Graphics.Capture命名空间的调用。这个DLL被编译为Any CPU或x64根据Unity项目设置并作为插件资源放入Unity的Plugins文件夹。Unity在运行时自动加载它我们的C#脚本通过引用这个DLL中暴露的托管类来创建捕获会话、获取帧数据。2.3 纹理流与低延迟渲染捕获到帧数据通常是内存中的一块图像数据后如何高效地传递给Unity并显示共享纹理与GPU上传最理想的路径是避免CPU和GPU之间的大规模内存拷贝。Windows Graphics Capture API支持将捕获的帧放入一个DXGI纹理。我们可以通过C/CLI层获取这个纹理的共享句柄HANDLE或直接指针。Unity中的纹理创建在C#端我们可以使用System.IntPtr接收这个原生指针然后通过Texture2D.CreateExternalTexture方法在Unity中创建一个“外部纹理”。这个Unity纹理并不真正拥有像素数据而是直接指向GPU上已有的那块DXGI纹理内存。这样就实现了零拷贝的纹理共享延迟最低。回退方案CPU回读如果某些系统或显卡不支持纹理共享或者为了最大兼容性我们也可以选择将DXGI纹理的内容回读到CPU内存Map/Unmap然后再通过Texture2D.LoadRawTextureData或SetPixels更新Unity纹理。这会增加一帧的延迟和CPU开销但作为保底方案是可靠的。在插件实现中我优先尝试GPU路径并自动检测兼容性若不支持则无缝切换到CPU路径确保在不同机器上的可用性。3. 插件设计与关键实现细节3.1 项目结构与依赖管理为了让插件真正做到“即拖即用”项目结构必须清晰且所有依赖必须内嵌或明确声明。YourUnityProject/ ├── Assets/ │ ├── DesktopCapture/ │ │ ├── Plugins/ │ │ │ ├── x86/ (可选32位运行时DLL) │ │ │ ├── x86_64/ (64位运行时DLL主要) │ │ │ └── DesktopCaptureBridge.dll (我们的核心C/CLI桥接库) │ │ ├── Scripts/ │ │ │ ├── Runtime/ │ │ │ │ ├── DesktopCaptureManager.cs (主管理器单例) │ │ │ │ ├── CaptureSession.cs (封装一次捕获会话) │ │ │ │ └── Models/ (数据模型如WindowInfo) │ │ │ └── Editor/ (编辑器扩展工具) │ │ ├── Shaders/ (可能需要的特殊显示Shader) │ │ └── Resources/ (图标等) │ └── ... (你的其他资源)关键点DesktopCaptureBridge.dll是我们编译好的C/CLI桥接库。它内部静态链接了必要的C运行时但动态依赖于系统的Windows.Graphics.Capture相关WinRT组件。由于这些组件是系统自带因此我们无需分发。在Unity的Plugin Inspector中需要正确设置DesktopCaptureBridge.dll的加载设置Load on Startup建议为truePlatform仅勾选Windows和Windows Store如果适用。3.2 C/CLI桥接层核心代码剖析桥接层DesktopCaptureBridge是插件的引擎。它主要暴露以下几个关键的托管类和方法给C#// DesktopCaptureBridge.h (托管部分) namespace DesktopCaptureBridge { public ref class CaptureFactory sealed { public: static bool IsCaptureSupported(); // 检查系统是否支持此API static ListWindowInfo^^ GetAvailableWindows(); // 获取可捕获的窗口列表 static CaptureSession^ CreateSessionForWindow(IntPtr hwnd); // 为指定窗口创建会话 static CaptureSession^ CreateSessionForDisplay(IntPtr monitorHandle); // 为指定显示器创建会话 }; public ref class CaptureSession sealed { public: property bool IsCapturing { bool get(); } event FrameArrivedEventHandler^ FrameArrived; // 新帧到达事件 void Start(); void Stop(); void Dispose(); IntPtr GetLatestFrameTextureHandle(); // 获取最新帧的纹理句柄(用于GPU路径) bool CopyFrameToBuffer(IntPtr buffer, int bufferSize); // 复制帧到缓冲区(用于CPU路径) }; }在C/CLI的实现文件(.cpp)中则包含了与WinRT交互的所有细节初始化WinRT使用winrt::init_apartment(winrt::apartment_type::multi_threaded)。创建GraphicsCaptureItem通过GraphicsCaptureItem::CreateFromWindowId或CreateFromMonitorId。设置CaptureSession创建Direct3D11CaptureFramePool和GraphicsCaptureSession。帧池负责管理纹理资源池避免频繁分配释放。帧到达事件处理订阅FrameArrived事件。当新帧就绪时从帧池中获取Direct3D11CaptureFrame进而获取其Surface或Texture。纹理共享通过CreateSharedHandle获取纹理的共享句柄或者使用ID3D11DeviceContext::Map将纹理内容映射到CPU可访问的内存。注意线程模型。WinRT的调用和Unity的主线程游戏线程可能不在同一个线程。桥接层内部必须妥善处理线程同步例如将捕获到的帧数据先暂存然后通过Unity的UnityEngine.WSA.Window调度器UnityEngine.WSA.Application.InvokeOnAppThread或简单的线程安全队列将更新纹理的请求抛回主线程执行否则会导致Unity崩溃。3.3 Unity C#脚本层封装C#层的作用是提供一个干净、易用、符合Unity习惯的接口。DesktopCaptureManager.cs (单例模式)这是用户的主要交互入口。它负责初始化桥接层检查系统兼容性。提供静态方法枚举所有可捕获的窗口返回包含窗口标题、句柄、缩略图等信息的列表。创建和管理CaptureSession实例。处理应用程序焦点丢失/获得时的捕获暂停与恢复出于隐私考虑当Unity应用失焦时应自动停止捕获。CaptureSession.cs这个类对应桥接层的一个捕获会话。它内部持有桥接层CaptureSession对象的引用。在Start()时订阅桥接层的FrameArrived事件。在事件回调中根据设置GPU或CPU路径获取纹理数据。GPU路径调用GetLatestFrameTextureHandle()获取句柄使用Texture2D.CreateExternalTexture创建或更新一个Unity纹理。CPU路径调用CopyFrameToBuffer将帧数据复制到预分配的byte[]然后更新Texture2D。提供一个UnityEngine.Texture2D类型的属性TargetTexture供用户直接赋值给RawImage的texture或Material的_MainTex。一个简单的使用示例using UnityEngine; using UnityEngine.UI; using DesktopCaptureBridge; public class SimpleDesktopViewer : MonoBehaviour { public RawImage displayImage; // UI上的RawImage组件 private CaptureSession _session; void Start() { // 1. 获取第一个非自身窗口简单演示 var windows DesktopCaptureManager.GetAvailableWindows(); if (windows.Count 1) // 通常第0个是自己的窗口 { var targetWindow windows[1]; // 2. 创建捕获会话 _session DesktopCaptureManager.CreateSessionForWindow(targetWindow.Handle); // 3. 订阅纹理更新管理器内部已处理 _session.TargetTextureUpdated OnTextureUpdated; // 4. 开始捕获 _session.Start(); } } void OnTextureUpdated(Texture2D newTexture) { if (displayImage ! null) displayImage.texture newTexture; } void OnDestroy() { if (_session ! null) { _session.Stop(); _session.Dispose(); } } }4. 实战应用从导入到运行的完整流程假设你拿到了一个名为DesktopCapture_UnityPlugin_v1.0.unitypackage的插件包。4.1 导入与基础配置导入包在Unity编辑器中Assets - Import Package - Custom Package...选择该.unitypackage文件导入全部资源。检查插件设置导入后找到Assets/DesktopCapture/Plugins/DesktopCaptureBridge.dll。选中它在Inspector面板中确保Platform Settings里Windows和Windows Store是勾选的Any OS是未勾选的。Load on Startup可以保持为Always。创建测试场景新建一个场景创建一个UI Canvas在Canvas下添加一个RawImage组件将其铺满屏幕或调整到合适大小。4.2 编写一个简单的窗口选择器为了让用户选择捕获哪个窗口我们可以创建一个简单的编辑器工具或运行时UI。// WindowPickerUI.cs using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using DesktopCaptureBridge; public class WindowPickerUI : MonoBehaviour { public Dropdown windowDropdown; public Button startButton; public Button stopButton; public RawImage previewImage; private ListWindowInfo _windowList; private CaptureSession _currentSession; void Start() { startButton.onClick.AddListener(StartCapture); stopButton.onClick.AddListener(StopCapture); stopButton.interactable false; RefreshWindowList(); } void RefreshWindowList() { _windowList DesktopCaptureManager.GetAvailableWindows(); windowDropdown.ClearOptions(); Liststring options new Liststring(); foreach (var win in _windowList) { options.Add(${win.Title} (PID: {win.ProcessId})); } windowDropdown.AddOptions(options); } void StartCapture() { if (_windowList null || windowDropdown.value _windowList.Count) return; StopCapture(); // 先停止之前的 var selectedWindow _windowList[windowDropdown.value]; _currentSession DesktopCaptureManager.CreateSessionForWindow(selectedWindow.Handle); if (_currentSession ! null) { // 直接关联会话的TargetTexture到UI previewImage.texture _currentSession.TargetTexture; _currentSession.Start(); startButton.interactable false; stopButton.interactable true; } } void StopCapture() { if (_currentSession ! null) { _currentSession.Stop(); _currentSession.Dispose(); _currentSession null; previewImage.texture null; startButton.interactable true; stopButton.interactable false; } } void OnApplicationQuit() { StopCapture(); } }将这个脚本挂载到Canvas下的一个空物体上并将UI组件拖拽赋值。运行后下拉框会列出所有窗口点击开始即可捕获。4.3 性能优化与参数调节在真实项目中直接显示原始分辨率如4K的捕获纹理可能对性能造成压力。插件内部可以集成几种优化策略帧率限制不是所有场景都需要60FPS的捕获。可以在创建CaptureSession时指定一个较低的帧率如30FPS或15FPS这能显著降低CPU/GPU占用。这通过配置Direct3D11CaptureFramePool的创建参数实现。分辨率缩放GraphicsCaptureSession支持设置Size。我们可以将捕获目标设置为比原窗口小的尺寸在GPU端直接进行下采样比捕获全尺寸再在Unity里缩放要高效得多。纹理格式默认可能是BGRA8。如果不需要Alpha通道可以尝试使用BGRX8格式。对于性能极度敏感的场景甚至可以研究使用NV12等视频格式但这需要Shader进行YUV到RGB的转换。异步纹理更新确保纹理更新无论是GPU共享还是CPU拷贝发生在Unity主线程但帧抓取和准备过程在后台线程。桥接层的事件机制天然支持这种异步模型。在插件的CaptureSession创建方法中可以增加可选参数让用户配置这些选项public static CaptureSession CreateSessionForWindow(IntPtr hwnd, int targetWidth 0, int targetHeight 0, int frameRate 30);5. 常见问题、排查技巧与避坑指南在实际开发和测试中我遇到了不少坑。这里总结一下希望能帮你节省时间。5.1 捕获失败或黑屏这是最常见的问题。请按以下步骤排查检查系统版本确保是Windows 10 1809 (17763) 或更高版本。可以在C#中用Environment.OSVersion检查但更可靠的是在桥接层用ApiInformation.IsApiContractPresent来检测Windows.Graphics.Capture合约是否存在。检查权限与隐私提示捕获其他窗口时系统会在目标窗口周围显示黄色边框。如果没看到可能是捕获了无效的窗口句柄或者目标窗口是受保护的如安全桌面、某些UWP应用。尝试捕获资源管理器或记事本等标准窗口进行测试。特别注意在Unity编辑器中运行时如果以管理员身份运行编辑器而目标程序不是也可能导致捕获失败。尽量保持权限一致。检查Unity图形API插件依赖于Direct3D 11。确保Unity Player Settings中Graphics APIs列表里Direct3D11在首位对于Windows独立构建。在编辑器内默认使用DX11。查看日志插件内部应该实现一个简单的日志系统如写入文件或Unity的Debug.Log记录初始化、创建会话、帧到达等关键步骤的成功与失败信息。这是定位问题的第一手资料。5.2 性能问题高CPU/GPU占用、高延迟帧率过高默认可能尝试捕获显示器的刷新率。如果只是预览将帧率限制在30或更低。分辨率过高捕获4K窗口对带宽和渲染压力巨大。尝试在创建会话时指定一个较小的输出尺寸。CPU回读路径确认是否在使用CPU回读路径。可以在插件中输出当前使用的路径。如果显卡或驱动不支持纹理共享会回退到CPU路径此时延迟和占用都会显著增加。Unity渲染开销即使捕获很高效在Unity中每帧更新一个全屏的RawImage也会带来Draw Call。考虑将捕获的纹理用于更必要的渲染或者降低显示UI的更新频率。5.3 内存泄漏与资源管理这是原生插件开发的重灾区。及时释放COM对象WinRT对象本质是COM对象必须正确释放。在C/CLI中使用winrt::com_ptr或确保在析构函数中调用.close()或置空。在C#中确保CaptureSession实现了IDisposable并在Dispose()方法中调用桥接层的释放函数。帧池管理Direct3D11CaptureFramePool需要在使用完毕后调用Close()。确保在会话停止和销毁时清理顺序正确先停止捕获(session.Close())再释放帧池(framePool.Close())。Unity纹理生命周期通过CreateExternalTexture创建的纹理其生命周期依赖于外部资源。当外部纹理失效后再使用这个Unity纹理会导致错误。安全的做法是在每次从桥接层获取新的纹理句柄时都重新创建Unity的Texture2D并销毁旧的。5.4 编辑器模式与构建后运行不一致DLL加载路径确保插件DLL放在正确的Plugins子目录下并且平台设置正确。编辑器模式是x64构建时也要对应。管理员权限某些受保护的程序窗口如任务管理器在非管理员权限下无法捕获。构建后的exe如果以普通权限运行行为会与以管理员权限运行的编辑器不同。单实例问题桥接层如果是单例设计要小心在编辑器停止播放时是否完全清理了所有资源。否则第二次播放时可能会初始化失败。在C/CLI的静态析构函数或C#的AppDomain.DomainUnload事件中做清理是好的实践。5.5 进阶捕获全屏与特定显示器除了捕获窗口插件也支持捕获整个屏幕或特定显示器。原理类似使用GraphicsCaptureItem::CreateFromMonitorId。关键是如何获取显示器的句柄(HMONITOR)。可以通过EnumDisplayMonitorsAPI枚举显示器并在UI中让用户选择。这部分代码可以同样封装在桥接层中。一个实用的技巧是可以捕获一个不包含任务栏的“工作区域”。Windows APIGetMonitorInfo可以返回monitorInfo.rcWork工作区域矩形和monitorInfo.rcMonitor整个监视器矩形在创建捕获项时可以指定捕获区域。开发这个插件的过程是一次对Windows现代图形API、Unity原生插件交互和性能优化的深度之旅。最大的体会是“免安装即拖即用”这个目标背后需要极其精细的依赖管理和健壮的错误处理。它不仅仅是一个功能实现更是一个完整的产品化思考。现在你可以将几十MB的.unitypackage交给你的团队或用户他们就能在几分钟内为任何Unity项目赋予实时桌面捕获的能力这其中的便利性和带来的可能性远超最初的想象。如果你在集成过程中遇到任何本文未覆盖的古怪问题不妨从系统日志、驱动版本和权限这三个最基础的维度再去审视一下往往能发现端倪。
郑州网站建设
网页设计
企业官网