Avatar customization: drag-to-rotate preview, fill the dead space, seam fix
checkpoint / checkpoint (push) Successful in 1m45s
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:
Binary file not shown.
@@ -76,6 +76,16 @@ var _preview_root: Node3D = null
|
|||||||
## Currently selected slot id.
|
## Currently selected slot id.
|
||||||
var _selected_slot: String = ""
|
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.
|
## UI node references.
|
||||||
var _slot_list: VBoxContainer = null
|
var _slot_list: VBoxContainer = null
|
||||||
var _item_list: VBoxContainer = null
|
var _item_list: VBoxContainer = null
|
||||||
@@ -142,10 +152,11 @@ func _build_ui() -> void:
|
|||||||
root_hbox.add_theme_constant_override("separation", 16)
|
root_hbox.add_theme_constant_override("separation", 16)
|
||||||
add_child(root_hbox)
|
add_child(root_hbox)
|
||||||
|
|
||||||
# --- Left: preview ---
|
# --- Left: preview (takes all the leftover width) ---
|
||||||
var preview_panel := PanelContainer.new()
|
var preview_panel := PanelContainer.new()
|
||||||
preview_panel.name = "PreviewPanel"
|
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)
|
root_hbox.add_child(preview_panel)
|
||||||
|
|
||||||
var preview_margin := MarginContainer.new()
|
var preview_margin := MarginContainer.new()
|
||||||
@@ -155,15 +166,30 @@ func _build_ui() -> void:
|
|||||||
preview_margin.add_theme_constant_override("margin_bottom", 8)
|
preview_margin.add_theme_constant_override("margin_bottom", 8)
|
||||||
preview_panel.add_child(preview_margin)
|
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 = SubViewportContainer.new()
|
||||||
_preview_container.name = "PreviewViewportContainer"
|
_preview_container.name = "PreviewViewportContainer"
|
||||||
_preview_container.stretch = true
|
_preview_container.stretch = true
|
||||||
_preview_container.custom_minimum_size = Vector2(464, 560)
|
_preview_container.custom_minimum_size = Vector2(400, 520)
|
||||||
preview_margin.add_child(_preview_container)
|
_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 = SubViewport.new()
|
||||||
_preview_viewport.name = "PreviewViewport"
|
_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.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||||
_preview_viewport.transparent_bg = true
|
_preview_viewport.transparent_bg = true
|
||||||
# Its own 3D world -- a SubViewport that shares the (Control-only) main
|
# 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_mode = Environment.BG_COLOR
|
||||||
env.background_color = Color(0.10, 0.11, 0.14)
|
env.background_color = Color(0.10, 0.11, 0.14)
|
||||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||||
env.ambient_light_color = Color(0.6, 0.6, 0.65)
|
env.ambient_light_color = Color(0.62, 0.64, 0.7)
|
||||||
env.ambient_light_energy = 1.0
|
env.ambient_light_energy = 0.7
|
||||||
world_env.environment = env
|
world_env.environment = env
|
||||||
_preview_viewport.add_child(world_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 = Camera3D.new()
|
||||||
_preview_camera.name = "PreviewCamera"
|
_preview_camera.name = "PreviewCamera"
|
||||||
# look_at needs a global transform, which only exists once in the tree.
|
# look_at needs a global transform, which only exists once in the tree.
|
||||||
_preview_camera.look_at_from_position(
|
_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_camera.current = true
|
||||||
_preview_viewport.add_child(_preview_camera)
|
_preview_viewport.add_child(_preview_camera)
|
||||||
|
|
||||||
@@ -201,12 +227,12 @@ func _build_ui() -> void:
|
|||||||
var key := DirectionalLight3D.new()
|
var key := DirectionalLight3D.new()
|
||||||
key.name = "PreviewKeyLight"
|
key.name = "PreviewKeyLight"
|
||||||
key.rotation_degrees = Vector3(-35, 35, 0)
|
key.rotation_degrees = Vector3(-35, 35, 0)
|
||||||
key.light_energy = 1.2
|
key.light_energy = 1.0
|
||||||
_preview_viewport.add_child(key)
|
_preview_viewport.add_child(key)
|
||||||
var fill := DirectionalLight3D.new()
|
var fill := DirectionalLight3D.new()
|
||||||
fill.name = "PreviewFillLight"
|
fill.name = "PreviewFillLight"
|
||||||
fill.rotation_degrees = Vector3(-20, -140, 0)
|
fill.rotation_degrees = Vector3(-20, -140, 0)
|
||||||
fill.light_energy = 0.4
|
fill.light_energy = 0.35
|
||||||
_preview_viewport.add_child(fill)
|
_preview_viewport.add_child(fill)
|
||||||
|
|
||||||
# --- Right: controls ---
|
# --- Right: controls ---
|
||||||
@@ -403,6 +429,31 @@ func _on_equip_pressed(asset_id: String) -> void:
|
|||||||
loadout_changed.emit(loadout)
|
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
|
# Preview refresh
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
# weighted to its bone -- so the rig still drives it, but it reads as a clean
|
||||||
# solid block (no banding, no capsule taper).
|
# solid block (no banding, no capsule taper).
|
||||||
const PARTS := [
|
const PARTS := [
|
||||||
["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.56), C_SKIN],
|
# Torso, arms and legs all share the same 0.46 depth so a limb always fully
|
||||||
["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.48), C_SHIRT],
|
# covers the slice of torso behind it -- otherwise a brightly-lit sliver of
|
||||||
["LeftUpperArm", Vector3(0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN],
|
# the torso side peeks past the arm and reads as a cyan seam.
|
||||||
["RightUpperArm", Vector3(-0.58, 1.14, 0.00), Vector3(0.34, 0.78, 0.40), C_SKIN],
|
["Head", Vector3(0.00, 1.74, 0.00), Vector3(0.58, 0.56, 0.58), C_SKIN],
|
||||||
["LeftUpperLeg", Vector3(0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS],
|
["Spine", Vector3(0.00, 1.14, 0.00), Vector3(0.90, 0.80, 0.46), C_SHIRT],
|
||||||
["RightUpperLeg", Vector3(-0.21, 0.38, 0.00), Vector3(0.40, 0.78, 0.40), C_PANTS],
|
["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
|
# 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.30), 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.13, 1.78, -0.30), 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.00, 1.64, -0.30), Vector3(0.32, 0.05, 0.02), C_FACE],
|
||||||
]
|
]
|
||||||
|
|
||||||
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
||||||
|
|||||||
Reference in New Issue
Block a user