资讯详情

Unity实现Windows桌面实时捕获与低延迟显示的关键技术解析

📅 2026/9/15 17:15:36 | 华诺云谱 👁 阅读
Unity实现Windows桌面实时捕获与低延迟显示的关键技术解析
简介面向Unity开发者的Windows桌面实时捕获插件解决在Unity中同步展示Windows桌面画面的需求适用于录屏、多屏联动、远程协助、大屏展示等场景。压缩包共141个文件包含26个C#核心脚本、13个Unity场景、3个预制体、4个材质、2个着色器、2个DLL运行库以及编辑器扩展配置文件整体仅600KB轻量且易集成。其中DLL用于底层系统API封装Shader负责桌面画面渲染C#脚本则覆盖窗口管理、桌面布局与纹理更新等核心逻辑并提供Inspector面板方便实时调整。脚本分工明确从窗口捕获到材质呈现的关键环节均有对应模块支撑便于阅读和二次修改。资源还附带基础示例可直接验证效果作者亦发布有配套用法说明可辅助理解各脚本间的协作关系。相比商业捕获方案该包更适合轻量接入。目前已有565人学习研究适合有一定Unity基础、希望在项目中快速加入桌面捕获能力的开发者也适用于桌面工具开发、视讯会议互动等方向。1. 桌面实时展现进 Unity先想清楚是“截屏”还是“视频源”把 Windows 桌面实时展现到 Unity 场景里很多人第一次接这个需求第一反应都是“截个屏再贴上去”。但真做完一版你就知道问题从来不出在“截不到”而出在“截到了也用不动”桌面窗口一拖动画面全是残影远程控制不够跟手CPU 被反复压缩拷贝吃满主线程卡到掉帧。桌面在这类需求里不是一张图片它是一个每秒更新 60 次的实时视频源你要解决的是持续取帧、低延迟上传纹理、并同步输入事件的完整链路。这篇文章就围绕 Windows 下的 Capture 工作流讲清三条捕获路线的选型边界、RenderTexture 中转的兼容参数、实时调优的具体手段以及鼠标键盘怎么回传适合正在做远程协助、会议投屏、桌面演示或 Unity 作为宿主应用的开发者参考。2. Unity 里做 Windows 桌面捕获的三条技术路线2.1 先看三条路GDI、DXGI 桌面复制、Windows.Graphics.Capture做 Unity 的 Windows 桌面实时展现本质上是“拿桌面帧 - 上传 GPU - 绘制到场景”。不同阶段有各自的方案先横向列一下方案核心 API取帧方式CPU/GPU 消耗延迟特征典型用途GDI BitBltBitBlt/PrintWindow主动拉取同步拷贝到内存CPU 高整块内存拷贝50~200ms窗口拖动易撕裂原型验证、低帧率截图DXGI Desktop DuplicationIDXGIOutputDuplication拉取 脏矩形增量GPU 为主拷贝少10~30ms画面连贯录屏软件、远程控制Windows.Graphics.CaptureGraphicsCaptureItemDirect3D11CaptureFramePool异步事件推送GPU 为主CPU 低10~40ms受会话调度影响Windows 10 1803 桌面工具GDI 的优势只有“代码短”它在 1080p 全屏下的内存拷贝开销就把收益吃光了。DXGI 桌面复制性能最好但要注意两个边界它只能复制整个输出显示器不能单独抓某个窗口而且在远程桌面会话或 GPU 资源被独占时容易取不到帧。Windows.Graphics.Capture 是微软后来主推的方向支持按窗口或按显示器捕获异步事件驱动和 Unity 的 D3D11 设备也更容易对接。2.2 为什么 GDI 只适合做原型验证GDI 截屏在 Unity 里可以先跑通“桌面 - Texture2D - RawImage”的逻辑非常适合验证 UI 交互和坐标换算后续换成性能方案时上层逻辑不用改。一个最小实现是这样using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using UnityEngine; public class GdiDesktopCapture : MonoBehaviour { [DllImport(user32.dll)] static extern IntPtr GetDC(IntPtr hwnd); [DllImport(user32.dll)] static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); [DllImport(gdi32.dll)] static extern bool BitBlt(IntPtr hdcDest, int x, int y, int w, int h, IntPtr hdcSrc, int x1, int y1, int rop); public RawImage display; Texture2D texture; int screenW, screenH; void Start() { screenW Screen.width; screenH Screen.height; texture new Texture2D(screenW, screenH, TextureFormat.BGRA32, false); } void Update() { IntPtr srcDc GetDC(IntPtr.Zero); using (Bitmap bmp new Bitmap(screenW, screenH)) { using (Graphics g Graphics.FromImage(bmp)) { IntPtr dstDc g.GetHdc(); BitBlt(dstDc, 0, 0, screenW, screenH, srcDc, 0, 0, 0x00CC0020); g.ReleaseHdc(dstDc); } var data bmp.LockBits(new Rectangle(0, 0, screenW, screenH), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); texture.LoadRawTextureData(data.Scan0, data.Stride * screenH); texture.Apply(); bmp.UnlockBits(data); } ReleaseDC(IntPtr.Zero, srcDc); display.texture texture; } }这段代码每帧做的事情是拿到桌面设备上下文用 BitBlt 把屏幕像素拷到一张 Bitmap再通过 LockBits 取到内存指针最后 LoadRawTextureData 上传 GPU。注意它的参数里0x00CC0020是 SRCCOPY 操作码表示直接拷贝TextureFormat.BGRA32对应桌面常见的 32 位 BGRA 布局比转 ARGB 省一次像素遍历。原型版本能跑但 Update 里同步整块拷贝会让主线程每帧多出 10~20ms 阻塞窗口拖动时撕裂明显所以它的定位是验证 UI 和交互不是生产方案。2.3 生产环境选择Windows.Graphics.Capture 还是 DXGI 桌面复制真实项目里大多数时候会在 DXGI 和 WGC 之间选。我的建议判断标准是如果需求是“捕获整个显示器且延迟越低越好”比如远程控制、低延迟投屏选 DXGI Desktop Duplication因为它是拉取式你可以自己控制取帧节奏如果需求是“捕获某个应用窗口或者要跟着窗口移动自动切换内容”选 Windows.Graphics.Capture它按窗口捕获的特性是 DXGI 做不到的。WGC 另一个优势是不要求独占输出DXGI 在某些 GPU 驱动下捕获时会干扰全屏游戏。但 WGC 有一个在实践中很关键的“性格”它是异步事件推送帧到达时间不完全由你控制而且应用第一次捕获时会弹出系统授权窗必须在用户点击允许之后才能拿到帧。在 Unity 编辑器里调试时授权弹窗可能会让编辑器失焦需要提醒测试人员先点掉弹窗再操作场景。综合下来做 Windows 桌面实时展现插件我一般会优先封装 WGC 暴露给 C#底层预留 DXGI 的替换接口。这样既能满足窗口跟随需求也能在低延迟场景切到桌面复制模式。3. 用 Windows.Graphics.Capture 把桌面帧送进 RenderTexture3.1 捕获链路设计采集线程、共享纹理、主线程上传WGC 拿到的原始帧是Direct3D11CaptureFrame它内部是一个 DXGI 表面不是 Unity 的 Texture2D。跨过这层差异的关键是“共享纹理”把 DXGI 表面转成一个 Unity 可以访问的 native 纹理指针然后在 C# 侧用Texture2D.CreateExternalTexture包一层最后Blit到 RenderTexture。整体链路是Windows.Graphics.Capture 帧到达 - native 插件把 IDXGISurface 转成共享纹理 - 主线程拿到纹理指针UpdateExternalTexture - Graphics.Blit 到 RenderTexture - RawImage 或自定义 Shader 显示注意 WGC 的 FrameArrived 事件运行在后台线程不能在回调里直接调用任何 Unity API。常见做法是回调里只保存一个“帧已到达”的标记真正的取帧和上传放在主线程 Update 中执行。帧率不太高时这种模式比开专用渲染线程简单得多也避开 Unity 渲染线程与主线程同步的坑。3.2 C# 侧最小实现从 native 指针到 RenderTexture假设你已经有一个 native 插件封装了 WGC对外暴露三个接口开始捕获、获取最新帧 surface 指针、释放 surface。C# 侧流程是这样的using UnityEngine; using UnityEngine.UI; using System.Runtime.InteropServices; public class WGCDesktopDisplay : MonoBehaviour { [DllImport(unity_desktop_capture)] static extern System.IntPtr CaptureStart(int monitorIndex); [DllImport(unity_desktop_capture)] static extern bool CaptureGetFrame(System.IntPtr handle, out System.IntPtr surface, out int width, out int height); [DllImport(unity_desktop_capture)] static extern void CaptureReleaseFrame(System.IntPtr handle); [DllImport(unity_desktop_capture)] static extern void CaptureStop(System.IntPtr handle); public RawImage display; System.IntPtr captureHandle; Texture2D externalTexture; RenderTexture renderTexture; int texW, texH; void Start() { captureHandle CaptureStart(0); Application.targetFrameRate 60; } void Update() { System.IntPtr surfacePtr; int w, h; if (!CaptureGetFrame(captureHandle, out surfacePtr, out w, out h)) return; if (externalTexture null || texW ! w || texH ! h) { texW w; texH h; externalTexture Texture2D.CreateExternalTexture(w, h, TextureFormat.BGRA32, false, false, surfacePtr); renderTexture new RenderTexture(w, h, 0); display.texture renderTexture; } else { externalTexture.UpdateExternalTexture(surfacePtr); } Graphics.Blit(externalTexture, renderTexture); CaptureReleaseFrame(captureHandle); } void OnDestroy() { if (captureHandle ! System.IntPtr.Zero) CaptureStop(captureHandle); if (externalTexture ! null) Destroy(externalTexture); if (renderTexture ! null) renderTexture.Release(); } }这段代码的核心逻辑是每一帧先向 native 层要最新的桌面帧指针如果纹理还没创建就根据宽高建一个外部纹理否则直接调用UpdateExternalTexture更新指针。注意这里有一个不能省的细节——TextureFormat.BGRA32必须和 DXGI 表面的像素格式一致WGC 默认给的是DXGI_FORMAT_B8G8R8A8_UNORM用 ARGB32 会导致红蓝通道互换。CreateExternalTexture的第四个参数linear设为 false是因为桌面像素是 sRGB 颜色空间让采样器做色彩空间转换否则画面会偏灰。最后 Blit 到 RenderTexture 而不是直接把 external texture 给 RawImage是为了给后续加 Shader 效果、缩放、裁剪留一个中间层。3.3 RenderTexture 参数设置与 Unity D3D 兼容要点外部纹理对接 D3D11 时RenderTexture 的创建参数会影响性能和兼容性。直接给出一组我常用的参数RenderTexture 参数推荐值说明depthBuffer0桌面画面不需要深度省显存antiAliasing1关闭 MSAA避免 Blit 时发生解析格式RenderTextureFormat.BGRA32与外部纹理格式保持一致useMipMapfalse实时视频不需要 mipmap避免每帧生成开销另外在 Project Settings 的 Player 设置里Graphics API必须保留 Direct3D11并且最好把Auto Graphics API关掉把 D3D11 放在第一位。如果项目里同时启用了 VulkanUnity 会优先走 Vulkan外部纹理的 D3D11 surface 指针就接不上了。编辑器和打包后的行为也可能不一样编辑器下外部纹理指针可以直接用打包后因为图形设备略有差异建议在真机上单独验证一次通道顺序和线性空间表现。4. 让“实时展现”真正实时帧率控制、延迟测量和丢帧策略4.1 先明确“实时”的度量方式桌面实时展现的“实时”不是看Application.targetFrameRate而是看三个数取帧到显示的端到端延迟、单位时间内画面更新的次数、以及最坏情况下的卡顿时间。帧率只衡量了第二项而用户感知到的“跟手”主要来自第一项和第三项。在同一个场景里放一个桌面时钟的实时视频流你不难发现画面明明是 60 FPS但移动鼠标时指针轨迹总比真实鼠标慢半拍这就是端到端延迟在作怪。所以调优之前先给项目加一个延迟测量点。一个简单做法是桌面端放一个带毫秒计时器的窗口Unity 场景里同一个计时器做对象对照截图对比差值如果还嫌麻烦可以直接在更新纹理时记录Time.realtimeSinceStartup - frameArriveTime但这个数据只能在 native 层拿到精确的取帧时间戳。4.2 三个影响同步质量的参数调整第一关闭 Unity 的垂直同步对实时桌面展现的影响。QualitySettings.vSyncCount设为 0然后把Application.targetFrameRate设成和桌面刷新率一致。如果你保持默认 60桌面是 144Hz 时捕获帧会被丢弃三分之一鼠标移动看起来就像在跳帧。第二RenderTexture 的尺寸不要盲目跟随桌面分辨率。4K 桌面直接建 4K RenderTextureBlit 和后续 UI 绘制开销都不小。常见做法是捕获侧就限制最大宽度比如超过 2560 时按比例缩放代价是文字清晰度下降但换来的是帧时间更稳。对远程协助这种场景清晰度优先级低于流畅度缩放是值得的。第三注意纹理上传的“阻塞”问题。UpdateExternalTexture本身不拷数据它只是把帧指针交给 GPU真正的阻塞通常发生在Graphics.Blit之后的下一帧 GPU 等待上。如果发现帧时间有一截突然变高先检查桌面是不是处于高 GPU 占用状态比如在播放视频或运行 3D 应用。这时候 CPU 并不知道 GPU 忙它会继续往 RenderTexture 写导致缓冲区堆积、延迟上跳。下面是加入帧时间监控和延迟告警的最小代码片段using System.Diagnostics; using UnityEngine; public class FrameSyncMonitor : MonoBehaviour { public float warningThresholdMs 50f; Stopwatch sw new Stopwatch(); long lastTimestamp; void Update() { sw.Stop(); long frameTimeMs sw.ElapsedMilliseconds; sw.Restart(); if (frameTimeMs warningThresholdMs) UnityEngine.Debug.LogWarning($Capture frame sync exceeded: {frameTimeMs}ms); lastTimestamp frameTimeMs; } }这里用Stopwatch而不是Time.deltaTime是因为Time.deltaTime受Time.timeScale影响而且单位精度下看不出 10ms 级别的波动。把阈值放在 Inspector 里在真机和编辑器分别调一次你会看到普通桌面操作时帧时间在 8~20ms 内波动全屏拖窗口时可能冲到 40ms 以上那个场景才是优化重点。4.3 丢帧策略显示最新帧还是排队显示桌面实时展现里最常犯的错是“取帧不够快导致队列越积越长延迟越走越大”。WGC 的 Direct3D11CaptureFramePool 支持多个帧缓冲你可以设置缓冲帧数但实践中我发现根本不需要排队桌面本身是无限帧源丢掉中间帧用户不会察觉延迟积累才会被立刻感知。正确的策略是“永远只显示最新到达的帧”。在 C# 侧这就意味着每一帧 Update 只取一次指针取不到就跳过绝不阻塞等待后台线程产生新帧。在 native 层如果你用的是 DXGI Desktop Duplication拉取接口本身就支持返回DXGI_ERROR_WAIT_TIMEOUT遇到这个码就表示没有新画面直接跳过不要死等。void Update() { // 非阻塞取帧失败立刻返回不等待新桌面帧 if (!CaptureGetFrame(captureHandle, out surfacePtr, out w, out h)) return; // 只要指针有效就立刻更新并 Blit externalTexture.UpdateExternalTexture(surfacePtr); Graphics.Blit(externalTexture, renderTexture); CaptureReleaseFrame(captureHandle); }这段代码的要点是CaptureGetFrame内部不轮询、不阻塞它只检查“最新帧是否已到达”。大多数情况下这个函数返回 true 的节奏会略低于桌面刷新率比如桌面是 60Hz 时大约每 16ms 有一个真帧但偶尔会出现连续两帧在同一个 Unity Update 周期内到达这时候只显示后一帧前一帧直接丢弃视觉上没有任何影响延迟反而更低。5. 把“看得见”升级成“操作得动”输入回传与多屏验证5.1 鼠标坐标换算与 SendInput 回传实时展现解决了“看得见”同步桌面还差最后一步鼠标和键盘事件回传。这里最隐蔽的坑是坐标换算。Unity 的 RawImage 坐标系以左下角为原点而 Windows 桌面坐标以左上角为原点再加上系统 DPI 缩放你从 UI 上拿到的点击位置必须经过两次转换才能变成桌面坐标。public void OnClickScreen(Vector2 uvPos) { // 假设 RawImage 覆盖整个可用区域uvPos 是归一化坐标 float deskW 1920f; // 从捕获纹理的物理宽度读取 float deskH 1080f; int x Mathf.RoundToInt(uvPos.x * deskW); int y Mathf.RoundToInt((1f - uvPos.y) * deskH); // 缩放因子纹理分辨率 / 物理桌面分辨率 float scaleX Screen.width / (float)texW; float scaleY Screen.height / (float)texH; x Mathf.RoundToInt(x / scaleX); y Mathf.RoundToInt(y / scaleY); Win32Input.SendMouseMove(x, y); }注意这段代码里 y 方向先做了1f - uvPos.y的翻转这步漏了的话点击位置会上下镜像。DPI 方面如果你捕获的是 4K 桌面但纹理缩到了 2560那么scaleX / scaleY就不是 1必须参与换算。Win32Input.SendMouseMove底层用mouse_event或SendInput都可以现代系统推荐SendInput它在 UIPI 下更可靠。5.2 多显示器捕获与授权提示处理WGC 的多显示器捕获方式有两种一种是让用户每次从系统弹窗里选一个显示器适合工具类应用另一种是枚举GraphicsCaptureItem给每个显示器建一个捕获会话适合固定工位的桌面同步场景。我一般建议后者的版本里加个下拉框运行时切换目标显示器而不停止采集线程。注意切换时要先释放旧的捕获会话再启动新的否则会触发“0x80070569”之类的访问拒绝错误。第一次捕获时弹出的系统授权窗无法跳过在自动测试环境里需要预先手动点一次然后把授权状态保存到本机后续启动不会再弹。5.3 验证同步质量的一个小技巧最后分享一个在实际排障里最有用的验证方法在物理桌面上打开一个网页秒表Unity 场景里用 RawImage 实时显示这个秒表同时用手机或另一块屏幕录像暂停视频后对比两份画面中毫秒数的差值就是端到端延迟。如果延迟大于 100ms优先检查捕获线程是否被阻塞而不是先去调 Unity 渲染设置如果延迟在 50ms 以内但鼠标移动不连贯把Application.targetFrameRate往上提到显示器刷新率的整数倍。这套“真实时钟对照法”比任何 Profiler 数据都直观也是我验收 Capture 插件时必做的最后一项测试。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。