简介:面向需要在 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.cs | P/Invoke封装 | 把C++接口封成C#可调用的DllImport |
UwcCommon.cginc | Shader片段 | 内置着色器中对纹理采样和色彩空间处理的公共宏 |
关键点在于UwcManager是单例入口,它只负责建立会话,不直接产生Texture。UwcWindow才是真正代表一个可采集目标,UwcWindowTexture在Start时根据UwcWindow的共享资源句柄创建Unity Texture2D,并每帧调用Update从共享纹理读数据。
2.2 管理器的启动流程与生命周期控制
打开UwcManager.cs,核心方法集中在Start和OnApplicationQuit,伪代码如下:
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的OnWindowClosed和OnWindowSizeChanged事件,再通过Unity的SendMessage或者其他事件系统通知业务层。
比如做产线数字孪生系统,经常需要把组态软件的画面集成进来,用西门子PLC通信指令控制设备,同时把PLC面板的桌面画面作为背景叠加在虚拟机模型上。典型做法:先用UwcWindowTexture把WinCC画面加载到场景中的大屏模型面片,再用异步Socket接收PLC的交互指令,当PLC值变化时修改窗口的显隐和位置。这个插件的职责边界就是提供画面层,具体业务逻辑完全在Unity侧自己拼装。
3.4 参数表格:不同场景推荐配置
| 场景 | Capture Mode | Update Mode | 更新间隔 | 备注 |
|---|---|---|---|---|
| 数字孪生大屏 | Screen | Manual | 0.05s | 保证20FPS以上,不要跑满60 |
| 录屏/直播 | Window | Manual | 0.016s | 需要60FPS时开启垂直同步 |
| 低配工控机 | Screen | Auto | 0.1s | 默认值就够 |
| 远程桌面画中画 | Window | Manual | 0.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_ALPHACLIP和UNITY_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 |
0x8889000B | GPU资源丢失,长时间锁屏或驱动重置 | 监听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时必须做的改动。
本文还有配套的精品资源,点击获取