Unity双屏显示实战:Multi-Display与RenderTexture方案踩坑总结
2026/9/18 11:02:28 网站建设 项目流程

前阵子接了一个线下展览馆的互动项目,需求一句话就能说清楚:一台主机带两块屏,一块主屏播放沉浸画面给观众看,一块副屏给现场工作人员做控制和数据监控。我当时第一反应是“这还不简单,把Unity打开,两个Game视图往两块显示器上一拖就行”,真做完才发现,从双屏激活、分辨率适配到UI事件处理,坑比想象中多。用Unity做双屏显示,官方确实提供了Multi-Display方案,但网上多数教程只写到“调一下targetDisplay就算完了”,一到实际现场,各种黑屏、同屏、鼠标偏位、UI跑到错误屏幕的问题全冒出来了。这篇文章把我这次用Unity 2022 LTS落地双屏显示的完整过程整理出来,从方案选型讲到底层原理,再给出一套可以直接抄的工程配置和踩坑清单,适合展览展示、数字孪生监控大屏、游戏副屏这类项目参考,也适合刚接触双屏开发的Unity开发者提前避坑。

1. 开始之前先想清楚:你的双屏显示方案选对了吗

1.1 先分清你的双屏需求是哪种

双屏显示这四个字看着简单,实际项目里往往会指向完全不同的技术路线。我习惯把需求先归类成三种。

第一种是“主屏展示、副屏控制”。观众看大屏,工作人员在小屏上点按钮、切画面、看参数。这种需求的特点是主副屏内容差异大,主屏要干净,副屏要有完整交互界面。第二种是“两屏同步展示同一内容”,常见于广告机、指挥中心大屏、多块显示墙,这种需求思考的重点不是画面怎么分开,而是如何避免重复渲染导致性能浪费。第三种是“一个程序在多个窗口输出不同视角”,比如赛车游戏主屏放车手视角、副屏放仪表盘或者俯视地图,或者数字孪生项目主屏看整体场景、副屏看某个设备的细节数据。

这三种需求在Unity里对应的实现方式完全不同。如果一开始就闷头写Display.Activate加targetDisplay,很可能写到一半发现方案不匹配,回头返工。

1.2 三种实现方案怎么选

做双屏显示,Unity里面有三种常见做法,我把它们叫原生多显示器方案、RenderTexture画中画方案、Viewport Rect单窗口分区方案。

原生多显示器方案听名字就知道,是Unity官方推荐的Multi-Display机制。用Display类获取系统显示器列表,把摄像机直接指定到不同显示器上输出。这种方式适合真正的独立双屏,每一块屏幕都是全屏原生窗口,可以设置各自独立的全屏模式和分辨率,互不干扰。但它有一个容易被忽略的限制:一台摄像机只能输出到一个目标显示器。如果你想让主屏和副屏显示完全相同的画面,用这个方法反而要多开一台摄像机重新渲染一遍。

RenderTexture画中画方案是我在展览项目里用得最多的套路。思路是把某一台摄像机的输出抓成一张纹理,然后当成贴图显示在另一块屏幕的UI上。它很适合“主屏内容基本一致、副屏只是想多一个监视窗口或控制台”的场景。好处是省摄像机、省渲染开销,坏处是RT是一次纹理拷贝,分辨率太高会占用不少显存带宽,而且你要自己管理RT的尺寸和释放。

Viewport Rect方案严格来说不算真正双屏,它是把同一个屏幕的渲染区切成几块,典型做法是Camera的Viewport Rect设置成Rect(0,0,0.5f,1)和Rect(0.5f,0,0.5f,1),左右分屏。如果项目是在一块超宽屏上做分屏,这个方案够用;但如果目标是两块物理显示器独立全屏,它处理不了窗口拖拽和显示器分辨率不一致的问题。

三种方案对比可以看下面的表格:

方案是否需要多显示器副屏独立分辨率渲染开销典型场景
原生Multi-Display支持高,多摄像机多场景大屏展示、多视角游戏
RenderTexture画中画否,同屏即可跟随UI低,一次纹理拷贝控制台、监视器、副屏UI
Viewport Rect分区不支持低,单摄像机超宽屏分屏、单窗口分视角

1.3 别急着写代码,先画一张信息流向图

选定方案之后,我会在动手前先画一张简单的信息流向图,不画也行,但心里得有一张表:哪个摄像机输出到哪个显示器,哪层UI挂在哪块屏,鼠标事件在哪块屏生效。这个环节看起来多余,实际能省掉后面一堆问题。比如你决定用原生Multi-Display方案,那就要清楚主屏摄像机A渲染哪些Layer,副屏摄像机B渲染哪些Layer,两个Canvas分别挂在Target Display几号上。如果这些信息不提前理清,代码写一半就会发现副屏UI挂错Canvas、摄像机层级重复之类的低级问题。

2. Unity双屏显示核心原理:Display、targetDisplay 与分辨率的关系

2.1 Display.displays 到底代表了什么

Unity的Display类从5.x时代就开始支持,本质上做的不是屏幕切割,而是让Unity程序能把渲染结果输出到操作系统的多个原生窗口。每一个Display对象对应你显卡上识别到的一块显示器或窗口。主屏永远是Display.displays[0],后面依次排下去。

很多人以为只要电脑接了双屏,Unity启动后天然就会在第二块屏上出画面,这是错的。Unity默认只驱动主显示器,副显示器需要你在代码里明确调用Activate方法激活。这里可以打个比方:Display.displays数组是硬件设备列表,列表存在不代表设备已经被Unity的渲染管线接管。Activate的作用就是告诉Unity,这块显示器我要用,请给我创建一个渲染窗口,指定宽高和刷新率。

副屏激活以后,程序里会出现两个窗口,窗口栏上都能看到Unity的标志,如果你没有设置无边框全屏,还能用鼠标直接拖动副屏窗口到另一块显示器上。这也是现场调试时判断激活是否成功的直观信号。

2.2 targetDisplay 不是你想得那么简单的参数

targetDisplay这个词是理解双屏显示的第二个钥匙。Camera组件上有targetDisplay,Canvas组件上也有targetDisplay,它们都接受Display 1到Display 8的索引,默认值是Display 1,也就是0号主屏。

我见过很多新手在搞双屏时直接复制一个摄像机,设置targetDisplay为Display 2,结果发现副屏完全没反应。问题往往不在targetDisplay本身,而在于他复制出来的摄像机把所有渲染层级都剔除了,或者这个摄像机的Depth比原主摄像机小,导致画面被覆盖。

更深一层的坑是,当多个摄像机的targetDisplay指向同一块屏幕时,最终显示结果是这些摄像机按Depth顺序混合后的叠加画面。也就是说,想让副屏显示一套完全独立的画面,你不仅要设置targetDisplay,还要注意Culling Mask只保留副屏需要的Layer,Depth也要单独安排,否则副屏会看到主摄像机和副摄像机画面叠在一起的效果。

Canvas的targetDisplay同理。一个Screen Space Overlay的Canvas一旦指定了Target Display,它就会在对应的显示器上按UI布局重新排版。这个属性很容易被遗漏,很多项目副屏黑屏的原因不是摄像机,而是UI Canvas忘改成Display 2。

2.3 副屏分辨率设置与UI坐标换算

双屏项目里有一类问题特别隐蔽,就是分辨率的不一致。Windows桌面如果是两块屏幕扩展模式,主屏1920x1080,副屏4K,Unity里如果只按主屏分辨率激活副屏,你会看到副屏画面被拉升变形,UI错位。

正确思路是:在Player Settings里为每个Display设置独立分辨率。Unity的Display相关设置中,每个显示器都有独立的Width和Height,这个值建议直接填副屏物理分辨率,别图省事把主副屏都设成一样。注意,这里说的是Unity内部Render Texture的目标分辨率,不是操作系统桌面分辨率,两者不一样。

还有一个细节是Screen.width和Screen.height代表的是当前主窗口的分辨率,不是副屏的分辨率。如果你在代码里写Screen.width去计算副屏UI尺寸,在主屏1920x1080、副屏3840x2160的情况下,副屏UI会全部挤在左上角四分之一区域。正确做法是读取Display.displays[i].renderingWidth和renderingHeight来获取对应显示器的渲染尺寸。

2.4 多显示器场景下的事件系统

双屏显示项目的鼠标事件问题,是我在最初写方案时严重低估的一块。StandaloneInputModule默认只监听主窗口的鼠标输入,Input.mousePosition返回的永远是当前鼠标所在窗口内的坐标,Unity官方没有提供现成的跨屏UI事件分发工具。

如果副屏只是展示画面,不需要人点它,这完全无所谓。但如果副屏上有按钮、开关、滑条,事情就复杂了。方案上我一般两条路选一条:要么把交互UI全部放在主屏,副屏只做被动展示;要么在代码里用Display.RelativeMouseAt把鼠标坐标换算到对应屏幕,再手动往对应Canvas分发射线事件。后者工作量和调试成本都不小,如果是纯Unity团队做项目,我建议优先把交互收敛到主屏。

3. 实操:一个展览互动项目的双屏显示完整搭建过程

3.1 搭建前的 Player Settings 配置

进入实战环节,以我那个展览项目为例,主屏是一台1920x1080的电视,副屏是一台1920x1080的竖屏广告机,实际显示方向是1080x1920。注意,副屏是竖屏,这就对分辨率配置提出了额外要求。

打开Project Settings,找到Player选项卡,在Resolution and Presentation区域往下翻,能找到Display相关的一串设置。这里可以分别配置Display 1到Display 8的默认宽高。我把Display 1改成1080x1920,Fullscreen Mode选成Fullscreen Window,这样副屏会以无边框窗口方式全屏显示,避免独占全屏模式在Alt+Tab切换时卡死。

这个环节最容易被忽略的是启动分辨率弹窗。Unity默认在游戏启动时可能会弹出分辨率选择框,现场开机如果没人去点,程序永远停在弹窗界面。我一般会把Run In Background打开,同时关闭Resolution Dialog,确保程序一启动就按配置进入全屏状态。这一点对无人值守的展厅场景极其重要。

3.2 用代码激活第二个显示器

配置好Editor侧的设置后,运行时还需要调用Display.Activate。最稳妥的位置是主场景里某个单例对象的Start方法,一进场景就执行。

void Start() { int screenCount = Display.displays.Length; Debug.Log("检测到显示器数量: " + screenCount); if (screenCount > 1) { Display.displays[1].Activate(1080, 1920, 60); } }

Activate的三个参数分别是宽、高、刷新率。这里的宽高要和Player Settings里Display 1的配置一致,不然会以代码里传入的值重新创建窗口,Player Settings里那套配置反而被覆盖,容易造成现场效果和本地调试对不上。刷新率如果你的副屏是60Hz就填60,是120Hz的显示器也可以填120,但要先确认显卡输出支持,别乱填。

激活之后我还会在下一帧打印一次激活结果,判断Display状态是否正常。这里有一个经验,Display.Activate如果传入的分辨率跟系统桌面分辨率不匹配,有些显卡驱动会自动缩放,画面会出现黑边或拉伸,所以现场调试时第一件事就是看副屏是否有黑边。

3.3 配置主副摄像机与UI

激活副屏后,开始搭建场景。

主场景里原本有一个Main Camera,我把它指定到Display 1,也就是0号主屏。这个摄像机的Culling Mask只保留主屏要看到的物体层,Depth保持默认0即可。

然后新建一台摄像机作为Sub Camera,targetDisplay设为Display 2,也就是1号副屏。它的Culling Mask设置成副屏专用的Layer,比如我先建了一个叫“ControlUI”的层,把副屏要显示的场景物体、模型、特效全扔到这个层里。Depth设为1,避免和主屏摄像机混淆。

UI部分,我建了两个Canvas。主屏Canvas的Target Display保持Display 1,用来放观众看到的沉浸界面;副屏Canvas的Target Display改成Display 2,里面放工作人员操作面板、数据统计、警示信息。Canvas Scaler建议用Constant Pixel Size,不要用Scale With Screen Size,因为两块屏物理DPI可能不同,按比例缩放容易让副屏UI忽大忽小。

这里顺带提一个很多项目纠结的点:UI显示隐藏用SetActive还是改localScale还是移出相机。如果你问我的习惯,主屏展示层我尽量少做频繁的SetActive,因为SetActive会触发Graphic重建,在GPU和CPU上都有瞬时开销;副屏控制面板这种低频切换的,我直接用Canvas.enabled控制整体显隐,既能看到切换效果,又不用跟Graphic重建较劲。

3.4 RenderTexture 画中画与性能优化

前面说的是原生双显示器,两台摄像机各自渲染一套画面。如果项目里主副屏内容大量重复,比如副屏只是主屏画面的一个监控小窗,再开一台摄像机重新渲染性价比太低。

这种时候我推荐用RenderTexture。做法是建一块RT,把需要复用的摄像机输出到RT上,再用RawImage显示到指定Canvas。

RenderTexture rt = new RenderTexture(960, 540, 24); rt.name = "MonitorRT"; rt.Create(); Camera monitorCamera = new GameObject("MonitorCamera").AddComponent<Camera>(); monitorCamera.targetTexture = rt; monitorCamera.cullingMask = 1 << LayerMask.NameToLayer("MainScene"); monitorCamera.targetDisplay = 0; // 不直接输出到屏幕,只写RT rawImage.texture = rt;

注意,给摄像机设置了targetTexture之后,这个摄像机默认就不会再输出到屏幕了,它输出的内容只进RT。如果既想让画面出现在副屏,又不想开额外的摄像机,直接在RawImage里显示RT是完全够用的。

性能优化上,我有三个习惯。第一,RT的分辨率不要无脑跟副屏走,控制台小窗给960x540,肉眼已经看得很清楚。第二,副屏如果没有动态刷新需求,把RawImage对应的摄像机帧率限制一下,或者直接隔帧渲染。第三,如果副屏只是低频监控,可以把这个Canvas整个放到另一个层,在不需要的时候用Canvas.enabled关掉,能省下不少UI绘制开销。

4. 双屏显示常见问题与排查记录

4.1 副屏黑屏,但显示器有信号

这个问题出现的概率最高,基本每个双屏项目都会遇到一次。显示器本身有信号,说明显卡输出正常,问题出在Unity没把内容喂过去。

排查步骤我一般从代码开始:先看Project Settings里Display数量是否大于1,再看Start里有没有调用Activate。有些项目是在异步逻辑里尝试激活副屏,结果场景切换时显示器信息还没准备好,就掉进了黑屏的坑。稳妥做法是放到第一个场景的对象Start里,不要依赖异步加载。

如果Activate调了还是黑屏,接着看副屏Camera的Culling Mask和Depth。Culling Mask如果全部剔除,副屏当然什么都渲染不出来;Depth如果设置不正确,画面也会被其他相机覆盖。我的调试习惯是把这个Camera的Clear Flags临时设成纯蓝色,如果副屏能看到蓝色,说明输出通道没问题,是后续场景物体和层的问题。

4.2 两块屏画面内容一样

这个问题也有两种来源。第一种是副屏UI的Canvas targetDisplay没有改成Display 2,导致两块屏显示的是同一套UI,看起来就是画面一样。第二种是副屏用了RenderTexture,而RT的主摄像机Culling Mask恰好包含了主屏所有物体,等于是把主屏画面复制了一份。

遇到这种情况没有必要一行行查代码,先切到Scene视图看两块屏分别渲染了什么物体,然后检查摄像机tagetDisplay和Canvas targetDisplay。我最常犯的错误就是复制Camera时忘了改targetDisplay,两台摄像机都指向Display 1,结果主屏叠了两个画面,副屏黑屏。先改targetDisplay,再检查Culling Mask,基本能解决。

4.3 副屏UI不见了或显示到主屏

UI挂到错误的屏幕,属于一个很低级但很让人头疼的问题。Canvas组件上的Target Display选项非常隐蔽,它默认是Display 1,就算工程里所有UI都是新建的,只要在一个Canvas上做错了设置,副屏UI就会跑到主屏去,或者在主屏上叠一套看起来完全一样的UI。

排查时可以把两个Canvas都选中,在Inspector里检查Target Display数值。主屏Canvas为0,副屏Canvas为1。如果副屏UI使用Screen Space Camera模式,另一个常见问题是Plane Distance设置太大,UI被摄像机裁剪到场景后面去了。我的建议是副屏Canvas能使用Overlay模式就尽量用Overlay,简单直接,少一个摄像机参与裁剪,少一重出错概率。

4.4 鼠标点击副屏按钮没反应

这个问题的根源在InputModule上,前面原理部分已经提过。实际项目中,如果副屏需要交互,我基本不会在副屏上放依赖鼠标点击的UI。真的要做,也建议用数据回传方式:副屏程序把状态同步到主屏,由主屏的逻辑做决策,副屏只负责展示。

如果是开发期想验证副屏UI能不能点击,可以临时把鼠标事件绑定到主屏按钮上,或者用Screen空间坐标手动判断鼠标是否落在副屏窗口范围内。这里要关注Display.RelativeMouseAt方法,它能返回鼠标在哪个显示器以及相对坐标,返回值是一个Vector3,x和y是对应显示器上的坐标,z分量的含义在不同Unity版本里有点微妙,项目里务必以当前版本的官方文档为准。

4.5 不是所有平台都支持多显示器

做Unity双屏开发,还要意识到平台限制。WebGL平台根本不会暴露多显示器,Display.displays长度永远是1;iOS和Android原生一般也只支持主屏幕,副屏的硬件支持得依赖厂商Drivers;最稳的还是PC桌面平台,Windows、macOS、Linux都能识别多屏。做展览展示和数字孪生项目,PC是主流,这一块问题不大。但如果团队准备发WebGL版或者移动端版本,双屏功能就得提前做条件编译或者设计降级方案。

4.6 编辑器里怎么看双屏效果

这也是双屏开发常见的障碍。Unity编辑器里的Game视图通常只显示一个窗口,没有直接对应多显示器的编辑器模拟。Unity 2019之后Game视图左上角多了一个Display切换下拉框,可以临时切到Display 1预览副屏画面,但它是切换,不是同时预览,调试效率有限。

最靠谱的调试方式还是直接Build出去,在真实双屏环境里跑。开发期如果不想频繁打包,也可以临时把副屏摄像机输出到RT,然后放到主屏的一个角落窗口里观察,这样至少能看到副屏角度的画面有没有问题,等打包后把RT调试窗去掉即可。

5. 落地经验:几个让我少加班的习惯

5.1 现场布线前的分辨率清单

双屏项目最怕的不是功能写不出来,而是到了现场发现显示器和预设参数完全对不上。我现在接到双屏需求,第一步永远是找现场负责人要一份屏幕清单,记录每块屏的物理分辨率、接口类型、是否支持独立分辨率、Windows桌面显示设置是扩展模式还是复制模式。复制模式只适合两块屏完全相同分辨率的情况,一旦副屏比例不同,画面必被拉伸,所以现场必须设成扩展模式。

到了现场以后,我会先跑一个测试脚本,把Display.displays里所有显示器的renderingWidth和renderingHeight全部打印到日志里,确认Unity识别到的顺序和Windows桌面栏里的顺序一致。有些现场的显卡驱动会乱序,你以为副屏是Display 1,实际它是Display 0,导致主副屏内容对调。这个脚本一跑就知道了。

5.2 几个隐藏坑的复盘

这次项目里我踩了三个比较值得记录的坑。第一个是副屏采用竖屏显示,Player Settings和Activate参数都写了1080x1920,但现场还是黑边,原因是操作系统桌面里副屏本身没设成竖屏方向,Unity拿到的是物理分辨率而不是逻辑方向。解决办法是提前在系统层面把屏幕方向配置好,程序这边不要硬转。

第二个坑是双屏全屏窗口的焦点问题。主屏播放视频时,只要副屏被点击一下窗口,主屏视频有时会掉帧,原因是窗口失焦后Unity默认的逻辑更新优先级变了。把Project Settings里Run In Background打开能缓解大部分情况。

第三个坑是GPU性能波动。原生多显示器方案意味着主副屏两套画面同时由同一块GPU渲染,帧率在主屏有大量粒子特效时掉得很厉害。后来我用RT方案把副屏内容降到了960x540,渲染压力小了一半,帧率稳下来了。做双屏显示,性能预算从第一天就要算进去,不能等项目写完再回头优化。

说实话,双屏显示最麻烦的不是Unity API,而是现场那一堆物理层的不确定性。我现在接到这种需求,第一步永远是先把所有屏幕参数跑出来,Debug.Log打印清楚,再开始写业务逻辑。这个习惯帮我少加了不少班,也分享给你。

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

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

立即咨询