“一台主机,一个窗口,三块画面同时显示,还不能上Switchboard”,这是我在一个展厅交互项目里被临时加上的需求。当时项目基于UE5.6,nDisplay是官方多屏渲染方案,但默认工作流离不开Switchboard这套调度工具,团队里没人想为一个单机项目去维护额外的进程管理服务。折腾了一周,最终走通了单节点、多Viewport、免Switchboard的完整流程。这篇就把配置思路、配置文件怎么改、打包注意什么一次写清楚,给同样被这个需求卡住的同学参考。
1. 先把方案看明白:nDisplay单机单窗口是怎么个玩法
1.1 传统nDisplay集群架构的“重”,和这次要绕开的东西
nDisplay从诞生起就是为分布式渲染设计的。常规用法是搞一台Primary Node(主节点)加若干台Secondary Node(从节点),每台机器跑一个或多个UE进程,通过集群同步协议把渲染帧对齐,输出到LED屏、环幕或投影矩阵。Switchboard就是干这个的:帮你发现节点机器、推送配置、统一启动进程、监控状态,还能控制USB设备。
但注意,Switchboard只是“调度工具”,它不等于nDisplay。nDisplay渲染模块本身是直接内嵌在引擎里的,启动时读一个.ndisplay格式的配置文件,按里面定义好的节点、窗口、视口、相机、投影关系去渲染。也就是说,只要你能手动把配置文件准备好,把UE进程以正确的参数跑起来,完全可以不启动Switchboard。单机场景下尤其合适:没有跨机器帧同步需求,没有网络延迟问题,节点只有自己一台,配置一次到位。
如果你想在UE5.6里做“单窗口多画面”,本质上就是让其中一个Node进程在自己的窗口里开出多个Viewport,每个Viewport指向不同的相机。这个能力nDisplay原生就支持,只不过很多资料只讲集群案例,很少有人专门整理单机单窗口的配置方式。
1.2 单窗口多画面的三种实现路径
我在项目里比对了三条路,各有取舍:
第一种是纯常规UE方式:关卡蓝图里用Scene Capture 2D组件捕捉不同相机画面,渲染到RenderTarget,再把RenderTarget贴到UI或3D物件上。优势是理解门槛低,但画面数量一多,每多一个Scene Capture就多一遍场景绘制,性能损耗很大,而且矩阵校正、后期处理、HDR输出都麻烦,展览项目根本不够用。
第二种是真正启动nDisplay集群模式,但只用本机一个Node,多Viewport铺满窗口,这也是本文要展开的做法。它的好处是渲染路径完全走nDisplay,支持每视口独立相机、独立投影、独立曝光、独立后期,能拿来做LED屏仿真和立体画面拼接。不依赖Switchboard的话,只需要通过命令行参数把配置文件和节点名传进去即可。
第三种是UE5.6里新增的“多进程/多用户”方案,但那个需要额外进程管理,更偏向多机协同,和“单机单窗口”的目标相反,直接排除。
结论很明确:想拿nDisplay做单机多画面,第二路径最靠谱。
1.3 这套方案适合谁、不适合谁
说白了,这套方案适合下面几类场景:展厅里一台高性能工作站推多屏内容但物理显示器不多,想在一个窗口里先预览多个机位画面;舞台预演里需要同时看正视角、侧视角、顶视角;测试nDisplay配置时不想起完整集群,想在单进程里快速验证。我个人做过最典型的案例就是客户要求在同一个窗口里并行显示“主视角+特写视角+俯视小地图”三种画面,最后用这个方案交付。
不适合谁呢?如果你要做的是大规模LED屏精确拼接、需要硬件Genlock同步、或跨多台机器分布式渲染,那还是老实回到Switchboard+标准集群流程。单机单窗口在帧率同步上主要靠引擎内部同步机制,无法做到专业级硬件同步。
2. 场景与插件准备,一个都不能少
2.1 引擎版本、项目设置和插件清单
建议直接使用UE5.6系列版本。较老版本比如4.27的nDisplay配置结构和新版有差异,很多字段不通用,本文提到的东西建议统一理解为UE5.x的配置口径。
创建项目后,先到Edit -> Plugins里搜索并启用以下插件模块:
- nDisplay(必须)
- DisplayCluster Configuration(必须)
- nDisplay Render Sync(建议保留默认)
- DisplayCluster Stage Monitoring(可以禁用,这是运维监控用的)
启用后重启编辑器。如果项目是C++项目,还必须在Build.cs文件的PublicDependencyModuleNames或PrivateDependencyModuleNames里加入DisplayCluster和DisplayClusterConfiguration这两个模块,不然打包时候nDisplay相关类会被裁剪掉。我吃过这个亏,后面打包章节再细说。
2.2 场景里该摆什么,先画个“相机矩阵”
这一步要求你在场景里先把多个相机摆好。单机多画面的本质是“多个相机渲染进同一窗口的不同矩形区域”,所以相机数量直接决定画面数量。我通常习惯在场景中按功能命名相机,比如Cam_Main、Cam_Focus、Cam_Overview。名字很关键,因为配置文件里每个Viewport要引用相机的Actor名称,名字对不上直接黑屏。
如果涉及LED或投影屏幕,建议再用DisplayCluster的Screen组件在场景中摆出对应的屏幕面。简单场景可以用DisplayClusterScreenActor,摆好后记得给Screen也是一个可识别的名称,比如Scr_MainScreen。窗口Viewport通过投影设置关联到这个Screen,引擎会算出正确的透视投影矩阵。
2.3 配置nDisplay的两种方式
第一种是用编辑器自带面板:Window -> Display Cluster Configuration,新建配置,然后在编辑器里可视化设置节点、窗口、视口。这个适合新手,界面直观,但它最终生成的还是.ndisplay JSON文件。第二种是直接写JSON配置,然后用命令行加载,这也是免Switchboard流程里最核心的一环。
我个人建议先用面板生成一个模板,把JSON导出来,再手工修改。这样既能看到正确字段名,又不会因为最初手写语法错误浪费大量时间。把”工具生成+手改”结合起来最稳妥。
3. 手写 .ndisplay 配置:核心中的核心
3.1 配置文件骨架:cluster、nodes、window、viewport
nDisplay配置文件长得像这样,我简化掉了一部分字段,保留最关键的结构:
{ "Version": "9.0", "Meta": { "Name": "SingleWindowPreview", "Description": "单机单窗口多画面配置示例", "VersionId": "1.0" }, "Cluster": { "PrimaryNode": { "Id": "node1", "Address": "127.0.0.1", "Port_ClusterSync": 41001, "Port_RenderSync": 41002, "Port_ClusterEventsJson": 41003, "Port_ClusterEventsBinary": 41004, "Port_SoundSync": 41005 }, "ClusterSync": { "Master": "node1", "TimeOut": 2000, "ConnectRetriesAmount": 15, "ConnectRetryDelay": 0.1, "FrameSyncPolicy": { "Type": "None" }, "RenderSyncPolicy": { "Type": "None" } }, "Nodes": { "node1": { "Host": "127.0.0.1", "Renderer": "Engine", "Sound": true, "Window": { "Id": "win1", "Title": "NCnDisplay", "Size": { "Width": 3840, "Height": 1080 }, "Viewports": [] } } } } }在单机部署下,最核心的修改点有三个:
PrimaryNode.Id要和Nodes里唯一节点的Id保持一致,因为我们没有从节点,主节点就是它自己。ClusterSync里的FrameSyncPolicy和RenderSyncPolicy都设成None。多机集群要做帧同步和渲染同步,单机单窗口没必要,反而会因为等待同步信号造成卡顿。- 所有网络地址填
127.0.0.1即可。
3.2 单节点多Viewport配置实例:三画面平铺
窗口准备开3840x1080,三个画面横向铺开,每个1280宽。对应Viewports配置如下:
"Viewports": [ { "Id": "vp_main", "Camera": "Cam_Main", "Projection": { "Type": "Screen", "ScreenId": "Scr_MainScreen" }, "Pos": { "X": 0, "Y": 0 }, "Size": { "X": 1280, "Y": 1080 } }, { "Id": "vp_focus", "Camera": "Cam_Focus", "Projection": { "Type": "Screen", "ScreenId": "Scr_FocusScreen" }, "Pos": { "X": 1280, "Y": 0 }, "Size": { "X": 1280, "Y": 1080 } }, { "Id": "vp_overview", "Camera": "Cam_Overview", "Projection": { "Type": "Screen", "ScreenId": "Scr_OverviewScreen" }, "Pos": { "X": 2560, "Y": 0 }, "Size": { "X": 1280, "Y": 1080 } } ]这里Pos是Viewport左上角在窗口内的像素坐标。Size是像素宽高。三个Viewport相加正好等于窗口分辨率,画面之间没有缝也没有重叠,视觉上就像一个窗口里的三分屏内容。
如果要带一点缝隙或画框效果,可以故意让每个Viewport的Size略小于均分值,比如1280改成1256,坐标保持1280的步进,画面之间就会露出背景色,UI上更像监控墙。这个技巧在预览项目中特别实用。
3.3 视口坐标计算方法
多画面铺排本质就是计算矩形布局。公式不复杂,用总窗口宽度W和高度H,横向分N列,纵向分M行:
- 每格宽度:
cellW = W / N - 每格高度:
cellH = H / M - 第i行第j列格子坐标:
Pos.X = j * cellWPos.Y = i * cellH
比如一个3x2布局,窗口1920x1080,每个格子就是640x540,坐标从(0,0)、(640,0)、(1280,0)换行到(0,540)、(640,540)、(1280,540)依次排开。这个逻辑跟写CSS栅格系统一个道理,先拿计算器把像素坐标算好,再填进配置文件。
比较常见的是不同画面比例不一样,比如主视角占大区域,侧视和顶视占小区域。那就先把窗口做主次划分,再按区域给每个Viewport起止坐标。只要保证坐标和尺寸不越界、不重叠,nDisplay都会按你给的区域渲染。
3.4 投影与相机:为什么你的画面总是“歪”的
很多新手只填Camera,不填Projection,结果渲染出来的画面视角完全对不上。nDisplay里Camera决定“从哪个位置看”,Projection决定“往哪个方向以什么方式看”。两者的关系类似摄影时选择机位和镜头,机位定了、焦段变了画面也会变。
单机调试时最省心的Projection方式是挂Screen。场景里每个相机配一个DisplayClusterScreenActor,相当于给相机定了一个虚拟取景框,nDisplay会计算相机相对该Screen的透视关系,生成匹配的投影矩阵。这样只要保证相机的FOV范围能覆盖Screen,画面基本就是你要的效果。
如果你做的是纯预览用途,不需要物理屏幕面,也可以把Projection类型设为Manual或Camera,手动给一个标准透视投影矩阵,或者直接按视口矩形比例生成相机投影。但手动矩阵调起来很反直觉,我建议第一版先用Screen方式跑通,再根据需要改。
4. 不装Switchboard,也能把nDisplay跑起来
4.1 从命令行直接启动
这是整套流程里最关键的“技巧”。nDisplay支持启动参数指定配置文件和节点名。打包后或编辑器运行时,格式如下:
YourProject.exe /Game/Maps/YourMap -dc_cfg=SingleWindowPreview.ndisplay -dc_node=node1 -windowed -ResX=3840 -ResY=1080参数细节说明:
-dc_cfg指定ndisplay配置文件路径,必须是相对路径或绝对路径。建议放在打包exe同级目录下的Config文件夹里,路径最省心。-dc_node指定当前进程以哪个节点身份启动,这里填配置里Nodes下的Id,也就是node1。-windowed指定窗口模式运行。-ResX / -ResY设置窗口初始分辨率,要和配置里的窗口大小一致,否则布局会被系统强行缩放。
如果你在编辑器里测试,入口类似:
UnrealEditor.exe "D:/Projects/MyProject/MyProject.uproject" /Game/Maps/YourMap -game -dc_cfg=SingleWindowPreview.ndisplay -dc_node=node1 -windowed -ResX=3840 -ResY=1080实测下来,只要能正确读到配置文件,进程启动后nDisplay模块会在初始化阶段直接读取配置,激活对应Viewport渲染路径。不装Switchboard完全没问题。
4.2 配套批处理脚本,一键启动
命令行参数太长,现场部署不可能让人手敲。我习惯在打包目录下放一个start_multi.bat,内容大概这样:
@echo off set EXE=YourProject.exe set MAP=/Game/Maps/YourMap set CFG=Config/SingleWindowPreview.ndisplay set NODE=node1 start "" "%EXE%" %MAP% -dc_cfg=%CFG% -dc_node=%NODE% -windowed -ResX=3840 -ResY=1080需要窗口无边框时,可以在配置文件的Window节点下加:
"Window": { "Id": "win1", "Title": "NCnDisplay", "Size": { "Width": 3840, "Height": 1080 }, "Borders": false }Borders设成false之后窗口就没有标题栏和边框,屏幕利用率更高。这在展览机器上几乎是标配。
4.3 项目设置里固化默认配置
如果不想每次启动都传参,可以在Project Settings里找到nDisplay(DisplayCluster)相关设置,把配置文件和节点名直接填进默认参数。这样打包程序双击启动就会自动按配置进入多画面显示模式。
注意这里有个坑:如果你同时指定了项目设置默认配置、又在命令行传了参数,命令行参数优先级更高。现场部署用批处理传参反而更灵活,项目设置里的作为兜底。
4.4 关卡蓝图的自动启动逻辑
有些项目启动阶段需要先播放一段片头、等资源加载完再进入nDisplay显示模式。这种情况可以把启动逻辑放到关卡蓝图或GameMode里,在BeginPlay后延迟几秒,用控制台命令动态加载nDisplay。
伪代码思路如下:
BeginPlay Delay 3.0 Execute Console Command: "DisplayClusterStartNode"具体控制台命令名在不同版本里可能有差异,可以先用DisplayCluster.前缀在控制台里Tab搜索补全。我当初为了做到“程序启动自动进入多画面,同时保留跳过键”,费了点时间,但最终效果很好。这个方案的好处是不用依赖外部传参,缺点是代码侵入性高,后续维护起来要多留注释。
5. 打包流程与部署检查
5.1 打包前的插件与模块确认
在File -> Package Project之前,先检查三件事:
- 插件列表里nDisplay相关插件要勾选Enabled for Target Platform,尤其Windows平台。
- 如果有C++模块,项目Build.cs要确保包含DisplayCluster模块,否则nDisplay类不会进二进制,打包后启动会静默失败。
- 场景关卡一定要在Project Settings的Maps & Modes里加入Packaging List,否则可能打包后空白场景或场景加载失败。
纯蓝图项目因为插件是随着项目一起打包的,上面第一点检查到位就行。
5.2 配置文件随包走
.ndisplay配置文件默认不会被引擎自动打包到Cooked目录,因为它是运行时外部配置文件,不是UAsset。我建议把配置文件放到工程根目录下像Config/DisplayCluster/这样的子目录,然后在打包后的exe同级目录下也放一份。批处理脚本里的-dc_cfg直接用相对路径指向它。
曾经有同事把配置文件放在Content目录里,以为会被一起Cook,结果打包后启动直接报找不到配置。配置文件不能依赖Cook流程,手动复制最稳妥。现场部署时可以加一个--cfg-path判断逻辑,找不到配置文件就弹提示框,避免演示时黑屏都不知道原因。
5.3 实机验证步骤
打包完成后,按这个顺序验证:
- 先不带
-dc_cfg参数启动,确认关卡正常加载、普通渲染没问题。这一步排除地图和资源缺失。 - 再带
-dc_cfg参数启动,确认窗口标题、分辨率正确。 - 检查每个Viewport是否出现对应相机画面,画面比例是否正确。
- 用性能分析工具看单个Viewport的GPU渲染耗时,确认多视口没有造成重复绘制浪费。
验收现场我建议准备一台无边框显示器或直接把画面投射到大屏,观察画面边缘是否对齐。单机单窗口因为都是同一个进程渲染,画面延迟是一致的,不会出现多机模式下的同步偏差。
6. 踩坑记录与问题速查
6.1 启动后只有一个全屏画面,没有多视口
现象是窗口确实开了,但只有一个视角铺满全屏。排查路径:
- 确认
-dc_node参数和配置文件里Nodes节点Id一致,如果没匹配到节点,nDisplay会退化成默认单视口模式。 - 确认
Viewports数组里所有条目坐标加起来等于窗口尺寸。比如窗口3840宽,三个Viewport却都设了X=0,它们会相互覆盖,看起来还是单画面。 - 确认
Window.Size和命令行的-ResX/-ResY一致,不一致时系统会缩放布局,出现异常排列。
6.2 视口黑屏或报“Can't find camera”
这是最常见的配置问题。nDisplay运行时按名称查找场景Actor,只要相机名字和配置里的Camera字段不一致,对应Viewport直接黑屏。
解决方式:打开Outliner,逐个确认相机Actor的Name,连同父级路径都最好记录。比如Blueprint_Camera里挂的Camera组件,默认Name可能不是你在Outliner里看到的那个,配置时要填组件名称而非Actor名称。建议直接使用Level上放置的CameraActor,名称最好全英文字母加下划线,避免中文或特殊字符导致编码问题。
6.3 打包后配置路径读不到
打包后启动时配置加载失败,常见原因就是在打包目录里没有同步.ndisplay文件。因为配置文件是运行时加载的外部文件,Cook不会自动带过去。
另外注意路径里的斜杠方向,Windows下用相对路径最保险。直接放exe同级目录,-dc_cfg=SingleWindowPreview.ndisplay最简单。
6.4 渲染性能突然爆炸
单窗口多画面本质是同一场景渲染多遍,三画面就是三遍Draw Call加光照计算。如果项目里GPU粒子、动态阴影、体积雾开得很高,性能下降会非常明显。
缓解手段:
- 每个相机设置合适的Culling Distance和Max Draw Distance,远景内容不要进入渲染范围。
- 小画面如果只是监控用途,可以把对应Viewport的分辨率降低,比如
Size改成小于实际窗口格子的值,让它实际渲染更少像素。 - 适当降低阴影分辨率和后期特效,多视口下这些成本会被成倍放大。
- 同一个相机画面重复出现在不同Viewport时,可以利用nDisplay的Viewport关联功能共用渲染结果,减少重复计算。
6.5 常见配置项速查表
| 配置项 | 作用 | 单机单窗口建议值 |
|---|---|---|
| PrimaryNode.Address | 主节点通信地址 | 127.0.0.1 |
| FrameSyncPolicy.Type | 帧同步策略 | None |
| RenderSyncPolicy.Type | 渲染同步策略 | None |
| Window.Size | 窗口像素宽高 | 与你期望输出一致 |
| Viewports.Pos / Size | 视口位置 / 尺寸 | 按像素坐标手算 |
| Viewports.Camera | 视口对应相机 | 场景中CameraActor名称 |
| Projection.Type | 投影方式 | Screen或Manual |
| Window.Borders | 是否显示窗口边框 | false |
这个表是我每次搭建配置前都会过一遍的清单。不要嫌字段多,绝大多数问题都出在字段没有对齐上。
最后聊一点我自己的体会:这套免Switchboard的单机多画面方案,本质上没有绕过任何核心机制,而是利用了nDisplay本身配置驱动、启动灵活的特点。对中小型项目来说,省掉Switchboard之后部署成本大幅下降,现场一台主机拷贝完exe和配置文件就能跑。要扩展成多窗口预览,就复制一个Node配置,改一下Window尺寸和Viewport坐标,再用批处理多启动一次exe。只要理解“Node是进程、Window是窗口、Viewport是窗口内渲染区域”这三层关系,后续改布局、加画面都是顺水推舟的事。