new repo who this
This commit is contained in:
@@ -0,0 +1,878 @@
|
||||
extends Node3D
|
||||
|
||||
const TILE_SCENE := preload("res://tile.tscn")
|
||||
const NINE_PATCH_GRID_SPACING := 1.1
|
||||
|
||||
enum PresentationFormat {
|
||||
HEMISPHERE = 0,
|
||||
WRAPAROUND,
|
||||
NINE_PATCH,
|
||||
FULL_SPHERE,
|
||||
RING_OF_RINGS,
|
||||
SPIRAL_STAIRCASE
|
||||
}
|
||||
|
||||
var shared_viewport: SubViewport
|
||||
var shared_player: VideoStreamPlayer
|
||||
var viewer_camera: Node3D
|
||||
var is_initialized := false
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var shell_time := 0.0
|
||||
var slot_directions: Array[Vector3] = []
|
||||
var slot_phase_offsets: Array[float] = []
|
||||
var slot_tiles: Array[Tile] = []
|
||||
|
||||
# --- format selection ---
|
||||
@export var presentation_format: PresentationFormat = PresentationFormat.HEMISPHERE:
|
||||
get:
|
||||
return _current_format
|
||||
set(val):
|
||||
# Guard: only start a transition if the value is different from
|
||||
# what is currently displayed (_current_format).
|
||||
if val == _current_format and _format_transition_progress >= 1.0:
|
||||
return
|
||||
_set_presentation_format(val)
|
||||
|
||||
# --- common params ---
|
||||
@export var slot_limit := 32
|
||||
@export var viewport_size: Vector2i = Vector2i(840, 420)
|
||||
@export var video_stream: VideoStream
|
||||
@export_range(1.0, 24.0, 0.1) var base_radius := 4.8
|
||||
@export_range(0.0, 8.0, 0.05) var pulse_amplitude := 0.7
|
||||
@export_range(0.01, 1.0, 0.01) var pulse_speed_hz := 0.09
|
||||
@export_range(60.0, 180.0, 1.0) var hemisphere_coverage_degrees := 160.0
|
||||
@export_range(3, 48, 1) var sphere_rings := 14
|
||||
@export_range(6, 96, 1) var sphere_segments := 24
|
||||
@export_range(0.5, 2.0, 0.05) var tile_scale_min := 0.9
|
||||
@export_range(0.5, 2.0, 0.05) var tile_scale_max := 1.2
|
||||
@export var deterministic_seed := -1
|
||||
@export_range(0.1, 2.0, 0.05) var transition_duration_sec := 0.6
|
||||
|
||||
# --- wraparound params ---
|
||||
@export_group("Wraparound")
|
||||
@export_range(1.0, 20.0, 0.1) var wraparound_radius := 6.0
|
||||
@export_range(0.0, 1.0, 0.001) var wraparound_rotation_speed_rad_s := 0.08
|
||||
|
||||
# --- 9-patch params ---
|
||||
@export_group("Nine-Patch")
|
||||
@export_range(1.0, 20.0, 0.1) var nine_patch_radius := 8.0
|
||||
@export_range(0.0, 2.0, 0.01) var nine_patch_wobble_speed_hz := 0.15
|
||||
@export_range(0.0, 1.0, 0.01) var nine_patch_wobble_amplitude := 0.12
|
||||
@export_range(0.0, 3.0, 0.05) var nine_patch_phase_spread := 1.5
|
||||
@export_range(0.0, 10.0, 0.1) var nine_patch_orientation_lag_sec := 3.0
|
||||
|
||||
# --- full sphere params ---
|
||||
@export_group("Full Sphere")
|
||||
@export_range(1.0, 20.0, 0.1) var full_sphere_radius := 8.0
|
||||
@export var full_sphere_use_fibonacci := true
|
||||
|
||||
# --- ring of rings params ---
|
||||
@export_group("Ring of Rings")
|
||||
@export_range(1.0, 15.0, 0.1) var ring_of_rings_inner_radius := 3.5
|
||||
@export_range(1.0, 15.0, 0.1) var ring_of_rings_middle_radius := 6.5
|
||||
@export_range(1.0, 15.0, 0.1) var ring_of_rings_outer_radius := 10.0
|
||||
@export_range(0.0, 1.0, 0.001) var ring_of_rings_inner_speed_rad_s := 0.15
|
||||
@export_range(0.0, 1.0, 0.001) var ring_of_rings_middle_speed_rad_s := 0.08
|
||||
@export_range(0.0, 1.0, 0.001) var ring_of_rings_outer_speed_rad_s := 0.03
|
||||
@export_range(0.0, 2.0, 0.01) var ring_of_rings_vertical_wobble := 0.0
|
||||
|
||||
# --- spiral staircase params ---
|
||||
@export_group("Spiral Staircase")
|
||||
@export_range(1.0, 20.0, 0.1) var spiral_staircase_radius := 5.0
|
||||
@export_range(0.5, 10.0, 0.1) var spiral_staircase_height := 6.0
|
||||
@export_range(1.0, 20.0, 0.5) var spiral_staircase_turns := 5.0
|
||||
@export_range(0.0, 0.5, 0.001) var spiral_staircase_rotation_speed_rad_s := 0.04
|
||||
@export_range(0.0, 2.0, 0.01) var spiral_staircase_wobble_amplitude := 0.08
|
||||
|
||||
# --- format transition state ---
|
||||
var _current_format: PresentationFormat = PresentationFormat.HEMISPHERE
|
||||
var _target_format: PresentationFormat = PresentationFormat.HEMISPHERE
|
||||
var _format_transition_progress: float = 1.0
|
||||
var _transition_start_positions := PackedVector3Array()
|
||||
var _transition_target_positions := PackedVector3Array()
|
||||
|
||||
# cached hemisphere data so we can restore it
|
||||
var _cached_hemisphere_directions: Array[Vector3] = []
|
||||
var _cached_hemisphere_phase_offsets: Array[float] = []
|
||||
|
||||
# ring assignment for ring_of_rings
|
||||
var _slot_ring_assignment: Array[int] = []
|
||||
|
||||
# per-format rotation accumulators
|
||||
var _wraparound_accumulator: float = 0.0
|
||||
var _ring_accumulators := [0.0, 0.0, 0.0]
|
||||
var _spiral_accumulator: float = 0.0
|
||||
|
||||
# --- nine-patch orientation lag ---
|
||||
var _nine_patch_lagged_basis := Basis.IDENTITY
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
if deterministic_seed >= 0:
|
||||
rng.seed = deterministic_seed
|
||||
set_process(true)
|
||||
_current_format = presentation_format
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if shared_player != null:
|
||||
shared_player.paused = true
|
||||
shared_player.stop()
|
||||
|
||||
func set_viewer_camera(camera: Node3D) -> void:
|
||||
viewer_camera = camera
|
||||
|
||||
func initialize_video_pool() -> void:
|
||||
if is_initialized:
|
||||
return
|
||||
if slot_limit < 1:
|
||||
slot_limit = 1
|
||||
if tile_scale_max < tile_scale_min:
|
||||
var scale_swap: float = tile_scale_min
|
||||
tile_scale_min = tile_scale_max
|
||||
tile_scale_max = scale_swap
|
||||
|
||||
shared_viewport = SubViewport.new()
|
||||
shared_viewport.name = "SubViewport_shared"
|
||||
shared_viewport.size = viewport_size
|
||||
shared_viewport.disable_3d = true
|
||||
shared_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
add_child(shared_viewport)
|
||||
|
||||
shared_player = VideoStreamPlayer.new()
|
||||
shared_player.name = "VideoStreamPlayer_shared"
|
||||
shared_player.stream = _create_unique_stream_instance()
|
||||
shared_player.expand = true
|
||||
shared_player.loop = true
|
||||
shared_player.size = Vector2(viewport_size)
|
||||
shared_viewport.add_child(shared_player)
|
||||
shared_player.play()
|
||||
|
||||
var shared_texture := shared_viewport.get_texture()
|
||||
_compute_slots_for_format(_current_format)
|
||||
slot_tiles.resize(slot_directions.size())
|
||||
for i in range(slot_directions.size()):
|
||||
var tile: Tile = TILE_SCENE.instantiate()
|
||||
tile.set_display_texture(shared_texture)
|
||||
tile.set_phase_offset(slot_phase_offsets[i])
|
||||
tile.set_scale_multiplier(rng.randf_range(tile_scale_min, tile_scale_max))
|
||||
slot_tiles[i] = tile
|
||||
add_child(tile)
|
||||
|
||||
is_initialized = true
|
||||
_update_shell(0.0)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_initialized:
|
||||
return
|
||||
if viewer_camera == null:
|
||||
viewer_camera = get_viewport().get_camera_3d()
|
||||
if viewer_camera == null:
|
||||
return
|
||||
_update_orientation_lag(delta)
|
||||
shell_time += delta
|
||||
_update_shell(shell_time)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Presentation format switching
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _set_presentation_format(new_format: PresentationFormat) -> void:
|
||||
if new_format == _target_format:
|
||||
return
|
||||
|
||||
if _current_format == PresentationFormat.HEMISPHERE:
|
||||
_cached_hemisphere_directions = slot_directions.duplicate()
|
||||
_cached_hemisphere_phase_offsets = slot_phase_offsets.duplicate()
|
||||
|
||||
_transition_start_positions.resize(slot_tiles.size())
|
||||
for i in range(slot_tiles.size()):
|
||||
if slot_tiles[i] != null:
|
||||
_transition_start_positions[i] = slot_tiles[i].global_position
|
||||
else:
|
||||
_transition_start_positions[i] = Vector3.ZERO
|
||||
|
||||
_target_format = new_format
|
||||
_compute_slots_for_format(new_format)
|
||||
_transition_target_positions.resize(slot_tiles.size())
|
||||
for i in range(slot_tiles.size()):
|
||||
if i < slot_directions.size():
|
||||
_transition_target_positions[i] = _compute_slot_world_position(i)
|
||||
else:
|
||||
_transition_target_positions[i] = Vector3.INF
|
||||
|
||||
_wraparound_accumulator = 0.0
|
||||
_ring_accumulators = [0.0, 0.0, 0.0]
|
||||
_spiral_accumulator = 0.0
|
||||
_format_transition_progress = 0.0
|
||||
|
||||
# Cycle to the next presentation format.
|
||||
func cycle_format() -> void:
|
||||
if _format_transition_progress < 1.0:
|
||||
return
|
||||
var next: int = int(presentation_format) + 1
|
||||
if next > int(PresentationFormat.SPIRAL_STAIRCASE):
|
||||
next = 0
|
||||
presentation_format = next as PresentationFormat
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Slot computation
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _compute_slots_for_format(format: PresentationFormat) -> void:
|
||||
slot_directions.clear()
|
||||
slot_phase_offsets.clear()
|
||||
_slot_ring_assignment.clear()
|
||||
|
||||
var slot_count: int = \
|
||||
slot_tiles.size() if slot_tiles.size() > 0 else slot_limit
|
||||
|
||||
match format:
|
||||
PresentationFormat.HEMISPHERE:
|
||||
_compute_slots_hemisphere()
|
||||
PresentationFormat.WRAPAROUND:
|
||||
_compute_slots_wraparound(slot_count)
|
||||
PresentationFormat.NINE_PATCH:
|
||||
_compute_slots_nine_patch(slot_count)
|
||||
PresentationFormat.FULL_SPHERE:
|
||||
_compute_slots_full_sphere(slot_count)
|
||||
PresentationFormat.RING_OF_RINGS:
|
||||
_compute_slots_ring_of_rings(slot_count)
|
||||
PresentationFormat.SPIRAL_STAIRCASE:
|
||||
_compute_slots_spiral_staircase(slot_count)
|
||||
|
||||
|
||||
func _compute_slots_hemisphere() -> void:
|
||||
if not _cached_hemisphere_directions.is_empty():
|
||||
slot_directions = _cached_hemisphere_directions.duplicate()
|
||||
slot_phase_offsets = _cached_hemisphere_phase_offsets.duplicate()
|
||||
return
|
||||
|
||||
var face_directions := _sample_sphere_face_directions()
|
||||
if face_directions.is_empty():
|
||||
face_directions = _fallback_uv_directions()
|
||||
|
||||
var forward := _get_view_forward_axis()
|
||||
var half_angle := deg_to_rad(hemisphere_coverage_degrees * 0.5)
|
||||
var min_dot := cos(half_angle)
|
||||
|
||||
var filtered: Array[Vector3] = []
|
||||
for dir in face_directions:
|
||||
if dir.dot(forward) >= min_dot:
|
||||
filtered.push_back(dir)
|
||||
|
||||
if filtered.is_empty():
|
||||
filtered = face_directions
|
||||
|
||||
if filtered.size() > slot_limit:
|
||||
var downsampled: Array[Vector3] = []
|
||||
var step := float(filtered.size()) / float(slot_limit)
|
||||
for i in range(slot_limit):
|
||||
var idx := int(floor(float(i) * step))
|
||||
downsampled.push_back(
|
||||
filtered[clampi(idx, 0, filtered.size() - 1)]
|
||||
)
|
||||
filtered = downsampled
|
||||
|
||||
for dir in filtered:
|
||||
slot_directions.push_back(dir)
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
|
||||
|
||||
func _compute_slots_wraparound(slot_count: int) -> void:
|
||||
for i in range(slot_count):
|
||||
var angle: float = TAU * float(i) / float(slot_count)
|
||||
slot_directions.push_back(Vector3(cos(angle), 0.0, sin(angle)))
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
|
||||
|
||||
func _compute_slots_nine_patch(slot_count: int) -> void:
|
||||
for i in range(slot_count):
|
||||
var cell_index: int = i % 9
|
||||
var row: int = int(float(cell_index) / 3.0)
|
||||
var col: int = cell_index % 3
|
||||
var x: float = float(col - 1)
|
||||
var z: float = float(row - 1)
|
||||
slot_directions.push_back(Vector3(x, 0.0, z).normalized())
|
||||
slot_phase_offsets.push_back(
|
||||
float(cell_index) / 9.0 * nine_patch_phase_spread
|
||||
)
|
||||
|
||||
|
||||
func _compute_slots_full_sphere(slot_count: int) -> void:
|
||||
if full_sphere_use_fibonacci:
|
||||
var golden_angle: float = PI * (3.0 - sqrt(5.0))
|
||||
for i in range(slot_count):
|
||||
var y: float = 1.0 - (2.0 * float(i) + 1.0) \
|
||||
/ float(slot_count)
|
||||
var radius_at_y: float = sqrt(maxf(0.0, 1.0 - y * y))
|
||||
var theta: float = golden_angle * float(i)
|
||||
slot_directions.push_back(
|
||||
Vector3(cos(theta) * radius_at_y,
|
||||
y, sin(theta) * radius_at_y)
|
||||
)
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
else:
|
||||
var rings: float = max(1, int(sqrt(float(slot_count))))
|
||||
var per_ring: float = slot_count / rings
|
||||
var idx: int = 0
|
||||
for r in range(rings):
|
||||
var theta: float = PI * float(r + 0.5) / float(rings)
|
||||
var slots_in_ring: int
|
||||
if r < rings - 1:
|
||||
slots_in_ring = int(per_ring)
|
||||
else:
|
||||
slots_in_ring = slot_count - idx
|
||||
var ring_radius: float = sin(theta)
|
||||
if ring_radius < 0.001:
|
||||
if r < rings / 2:
|
||||
slot_directions.push_back(Vector3.UP)
|
||||
else:
|
||||
slot_directions.push_back(-Vector3.UP)
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
idx += 1
|
||||
continue
|
||||
for s in range(slots_in_ring):
|
||||
var phi: float = TAU * float(s) / float(slots_in_ring)
|
||||
slot_directions.push_back(
|
||||
Vector3(cos(phi) * ring_radius,
|
||||
cos(theta), sin(phi) * ring_radius)
|
||||
)
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
idx += 1
|
||||
|
||||
|
||||
func _compute_slots_ring_of_rings(slot_count: int) -> void:
|
||||
var inner_count: int = max(1, int(float(slot_count) / 4.0))
|
||||
var middle_count: int = max(1, int(float(slot_count) / 2.0))
|
||||
var outer_count: int = slot_count - inner_count - middle_count
|
||||
if inner_count < 1:
|
||||
inner_count = 1
|
||||
if middle_count < 1:
|
||||
middle_count = 1
|
||||
outer_count = slot_count - inner_count - middle_count
|
||||
|
||||
_slot_ring_assignment.resize(slot_count)
|
||||
var idx: int = 0
|
||||
for ring in range(3):
|
||||
var count: int
|
||||
match ring:
|
||||
0: count = inner_count
|
||||
1: count = middle_count
|
||||
2: count = max(1, outer_count)
|
||||
for s in range(count):
|
||||
if idx >= slot_count:
|
||||
break
|
||||
var angle: float = TAU * float(s) / float(count)
|
||||
slot_directions.push_back(
|
||||
Vector3(cos(angle), 0.0, sin(angle))
|
||||
)
|
||||
slot_phase_offsets.push_back(rng.randf_range(0.0, TAU))
|
||||
_slot_ring_assignment[idx] = ring
|
||||
idx += 1
|
||||
while _slot_ring_assignment.size() < idx:
|
||||
_slot_ring_assignment.push_back(ring)
|
||||
|
||||
|
||||
func _compute_slots_spiral_staircase(slot_count: int) -> void:
|
||||
for i in range(slot_count):
|
||||
var t: float = float(i) / max(1, float(slot_count - 1))
|
||||
var angle: float = t * TAU * spiral_staircase_turns
|
||||
slot_directions.push_back(
|
||||
Vector3(cos(angle), 0.0, sin(angle))
|
||||
)
|
||||
slot_phase_offsets.push_back(t * TAU * 2.0)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# World position computation
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _compute_slot_world_position(index: int) -> Vector3:
|
||||
var center := viewer_camera.global_position
|
||||
match _target_format:
|
||||
PresentationFormat.HEMISPHERE:
|
||||
var dir: Vector3 = Vector3.FORWARD
|
||||
if index < slot_directions.size():
|
||||
dir = slot_directions[index]
|
||||
var radius: float = base_radius + pulse_amplitude * \
|
||||
sin(shell_time * TAU * pulse_speed_hz)
|
||||
return center + dir * max(radius, 0.2)
|
||||
|
||||
PresentationFormat.WRAPAROUND:
|
||||
var angle: float = TAU * float(index) / \
|
||||
float(slot_directions.size()) + _wraparound_accumulator
|
||||
var dir: Vector3 = Vector3(cos(angle), 0.0, sin(angle))
|
||||
var radius: float = base_radius + pulse_amplitude * \
|
||||
sin(shell_time * TAU * pulse_speed_hz)
|
||||
return center + dir * max(radius, 0.2)
|
||||
|
||||
PresentationFormat.NINE_PATCH:
|
||||
var cell_index: int = index % 9
|
||||
var layer: int = int(float(index) / 9.0)
|
||||
var row: int = int(float(cell_index) / 3.0)
|
||||
var col: int = cell_index % 3
|
||||
var cam_basis: Basis = _nine_patch_lagged_basis
|
||||
var spacing: float = NINE_PATCH_GRID_SPACING \
|
||||
* nine_patch_radius
|
||||
var layer_depth_step: float = nine_patch_radius * 0.35
|
||||
var offset: Vector3 = cam_basis.x * (
|
||||
float(col - 1) * spacing
|
||||
) + cam_basis.y * (
|
||||
float(row - 1) * spacing * 0.5625
|
||||
) - cam_basis.z * (
|
||||
nine_patch_radius + float(layer) * layer_depth_step
|
||||
)
|
||||
return center + offset
|
||||
|
||||
PresentationFormat.FULL_SPHERE:
|
||||
var dir: Vector3 = Vector3.FORWARD
|
||||
if index < slot_directions.size():
|
||||
dir = slot_directions[index]
|
||||
var radius: float = full_sphere_radius + pulse_amplitude * \
|
||||
sin(shell_time * TAU * pulse_speed_hz)
|
||||
return center + dir * max(radius, 0.2)
|
||||
|
||||
PresentationFormat.RING_OF_RINGS:
|
||||
var ring: int = 2
|
||||
if index < _slot_ring_assignment.size():
|
||||
ring = _slot_ring_assignment[index]
|
||||
var ring_count: int
|
||||
match ring:
|
||||
0: ring_count = max(1, int(float(slot_limit) / 4.0))
|
||||
1: ring_count = max(1, int(float(slot_limit) / 2.0))
|
||||
2: ring_count = slot_limit - ring_count
|
||||
var ring_radius: float
|
||||
match ring:
|
||||
0: ring_radius = ring_of_rings_inner_radius
|
||||
1: ring_radius = ring_of_rings_middle_radius
|
||||
2: ring_radius = ring_of_rings_outer_radius
|
||||
var s: int = index - ring_count if ring == 0 else \
|
||||
index - (int(float(slot_limit) / 4.0)
|
||||
+ int(float(slot_limit) / 2.0))
|
||||
var angle: float = TAU * float(s) / float(ring_count) + \
|
||||
_ring_accumulators[ring]
|
||||
var y_drift: float = sin(
|
||||
shell_time * TAU * 0.2
|
||||
) * ring_of_rings_vertical_wobble
|
||||
var dir: Vector3 = Vector3(
|
||||
cos(angle), y_drift, sin(angle)
|
||||
).normalized()
|
||||
return center + dir * ring_radius
|
||||
|
||||
PresentationFormat.SPIRAL_STAIRCASE:
|
||||
var i: int = index
|
||||
var t: float = float(i) / max(1, float(slot_limit - 1))
|
||||
var angle: float = t * TAU * spiral_staircase_turns + \
|
||||
_spiral_accumulator
|
||||
var height: float = (t - 0.5) * spiral_staircase_height
|
||||
var drift: float = sin(
|
||||
shell_time * TAU * 0.3 + t * TAU * 2
|
||||
) * spiral_staircase_wobble_amplitude
|
||||
return center + Vector3(
|
||||
cos(angle) * spiral_staircase_radius,
|
||||
height + drift,
|
||||
sin(angle) * spiral_staircase_radius
|
||||
)
|
||||
|
||||
return center
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Per-frame animation dispatch
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_shell(time_sec: float) -> void:
|
||||
if _format_transition_progress < 1.0:
|
||||
_handle_transition(time_sec)
|
||||
return
|
||||
|
||||
var center := viewer_camera.global_position
|
||||
match _current_format:
|
||||
PresentationFormat.HEMISPHERE:
|
||||
_update_hemisphere(time_sec, center)
|
||||
PresentationFormat.WRAPAROUND:
|
||||
_update_wraparound(time_sec, center)
|
||||
PresentationFormat.NINE_PATCH:
|
||||
_update_nine_patch(time_sec, center)
|
||||
PresentationFormat.FULL_SPHERE:
|
||||
_update_full_sphere(time_sec, center)
|
||||
PresentationFormat.RING_OF_RINGS:
|
||||
_update_ring_of_rings(time_sec, center)
|
||||
PresentationFormat.SPIRAL_STAIRCASE:
|
||||
_update_spiral_staircase(time_sec, center)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Transition system (two-phase lerp)
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _handle_transition(_time_sec: float) -> void:
|
||||
_format_transition_progress += \
|
||||
get_process_delta_time() / transition_duration_sec
|
||||
if _format_transition_progress >= 1.0:
|
||||
_format_transition_progress = 1.0
|
||||
_current_format = _target_format
|
||||
for i in range(slot_tiles.size()):
|
||||
if slot_tiles[i] != null \
|
||||
and i < _transition_target_positions.size():
|
||||
slot_tiles[i].global_position = \
|
||||
_transition_target_positions[i]
|
||||
for i in range(slot_tiles.size()):
|
||||
if slot_tiles[i] != null:
|
||||
slot_tiles[i].visible = true
|
||||
return
|
||||
|
||||
var t: float = _format_transition_progress
|
||||
var lerp_t: float = smoothstep(0.0, 1.0, t)
|
||||
|
||||
for i in range(slot_tiles.size()):
|
||||
if slot_tiles[i] == null:
|
||||
continue
|
||||
if i < _transition_target_positions.size() \
|
||||
and _transition_target_positions[i] != Vector3.INF:
|
||||
var target: Vector3 = _transition_target_positions[i]
|
||||
slot_tiles[i].global_position = \
|
||||
_transition_start_positions[i].lerp(target, lerp_t)
|
||||
|
||||
var fade: float = smoothstep(0.5, 1.0, t)
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if fade < 1.0:
|
||||
var current_scale := tile.mesh_instance.scale
|
||||
var denom: float = tile.scale_multiplier * (
|
||||
1.0 + sin(shell_time * TAU
|
||||
* tile.pulse_frequency_hz)
|
||||
* tile.pulse_strength
|
||||
)
|
||||
if denom > 0.0001:
|
||||
var base_scale: Vector3 = current_scale / denom
|
||||
tile.mesh_instance.scale = base_scale * (
|
||||
1.0 + fade * 0.05
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Hemisphere (existing)
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_hemisphere(time_sec: float, center: Vector3) -> void:
|
||||
var base_phase := time_sec * TAU * pulse_speed_hz
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
var slot_phase: float = slot_phase_offsets[i]
|
||||
var slot_radius: float = base_radius + pulse_amplitude * \
|
||||
sin(base_phase + slot_phase * 0.35)
|
||||
tile.place_on_shell(
|
||||
center, slot_directions[i],
|
||||
max(slot_radius, 0.2), viewer_camera
|
||||
)
|
||||
tile.visible = true
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Wraparound
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_wraparound(time_sec: float, center: Vector3) -> void:
|
||||
_wraparound_accumulator += \
|
||||
wraparound_rotation_speed_rad_s * get_process_delta_time()
|
||||
var base_phase := time_sec * TAU * pulse_speed_hz
|
||||
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
var angle: float = TAU * float(i) / \
|
||||
float(slot_directions.size()) + _wraparound_accumulator
|
||||
var dir: Vector3 = Vector3(cos(angle), 0.0, sin(angle))
|
||||
var radius: float = base_radius + pulse_amplitude * \
|
||||
sin(base_phase + slot_phase_offsets[i] * 0.35)
|
||||
tile.global_position = center + dir * max(radius, 0.2)
|
||||
tile.viewer_camera = viewer_camera
|
||||
tile.visible = true
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Nine-patch orientation lag
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_orientation_lag(delta: float) -> void:
|
||||
var target_basis: Basis = viewer_camera. \
|
||||
global_transform.basis.orthonormalized()
|
||||
if nine_patch_orientation_lag_sec <= 0.0:
|
||||
_nine_patch_lagged_basis = target_basis
|
||||
return
|
||||
var factor: float = 1.0 - exp(
|
||||
-delta / nine_patch_orientation_lag_sec
|
||||
)
|
||||
_nine_patch_lagged_basis.x = _nine_patch_lagged_basis.x.lerp(
|
||||
target_basis.x, factor)
|
||||
_nine_patch_lagged_basis.y = _nine_patch_lagged_basis.y.lerp(
|
||||
target_basis.y, factor)
|
||||
_nine_patch_lagged_basis.z = _nine_patch_lagged_basis.z.lerp(
|
||||
target_basis.z, factor)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Nine-patch
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_nine_patch(time_sec: float, center: Vector3) -> void:
|
||||
var cam_basis: Basis = _nine_patch_lagged_basis
|
||||
var spacing: float = NINE_PATCH_GRID_SPACING * nine_patch_radius
|
||||
var layer_depth_step: float = nine_patch_radius * 0.35
|
||||
var wobble_phase: float = time_sec * TAU * nine_patch_wobble_speed_hz
|
||||
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
|
||||
var cell_index: int = i % 9
|
||||
var layer: int = int(float(i) / 9.0)
|
||||
var row: int = int(float(cell_index) / 3.0)
|
||||
var col: int = cell_index % 3
|
||||
var offset: Vector3 = cam_basis.x * (
|
||||
float(col - 1) * spacing
|
||||
) + cam_basis.y * (
|
||||
float(row - 1) * spacing * 0.5625
|
||||
) - cam_basis.z * (
|
||||
nine_patch_radius + float(layer) * layer_depth_step
|
||||
)
|
||||
|
||||
tile.visible = true
|
||||
tile.global_position = center + offset
|
||||
|
||||
var tile_wobble: float = sin(wobble_phase + \
|
||||
float(cell_index) * nine_patch_phase_spread / 9.0) * \
|
||||
nine_patch_wobble_amplitude
|
||||
var base: Vector3 = tile.base_scale * tile.scale_multiplier
|
||||
tile.mesh_instance.scale = base * (1.0 + tile_wobble)
|
||||
tile.viewer_camera = viewer_camera
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Full sphere
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_full_sphere(time_sec: float, center: Vector3) -> void:
|
||||
var base_phase := time_sec * TAU * pulse_speed_hz
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
var radius: float = full_sphere_radius + pulse_amplitude * \
|
||||
sin(base_phase + slot_phase_offsets[i] * 0.35)
|
||||
tile.place_on_shell(
|
||||
center, slot_directions[i],
|
||||
max(radius, 0.2), viewer_camera
|
||||
)
|
||||
tile.visible = true
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Ring of rings
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_ring_of_rings(time_sec: float, center: Vector3) -> void:
|
||||
var delta: float = get_process_delta_time()
|
||||
_ring_accumulators[0] += ring_of_rings_inner_speed_rad_s * delta
|
||||
_ring_accumulators[1] += ring_of_rings_middle_speed_rad_s * delta
|
||||
_ring_accumulators[2] += ring_of_rings_outer_speed_rad_s * delta
|
||||
|
||||
var base_phase := time_sec * TAU * pulse_speed_hz
|
||||
var ring_radii := [
|
||||
ring_of_rings_inner_radius,
|
||||
ring_of_rings_middle_radius,
|
||||
ring_of_rings_outer_radius
|
||||
]
|
||||
var n: int = slot_tiles.size()
|
||||
var ring_counts := [
|
||||
max(1, int(float(n) / 4.0)),
|
||||
max(1, int(float(n) / 2.0)),
|
||||
n - max(1, int(float(n) / 4.0))
|
||||
- max(1, int(float(n) / 2.0))
|
||||
]
|
||||
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
var ring: int = 2
|
||||
if i < _slot_ring_assignment.size():
|
||||
ring = _slot_ring_assignment[i]
|
||||
var ring_r: float = ring_radii[ring]
|
||||
var ring_c: int = ring_counts[ring]
|
||||
|
||||
var offset: int = ring_counts[0] if ring == 0 else \
|
||||
ring_counts[0] + ring_counts[1]
|
||||
var local_index: int = i - offset
|
||||
var angle: float = TAU * float(local_index) / float(ring_c) + \
|
||||
_ring_accumulators[ring]
|
||||
if i >= slot_directions.size():
|
||||
break
|
||||
var y_drift: float = sin(time_sec * TAU * 0.2) * \
|
||||
ring_of_rings_vertical_wobble
|
||||
var dir: Vector3 = Vector3(
|
||||
cos(angle), y_drift, sin(angle)
|
||||
).normalized()
|
||||
var radius: float = ring_r + pulse_amplitude * \
|
||||
sin(base_phase + slot_phase_offsets[i] * 0.35)
|
||||
tile.global_position = center + dir * radius
|
||||
tile.viewer_camera = viewer_camera
|
||||
tile.visible = true
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Spiral staircase
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _update_spiral_staircase(time_sec: float, center: Vector3) -> void:
|
||||
_spiral_accumulator += spiral_staircase_rotation_speed_rad_s * \
|
||||
get_process_delta_time()
|
||||
|
||||
for i in range(slot_tiles.size()):
|
||||
var tile: Tile = slot_tiles[i]
|
||||
if tile == null:
|
||||
continue
|
||||
|
||||
var t: float = float(i) / max(1, float(slot_tiles.size() - 1))
|
||||
var angle: float = t * TAU * spiral_staircase_turns + \
|
||||
_spiral_accumulator
|
||||
var height: float = (t - 0.5) * spiral_staircase_height
|
||||
var drift: float = sin(time_sec * TAU * 0.3 + t * TAU * 2) * \
|
||||
spiral_staircase_wobble_amplitude
|
||||
|
||||
if i >= slot_directions.size():
|
||||
break
|
||||
|
||||
var world_pos: Vector3 = center + Vector3(
|
||||
cos(angle) * spiral_staircase_radius,
|
||||
height + drift,
|
||||
sin(angle) * spiral_staircase_radius
|
||||
)
|
||||
|
||||
tile.global_position = world_pos
|
||||
|
||||
# Partial billboard
|
||||
var to_camera: Vector3 = (center - world_pos).normalized()
|
||||
var current_dir: Vector3 = tile.global_transform.basis.z
|
||||
var dot: float = to_camera.dot(current_dir)
|
||||
if dot < 0.98:
|
||||
var axis: Vector3 = Vector3.UP.cross(to_camera)
|
||||
if axis.length_squared() > 0.001:
|
||||
var angle_between: float = acos(clampf(dot, -1.0, 1.0))
|
||||
tile.rotate_object_local(
|
||||
axis.normalized(),
|
||||
angle_between * 0.6
|
||||
)
|
||||
|
||||
tile.visible = true
|
||||
|
||||
var osc: float = Time.get_ticks_msec() * 0.001
|
||||
var pulse: float = 1.0 + sin(
|
||||
osc * TAU * tile.pulse_frequency_hz
|
||||
+ tile.phase_offset
|
||||
) * tile.pulse_strength
|
||||
tile.mesh_instance.scale = tile.base_scale * \
|
||||
tile.scale_multiplier * pulse
|
||||
|
||||
if tile.tile_material != null:
|
||||
tile.tile_material.emission_energy_multiplier = \
|
||||
tile.base_emission_energy * (
|
||||
0.9 + tile.pulse_strength
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Legacy helpers
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
func _build_hemisphere_slots() -> void:
|
||||
_compute_slots_for_format(PresentationFormat.HEMISPHERE)
|
||||
|
||||
func _sample_sphere_face_directions() -> Array[Vector3]:
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 1.0
|
||||
sphere.height = 2.0
|
||||
sphere.rings = sphere_rings
|
||||
sphere.radial_segments = sphere_segments
|
||||
|
||||
var arrays := sphere.get_mesh_arrays()
|
||||
if arrays.is_empty():
|
||||
return []
|
||||
|
||||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
if vertices.is_empty():
|
||||
return []
|
||||
|
||||
var out: Array[Vector3] = []
|
||||
if indices.is_empty():
|
||||
var tri_count: int = int(vertices.size() / 3.0)
|
||||
for tri in range(tri_count):
|
||||
var i0: int = tri * 3
|
||||
var v0: Vector3 = vertices[i0]
|
||||
var v1: Vector3 = vertices[i0 + 1]
|
||||
var v2: Vector3 = vertices[i0 + 2]
|
||||
var center: Vector3 = (v0 + v1 + v2) / 3.0
|
||||
if center.length_squared() > 0.0001:
|
||||
out.push_back(center.normalized())
|
||||
else:
|
||||
var tri_count_indexed: int = int(indices.size() / 3.0)
|
||||
for tri in range(tri_count_indexed):
|
||||
var idx: int = tri * 3
|
||||
var v0: Vector3 = vertices[indices[idx]]
|
||||
var v1: Vector3 = vertices[indices[idx + 1]]
|
||||
var v2: Vector3 = vertices[indices[idx + 2]]
|
||||
var center: Vector3 = (v0 + v1 + v2) / 3.0
|
||||
if center.length_squared() > 0.0001:
|
||||
out.push_back(center.normalized())
|
||||
|
||||
return out
|
||||
|
||||
func _fallback_uv_directions() -> Array[Vector3]:
|
||||
var out: Array[Vector3] = []
|
||||
for ring in range(1, sphere_rings):
|
||||
var v: float = float(ring) / float(max(2, sphere_rings))
|
||||
var theta: float = v * PI
|
||||
var sin_theta: float = sin(theta)
|
||||
var cos_theta: float = cos(theta)
|
||||
for segment in range(sphere_segments):
|
||||
var u: float = float(segment) / float(max(1, sphere_segments))
|
||||
var phi: float = u * TAU
|
||||
var dir: Vector3 = Vector3(
|
||||
cos(phi) * sin_theta, cos_theta,
|
||||
sin(phi) * sin_theta
|
||||
)
|
||||
if dir.length_squared() > 0.0001:
|
||||
out.push_back(dir.normalized())
|
||||
return out
|
||||
|
||||
func _get_view_forward_axis() -> Vector3:
|
||||
if viewer_camera == null:
|
||||
viewer_camera = get_viewport().get_camera_3d()
|
||||
if viewer_camera == null:
|
||||
return -global_transform.basis.z.normalized()
|
||||
|
||||
var camera_basis := viewer_camera.global_transform.basis.orthonormalized()
|
||||
return (-camera_basis.z).normalized()
|
||||
|
||||
func _create_unique_stream_instance() -> VideoStream:
|
||||
if video_stream == null:
|
||||
return null
|
||||
|
||||
if video_stream.resource_path.is_empty():
|
||||
return video_stream.duplicate(true)
|
||||
|
||||
var loaded := ResourceLoader.load(
|
||||
video_stream.resource_path,
|
||||
"VideoStream",
|
||||
ResourceLoader.CACHE_MODE_REUSE
|
||||
)
|
||||
if loaded is VideoStream:
|
||||
return loaded as VideoStream
|
||||
|
||||
return video_stream.duplicate(true)
|
||||
Reference in New Issue
Block a user