From ee075f162d2316b6c7972f3bff12b4da17ba1c05 Mon Sep 17 00:00:00 2001 From: DoS Date: Fri, 24 Apr 2026 23:16:21 +0200 Subject: [PATCH] Add multimonitor configuration --- .../libmaszyna/editor/user_settings_dock.tscn | 105 +++++++++++ .../editor/user_settings_numeric.gd | 17 ++ .../editor/user_settings_numeric.gd.uid | 1 + .../player/multi_monitor_manager.gd | 175 ++++++++++++++++++ .../player/multi_monitor_manager.gd.uid | 1 + addons/libmaszyna/player/player.gd | 5 +- addons/libmaszyna/rail_vehicle_3d.gd | 11 +- src/core/UserSettings.cpp | 12 +- 8 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 addons/libmaszyna/editor/user_settings_numeric.gd create mode 100644 addons/libmaszyna/editor/user_settings_numeric.gd.uid create mode 100644 addons/libmaszyna/player/multi_monitor_manager.gd create mode 100644 addons/libmaszyna/player/multi_monitor_manager.gd.uid diff --git a/addons/libmaszyna/editor/user_settings_dock.tscn b/addons/libmaszyna/editor/user_settings_dock.tscn index 51393fd5..a2a1b03e 100644 --- a/addons/libmaszyna/editor/user_settings_dock.tscn +++ b/addons/libmaszyna/editor/user_settings_dock.tscn @@ -3,6 +3,7 @@ [ext_resource type="Script" uid="uid://bgejg0pbyfexc" path="res://addons/libmaszyna/editor/user_settings_dock.gd" id="1_c24dg"] [ext_resource type="Script" uid="uid://bc8likdrai4yp" path="res://addons/libmaszyna/editor/user_settings_check_button.gd" id="2_2cs74"] [ext_resource type="Script" uid="uid://c3o4dhxm72pj1" path="res://addons/libmaszyna/editor/user_settings_dropdown.gd" id="3_hwkpe"] +[ext_resource type="Script" uid="uid://dsysphlfnqhm" path="res://addons/libmaszyna/editor/user_settings_numeric.gd" id="4_numeric"] [node name="Maszyna Settings" type="PanelContainer" unique_id=1678337651] anchors_preset = 15 @@ -242,6 +243,110 @@ key = "anisotropic_filtering_level" layout_mode = 2 text = "Anisotropic filtering level" +[node name="LabelMultiMonitor" type="Label" parent="VBoxContainer" unique_id=830455753] +layout_mode = 2 +text = "Multi Monitor Settings" + +[node name="HBoxMode" type="HBoxContainer" parent="VBoxContainer" unique_id=1611371117] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxMode" unique_id=1668562301] +layout_mode = 2 +text = "Mode (0: Off, 1: 2 Screens, 2: 3 Screens)" + +[node name="SpinBox" type="SpinBox" parent="VBoxContainer/HBoxMode" unique_id=117545305] +layout_mode = 2 +max_value = 2.0 +value = 2.0 +script = ExtResource("4_numeric") +section = "window" +key = "multi_monitor_mode" + +[node name="HBoxRotation" type="HBoxContainer" parent="VBoxContainer" unique_id=1089496719] +layout_mode = 2 + +[node name="LabelRotL" type="Label" parent="VBoxContainer/HBoxRotation" unique_id=1621369721] +layout_mode = 2 +text = "Rotation Left" + +[node name="SpinBoxRotL" type="SpinBox" parent="VBoxContainer/HBoxRotation" unique_id=1672907741] +layout_mode = 2 +min_value = -360.0 +max_value = 360.0 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_rotation_left" + +[node name="LabelRotR" type="Label" parent="VBoxContainer/HBoxRotation" unique_id=733294976] +layout_mode = 2 +text = "Rotation Right" + +[node name="SpinBoxRotR" type="SpinBox" parent="VBoxContainer/HBoxRotation" unique_id=648214065] +layout_mode = 2 +min_value = -360.0 +max_value = 360.0 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_rotation_right" + +[node name="HBoxOffset" type="HBoxContainer" parent="VBoxContainer" unique_id=1237172646] +layout_mode = 2 + +[node name="LabelOffL" type="Label" parent="VBoxContainer/HBoxOffset" unique_id=2036809170] +layout_mode = 2 +text = "Offset Left" + +[node name="SpinBoxOffL" type="SpinBox" parent="VBoxContainer/HBoxOffset" unique_id=729655493] +layout_mode = 2 +min_value = -1000.0 +max_value = 1000.0 +step = 0.1 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_offset_left" + +[node name="LabelOffR" type="Label" parent="VBoxContainer/HBoxOffset" unique_id=1363556279] +layout_mode = 2 +text = "Offset Right" + +[node name="SpinBoxOffR" type="SpinBox" parent="VBoxContainer/HBoxOffset" unique_id=396858379] +layout_mode = 2 +min_value = -1000.0 +max_value = 1000.0 +step = 0.1 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_offset_right" + +[node name="HBoxTilt" type="HBoxContainer" parent="VBoxContainer" unique_id=1047807381] +layout_mode = 2 + +[node name="LabelTiltL" type="Label" parent="VBoxContainer/HBoxTilt" unique_id=1706646835] +layout_mode = 2 +text = "Tilt Left" + +[node name="SpinBoxTiltL" type="SpinBox" parent="VBoxContainer/HBoxTilt" unique_id=861828296] +layout_mode = 2 +min_value = -90.0 +max_value = 90.0 +step = 0.1 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_tilt_left" + +[node name="LabelTiltR" type="Label" parent="VBoxContainer/HBoxTilt" unique_id=836358378] +layout_mode = 2 +text = "Tilt Right" + +[node name="SpinBoxTiltR" type="SpinBox" parent="VBoxContainer/HBoxTilt" unique_id=834709548] +layout_mode = 2 +min_value = -90.0 +max_value = 90.0 +step = 0.1 +script = ExtResource("4_numeric") +section = "window" +key = "monitor_tilt_right" + [node name="InfoMessageWindow" type="Window" parent="." unique_id=308881657] unique_name_in_owner = true initial_position = 4 diff --git a/addons/libmaszyna/editor/user_settings_numeric.gd b/addons/libmaszyna/editor/user_settings_numeric.gd new file mode 100644 index 00000000..169b3ff4 --- /dev/null +++ b/addons/libmaszyna/editor/user_settings_numeric.gd @@ -0,0 +1,17 @@ +@tool +extends SpinBox + +@export var section: String +@export var key: String +@export var default: float = 0.0 + +func _ready(): + UserSettings.config_changed.connect(_load_from_settings) + value_changed.connect(_on_value_changed) + _load_from_settings() + +func _load_from_settings(): + value = UserSettings.get_setting(section, key, default) + +func _on_value_changed(new_value: float): + UserSettings.save_setting(section, key, new_value) diff --git a/addons/libmaszyna/editor/user_settings_numeric.gd.uid b/addons/libmaszyna/editor/user_settings_numeric.gd.uid new file mode 100644 index 00000000..9b47d487 --- /dev/null +++ b/addons/libmaszyna/editor/user_settings_numeric.gd.uid @@ -0,0 +1 @@ +uid://dsysphlfnqhm diff --git a/addons/libmaszyna/player/multi_monitor_manager.gd b/addons/libmaszyna/player/multi_monitor_manager.gd new file mode 100644 index 00000000..9a10e103 --- /dev/null +++ b/addons/libmaszyna/player/multi_monitor_manager.gd @@ -0,0 +1,175 @@ +extends Node +class_name MultiMonitorManager + +var windows: Array[Window] = [] +var cameras: Array[Dictionary] = [] + +@onready var player: Node = get_parent() +@onready var main_camera: Camera3D = get_viewport().get_camera_3d() + +var _last_mode: int = -1 + +func _ready() -> void: + process_priority = 100 + UserSettings.config_changed.connect(_update_windows) + # Wait a bit for the main camera to be initialized if needed + _update_windows() + +func _exit_tree() -> void: + _clear_windows() + +func _clear_windows() -> void: + for win in windows: + win.queue_free() + windows.clear() + cameras.clear() + +func _update_windows() -> void: + var mode: int = UserSettings.get_setting("window", "multi_monitor_mode", 0) + + # Ensure windows are not embedded + var main_vp: Viewport = get_viewport() + main_vp.gui_embed_subwindows = false + + if mode > 0: + var main_win: Window = main_vp.get_window() + if main_win: + # Only maximize if not already in a windowed mode + if main_win.mode != Window.MODE_WINDOWED: + main_win.mode = Window.MODE_MAXIMIZED + + if mode == _last_mode: + # Just sync settings for existing windows + for win: Window in windows: + _sync_window_settings(win) + return + + _last_mode = mode + _clear_windows() + + if mode == 0: + return + + var screen_count: int = DisplayServer.get_screen_count() + # if screen_count < 2: + # return + + if mode == 1: # 2 Screens: Front, Right + _create_window(1, 1) # monitor 1, side 1 (Right) + elif mode == 2: # 3 Screens: Left, Front, Right + _create_window(1 if screen_count > 1 else 0, -1) # Left + _create_window(2 if screen_count > 2 else 0, 1) # Right + +func _create_window(screen_index: int, side: int) -> void: + var win: Window = Window.new() + win.title = "MaSzyna - Screen " + str(screen_index) + (" (Right)" if side > 0 else " (Left)") + + # Set position to the target screen + win.current_screen = screen_index + win.mode = Window.MODE_WINDOWED + win.size = Vector2i(1280, 720) + + # Share the world + win.world_3d = get_viewport().world_3d + + var cam: Camera3D = Camera3D.new() + # Copy main camera settings + cam.fov = main_camera.fov + cam.near = main_camera.near + cam.far = main_camera.far + + win.add_child(cam) + get_tree().root.add_child(win) + + # Forward input to main camera + win.window_input.connect(_on_window_input) + + # Sync render settings + _sync_window_settings(win) + + windows.append(win) + cameras.append({"camera": cam, "side": side}) + + win.show() + +func _sync_window_settings(win: Window) -> void: + var main_vp: Viewport = get_viewport() + win.msaa_3d = main_vp.msaa_3d + win.screen_space_aa = main_vp.screen_space_aa + win.use_debanding = main_vp.use_debanding + win.use_occlusion_culling = main_vp.use_occlusion_culling + win.mesh_lod_threshold = main_vp.mesh_lod_threshold + +func _on_window_input(event: InputEvent) -> void: + get_viewport().push_input(event) + +func _process(_delta: float) -> void: + if cameras.is_empty(): + return + + main_camera = get_viewport().get_camera_3d() + if not main_camera: + return + + var main_vp: Viewport = get_viewport() + var main_vp_size: Vector2 = main_vp.get_visible_rect().size + + var main_v_fov: float = 0.0 + var main_h_fov: float = 0.0 + var focal_length: float = 0.0 + + if main_camera.keep_aspect == Camera3D.KEEP_WIDTH: + main_h_fov = deg_to_rad(main_camera.fov) + focal_length = (main_vp_size.x / 2.0) / tan(main_h_fov / 2.0) + main_v_fov = 2.0 * atan((main_vp_size.y / 2.0) / focal_length) + else: + main_v_fov = deg_to_rad(main_camera.fov) + focal_length = (main_vp_size.y / 2.0) / tan(main_v_fov / 2.0) + main_h_fov = 2.0 * atan((main_vp_size.x / 2.0) / focal_length) + + for cam_info: Dictionary in cameras: + var cam: Camera3D = cam_info["camera"] + var side: int = cam_info["side"] + var win: Window = cam.get_window() + var win_size: Vector2 = Vector2(win.size) + + # Adjust side camera FOV to match main camera's pixel scale + # This ensures objects have the same size across screens regardless of window resolution + var side_v_fov: float = 2.0 * atan((win_size.y / 2.0) / focal_length) + var side_h_fov: float = 2.0 * atan((win_size.x / 2.0) / focal_length) + + var rot_deg: float = 0.0 + var offset: float = 0.0 + var tilt: float = 0.0 + if side > 0: + rot_deg = UserSettings.get_setting("window", "monitor_rotation_right", 0.0) + offset = UserSettings.get_setting("window", "monitor_offset_right", 0.0) + tilt = UserSettings.get_setting("window", "monitor_tilt_right", 0.0) + else: + rot_deg = UserSettings.get_setting("window", "monitor_rotation_left", 0.0) + offset = UserSettings.get_setting("window", "monitor_offset_left", 0.0) + tilt = UserSettings.get_setting("window", "monitor_tilt_left", 0.0) + + var angle: float = 0.0 + if rot_deg != 0.0: + angle = deg_to_rad(rot_deg) + else: + # Auto-calculate angle to match edges perfectly + angle = (main_h_fov + side_h_fov) / 2.0 + + cam.global_transform = main_camera.global_transform + + # Use world-space rotation to keep the horizon level and the locomotive straight + # across physically vertical monitors even when looking up or down. + cam.global_rotate(Vector3.UP, -angle * side) + + # Apply manual tilt (roll) if specified + if tilt != 0.0: + cam.rotate_object_local(Vector3.FORWARD, deg_to_rad(tilt) * side) + + cam.fov = rad_to_deg(side_v_fov) + cam.h_offset = main_camera.h_offset + (offset * side) + cam.v_offset = main_camera.v_offset + cam.near = main_camera.near + cam.far = main_camera.far + cam.keep_aspect = main_camera.keep_aspect diff --git a/addons/libmaszyna/player/multi_monitor_manager.gd.uid b/addons/libmaszyna/player/multi_monitor_manager.gd.uid new file mode 100644 index 00000000..627a9d98 --- /dev/null +++ b/addons/libmaszyna/player/multi_monitor_manager.gd.uid @@ -0,0 +1 @@ +uid://dgyswajs3fyny diff --git a/addons/libmaszyna/player/player.gd b/addons/libmaszyna/player/player.gd index 33df5ef3..5b999e02 100644 --- a/addons/libmaszyna/player/player.gd +++ b/addons/libmaszyna/player/player.gd @@ -15,7 +15,10 @@ var controlled_vehicle:RailVehicle3D var _dirty: bool = false func _ready() -> void: - pass + if not has_node("MultiMonitorManager"): + var manager: MultiMonitorManager = MultiMonitorManager.new() + manager.name = "MultiMonitorManager" + add_child(manager) func _process(delta): if _dirty: diff --git a/addons/libmaszyna/rail_vehicle_3d.gd b/addons/libmaszyna/rail_vehicle_3d.gd index b2bb6de4..9050f7d0 100644 --- a/addons/libmaszyna/rail_vehicle_3d.gd +++ b/addons/libmaszyna/rail_vehicle_3d.gd @@ -155,7 +155,12 @@ func _update_head_display(): _needs_head_display_update = false -func _process(delta): +func _physics_process(delta: float) -> void: + if not Engine.is_editor_hint(): + if _controller: + global_position += global_basis.z * delta * _controller.state.get("velocity", 0.0) + +func _process(delta: float) -> void: if _dirty: _dirty = false if head_display_e3d_path: @@ -171,10 +176,6 @@ func _process(delta): _t = 0.0 _update_head_display() - if not Engine.is_editor_hint(): - if _controller: - position += Vector3.FORWARD * delta * _controller.state.get("velocity", 0.0) - func _schedule_head_display_update(): _needs_head_display_update = true diff --git a/src/core/UserSettings.cpp b/src/core/UserSettings.cpp index 043128c3..d02b870e 100644 --- a/src/core/UserSettings.cpp +++ b/src/core/UserSettings.cpp @@ -1,11 +1,9 @@ #include "UserSettings.hpp" -#include #include #include #include #include -#include namespace godot { @@ -48,9 +46,19 @@ namespace godot { render["anisotropic_filtering_level"] = RenderingServer::VIEWPORT_ANISOTROPY_DISABLED; render["use_taa"] = true; + Dictionary window; + window["multi_monitor_mode"] = 0; + window["monitor_rotation_right"] = 0.0; + window["monitor_offset_right"] = 0.0; + window["monitor_tilt_right"] = 0.0; + window["monitor_rotation_left"] = 0.0; + window["monitor_offset_left"] = 0.0; + window["monitor_tilt_left"] = 0.0; + defaults["e3d"] = e3d; defaults["maszyna"] = maszyna; defaults["render"] = render; + defaults["window"] = window; } void UserSettings::_apply_defaults() {