diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb index c4465cd..36c5536 100644 Binary files a/assets/avatars/base_human_male_01.glb and b/assets/avatars/base_human_male_01.glb differ diff --git a/scripts/avatar/avatar_customization_ui.gd b/scripts/avatar/avatar_customization_ui.gd index 5cec4a6..3ef9e1f 100644 --- a/scripts/avatar/avatar_customization_ui.gd +++ b/scripts/avatar/avatar_customization_ui.gd @@ -76,6 +76,16 @@ var _preview_root: Node3D = null ## Currently selected slot id. var _selected_slot: String = "" +## Turntable: the preview avatar's Y rotation (radians), whether the player is +## currently dragging it, and how long since they last touched it (drives the +## slow idle auto-spin). Starts facing the camera. +var _turntable_yaw: float = PI +var _turntable_dragging: bool = false +var _turntable_idle: float = 0.0 +const _TURNTABLE_IDLE_SPIN_DELAY := 2.5 +const _TURNTABLE_IDLE_SPIN_SPEED := 0.5 +const _TURNTABLE_DRAG_SPEED := 0.01 + ## UI node references. var _slot_list: VBoxContainer = null var _item_list: VBoxContainer = null @@ -142,10 +152,11 @@ func _build_ui() -> void: root_hbox.add_theme_constant_override("separation", 16) add_child(root_hbox) - # --- Left: preview --- + # --- Left: preview (takes all the leftover width) --- var preview_panel := PanelContainer.new() preview_panel.name = "PreviewPanel" - preview_panel.custom_minimum_size = Vector2(480, 0) + preview_panel.custom_minimum_size = Vector2(420, 0) + preview_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL root_hbox.add_child(preview_panel) var preview_margin := MarginContainer.new() @@ -155,15 +166,30 @@ func _build_ui() -> void: preview_margin.add_theme_constant_override("margin_bottom", 8) preview_panel.add_child(preview_margin) + var preview_vbox := VBoxContainer.new() + preview_vbox.add_theme_constant_override("separation", 6) + preview_margin.add_child(preview_vbox) + _preview_container = SubViewportContainer.new() _preview_container.name = "PreviewViewportContainer" _preview_container.stretch = true - _preview_container.custom_minimum_size = Vector2(464, 560) - preview_margin.add_child(_preview_container) + _preview_container.custom_minimum_size = Vector2(400, 520) + _preview_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _preview_container.size_flags_vertical = Control.SIZE_EXPAND_FILL + _preview_container.mouse_filter = Control.MOUSE_FILTER_STOP + _preview_container.gui_input.connect(_on_preview_gui_input) + preview_vbox.add_child(_preview_container) + + var rotate_hint := Label.new() + rotate_hint.text = "drag to rotate" + rotate_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + rotate_hint.add_theme_color_override("font_color", Color(1, 1, 1, 0.5)) + rotate_hint.add_theme_font_size_override("font_size", 12) + preview_vbox.add_child(rotate_hint) _preview_viewport = SubViewport.new() _preview_viewport.name = "PreviewViewport" - _preview_viewport.size = Vector2i(464, 560) + _preview_viewport.size = Vector2i(400, 520) _preview_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS _preview_viewport.transparent_bg = true # Its own 3D world -- a SubViewport that shares the (Control-only) main @@ -178,17 +204,17 @@ func _build_ui() -> void: env.background_mode = Environment.BG_COLOR env.background_color = Color(0.10, 0.11, 0.14) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR - env.ambient_light_color = Color(0.6, 0.6, 0.65) - env.ambient_light_energy = 1.0 + env.ambient_light_color = Color(0.62, 0.64, 0.7) + env.ambient_light_energy = 0.7 world_env.environment = env _preview_viewport.add_child(world_env) - # Camera for the preview. + # Camera for the preview -- framed to fit the full ~2 m avatar. _preview_camera = Camera3D.new() _preview_camera.name = "PreviewCamera" # look_at needs a global transform, which only exists once in the tree. _preview_camera.look_at_from_position( - Vector3(0, 1.0, 3.2), Vector3(0, 0.9, 0), Vector3.UP) + Vector3(0, 1.05, 3.5), Vector3(0, 0.98, 0), Vector3.UP) _preview_camera.current = true _preview_viewport.add_child(_preview_camera) @@ -201,12 +227,12 @@ func _build_ui() -> void: var key := DirectionalLight3D.new() key.name = "PreviewKeyLight" key.rotation_degrees = Vector3(-35, 35, 0) - key.light_energy = 1.2 + key.light_energy = 1.0 _preview_viewport.add_child(key) var fill := DirectionalLight3D.new() fill.name = "PreviewFillLight" fill.rotation_degrees = Vector3(-20, -140, 0) - fill.light_energy = 0.4 + fill.light_energy = 0.35 _preview_viewport.add_child(fill) # --- Right: controls --- @@ -403,6 +429,31 @@ func _on_equip_pressed(asset_id: String) -> void: loadout_changed.emit(loadout) +# --------------------------------------------------------------------------- +# Turntable (drag to rotate the preview, slow auto-spin when idle) +# --------------------------------------------------------------------------- + +func _process(delta: float) -> void: + if _preview_root == null: + return + if _turntable_dragging: + _turntable_idle = 0.0 + else: + _turntable_idle += delta + if _turntable_idle > _TURNTABLE_IDLE_SPIN_DELAY: + _turntable_yaw += delta * _TURNTABLE_IDLE_SPIN_SPEED + _preview_root.rotation.y = _turntable_yaw + + +func _on_preview_gui_input(event: InputEvent) -> void: + if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: + _turntable_dragging = event.pressed + _turntable_idle = 0.0 + elif event is InputEventMouseMotion and _turntable_dragging: + _turntable_yaw -= event.relative.x * _TURNTABLE_DRAG_SPEED + _turntable_idle = 0.0 + + # --------------------------------------------------------------------------- # Preview refresh # --------------------------------------------------------------------------- diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index 546b0d5..fa06ac5 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -66,16 +66,19 @@ const C_FACE := Color(0.05, 0.05, 0.06) # near-black (eyes + smile) # weighted to its bone -- so the rig still drives it, but it reads as a clean # solid block (no banding, no capsule taper). const PARTS := [ - ["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.56), C_SKIN], - ["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.48), C_SHIRT], - ["LeftUpperArm", Vector3(0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN], - ["RightUpperArm", Vector3(-0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN], - ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], - ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS], + # Torso, arms and legs all share the same 0.46 depth so a limb always fully + # covers the slice of torso behind it -- otherwise a brightly-lit sliver of + # the torso side peeks past the arm and reads as a cyan seam. + ["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.58), C_SKIN], + ["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.46), C_SHIRT], + ["LeftUpperArm", Vector3(0.57, 1.14, 0.00), Vector3(0.34, 0.78, 0.46), C_SKIN], + ["RightUpperArm", Vector3(-0.57, 1.14, 0.00), Vector3(0.34, 0.78, 0.46), C_SKIN], + ["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.46), C_PANTS], + ["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.46), C_PANTS], # painted-on face -- thin plates on the front of the head (-Z), rigged to Head - ["Head", Vector3(0.13, 1.78, -0.28), Vector3(0.09, 0.12, 0.02), C_FACE], - ["Head", Vector3(-0.13, 1.78, -0.28), Vector3(0.09, 0.12, 0.02), C_FACE], - ["Head", Vector3(0.00, 1.64, -0.28), Vector3(0.32, 0.05, 0.02), C_FACE], + ["Head", Vector3(0.13, 1.78, -0.30), Vector3(0.09, 0.12, 0.02), C_FACE], + ["Head", Vector3(-0.13, 1.78, -0.30), Vector3(0.09, 0.12, 0.02), C_FACE], + ["Head", Vector3(0.00, 1.64, -0.30), Vector3(0.32, 0.05, 0.02), C_FACE], ] # Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]