Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions addons/libmaszyna/editor/user_settings_dock.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions addons/libmaszyna/editor/user_settings_numeric.gd
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions addons/libmaszyna/editor/user_settings_numeric.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dsysphlfnqhm
175 changes: 175 additions & 0 deletions addons/libmaszyna/player/multi_monitor_manager.gd
Original file line number Diff line number Diff line change
@@ -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)")
Comment thread
JezSonic marked this conversation as resolved.

# Set position to the target screen
win.current_screen = screen_index
win.mode = Window.MODE_WINDOWED
win.size = Vector2i(1280, 720)
Comment thread
JezSonic marked this conversation as resolved.

# 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
Comment thread
JezSonic marked this conversation as resolved.
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
1 change: 1 addition & 0 deletions addons/libmaszyna/player/multi_monitor_manager.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dgyswajs3fyny
5 changes: 4 additions & 1 deletion addons/libmaszyna/player/player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment thread
JezSonic marked this conversation as resolved.
manager.name = "MultiMonitorManager"
add_child(manager)

func _process(delta):
if _dirty:
Expand Down
11 changes: 6 additions & 5 deletions addons/libmaszyna/rail_vehicle_3d.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
12 changes: 10 additions & 2 deletions src/core/UserSettings.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#include "UserSettings.hpp"

#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/os.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/error_macros.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {

Expand Down Expand Up @@ -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() {
Expand Down
Loading