Avatar customization: skin tone, head swap, and hair now actually apply
checkpoint / checkpoint (push) Successful in 1m45s
checkpoint / checkpoint (push) Successful in 1m45s
Three dead controls on the customization screen: - Skin tone slider did nothing: neither the visualizer nor AvatarSystem ever read loadout.skin_tone. Added AvatarLoader.apply_skin_tone() -- it retints the base body's skin surfaces (matched by the placeholder skin albedo, i.e. head + arms) -- and both build paths call it. Slider now maps across a deep-to-pale skin range. - "Cyber Head" did nothing: its catalog entry was type "body_part", which load_attachment() rejects, and the placeholder body is one mesh so there was nothing to hide either. Retyped it as an attachment -> it mounts on the Head bone as a cyber visor. - Mohawk / ponytail sat inside the head: the Head and HeadTop bones were well below the visual head (bone at 1.58, head crown at ~2.03). Moved Head to the head's centre and HeadTop just above the crown (mesh is rigidly bound, so it doesn't move), and retuned the hair/visor/glasses meshes and offsets to match. Verified: avatar smoke 14/14 (source + pack), fresh-clone, 2p multiplayer, and drove the real customization screen in rendered screenshots (equip head+mohawk+mid skin, then ponytail+glasses+pale skin). 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.
@@ -8,7 +8,7 @@
|
|||||||
"display_name": "Human Male Base"
|
"display_name": "Human Male Base"
|
||||||
},
|
},
|
||||||
"head_cyber_01": {
|
"head_cyber_01": {
|
||||||
"type": "body_part",
|
"type": "attachment",
|
||||||
"slot": "head",
|
"slot": "head",
|
||||||
"path": "res://assets/avatars/head_cyber_01.glb",
|
"path": "res://assets/avatars/head_cyber_01.glb",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -489,15 +489,14 @@ func _refresh_preview() -> void:
|
|||||||
func _on_skin_tone_changed(value: float) -> void:
|
func _on_skin_tone_changed(value: float) -> void:
|
||||||
if loadout == null:
|
if loadout == null:
|
||||||
return
|
return
|
||||||
# Map slider 0..1 to a skin tone color range.
|
# Map slider 0..1 across a deep-to-pale skin range.
|
||||||
var t := value
|
var t := value
|
||||||
var skin := Color(
|
loadout.skin_tone = Color(
|
||||||
0.6 + 0.3 * t, # R
|
0.35 + 0.60 * t, # R
|
||||||
0.4 + 0.2 * t, # G
|
0.22 + 0.55 * t, # G
|
||||||
0.3 + 0.15 * t, # B
|
0.16 + 0.50 * t, # B
|
||||||
1.0
|
1.0,
|
||||||
)
|
)
|
||||||
loadout.skin_tone = skin
|
|
||||||
_refresh_preview()
|
_refresh_preview()
|
||||||
loadout_changed.emit(loadout)
|
loadout_changed.emit(loadout)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const AvatarCatalog := preload("res://scripts/avatar/avatar_catalog.gd")
|
|||||||
## Maximum number of attachments allowed in the accessory slot.
|
## Maximum number of attachments allowed in the accessory slot.
|
||||||
const MAX_ACCESSORY_COUNT := 8
|
const MAX_ACCESSORY_COUNT := 8
|
||||||
|
|
||||||
|
## The skin albedo baked into the placeholder base body by
|
||||||
|
## gen_placeholder_avatars.gd. apply_skin_tone() finds the mesh surfaces
|
||||||
|
## painted this colour (head + arms) and retints them.
|
||||||
|
const PLACEHOLDER_SKIN_ALBEDO := Color(0.96, 0.80, 0.19)
|
||||||
|
|
||||||
## Skeleton bone names that form the canonical rig (§3.1).
|
## Skeleton bone names that form the canonical rig (§3.1).
|
||||||
const CANONICAL_BONES := [
|
const CANONICAL_BONES := [
|
||||||
"Root",
|
"Root",
|
||||||
@@ -239,6 +244,33 @@ func set_base_mesh_visible(slot_id: String, visible: bool) -> void:
|
|||||||
mesh.visible = visible
|
mesh.visible = visible
|
||||||
|
|
||||||
|
|
||||||
|
## Retint the base body's skin surfaces (the ones the generator painted
|
||||||
|
## PLACEHOLDER_SKIN_ALBEDO -- head + arms) to [param color]. A no-op if the
|
||||||
|
## colour is unset (transparent). Real segmented bodies will do this per mesh.
|
||||||
|
func apply_skin_tone(color: Color) -> void:
|
||||||
|
if color.a <= 0.0:
|
||||||
|
return
|
||||||
|
if avatar_node == null or not is_instance_valid(avatar_node):
|
||||||
|
return
|
||||||
|
var skel := avatar_node.get_node_or_null("Skeleton3D") as Skeleton3D
|
||||||
|
if skel == null:
|
||||||
|
return
|
||||||
|
for child in skel.get_children():
|
||||||
|
if not (child is MeshInstance3D):
|
||||||
|
continue
|
||||||
|
var mi := child as MeshInstance3D
|
||||||
|
if mi.mesh == null:
|
||||||
|
continue
|
||||||
|
for s in mi.mesh.get_surface_count():
|
||||||
|
var mat := mi.get_active_material(s)
|
||||||
|
if mat is BaseMaterial3D and \
|
||||||
|
(mat as BaseMaterial3D).albedo_color.is_equal_approx(PLACEHOLDER_SKIN_ALBEDO):
|
||||||
|
var tinted := StandardMaterial3D.new()
|
||||||
|
tinted.albedo_color = color
|
||||||
|
tinted.roughness = 0.9
|
||||||
|
mi.set_surface_override_material(s, tinted)
|
||||||
|
|
||||||
|
|
||||||
## Get the current loadout as a dictionary (for serialization).
|
## Get the current loadout as a dictionary (for serialization).
|
||||||
func get_current_loadout() -> Dictionary:
|
func get_current_loadout() -> Dictionary:
|
||||||
var loadout: Dictionary = {
|
var loadout: Dictionary = {
|
||||||
|
|||||||
@@ -318,5 +318,7 @@ func assemble_avatar(loadout) -> Node3D:
|
|||||||
if v is String and not (v as String).is_empty():
|
if v is String and not (v as String).is_empty():
|
||||||
l.set_base_mesh_visible(slot_id, false)
|
l.set_base_mesh_visible(slot_id, false)
|
||||||
|
|
||||||
|
l.apply_skin_tone(loadout.skin_tone)
|
||||||
|
|
||||||
l.free()
|
l.free()
|
||||||
return base
|
return base
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ func load_loadout(loadout, root_node: Node3D = null) -> void:
|
|||||||
if value is String and not (value as String).is_empty():
|
if value is String and not (value as String).is_empty():
|
||||||
_loader.set_base_mesh_visible(slot_id, false)
|
_loader.set_base_mesh_visible(slot_id, false)
|
||||||
|
|
||||||
|
_loader.apply_skin_tone(loadout.skin_tone)
|
||||||
|
|
||||||
build_complete.emit()
|
build_complete.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ const BONES := {
|
|||||||
"Spine": ["Hips", Vector3(0.00, 1.00, 0.00)],
|
"Spine": ["Hips", Vector3(0.00, 1.00, 0.00)],
|
||||||
"Chest": ["Spine", Vector3(0.00, 1.30, 0.00)],
|
"Chest": ["Spine", Vector3(0.00, 1.30, 0.00)],
|
||||||
"Neck": ["Chest", Vector3(0.00, 1.50, 0.00)],
|
"Neck": ["Chest", Vector3(0.00, 1.50, 0.00)],
|
||||||
"Head": ["Neck", Vector3(0.00, 1.58, 0.00)],
|
# Head bone sits at the visual head's centre and HeadTop just above its
|
||||||
"HeadTop": ["Head", Vector3(0.00, 2.00, 0.00)],
|
# crown, so hats / hair / helmets mount where you'd expect. (The body mesh
|
||||||
"Jaw": ["Head", Vector3(0.00, 1.64, -0.16)],
|
# is rigidly bound, so moving these bones doesn't move the mesh.)
|
||||||
"LeftEye": ["Head", Vector3(0.12, 1.76, -0.27)],
|
"Head": ["Neck", Vector3(0.00, 1.74, 0.00)],
|
||||||
"RightEye": ["Head", Vector3(-0.12, 1.76, -0.27)],
|
"HeadTop": ["Head", Vector3(0.00, 2.06, 0.00)],
|
||||||
|
"Jaw": ["Head", Vector3(0.00, 1.66, -0.22)],
|
||||||
|
"LeftEye": ["Head", Vector3(0.12, 1.80, -0.28)],
|
||||||
|
"RightEye": ["Head", Vector3(-0.12, 1.80, -0.28)],
|
||||||
"LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)],
|
"LeftShoulder": ["Chest", Vector3(0.42, 1.50, 0.00)],
|
||||||
"LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)],
|
"LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 0.00)],
|
||||||
"LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)],
|
"LeftLowerArm": ["LeftUpperArm", Vector3(0.60, 1.14, 0.00)],
|
||||||
@@ -84,10 +87,10 @@ const PARTS := [
|
|||||||
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
||||||
# Mesh is built in _attachment_mesh().
|
# Mesh is built in _attachment_mesh().
|
||||||
const ATTACHMENTS := {
|
const ATTACHMENTS := {
|
||||||
"head_cyber_01": ["head", Vector3(0, 0.04, 0.0), Color(0.20, 0.85, 0.95)],
|
"head_cyber_01": ["head", Vector3(0, 0.12, 0.0), Color(0.20, 0.85, 0.95)],
|
||||||
"hair_mohawk_01": ["mohawk", Vector3(0, 0.0, 0.0), Color(0.90, 0.20, 0.35)],
|
"hair_mohawk_01": ["mohawk", Vector3(0, 0.10, 0.0), Color(0.90, 0.20, 0.35)],
|
||||||
"hair_ponytail_01": ["ponytail", Vector3(0, -0.06, 0.16), Color(0.45, 0.30, 0.15)],
|
"hair_ponytail_01": ["ponytail", Vector3(0, -0.22, 0.24), Color(0.45, 0.30, 0.15)],
|
||||||
"face_glasses_01": ["glasses", Vector3(0, 0.12, -0.16), Color(0.10, 0.10, 0.12)],
|
"face_glasses_01": ["glasses", Vector3(0, 0.12, -0.07), Color(0.10, 0.10, 0.12)],
|
||||||
"chest_bandit_01": ["torso", Vector3(0, -0.12, 0.0), Color(0.55, 0.20, 0.20)],
|
"chest_bandit_01": ["torso", Vector3(0, -0.12, 0.0), Color(0.55, 0.20, 0.20)],
|
||||||
"back_backpack_01": ["box", Vector3(0, -0.06, 0.28), Color(0.30, 0.35, 0.25)],
|
"back_backpack_01": ["box", Vector3(0, -0.06, 0.28), Color(0.30, 0.35, 0.25)],
|
||||||
"pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)],
|
"pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)],
|
||||||
@@ -261,13 +264,14 @@ func _gen_attachment(id: String, kind: String, offset: Vector3, col: Color) -> v
|
|||||||
func _attachment_mesh(kind: String) -> Mesh:
|
func _attachment_mesh(kind: String) -> Mesh:
|
||||||
match kind:
|
match kind:
|
||||||
"head":
|
"head":
|
||||||
var m := BoxMesh.new(); m.size = Vector3(0.60, 0.56, 0.56); return m
|
# A cyber visor band across the upper half of the ~0.58 head box.
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.64, 0.30, 0.62); return m
|
||||||
"mohawk":
|
"mohawk":
|
||||||
var m := PrismMesh.new(); m.size = Vector3(0.08, 0.20, 0.44); return m
|
var m := PrismMesh.new(); m.size = Vector3(0.10, 0.24, 0.50); return m
|
||||||
"ponytail":
|
"ponytail":
|
||||||
var m := BoxMesh.new(); m.size = Vector3(0.14, 0.30, 0.16); return m
|
var m := BoxMesh.new(); m.size = Vector3(0.16, 0.36, 0.20); return m
|
||||||
"glasses":
|
"glasses":
|
||||||
var m := BoxMesh.new(); m.size = Vector3(0.44, 0.10, 0.03); return m
|
var m := BoxMesh.new(); m.size = Vector3(0.48, 0.11, 0.05); return m
|
||||||
"torso":
|
"torso":
|
||||||
var m := BoxMesh.new(); m.size = Vector3(0.80, 0.66, 0.42); return m
|
var m := BoxMesh.new(); m.size = Vector3(0.80, 0.66, 0.42); return m
|
||||||
"box":
|
"box":
|
||||||
|
|||||||
Reference in New Issue
Block a user