Avatar customization: drag-to-rotate preview, fill the dead space, seam fix
checkpoint / checkpoint (push) Successful in 1m45s

- The preview is now a turntable: drag it to rotate the avatar and see all
  sides; it slowly auto-spins after ~2.5s of no input. Starts facing the
  camera so you see the face. "drag to rotate" hint under the viewport.
- The big grey void to the right of the controls is gone -- the preview
  panel now expands to fill the leftover width; the controls dock to the
  right edge at a fixed width.
- Camera reframed to fit the whole ~2 m body; softer preview lights.
- Placeholder avatar: torso/arms/legs now share one 0.46 m depth so a limb
  always covers the torso slice behind it -- kills the bright "cyan seam"
  (it was a lit sliver of the torso side peeking past the thinner arm).

Verified headless (boot, 2p multiplayer, avatar smoke source+pack,
fresh-clone) + eyeballed the customization screen and the avatar seams in
rendered screenshots.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-06 15:18:26 +00:00
parent 4200a8b358
commit 181ba6ae02
3 changed files with 74 additions and 20 deletions
+62 -11
View File
@@ -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
# ---------------------------------------------------------------------------
+12 -9
View File
@@ -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]