Created
August 25, 2025 14:31
-
-
Save fczuardi/3f8503472b0c5e7c5aabcfd2c007c35f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extends Node3D | |
| @export var rotation_time: float = 5.0 | |
| @export var display_time: float = 1.0 | |
| @export var camera: Camera3D | |
| @export var padding: float = 3.15 | |
| @export var zoom_time: float = 0.35 | |
| @export var zoom_trans: Tween.TransitionType = Tween.TRANS_QUAD | |
| @export var zoom_ease: Tween.EaseType = Tween.EASE_OUT | |
| @export var label: Label # assign in inspector | |
| var static_bodies: Array = [] | |
| var current_index: int = 0 | |
| var rotating: bool = false | |
| var elapsed: float = 0.0 | |
| var _cam_tween: Tween = null | |
| var _first_frame: bool = true | |
| func _ready() -> void: | |
| static_bodies.clear() | |
| for n in get_children(): | |
| if n is StaticBody3D: | |
| static_bodies.append(n) | |
| for body in static_bodies: | |
| _set_visuals_visible(body, false) | |
| if static_bodies.size() > 0: | |
| _show_current() | |
| func _process(delta: float) -> void: | |
| if not rotating: | |
| return | |
| elapsed += delta | |
| var t: float = clamp(elapsed / rotation_time, 0.0, 1.0) | |
| var angle: float = lerp(0.0, TAU, t) | |
| var body := static_bodies[current_index] as StaticBody3D | |
| var rot: Vector3 = body.rotation | |
| rot.y = angle | |
| body.rotation = rot | |
| if t >= 0.5: | |
| rotating = false | |
| await get_tree().create_timer(display_time).timeout | |
| _next_body() | |
| func _show_current() -> void: | |
| for i in static_bodies.size(): | |
| var body := static_bodies[i] as StaticBody3D | |
| _set_visuals_visible(body, i == current_index) | |
| body.rotation = Vector3.ZERO | |
| elapsed = 0.0 | |
| rotating = true | |
| _frame_asset(static_bodies[current_index] as StaticBody3D, _first_frame) | |
| _first_frame = false | |
| # update label | |
| if label: | |
| label.text = static_bodies[current_index].name | |
| func _next_body() -> void: | |
| current_index = (current_index + 1) % static_bodies.size() | |
| _show_current() | |
| func _set_visuals_visible(root: Node, vis: bool) -> void: | |
| if root is VisualInstance3D: | |
| (root as VisualInstance3D).visible = vis | |
| for child in root.get_children(): | |
| _set_visuals_visible(child, vis) | |
| # --- Camera framing with tween ----------------------------------------------- | |
| func _frame_asset(root: StaticBody3D, instant: bool = false) -> void: | |
| if camera == null: | |
| return | |
| var aabb: AABB = _combined_local_aabb(root) | |
| if aabb.size == Vector3.ZERO: | |
| return | |
| var vp_size: Vector2i = get_viewport().get_visible_rect().size | |
| var aspect: float = float(vp_size.x) / max(1.0, float(vp_size.y)) | |
| var vfov_rad: float = deg_to_rad(camera.fov) | |
| var hfov_rad: float = 2.0 * atan(tan(vfov_rad * 0.5) * aspect) | |
| var half_h: float = aabb.size.y * 0.5 | |
| var half_w: float = aabb.size.x * 0.5 | |
| var dist_v: float = half_h / max(0.001, tan(vfov_rad * 0.5)) | |
| var dist_h: float = half_w / max(0.001, tan(hfov_rad * 0.5)) | |
| var dist: float = max(dist_v, dist_h) * padding | |
| var center_local: Vector3 = aabb.get_center() | |
| var target_global: Vector3 = root.to_global(center_local) | |
| var forward: Vector3 = -camera.global_transform.basis.z.normalized() | |
| var cam_pos: Vector3 = target_global - forward * dist | |
| var dir: Vector3 = (target_global - cam_pos).normalized() | |
| var basis: Basis = Basis().looking_at(dir, Vector3.UP) | |
| var goal: Transform3D = Transform3D(basis, cam_pos) | |
| if instant: | |
| camera.global_transform = goal | |
| else: | |
| if _cam_tween: | |
| _cam_tween.kill() | |
| _cam_tween = create_tween() | |
| _cam_tween.set_trans(zoom_trans).set_ease(zoom_ease) | |
| _cam_tween.tween_property(camera, "global_transform", goal, zoom_time) | |
| # --- merge bounds ------------------------------------------------------------ | |
| func _combined_local_aabb(root: Node) -> AABB: | |
| var have_box := false | |
| var box := AABB() | |
| var root_node := root as Node3D | |
| if root_node == null: | |
| return AABB(Vector3.ZERO, Vector3.ONE) | |
| var root_to_local: Transform3D = root_node.global_transform.affine_inverse() | |
| var stack: Array = [root] | |
| while stack.size() > 0: | |
| var n: Node = stack.pop_back() | |
| if n is VisualInstance3D: | |
| var vis := n as VisualInstance3D | |
| var local_xform: Transform3D = root_to_local * vis.global_transform | |
| var a: AABB = vis.get_aabb() | |
| var p := a.position | |
| var s := a.size | |
| var mins := Vector3(INF, INF, INF) | |
| var maxs := Vector3(-INF, -INF, -INF) | |
| var corners := [ | |
| p, | |
| p + Vector3(s.x, 0, 0), | |
| p + Vector3(0, s.y, 0), | |
| p + Vector3(0, 0, s.z), | |
| p + Vector3(s.x, s.y, 0), | |
| p + Vector3(s.x, 0, s.z), | |
| p + Vector3(0, s.y, s.z), | |
| p + s | |
| ] | |
| for c in corners: | |
| var tc: Vector3 = local_xform * c | |
| mins = mins.min(tc) | |
| maxs = maxs.max(tc) | |
| var taabb := AABB(mins, maxs - mins) | |
| if not have_box: | |
| box = taabb | |
| have_box = true | |
| else: | |
| box = box.merge(taabb) | |
| for c in n.get_children(): | |
| stack.append(c) | |
| if not have_box: | |
| return AABB(Vector3.ZERO, Vector3.ONE) | |
| return box |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment