Files
modbison/scripts/studio/studio_viewport_camera.gd
T

174 lines
5.1 KiB
GDScript

class_name StudioViewportCamera
extends Camera3D
## Professional 3D Studio Camera controller supporting:
## - Freecam flying (WASD + Q/E)
## - Turntable Orbit & Panning (RMB/MMB drag)
## - Selection framing/focus (F key)
## - Orthographic view presets (Top, Front, Side, Perspective)
signal view_preset_changed(preset_name: String)
enum ViewPreset { PERSPECTIVE, TOP, FRONT, SIDE }
var current_preset: ViewPreset = ViewPreset.PERSPECTIVE
# Camera movement parameters
var move_speed: float = 12.0
var fast_multiplier: float = 2.5
var slow_multiplier: float = 0.3
var mouse_sensitivity: float = 0.003
var zoom_speed: float = 1.5
# Orbit / focus state
var focal_point: Vector3 = Vector3.ZERO
var orbit_distance: float = 15.0
var _yaw: float = -0.75
var _pitch: float = -0.5
var _rmb_down: bool = false
var _mmb_down: bool = false
var _move_input: Vector3 = Vector3.ZERO
func _ready() -> void:
current = true
_update_orbit_transform()
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_RIGHT:
_rmb_down = mb.pressed
elif mb.button_index == MOUSE_BUTTON_MIDDLE:
_mmb_down = mb.pressed
elif mb.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom_camera(-zoom_speed)
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
zoom_camera(zoom_speed)
elif event is InputEventMouseMotion:
var mm := event as InputEventMouseMotion
if _rmb_down:
# Freecam / FPS mouse look
_yaw -= mm.relative.x * mouse_sensitivity
_pitch = clampf(_pitch - mm.relative.y * mouse_sensitivity, -PI * 0.49, PI * 0.49)
rotation = Vector3(_pitch, _yaw, 0.0)
elif _mmb_down:
# Pan camera parallel to viewport
var pan_speed := orbit_distance * 0.0015
var right := global_transform.basis.x
var up := global_transform.basis.y
var delta := (-right * mm.relative.x + up * mm.relative.y) * pan_speed
global_position += delta
focal_point += delta
elif event is InputEventKey:
var k := event as InputEventKey
if k.pressed and not k.echo:
if k.keycode == KEY_F:
focus_on(focal_point, 5.0)
elif k.keycode == KEY_1:
set_view_preset(ViewPreset.FRONT)
elif k.keycode == KEY_3:
set_view_preset(ViewPreset.SIDE)
elif k.keycode == KEY_7:
set_view_preset(ViewPreset.TOP)
elif k.keycode == KEY_5:
toggle_perspective_ortho()
func _process(delta: float) -> void:
if _rmb_down:
_process_keyboard_flight(delta)
func _process_keyboard_flight(delta: float) -> void:
var dir := Vector3.ZERO
if Input.is_physical_key_pressed(KEY_W):
dir -= global_transform.basis.z
if Input.is_physical_key_pressed(KEY_S):
dir += global_transform.basis.z
if Input.is_physical_key_pressed(KEY_A):
dir -= global_transform.basis.x
if Input.is_physical_key_pressed(KEY_D):
dir += global_transform.basis.x
if Input.is_physical_key_pressed(KEY_E):
dir += Vector3.UP
if Input.is_physical_key_pressed(KEY_Q):
dir -= Vector3.UP
if dir.length_squared() > 0.01:
var speed := move_speed
if Input.is_physical_key_pressed(KEY_SHIFT):
speed *= fast_multiplier
elif Input.is_physical_key_pressed(KEY_CTRL):
speed *= slow_multiplier
global_position += dir.normalized() * speed * delta
focal_point = global_position - global_transform.basis.z * orbit_distance
func zoom_camera(amount: float) -> void:
orbit_distance = clampf(orbit_distance + amount, 1.0, 300.0)
if projection == PROJECTION_ORTHOGONAL:
size = clampf(size + amount, 1.0, 200.0)
else:
_update_orbit_transform()
func focus_on(target_pos: Vector3, radius: float = 3.0) -> void:
focal_point = target_pos
orbit_distance = clampf(radius * 2.5, 3.0, 50.0)
_update_orbit_transform()
func _update_orbit_transform() -> void:
var rot_basis := Basis.from_euler(Vector3(_pitch, _yaw, 0.0))
var offset := rot_basis * Vector3(0.0, 0.0, orbit_distance)
_set_camera_pos(focal_point + offset)
rotation = Vector3(_pitch, _yaw, 0.0)
func _set_camera_pos(pos: Vector3) -> void:
if is_inside_tree():
global_position = pos
else:
position = pos
func set_view_preset(preset: ViewPreset) -> void:
current_preset = preset
match preset:
ViewPreset.TOP:
projection = PROJECTION_ORTHOGONAL
size = orbit_distance
rotation_degrees = Vector3(-90, 0, 0)
_set_camera_pos(focal_point + Vector3(0, orbit_distance, 0))
view_preset_changed.emit("Top")
ViewPreset.FRONT:
projection = PROJECTION_ORTHOGONAL
size = orbit_distance
rotation_degrees = Vector3(0, 0, 0)
_set_camera_pos(focal_point + Vector3(0, 0, orbit_distance))
view_preset_changed.emit("Front")
ViewPreset.SIDE:
projection = PROJECTION_ORTHOGONAL
size = orbit_distance
rotation_degrees = Vector3(0, 90, 0)
_set_camera_pos(focal_point + Vector3(orbit_distance, 0, 0))
view_preset_changed.emit("Side")
ViewPreset.PERSPECTIVE:
projection = PROJECTION_PERSPECTIVE
_pitch = -0.5
_yaw = -0.75
_update_orbit_transform()
view_preset_changed.emit("Perspective")
func toggle_perspective_ortho() -> void:
if projection == PROJECTION_PERSPECTIVE:
projection = PROJECTION_ORTHOGONAL
size = orbit_distance
else:
projection = PROJECTION_PERSPECTIVE