尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity双屏显示实战:Multi-Display与RenderTexture方案踩坑总结

Unity双屏显示实战:Multi-Display与RenderTexture方案踩坑总结 前阵子接了一个线下展览馆的互动项目需求一句话就能说清楚一台主机带两块屏一块主屏播放沉浸画面给观众看一块副屏给现场工作人员做控制和数据监控。我当时第一反应是“这还不简单把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低一次纹理拷贝控制台、监视器、副屏UIViewport 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组件上有targetDisplayCanvas组件上也有targetDisplay它们都接受Display 1到Display 8的索引默认值是Display 1也就是0号主屏。我见过很多新手在搞双屏时直接复制一个摄像机设置targetDisplay为Display 2结果发现副屏完全没反应。问题往往不在targetDisplay本身而在于他复制出来的摄像机把所有渲染层级都剔除了或者这个摄像机的Depth比原主摄像机小导致画面被覆盖。更深一层的坑是当多个摄像机的targetDisplay指向同一块屏幕时最终显示结果是这些摄像机按Depth顺序混合后的叠加画面。也就是说想让副屏显示一套完全独立的画面你不仅要设置targetDisplay还要注意Culling Mask只保留副屏需要的LayerDepth也要单独安排否则副屏会看到主摄像机和副摄像机画面叠在一起的效果。Canvas的targetDisplay同理。一个Screen Space Overlay的Canvas一旦指定了Target Display它就会在对应的显示器上按UI布局重新排版。这个属性很容易被遗漏很多项目副屏黑屏的原因不是摄像机而是UI Canvas忘改成Display 2。2.3 副屏分辨率设置与UI坐标换算双屏项目里有一类问题特别隐蔽就是分辨率的不一致。Windows桌面如果是两块屏幕扩展模式主屏1920x1080副屏4KUnity里如果只按主屏分辨率激活副屏你会看到副屏画面被拉升变形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改成1080x1920Fullscreen Mode选成Fullscreen Window这样副屏会以无边框窗口方式全屏显示避免独占全屏模式在AltTab切换时卡死。这个环节最容易被忽略的是启动分辨率弹窗。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 CameratargetDisplay设为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).AddComponentCamera(); 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方法它能返回鼠标在哪个显示器以及相对坐标返回值是一个Vector3x和y是对应显示器上的坐标z分量的含义在不同Unity版本里有点微妙项目里务必以当前版本的官方文档为准。4.5 不是所有平台都支持多显示器做Unity双屏开发还要意识到平台限制。WebGL平台根本不会暴露多显示器Display.displays长度永远是1iOS和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打印清楚再开始写业务逻辑。这个习惯帮我少加了不少班也分享给你。
返回列表