class_name Camera extends Camera3D @export var center_offset := Vector3(0, 1.4, 0) var center: Vector3 var look_offset: Vector3 var target_position: Vector3 @export_group("Collision") @export var min_height_above_ground: float = 0 @export_flags_3d_physics var collision_mask := 1 var occlusion: Dictionary var occlusion_distance := 0.0 @export_group("Rotation") @export var sensitivity: float = 0.2 @export var min_angle_x: float = -80.0 @export var max_angle_x: float = 45.0 var angle_x: float var angle_y: float var look_enabled: bool @export_group("Zoom") @export var zoom_speed := 0.5 @export var zoom_interpolation := 7.5 @export var zoom_min := 2.5 @export var zoom_max := 8.0 var distance: float var target_distance: float func _ready(): distance = 7.5 target_distance = distance sensitivity = deg_to_rad(sensitivity) min_angle_x = deg_to_rad(min_angle_x) max_angle_x = deg_to_rad(max_angle_x) Global.camera = self update_look_offset() func _unhandled_input(event: InputEvent): if !event.is_action("look"): return match event.is_pressed(): true: start_look() false: end_look() func _input(event: InputEvent): if event.is_action_pressed("zoom_in", true): target_distance -= zoom_speed on_distance_changed() get_viewport().set_input_as_handled() return if event.is_action_pressed("zoom_out", true): target_distance += zoom_speed on_distance_changed() get_viewport().set_input_as_handled() return if !look_enabled: return if not event is InputEventMouseMotion: return angle_x -= event.relative.y * sensitivity angle_y -= event.relative.x * sensitivity angle_x = clampf(angle_x, min_angle_x, max_angle_x) update_look_offset() func _process(delta: float): if !Global.player: return center = Global.player.position + center_offset distance = Math.dampf(distance, target_distance, zoom_interpolation * delta) target_position = center + look_offset * distance if min_height_above_ground > 0: var result := check_terrain() if result && target_position.y - result.position.y < min_height_above_ground: target_position.y = result.position.y + min_height_above_ground global_position = target_position check_occlusion() if occlusion_distance < distance: global_position = center + look_offset * occlusion_distance look_at(center) func start_look(): DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_CAPTURED) look_enabled = true UI.unfocus() func end_look(): DisplayServer.mouse_set_mode(DisplayServer.MOUSE_MODE_VISIBLE) look_enabled = false func on_distance_changed(): target_distance = clampf(target_distance, zoom_min, zoom_max) Global.camera_attributes.dof_blur_far_distance = target_distance + 1.0 func update_look_offset(): look_offset = get_offset(Vector3.BACK) func get_offset(vec: Vector3) -> Vector3: vec = vec.rotated(Vector3.RIGHT, angle_x) vec = vec.rotated(Vector3.UP, angle_y) return vec func check_terrain() -> Dictionary: var space := get_world_3d().direct_space_state var from := Vector3(target_position.x, 100, target_position.z) var to := Vector3(target_position.x, -100, target_position.z) var query := PhysicsRayQueryParameters3D.create(from, to, collision_mask) return space.intersect_ray(query) func check_occlusion(): occlusion_distance = INF var rect := get_viewport().get_visible_rect() var space := get_world_3d().direct_space_state send_ray_to_screen(rect.position, space) send_ray_to_screen(Vector2(rect.end.x, rect.position.y), space) send_ray_to_screen(rect.get_center(), space) send_ray_to_screen(Vector2(rect.position.x, rect.end.y), space) send_ray_to_screen(rect.end, space) func send_ray_to_screen(point: Vector2, space: PhysicsDirectSpaceState3D): var screen_position := project_position(point, near) var query := PhysicsRayQueryParameters3D.create(center, screen_position, collision_mask) var hit := space.intersect_ray(query) if !hit: return var hit_point := hit.position as Vector3 var hit_distance := (hit_point - center).length() if hit_distance < occlusion_distance: occlusion = hit occlusion_distance = hit_distance