343 lines
10 KiB
GDScript
343 lines
10 KiB
GDScript
class_name ProfilePreview
|
|
extends SubViewportContainer
|
|
|
|
const ControllerMappingManagerType = preload(
|
|
"res://settings/controller_mapping_manager.gd"
|
|
)
|
|
const UIReferencePresentationType = preload(
|
|
"res://ui/ui_reference_presentation.gd"
|
|
)
|
|
const FRONT_FACING_YAW: float = PI
|
|
const DEFAULT_CAMERA_DISTANCE: float = 1.9
|
|
const DEFAULT_CAMERA_PITCH: float = 0.12
|
|
const MIN_CAMERA_DISTANCE: float = 1.05
|
|
const MAX_CAMERA_DISTANCE: float = 4.5
|
|
const CAMERA_ZOOM_STEP: float = 0.14
|
|
const CAMERA_TARGET_HEIGHT: float = 0.95
|
|
const CAMERA_FIT_MARGIN: float = 1.12
|
|
const CAMERA_FIT_MIN_RADIUS: float = 0.08
|
|
|
|
@export_range(0.1, 2.0, 0.05) var drag_sensitivity: float = 0.012
|
|
@export_range(0.1, 4.0, 0.1) var keyboard_speed: float = 1.8
|
|
|
|
@onready var _preview_root: Node3D = %PreviewRoot
|
|
@onready var _preview_environment: WorldEnvironment = %WorldEnvironment
|
|
@onready var _preview_sun: DirectionalLight3D = %KeyLight
|
|
@onready var _preview_camera: Camera3D = $Viewport/Camera3D
|
|
@onready var _pixelation_material: ShaderMaterial = material as ShaderMaterial
|
|
|
|
var _dragging: bool = false
|
|
var _camera_yaw: float = 0.0
|
|
var _camera_pitch: float = DEFAULT_CAMERA_PITCH
|
|
var _camera_distance: float = DEFAULT_CAMERA_DISTANCE
|
|
var _camera_fit_distance: float = DEFAULT_CAMERA_DISTANCE
|
|
var _camera_zoom_offset: float = 0.0
|
|
var _camera_target: Vector3 = Vector3(0.0, CAMERA_TARGET_HEIGHT, 0.0)
|
|
var _last_visible_bounds: AABB = AABB()
|
|
var _has_visible_bounds: bool = false
|
|
var _visuals: Node3D
|
|
var _controller_mapping_manager: ControllerMappingManagerType
|
|
var _source_environment: WorldEnvironment
|
|
var _source_sun: DirectionalLight3D
|
|
var _world_pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE
|
|
|
|
|
|
func setup_controller_mapping(
|
|
mapping_manager: ControllerMappingManagerType,
|
|
) -> void:
|
|
_controller_mapping_manager = mapping_manager
|
|
|
|
|
|
func setup_world_lighting(
|
|
world_environment: WorldEnvironment,
|
|
world_sun: DirectionalLight3D,
|
|
) -> void:
|
|
_source_environment = world_environment
|
|
_source_sun = world_sun
|
|
_sync_world_lighting()
|
|
|
|
|
|
func set_world_pixel_size(pixel_size: int) -> void:
|
|
_world_pixel_size = clampi(
|
|
pixel_size,
|
|
PlayerSettings.MIN_WORLD_PIXEL_SIZE,
|
|
PlayerSettings.MAX_WORLD_PIXEL_SIZE,
|
|
)
|
|
_refresh_pixelation_filter()
|
|
|
|
|
|
func _ready() -> void:
|
|
focus_mode = Control.FOCUS_NONE
|
|
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
gui_input.connect(_on_gui_input)
|
|
resized.connect(_refresh_pixelation_filter)
|
|
get_window().size_changed.connect(_refresh_pixelation_filter)
|
|
_configure_preview_lighting()
|
|
reset_view()
|
|
_visuals = PlayerVisualPresenter.instantiate_visuals()
|
|
_preview_root.add_child(_visuals)
|
|
var rod := _visuals.find_child("FishingRod", true, false) as Node3D
|
|
if rod != null:
|
|
rod.visible = false
|
|
var catch_display := _visuals.find_child("CatchDisplay", true, false) as Node3D
|
|
if catch_display != null:
|
|
catch_display.visible = false
|
|
call_deferred("_refresh_pixelation_filter")
|
|
|
|
|
|
func apply_appearance_profile(profile: Dictionary) -> void:
|
|
if _visuals != null:
|
|
PlayerVisualPresenter.apply_appearance(_visuals, profile)
|
|
_fit_camera_to_visible_parts()
|
|
|
|
|
|
func reset_view() -> void:
|
|
_preview_root.rotation.y = FRONT_FACING_YAW
|
|
_camera_yaw = 0.0
|
|
_camera_pitch = DEFAULT_CAMERA_PITCH
|
|
_camera_zoom_offset = 0.0
|
|
_camera_distance = _camera_fit_distance
|
|
_apply_camera_orbit()
|
|
|
|
|
|
func apply_controller_orbit(stick: Vector2, delta: float) -> void:
|
|
if absf(stick.x) > 0.1:
|
|
_rotate(stick.x * keyboard_speed * delta)
|
|
if absf(stick.y) > 0.1:
|
|
_zoom_preview(stick.y * CAMERA_ZOOM_STEP * 4.0 * delta)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_sync_world_lighting()
|
|
if not has_focus():
|
|
return
|
|
var axis := Input.get_axis("ui_left", "ui_right")
|
|
if absf(axis) > 0.1:
|
|
_rotate(axis * keyboard_speed * delta)
|
|
|
|
|
|
func _on_gui_input(event: InputEvent) -> void:
|
|
if event is InputEventMouseButton and event.shift_pressed:
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
|
_zoom_preview(-CAMERA_ZOOM_STEP)
|
|
accept_event()
|
|
return
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
|
|
_zoom_preview(CAMERA_ZOOM_STEP)
|
|
accept_event()
|
|
return
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
|
_dragging = event.pressed
|
|
if event.pressed:
|
|
accept_event()
|
|
elif event is InputEventMouseMotion and _dragging:
|
|
_rotate(event.relative.x * drag_sensitivity)
|
|
_camera_pitch = clampf(
|
|
_camera_pitch + event.relative.y * drag_sensitivity,
|
|
-0.55,
|
|
0.55,
|
|
)
|
|
_apply_camera_orbit()
|
|
accept_event()
|
|
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_VISIBILITY_CHANGED and not is_visible_in_tree():
|
|
_dragging = false
|
|
|
|
|
|
func _rotate(amount: float) -> void:
|
|
_camera_yaw = fposmod(_camera_yaw - amount, TAU)
|
|
_apply_camera_orbit()
|
|
|
|
|
|
func _zoom_preview(amount: float) -> void:
|
|
if _preview_camera == null:
|
|
return
|
|
_camera_distance = clampf(
|
|
_camera_distance + amount,
|
|
MIN_CAMERA_DISTANCE,
|
|
MAX_CAMERA_DISTANCE,
|
|
)
|
|
_camera_zoom_offset = _camera_distance - _camera_fit_distance
|
|
_apply_camera_orbit()
|
|
|
|
|
|
func _apply_camera_orbit() -> void:
|
|
if _preview_camera == null:
|
|
return
|
|
var horizontal_distance := cos(_camera_pitch) * _camera_distance
|
|
_preview_camera.position = Vector3(
|
|
sin(_camera_yaw) * horizontal_distance,
|
|
sin(_camera_pitch) * _camera_distance,
|
|
cos(_camera_yaw) * horizontal_distance,
|
|
) + _camera_target
|
|
_preview_camera.look_at(
|
|
_camera_target,
|
|
Vector3.UP,
|
|
)
|
|
|
|
|
|
func _fit_camera_to_visible_parts() -> void:
|
|
if _visuals == null or _preview_camera == null:
|
|
return
|
|
var bounds_result: Dictionary = _visible_mesh_bounds()
|
|
if not bool(bounds_result.get("has_bounds", false)):
|
|
return
|
|
var bounds: AABB = bounds_result.get("bounds", AABB()) as AABB
|
|
if (
|
|
_has_visible_bounds
|
|
and bounds.position.is_equal_approx(_last_visible_bounds.position)
|
|
and bounds.size.is_equal_approx(_last_visible_bounds.size)
|
|
):
|
|
return
|
|
_last_visible_bounds = bounds
|
|
_has_visible_bounds = true
|
|
_camera_target = bounds.get_center()
|
|
var viewport_size := Vector2(
|
|
_preview_camera.get_viewport().get_visible_rect().size
|
|
)
|
|
_camera_fit_distance = clampf(
|
|
calculate_camera_fit_distance(
|
|
bounds,
|
|
viewport_size,
|
|
_preview_camera.fov,
|
|
),
|
|
MIN_CAMERA_DISTANCE,
|
|
MAX_CAMERA_DISTANCE,
|
|
)
|
|
_camera_distance = clampf(
|
|
_camera_fit_distance + _camera_zoom_offset,
|
|
MIN_CAMERA_DISTANCE,
|
|
MAX_CAMERA_DISTANCE,
|
|
)
|
|
_apply_camera_orbit()
|
|
|
|
|
|
func _visible_mesh_bounds() -> Dictionary:
|
|
var has_bounds := false
|
|
var bounds := AABB()
|
|
for descendant: Node in _visuals.find_children(
|
|
"*", "MeshInstance3D", true, false
|
|
):
|
|
var mesh_instance := descendant as MeshInstance3D
|
|
if (
|
|
mesh_instance == null
|
|
or mesh_instance.mesh == null
|
|
or not _is_visible_within_preview(mesh_instance)
|
|
):
|
|
continue
|
|
var mesh_bounds := mesh_instance.get_aabb()
|
|
var world_transform := mesh_instance.global_transform
|
|
for corner_index: int in 8:
|
|
var corner := mesh_bounds.position + Vector3(
|
|
mesh_bounds.size.x if (corner_index & 1) != 0 else 0.0,
|
|
mesh_bounds.size.y if (corner_index & 2) != 0 else 0.0,
|
|
mesh_bounds.size.z if (corner_index & 4) != 0 else 0.0,
|
|
)
|
|
var point := world_transform * corner
|
|
if not has_bounds:
|
|
bounds = AABB(point, Vector3.ZERO)
|
|
has_bounds = true
|
|
else:
|
|
bounds = bounds.expand(point)
|
|
return {"has_bounds": has_bounds, "bounds": bounds}
|
|
|
|
|
|
func _is_visible_within_preview(node: Node3D) -> bool:
|
|
var current: Node = node
|
|
while current != null and current != _visuals:
|
|
var current_3d := current as Node3D
|
|
if current_3d != null and not current_3d.visible:
|
|
return false
|
|
current = current.get_parent()
|
|
return current == _visuals
|
|
|
|
|
|
static func calculate_camera_fit_distance(
|
|
bounds: AABB,
|
|
viewport_size: Vector2,
|
|
vertical_fov_degrees: float,
|
|
) -> float:
|
|
var radius := maxf(
|
|
bounds.size.length() * 0.5,
|
|
CAMERA_FIT_MIN_RADIUS,
|
|
)
|
|
var safe_height := maxf(viewport_size.y, 1.0)
|
|
var aspect := maxf(viewport_size.x, 1.0) / safe_height
|
|
var vertical_half_angle := deg_to_rad(vertical_fov_degrees) * 0.5
|
|
var horizontal_half_angle := atan(tan(vertical_half_angle) * aspect)
|
|
var limiting_half_angle := minf(
|
|
vertical_half_angle,
|
|
horizontal_half_angle,
|
|
)
|
|
return (
|
|
radius
|
|
/ maxf(sin(limiting_half_angle), 0.001)
|
|
* CAMERA_FIT_MARGIN
|
|
)
|
|
|
|
|
|
func _sync_world_lighting() -> void:
|
|
# The customization viewport deliberately does not follow gameplay time of
|
|
# day. Keep this method as a compatibility seam for existing setup callers.
|
|
pass
|
|
|
|
|
|
func _configure_preview_lighting() -> void:
|
|
if _preview_environment == null or _preview_sun == null:
|
|
return
|
|
var environment := Environment.new()
|
|
environment.background_mode = Environment.BG_COLOR
|
|
environment.background_color = Color("082431")
|
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
environment.ambient_light_color = Color("b9d8d4")
|
|
environment.ambient_light_energy = 1.15
|
|
environment.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED
|
|
_preview_environment.environment = environment
|
|
_preview_sun.rotation_degrees = Vector3(-35.0, -25.0, 0.0)
|
|
_preview_sun.light_color = Color("fff1d0")
|
|
_preview_sun.light_energy = 1.1
|
|
_preview_sun.light_indirect_energy = 0.0
|
|
_preview_sun.shadow_enabled = false
|
|
|
|
|
|
func _refresh_pixelation_filter() -> void:
|
|
if not is_node_ready() or _pixelation_material == null:
|
|
return
|
|
_pixelation_material.set_shader_parameter(
|
|
"logical_grid_size",
|
|
Vector2(calculate_pixelated_grid_size(
|
|
size,
|
|
Vector2(get_window().size),
|
|
_world_pixel_size,
|
|
)),
|
|
)
|
|
|
|
|
|
static func calculate_pixelated_grid_size(
|
|
control_size: Vector2,
|
|
window_size: Vector2,
|
|
pixel_size: int,
|
|
) -> Vector2i:
|
|
var safe_window_size := Vector2(
|
|
maxf(window_size.x, 1.0),
|
|
maxf(window_size.y, 1.0),
|
|
)
|
|
var world_grid_size: Vector2i = PlayerSettings.get_world_grid_size(
|
|
pixel_size,
|
|
Vector2i(roundi(safe_window_size.x), roundi(safe_window_size.y)),
|
|
)
|
|
var world_pixel_extent: float = (
|
|
safe_window_size.y / float(maxi(world_grid_size.y, 1))
|
|
)
|
|
var presentation_scale: float = UIReferencePresentationType.get_scale(
|
|
safe_window_size
|
|
)
|
|
var grid_size: Vector2 = (
|
|
control_size * presentation_scale / maxf(world_pixel_extent, 0.001)
|
|
)
|
|
return Vector2i(
|
|
maxi(roundi(grid_size.x), 1),
|
|
maxi(roundi(grid_size.y), 1),
|
|
)
|