uWindowCapture插件:Unity桌面捕获的工业级方案
2026/9/15 16:09:55 网站建设 项目流程

简介:面向需要在 Unity 中实时抓取并展示 Windows 桌面的开发者,一份可直接集成的插件资源可解决桌面内容嵌入游戏窗口、远程协作投屏等需求,覆盖桌面实时预览、屏幕采集与同步投射等常见用途。压缩包共 141 个文件,包括 26 个 C# 脚本、13 个 Unity 场景、材质与预制体,以及 DLL、Shader、编辑器配置文件等,核心模块涵盖桌面窗口管理、纹理同步、窗口布局与编辑器扩展,整体仅 600KB,功能集中且结构清晰。已有 565 人学习下载,说明该插件在 Unity 桌面接入场景中有较高的实用价值与学习参考意义,尤其适合桌面投射类项目。借助其中已有的窗口捕获、纹理更新与布局工具,能显著减少从底层抓屏到纹理映射的重复开发,也适合希望研究 Windows 桌面采集原理、加深对 Unity 渲染管线理解的中高级开发者参考学习。

1. 为什么Unity里取桌面画面,不能靠截屏循环

做数字孪生大屏、远程协助工具或者录屏类应用时,常遇到一个需求:把Windows桌面实时搬进Unity场景,作为UI背景、三维物体的贴图或者画中画窗口。很多人的第一反应是调用System.Drawing的CopyFromScreen,然后转Texture2D。这个方案在1080p下勉强能动,但只要分辨率上到2K,或者帧率要求超过30FPS,CPU占用就会飙到无法接受,而且鼠标轨迹还会出现明显的撕裂。

uWindowCapture这个Unity插件走的是另一条路:通过Windows Graphics Capture API直接抓取桌面窗口句柄,把D3D表面的帧拷贝到Unity的Texture上。整个过程不走GDI,也不经过CPU回读,性能开销比截屏低一个数量级,这才是它适合工业级项目的原因。这篇文章会把插件的文件结构、采集流程、参数配置和踩坑点完整拆一遍,确保你拿到源码包之后能直接落地。

2. 核心架构剖析:从窗口句柄到Unity纹理的完整链路

2.1 文件清单与职责划分

解压资源包后,Assets目录下会多出一组文件,每个文件的用途需要理清楚:

文件名类型职责
UwcManager.cs管理器初始化和释放Windows Graphics Capture,持有采集线程
UwcWindow.cs数据模型抽象桌面窗口,记录窗口类名、进程ID、共享资源指针
UwcWindowTexture.cs核心组件挂载到Unity物体上,把采集到的纹理帧更新到材质
UwcWindowTextureChildrenManager.cs子物体管理为多个子窗口同步创建纹理组件
UwcDesktopLayouter.cs自动化布局按网格或条幅方式排列多个窗口纹理的Transform位置
UwcWindowTextureEditor.cs编辑器扩展Inspector面板的定制显示和调试按钮
UwcLib.csP/Invoke封装把C++接口封成C#可调用的DllImport
UwcCommon.cgincShader片段内置着色器中对纹理采样和色彩空间处理的公共宏

关键点在于UwcManager是单例入口,它只负责建立会话,不直接产生Texture。UwcWindow才是真正代表一个可采集目标,UwcWindowTextureStart时根据UwcWindow的共享资源句柄创建Unity Texture2D,并每帧调用Update从共享纹理读数据。

2.2 管理器的启动流程与生命周期控制

打开UwcManager.cs,核心方法集中在StartOnApplicationQuit,伪代码如下:

void Start() { if (!isSupportedPlatform()) return; _direct3D11Device = GetUnityD3D11Device(); _desktopDuplication = new DesktopDuplicationManager(); _desktopDuplication.Initialize(_direct3D11Device); string[] args = System.Environment.GetCommandLineArgs(); foreach (string arg in args) { if (arg == "-uwcDebug") _isDebugMode = true; } Application.targetFrameRate = 60; } void Update() { if (_desktopDuplication != null) _desktopDuplication.Update(); } void OnApplicationQuit() { if (_desktopDuplication != null) { _desktopDuplication.Release(); _desktopDuplication = null; } }

GetUnityD3D11Device会从Unity底层拿到当前图形设备的ID3D11Device,这是整个采集链路的前提——Windows Graphics Capture要求采集器和目标纹理共享同一个D3D设备。Application.targetFrameRate = 60这一步容易被忽略,如果不锁帧,Update里的纹理更新次数会跟渲染帧率解耦,产生跳帧感。

关于Debug模式,插件支持在启动参数里加-uwcDebug,开启后会把每次采集的帧耗时、纹理拷贝大小输出到Console,排查性能问题时非常实用。注意这个模式只对使用Unity的Build方法构建出的exe有效,Editor里直接跑需要手动修改PlayerSettings的Scripting Define Symbols。

2.3 UwcWindowTexture的帧同步机制

UwcWindowTexture.cs的Update方法决定了画面更新到的时机,代码逻辑如下:

void Update() { if (_window == null) return; if (_isPresent) return; // 更新纹理内容 _updateTimer += Time.deltaTime; if (_updateTimer >= 0.1f) { _updateTimer = 0f; if (m_updateMode == UpdateMode.Auto) UpdateTexture(); } } void UpdateTexture() { // 从共享资源获取最新帧 _window.GetSharedSurfaceData(); // 处理纹理格式转换 if (_window.textureFormat == TextureFormat.BGRA32) { _texture = new Texture2D(_window.width, _window.height, TextureFormat.BGRA32, false); _texture.SetPixels32(_window.textureColors); _texture.Apply(); } }

UpdateMode.Auto是默认值,插件内部用计时器控制更新频率,默认0.1秒,也就是10FPS左右。如果项目需要录屏级的流畅度,需要把m_UpdateMode设为Manual,然后自己调用UpdateTexture,或者修改_updateTimer >= 0.016f让它跑满60FPS。

这段代码里还有一个细节:if (_isPresent) return;。该字段用于判断Texture是否已经被副本占用,如果上一帧拷贝还没完成,当前帧会直接跳过,避免产生资源竞争。在画面变化频繁的场景,比如视频播放器窗口,有可能出现跳帧,但只要拷贝开销小于间隔,_isPresent绝大多数情况下都是false。

3. 接入Unity项目的完整步骤与参数调优

3.1 基础接入:从解压到显示桌面

直接拖进Assets目录后,打开UwcWindowTexture.cs对应的Prefab或空物体,在Inspector面板上能看到相关配置项:

  • Window Title:窗口标题的关键字,插件会按标题模糊匹配
  • Process Name:进程名过滤,填chrome则只找Chrome窗口
  • Capture Mode:可选Screen或Window
  • Update Mode:Auto或Manual,建议Manual配合脚本自己控制节拍

在场景里创建一个RawImage用于承载纹理,写一个初始化脚本:

using UnityEngine; using UnityEngine.UI; using uWindowCapture; public class DesktopUser : MonoBehaviour { public UwcManager manager; UwcWindowTexture windowTexture; void Start() { // 按窗口标题获取桌面窗口 UwcWindow window = manager.FindWindowByTitle("Unity"); if (window == null) { Debug.LogError("未找到窗口"); return; } // 将窗口纹理绑定到RawImage windowTexture = gameObject.AddComponent<UwcWindowTexture>(); windowTexture.targetWindow = window; windowTexture.StartCapture(); RawImage image = GetComponent<RawImage>(); image.texture = windowTexture.texture; } }

FindWindowByTitle用的是子串匹配,传入"Unity"可以同时匹配到“Unity 2021.3.20f1c1”和“Unity Hub”,如果你的窗口标题过长,用部分关键字反而更灵活。绑定纹理的时机必须在StartCapture之后,否则拿到的texture为null。

如果脚本要兼容Editor和Build两种环境,预览窗口最好固定显示主监视器,避免在多显示器场景下选择出错。

3.2 多窗口同步显示的布局控制

UwcDesktopLayouter.cs解决的问题是:当你有多个UwcWindowTexture需要同时显示时,不想手动拖位置,用代码自动排布子物体。使用方法如下:

// 创建父物体承载多个窗口 GameObject root = new GameObject("DesktopGrid"); var layouter = root.AddComponent<UwcDesktopLayouter>(); // 添加两个窗口采集 var tex1 = CreateWindowTexture("Chrome"); tex1.transform.SetParent(root.transform); var tex2 = CreateWindowTexture("Outlook"); tex2.transform.SetParent(root.transform); // 调用自动布局 layouter.Layout(2, 1, 10f, 10f);

Layout方法接收行列数、间隔和边距值,内部按从左到右、从上到下的顺序把每个子物体的RectTransform铺满父区域。默认还提供了一个Arrange方法,可以配合ScrollRect实现一页展示多个窗口,翻页时自动调整缩放比例。

另外在UwcWindowTextureChildrenManager中,你可以动态监控子窗口的变化。例如进程内新弹出一个对话框,它会自动创建一个新的UwcWindowTexture显示在界面上。这个类适用于需要在主窗口内嵌若干子元素的场景,比如游戏内嵌浏览器、IDE中的调试面板。

3.3 与PLC通信等第三方系统的联动策略

这个插件本身不做数据交互,但它可以作为可视化层接收外部事件。常见的做法是写一个事件转发脚本,监听UwcWindowTexture的OnWindowClosedOnWindowSizeChanged事件,再通过Unity的SendMessage或者其他事件系统通知业务层。

比如做产线数字孪生系统,经常需要把组态软件的画面集成进来,用西门子PLC通信指令控制设备,同时把PLC面板的桌面画面作为背景叠加在虚拟机模型上。典型做法:先用UwcWindowTexture把WinCC画面加载到场景中的大屏模型面片,再用异步Socket接收PLC的交互指令,当PLC值变化时修改窗口的显隐和位置。这个插件的职责边界就是提供画面层,具体业务逻辑完全在Unity侧自己拼装。

3.4 参数表格:不同场景推荐配置

场景Capture ModeUpdate Mode更新间隔备注
数字孪生大屏ScreenManual0.05s保证20FPS以上,不要跑满60
录屏/直播WindowManual0.016s需要60FPS时开启垂直同步
低配工控机ScreenAuto0.1s默认值就够
远程桌面画中画WindowManual0.1s建议同时开启硬件缩放

这里强调一个容易踩的坑:Capture Mode选Screen会把整个桌面变成一个巨大的纹理,在4K分辨率下支持不了。建议优先用Window模式找主窗口,如果必须要整个桌面,可以先用Unity的Screen.SetResolution降分辨率再启动采集。

4. 性能优化、光标渲染与多显示器避坑

4.1 帧率控制在20~30FPS的工程取舍

做导览类应用时,桌面画面不需要非常流畅,20~30FPS是感官和性能的最佳平衡点。把UpdateTexture改成这样就能稳定输出:定义变量int targetFps = 25;,在Update中判断累计时间超过1f / targetFps时才刷新。

从显卡层面来看,即使你的采集纹理只更新25FPS,显卡渲染开销仍然存在。因此要达到省电节能的目的,需要把Application.targetFrameRate同样调到25,让Unity渲染本身也降帧。这个值不是越高越好,因为Windows的桌面合成器(DWM)刷新率通常锁定在显示器级别,抓桌面帧的间隔过短会产生额外的等待空转。

4.2 光标分量缺失的处理方式

Windows Graphics Capture默认采集的是整个桌面画面,但不会给光标单独建立一层纹理,因此插件的纹理里看不到鼠标指针,除非桌面窗口处于非活动状态时Windows自动隐藏光标。UwcWindowTextureEditor.cs里有个DrawCursor选项,开启后会在Texture上叠加一个由IngameCursorManager生成的CursorMesh,但不支持在Game视图下同步光标,因为它走的是鼠标事件监听而不是桌面COM接口。

如果需要与渲染内容完全一致的光标画面,建议改用Windows API的GetCursorPos加上DrawIcon绘制到单独层再混入,这是比较常见的配合手段。

4.3 多显示器场景下的窗口识别策略

插件的窗口查找逻辑使用枚举所有Desktop Window并将句柄、Width、Height写入UwcWindow对象。在多显示器场景下,某个窗口的Width会包含出它的整个尺寸,但你无法直接知道这个窗口在哪个显示器上。解决办法是读取Screen.currentResolution对比窗口坐标是否在该分辨率范围内,或者用UwcWindow暴露出的MonitorIndex属性。

此外,多显示器下抓整个桌面时,Texture的宽高是所有显示器拼接的总和,用UpdateTexture之前要先判断宽高是否超出SystemInfo.maxTextureSize,否则Unity会报错并停止更新,严重时直接白屏。经验阈值是单块4K屏拼接双屏时,必须手动切分采集区域,分成两个Window分别处理。

4.4 加入内存保护与异常恢复

长时间运行的场景,比如展厅播控系统,窗口最小化或者切换桌面会造成采集会话错误。UwcManager中内置了异常重连机制:当Update方法捕获到SharpDXException时,会触发OnErrorOccurred事件并尝试再次初始化会话。我建议在业务侧注册这个事件,因为默认的重连逻辑只恢复管理器,不会恢复你界面上已有的UwcWindowTexture对象。

以下是一个稳定的恢复策略示例:

void OnEnable() { manager.OnErrorOccurred += OnCaptureError; } void OnCaptureError(object sender, Exception e) { // 销毁所有客户端纹理对象 foreach (var tex in allTextures) { Destroy(tex.texture); tex.StopCapture(); } // 延迟重建 StartCoroutine(RestartCapture()); } IEnumerator RestartCapture() { yield return new WaitForSeconds(1f); foreach (var config in savedWindows) { CreateWindowTexture(config); } }

这里AllTextures需要你自己维护一个列表,UwcManager不做这个持久化。注意StopCapture后必须将UwcWindow的状态重置,否则重连时会因为旧句柄无效而报ArgumentException

5. 排错锦囊:版本兼容性、Shader定制与常用测试命令

5.1 Unity版本与系统环境不匹配时的症状

插件的README里明确写了一句“如果存在问题请更换Unity版本”,实际情况是它底层调用了较新的Windows SDK接口(Windows 10 2004及以上提供的CreateDirect3D11DeviceFromDXGIDevice),因此:

  • 如果你的系统是Windows 7或者未更新的Windows 10 1903,初始化时会在UwcWindowTexture.Start直接抛NullReferenceException,这类问题换Unity版本没有意义,建议先升级系统补丁
  • 在Unity 2020.3和2021.3下测试稳定,Unity 2022.2及之后版本需要把API Compatibility Level设置为.NET Standard 2.1,否则P/Invoke的DllImport会加载失败
  • 如果项目使用URP或HDRP渲染管线,默认的UwcCommon.cginc里的采样方式会失效,需要在Shader中手动定义UNITY_UI_ALPHACLIPUNITY_UI_CLIP_RECT等宏

5.2 Shader层纹理采样异常的处理

UwcCommon.cginc内部包含两个宏,一个是转换线性空间到Gamma空间,另一个是用于在URP下兼容旧版SampleTexture2D。如果你的画面看起来发灰或者发暗,多半是色彩空间问题:

// 在自定义Shader中强制开启Gamma修正 #define UWC_FORCE_GAMMA 1 #include "Assets/uWindowCapture/Shaders/UwcCommon.cginc" fixed4 frag(v2f i) : SV_Target { fixed4 color = tex2D(_MainTex, i.uv); #ifdef UWC_FORCE_GAMMA color.rgb = LinearToGammaSpace(color.rgb); #endif return color; }

LinearToGammaSpace是Unity的内置函数,在Built-in管线可用。Custom SRP下需要自己实现映射公式,或者直接用pow(color.rgb, 1.0 / 2.2)近似处理。

5.3 Editor中的调试命令

Inspector面板最底部的Debug区域提供以下三个按钮:

  • Reload:重新初始化会话,适合窗口重启后重新绑定
  • Capture Now:强制立即抓一帧,不等待Update计时器
  • Dump Info:把当前窗口的句柄、进程名、实际尺寸、DPI缩放值全部打印出来

在自动化测试脚本中也可以调用:

# 配合Unity命令行运行 Unity.exe -batchmode -projectPath . -executeMethod UwcTest.RunCapture -logFile capture_test.log

然后在编辑器脚本里写:

public static void RunCapture() { var manager = GameObject.FindObjectOfType<UwcManager>(); var result = manager.CaptureOnce(); Debug.Log($"Capture result: {result}"); }

这是验证插件是否正常工作的最快路径,不需要打开Play模式,直接在EditMode就能看采集会话是否建立成功。

5.4 常见的错误信息对照表

错误提示原因对策
0x80070005 Access is denied窗口属于更高权限的进程(如管理员命令行)以管理员身份运行Unity
0x80070057 E_INVALIDARG窗口最小化或不存在先检查IsWindowVisible
0x8889000BGPU资源丢失,长时间锁屏或驱动重置监听OnErrorOccurred并自动恢复
The request is not supported系统版本过旧,不支持DXGI 1.5升级到Windows 10 21H2及以上

注意第2条很频繁——当你用WindowTitle关键字匹配时,窗口的最小化状态会导致宽度为0,Texture创建立即失败。固定流程应该是先调用window.IsIconic判断,如果是则强制恢复窗口或用ShowWindow重新显示,再继续采集流程。

5.5 同步周期对功耗的影响

如果把Capture模式设置为Manual后暂停纹理更新,插件不会自动停止D3D资源占用的GPU拷贝操作。检测这个问题的方法:打开Windows自带的任务管理器,查看Desktop Window Manager进程的GPU占用率,如果持续高于20%,说明即使不更新纹理,后台采集线程仍在运行。解决方法是在窗口失焦时主动调用windowTexture.StopCapture(),聚焦后再恢复,这种方式在自动导览项目中能明显降低裸功耗和风扇噪声。

最后,在Shader中使用UNITY_SAMPLE_TEX2D_SAMPLER替代tex2D,能规避URP下因为平台差异带来的采样结果偏移,这也是把这个插件从Built-in管线迁移到URP时必须做的改动。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询