实现指南:SubViewport 双摄像机 + Shader 实时画面合成)
示例工程【免费下载链接】godot-demo-projectsDemonstration and Template Projects项目地址https://gitcode.com/GitHub_Trending/go/godot-demo-projects点击查看免费下载导读本文基于 godot-demo-projects 仓库中的 Dynamic Split Screen 官方示例系统讲解如何在 Godot 中实现动态分屏Dynamic / Voronoi Split Screen效果当两名玩家彼此靠近时共用一整块屏幕一旦分开则屏幕被一条随玩家位置变化的任意角度斜线一分为二各自拥有完整视野。读完本文你将掌握 SubViewport 双摄像机渲染、shader 逐像素画面选择、分割线几何计算与抗锯齿绘制、以及分裂阈值/线宽/线色等核心参数的调优方法可直接复刻到双人本地同屏游戏如合作平台跳跃、本地对战中。什么是动态分屏传统同屏游戏通常只有两种选择要么所有玩家共享同一个摄像机视野要么固定把屏幕切成左右或上下两半。动态分屏Dynamic Split Screen也常被称为 Voronoi Split Screen是一种折中方案由 Lego 系列视频游戏普及玩家靠近时两玩家距离低于阈值两架摄像机汇聚于同一位置屏幕不分割两人共享完整画面玩家分开时屏幕被分割分割线永远垂直于连接两位玩家的线段因此它可以是任意角度而不是固定的垂直或水平线。该示例位于 viewport/dynamic_split_screen/是一个独立的 Godot 工程说明文档见 viewport/dynamic_split_screen/README.md。语言为 GDScript Godot Shader Language 中的renderer/rendering_methodgl_compatibility因此它也能稳定运行在低端 GPU 与 Web 导出平台上。整个 viewport 目录下其他演示均只使用 GDScript动态分屏是其中唯一同时使用着色器语言的演示参见 viewport/README.md。核心架构两个 Viewport 一个全屏 TextureRect 一个 Shader整个效果只有三个关键拼图结构非常精简两个 SubViewport各自包含一架独立的 Camera3DViewport1/Camera1、Viewport2/Camera2分别渲染玩家 1 与玩家 2 的视野一个铺满全屏的 TextureRect节点名为View挂载ShaderMaterial其 shader 就是 split_screen.gdshader一个 CameraController 控制脚本camera_controller.gd负责每一帧移动两台摄像机、判断是否分屏、并把两个视口的纹理 玩家屏幕位置 分割线参数通过set_shader_parameter()送入 shader。数据流向是两架摄像机渲染各自的 SubViewport →viewport1.get_texture()/viewport2.get_texture()作为sampler2Duniform 传入 shader → shader 在fragment()中逐像素决定输出哪一方的画面。以 split_screen.tscn 为例场景树的骨架为World3D ├── Cameras (camera_controller.gd) │ ├── View # 全屏 TextureRect挂 ShaderMaterial │ ├── Viewport1 │ │ └── Camera1 │ └── Viewport2 │ └── Camera2 ├── Player1 (CharacterBody3D, 红色) ├── Player2 (CharacterBody3D, 蓝色) ├── Ground (StaticBody3D) └── Walls (7 组彩色墙体, 用于演示遮挡效果)两个 SubViewport 在场景中初始尺寸为100x100但 camera_controller.gd 的_on_size_changed()会在运行时把它们的尺寸同步为窗口可见区域大小func _on_size_changed() - void: var screen_size : get_viewport().get_visible_rect().size $Viewport1.size screen_size $Viewport2.size screen_size view.material.set_shader_parameter(viewport_size, screen_size)同时该方法把窗口尺寸写入 shader uniformviewport_size供 shader 内做像素级坐标换算。注意脚本还连接了get_viewport().size_changed信号因此窗口拉伸、全屏切换后两个 SubViewport 会自动跟随重设尺寸。SubViewport 还开启了msaa_3d 22x MSAA以获得更平滑的边缘。摄像机管理把两架摄像机放到连接玩家的线段上camera_controller.gd 的核心思想写在文件头注释中摄像机放在连接两个玩家的线段上——玩家足够近时放在中点否则放在相距固定距离的位置。每帧_process()都会依次调用_move_cameras()与_update_splitscreen()。摄像机位置计算func _move_cameras() - void: var position_difference : _get_position_difference_in_world() var distance : clampf(_get_horizontal_length(position_difference), 0, max_separation) position_difference position_difference.normalized() * distance camera1.position.x player1.position.x position_difference.x / 2.0 camera1.position.z player1.position.z position_difference.z / 2.0 camera2.position.x player2.position.x - position_difference.x / 2.0 camera2.position.z player2.position.z - position_difference.z / 2.0辅助函数把三维向量投影到水平面func _get_position_difference_in_world() - Vector3: return player2.position - player1.position func _get_horizontal_length(vec: Vector3) - float: return Vector2(vec.x, vec.z).length()距离只取(x, z)水平分量即摄像机的分裂与高度差无关这符合俯视视角双人游戏的直觉。distance被clampf限制在[0, max_separation]于是存在两种状态玩家实际水平距离 ≤max_separationdistance即实际距离两台摄像机对称地位于连线中点两侧、实际重叠在中点此时整个屏幕只用玩家 1 的视野渲染两人在同一块屏幕上游戏玩家实际水平距离 max_separationdistance被钳制为max_separation两架摄像机分别停在距各自玩家max_separation / 2的位置彼此相距固定值屏幕开始分裂。分屏状态判定func _is_split_state() - bool: var position_difference : _get_position_difference_in_world() var separation_distance : _get_horizontal_length(position_difference) return separation_distance max_separationmax_separation即屏幕开始分裂的临界距离。在 split_screen.tscn 中该值被场景覆盖为10.0脚本默认值为20.0并以split_active布尔值传给 shader。SplitScreen Shader逐像素选择画面与绘制分割线着色器 split_screen.gdshader 类型为canvas_item使用render_mode unshaded不参与光照直接输出合成结果。它通过 7 个 uniform 接收全部动态数据uniform类型含义viewport_sizevec2视口/窗口像素尺寸用于 UV 坐标到像素坐标的换算viewport1sampler2D玩家 1 所在 SubViewport 的渲染纹理viewport2sampler2D玩家 2 所在 SubViewport 的渲染纹理split_activebool是否处于分屏状态为false时整屏输出viewport1player1_positionvec2玩家 1 在屏幕上的 UV 坐标由 GDScript 侧unproject_position计算player2_positionvec2玩家 2 在屏幕上的 UV 坐标split_line_thicknessfloat分割线宽度hint_range(0, 10, 0.1)像素split_line_colorvec3分割线颜色分割线的几何构造分割线必须垂直于连接两位玩家的线段因此其斜率由两个玩家在屏幕上的 UV 差决定vec2 dx player2_position - player1_position; float split_slope; if (dx.y ! 0.0) { split_slope dx.x / dx.y; } else { split_slope 100000.0; // High value (vertical split) if dx.y 0 }分割线经过屏幕中心(0.5, 0.5)由斜率和屏幕宽高推出线的两端点起点在x0、终点在xwidth随后计算每个像素在直线上对应的 y 值vec2 split_origin vec2(0.5, 0.5); vec2 split_line_start vec2(0.0, height * ((split_origin.x - 0.0) * split_slope split_origin.y)); vec2 split_line_end vec2(width, height * ((split_origin.x - 1.0) * split_slope split_origin.y)); float split_current_y (split_origin.x - UV.x) * split_slope split_origin.y; float split_player1_position_y (split_origin.x - player1_position.x) * split_slope split_origin.y;像素归属判断比较当前像素UV.y与分割线在UV.x处的 y 值再结合玩家 1 相对分割线的位置决定该像素显示哪一方的画面dx.y 0时退化为近垂直分割if (UV.y split_current_y) { if (player1_position.y split_player1_position_y) { COLOR vec4(view1, 1.0); } else { COLOR vec4(view2, 1.0); } } else { if (player1_position.y split_player1_position_y) { COLOR vec4(view1, 1.0); } else { COLOR vec4(view2, 1.0); } }当split_active为false玩家靠得很近时整个fragment()直接输出view1} else { COLOR vec4(view1, 1.0); }这与 GDScript 侧两摄像机重叠、只渲染玩家 1 视野的逻辑完全对应——同一时刻只有一种有效画面画面切换天然无缝。抗锯齿分割线shader 内嵌了一个计算点到直线距离的辅助函数来源注释指向 StackOverflow 的经典线段距离公式float distance_to_line(vec2 p1, vec2 p2, vec2 point) { float a p1.y - p2.y; float b p2.x - p1.x; return abs(a * point.x b * point.y p1.x * p2.y - p2.x * p1.y) / sqrt(a * a b * b); }在fragment()末尾若像素到分割线的距离小于线宽则用mix在分割线颜色与画面颜色之间做线性混合距离越近越接近线色从而得到平滑的抗锯齿边界float distance_to_split_line distance_to_line(split_line_start, split_line_end, vec2(UV.x * width, UV.y * height)); if (distance_to_split_line split_line_thickness) { // Draw antialiased split line. COLOR.rgb mix(split_line_color, COLOR.rgb, distance_to_split_line / split_line_thickness); }核心可调参数分裂阈值、线宽与线色camera_controller.gd 通过 4 个export变量暴露全部调优点在编辑器 Inspector 中即可修改参数类型默认值作用max_separationfloat20.0场景覆盖为10.0两玩家水平距离达到该值时屏幕开始分裂低于该值则共享单屏split_line_thicknessfloat3.0分割线宽度像素split_line_colorColorColor.BLACK分割线颜色adaptive_split_line_thicknessbooltrue分割线宽度是否随玩家距离自适应变化其中自适应宽度由_update_splitscreen()中的插值实现玩家刚超过分裂阈值时线条几乎不可见随着距离增大逐渐变粗直到两倍阈值距离时达到完整宽度if adaptive_split_line_thickness: var position_difference : _get_position_difference_in_world() var distance : _get_horizontal_length(position_difference) thickness lerpf(0, split_line_thickness, (distance - max_separation) / max_separation) thickness clampf(thickness, 0, split_line_thickness) else: thickness split_line_thickness注意这里使用的是未钳制的实际距离因此distance可以大于max_separation插值比例(distance - max_separation) / max_separation在距离超过2 * max_separation后会被clampf封顶。若设为false线宽恒为split_line_thickness。每帧如何把数据送进 Shaderfunc _update_splitscreen() - void: var screen_size : get_viewport().get_visible_rect().size var player1_position : camera1.unproject_position(player1.position) / screen_size var player2_position : camera2.unproject_position(player2.position) / screen_size # ... 厚度计算 ... view.material.set_shader_parameter(split_active, _is_split_state()) view.material.set_shader_parameter(player1_position, player1_position) view.material.set_shader_parameter(player2_position, player2_position) view.material.set_shader_parameter(split_line_thickness, thickness) view.material.set_shader_parameter(split_line_color, split_line_color)unproject_position()把玩家的世界坐标投影到摄像机屏幕坐标再除以screen_size归一化为 UV从而让 shader 知道两位玩家此刻在屏幕上的位置。而在_ready()中两个视口的渲染纹理只绑定一次view.material.set_shader_parameter(viewport1, viewport1.get_texture()) view.material.set_shader_parameter(viewport2, viewport2.get_texture())因为 SubViewport 的纹理引用不会因尺寸变化而失效无需每帧重设。split_screen.tscn 中ShaderMaterial的初始参数如split_line_thickness 10.0仅作为编辑器中的占位初值运行时会被脚本覆盖。玩家控制与输入映射player.gd 是一个极简的CharacterBody3D通过player_id1 或 2拼接动作名用Input.get_vector()读取输入施加加速度后乘以0.9模拟摩擦力最后move_and_slide()export_range(1, 2) var player_id : 1 export var walk_speed : 2.0 func _physics_process(_delta: float) - void: var move_direction : Input.get_vector( move_left_player str(player_id), move_right_player str(player_id), move_up_player str(player_id), move_down_player str(player_id), ) velocity.x move_direction.x * walk_speed velocity.z move_direction.y * walk_speed velocity * 0.9 move_and_slide()输入动作定义在 project.godot 的[input]段完整映射如下动作玩家 1红色玩家 2蓝色上W、手柄左摇杆轴 1 负向device 0I、↑、手柄右摇杆轴 3 负向device 0/ 左摇杆轴 1 负向device 1下S、手柄左摇杆轴 1 正向K、↓、手柄右摇杆轴 3 正向 / 左摇杆轴 1 正向左A、手柄左摇杆轴 0 负向J、←、手柄右摇杆轴 2 负向 / 左摇杆轴 0 负向右D、手柄左摇杆轴 0 正向L、→、手柄右摇杆轴 2 正向 / 左摇杆轴 0 正向即WASD 控制玩家 1红色IJKL 或方向键控制玩家 2蓝色同时支持手柄两个手柄分别控制两位玩家。动作 deadzone 统一为0.2。此外 project.godot 还把物理帧率设为common/physics_ticks_per_second120使玩家移动与摄像机跟随更加顺滑场景中的两架摄像机都设置了current trueCamera1与Camera2各自在所属 SubViewport 内生效。运行与体验用 Godot 4.x工程config/features标记为4.7建议使用 4.7 或更新的 4.x 版本打开 viewport/dynamic_split_screen/project.godot直接运行主场景 split_screen.tscnWASD移动红色玩家 1IJKL或方向键移动蓝色玩家 2两人靠近时屏幕合并分开时屏幕被斜线一分为二分割线随相对方位实时改变角度。场景中的Ground使用FastNoiseLite 渐变色生成的NoiseTexture2D作为地表贴图Walls由 7 组共 42 面彩色墙体构成用于在分屏时制造明显的视角差异。墙面颜色来自演示辅助脚本 wall_coloring.gd它是个tool脚本_ready()时遍历walls组并给每面墙随机albedo_color让不同分屏区域更容易辨认若不需要随机染色移除该脚本即可。把该方案移植到自己的游戏要在自己的双人同屏项目里复刻这套系统只需按以下步骤搭建视口添加两个SubViewport各含一架Camera3D与一个全屏TextureRect把 split_screen.gdshader 挂到其ShaderMaterial上接入控制脚本复制 camera_controller.gd把$../Player1、$../Player2、$View、$Viewport1、$Viewport2等节点路径改为自己场景中的实际路径脚本通过onready按路径引用这些节点见 camera_controller.gd配置输入参照 project.godot 的[input]段定义两组移动动作让 player.gd 通过player_id动态读取调参在Cameras节点的 Inspector 中调整max_separation决定何时分屏、split_line_thickness与split_line_color分割线观感、adaptive_split_line_thickness是否让线宽随距离渐变。常见变体固定分屏把max_separation设为0则两玩家一旦分开就立即分屏分割线仍随相对方位任意旋转仅上下/左右分屏分割线角度完全由玩家相对位置决定这是动态的体现若想固定为水平/垂直可在 shader 中忽略split_slope计算、直接按player1_position.x或.y与UV.x或.y比较多玩家扩展两个以上玩家需要更多 SubViewport 与更复杂的区域划分如加权 Voronoi 分割本示例的双人结构是理解其原理的最佳起点。动态分屏最大的价值在于它既避免了固定分屏在两人靠近时的视野浪费又解决了共享单屏在两人远离时视野不足的问题且分界线随玩家方位连续变化、过渡平滑。配合 SubViewport 的独立渲染两套摄像机互不干扰天然支持两人看同一场景的不同角落这一核心需求。若希望深入了解 Viewport 节点的更多用法如 2D/3D 混合渲染、屏幕捕捉等可继续翻阅仓库 viewport/ 目录下的其他演示。赞分享示例工程【免费下载链接】godot-demo-projectsDemonstration and Template Projects项目地址https://gitcode.com/GitHub_Trending/go/godot-demo-projects点击查看免费下载相关推荐Universal Split Screen零成本实现多人游戏分屏的终极方案Universal Split Screen零成本实现多人游戏分屏的终极方案 还在为无法与朋友家人共享游戏乐趣而烦恼吗Universal Split Scr桌面应用游戏开发系统编程如何用Universal Split Screen实现电脑游戏分屏多人游玩完整指南如何用Universal Split Screen实现电脑游戏分屏多人游玩完整指南 想要在一台电脑上和朋友同时玩游戏的梦想现在可以轻松实现Universal桌面应用游戏开发系统编程Universal Split Screen轻松实现PC游戏本地多人分屏的神器Universal Split Screen轻松实现PC游戏本地多人分屏的神器 还在为PC游戏无法与朋友一起本地游玩而苦恼吗Universal Split桌面应用游戏开发系统编程上一篇Bilibili-Evolved性能预算监控CI/CD集成下一篇如何在CodeSandbox中利用AI辅助进行科研写作和Web应用开发终极完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考