174 lines
5.1 KiB
GDScript
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
|