diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb index 36c5536..d515718 100644 Binary files a/assets/avatars/base_human_male_01.glb and b/assets/avatars/base_human_male_01.glb differ diff --git a/assets/avatars/catalog.json b/assets/avatars/catalog.json index e69e50b..0a28f98 100644 --- a/assets/avatars/catalog.json +++ b/assets/avatars/catalog.json @@ -8,7 +8,7 @@ "display_name": "Human Male Base" }, "head_cyber_01": { - "type": "body_part", + "type": "attachment", "slot": "head", "path": "res://assets/avatars/head_cyber_01.glb", "version": "1.0.0", diff --git a/assets/avatars/face_glasses_01.glb b/assets/avatars/face_glasses_01.glb index 59d9111..8cc59c8 100644 Binary files a/assets/avatars/face_glasses_01.glb and b/assets/avatars/face_glasses_01.glb differ diff --git a/assets/avatars/hair_mohawk_01.glb b/assets/avatars/hair_mohawk_01.glb index 0caea8a..026fd38 100644 Binary files a/assets/avatars/hair_mohawk_01.glb and b/assets/avatars/hair_mohawk_01.glb differ diff --git a/assets/avatars/hair_ponytail_01.glb b/assets/avatars/hair_ponytail_01.glb index 6cb9e38..cf136ac 100644 Binary files a/assets/avatars/hair_ponytail_01.glb and b/assets/avatars/hair_ponytail_01.glb differ diff --git a/assets/avatars/head_cyber_01.glb b/assets/avatars/head_cyber_01.glb index 508b2f0..b6e5686 100644 Binary files a/assets/avatars/head_cyber_01.glb and b/assets/avatars/head_cyber_01.glb differ diff --git a/scripts/avatar/avatar_customization_ui.gd b/scripts/avatar/avatar_customization_ui.gd index 3ef9e1f..5cec953 100644 --- a/scripts/avatar/avatar_customization_ui.gd +++ b/scripts/avatar/avatar_customization_ui.gd @@ -489,15 +489,14 @@ func _refresh_preview() -> void: func _on_skin_tone_changed(value: float) -> void: if loadout == null: 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 skin := Color( - 0.6 + 0.3 * t, # R - 0.4 + 0.2 * t, # G - 0.3 + 0.15 * t, # B - 1.0 + loadout.skin_tone = Color( + 0.35 + 0.60 * t, # R + 0.22 + 0.55 * t, # G + 0.16 + 0.50 * t, # B + 1.0, ) - loadout.skin_tone = skin _refresh_preview() loadout_changed.emit(loadout) diff --git a/scripts/avatar/avatar_loader.gd b/scripts/avatar/avatar_loader.gd index 0430890..403ac6f 100644 --- a/scripts/avatar/avatar_loader.gd +++ b/scripts/avatar/avatar_loader.gd @@ -23,6 +23,11 @@ const AvatarCatalog := preload("res://scripts/avatar/avatar_catalog.gd") ## Maximum number of attachments allowed in the accessory slot. 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). const CANONICAL_BONES := [ "Root", @@ -239,6 +244,33 @@ func set_base_mesh_visible(slot_id: String, visible: bool) -> void: 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). func get_current_loadout() -> Dictionary: var loadout: Dictionary = { diff --git a/scripts/avatar/avatar_system.gd b/scripts/avatar/avatar_system.gd index cc6b65c..ff66199 100644 --- a/scripts/avatar/avatar_system.gd +++ b/scripts/avatar/avatar_system.gd @@ -318,5 +318,7 @@ func assemble_avatar(loadout) -> Node3D: if v is String and not (v as String).is_empty(): l.set_base_mesh_visible(slot_id, false) + l.apply_skin_tone(loadout.skin_tone) + l.free() return base diff --git a/scripts/avatar/avatar_visualizer.gd b/scripts/avatar/avatar_visualizer.gd index 3376c02..05ff78b 100644 --- a/scripts/avatar/avatar_visualizer.gd +++ b/scripts/avatar/avatar_visualizer.gd @@ -82,6 +82,8 @@ func load_loadout(loadout, root_node: Node3D = null) -> void: if value is String and not (value as String).is_empty(): _loader.set_base_mesh_visible(slot_id, false) + _loader.apply_skin_tone(loadout.skin_tone) + build_complete.emit() diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index fa06ac5..ef38ac9 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -25,11 +25,14 @@ const BONES := { "Spine": ["Hips", Vector3(0.00, 1.00, 0.00)], "Chest": ["Spine", Vector3(0.00, 1.30, 0.00)], "Neck": ["Chest", Vector3(0.00, 1.50, 0.00)], - "Head": ["Neck", Vector3(0.00, 1.58, 0.00)], - "HeadTop": ["Head", Vector3(0.00, 2.00, 0.00)], - "Jaw": ["Head", Vector3(0.00, 1.64, -0.16)], - "LeftEye": ["Head", Vector3(0.12, 1.76, -0.27)], - "RightEye": ["Head", Vector3(-0.12, 1.76, -0.27)], + # Head bone sits at the visual head's centre and HeadTop just above its + # crown, so hats / hair / helmets mount where you'd expect. (The body mesh + # is rigidly bound, so moving these bones doesn't move the mesh.) + "Head": ["Neck", Vector3(0.00, 1.74, 0.00)], + "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)], "LeftUpperArm": ["LeftShoulder", Vector3(0.60, 1.44, 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] # Mesh is built in _attachment_mesh(). const ATTACHMENTS := { - "head_cyber_01": ["head", Vector3(0, 0.04, 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_ponytail_01": ["ponytail", Vector3(0, -0.06, 0.16), Color(0.45, 0.30, 0.15)], - "face_glasses_01": ["glasses", Vector3(0, 0.12, -0.16), Color(0.10, 0.10, 0.12)], + "head_cyber_01": ["head", Vector3(0, 0.12, 0.0), Color(0.20, 0.85, 0.95)], + "hair_mohawk_01": ["mohawk", Vector3(0, 0.10, 0.0), Color(0.90, 0.20, 0.35)], + "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.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)], "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)], @@ -261,13 +264,14 @@ func _gen_attachment(id: String, kind: String, offset: Vector3, col: Color) -> v func _attachment_mesh(kind: String) -> Mesh: match kind: "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": - 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": - 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": - 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": var m := BoxMesh.new(); m.size = Vector3(0.80, 0.66, 0.42); return m "box":