From 43a3b888da540be5fb496bedb3699e39d8bcfff2 Mon Sep 17 00:00:00 2001 From: daddy Date: Sun, 6 Sep 2026 16:10:46 +0000 Subject: [PATCH] Avatar customization: skin tone, head swap, and hair now actually apply 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 Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ --- assets/avatars/base_human_male_01.glb | Bin 24900 -> 24912 bytes assets/avatars/catalog.json | 2 +- assets/avatars/face_glasses_01.glb | Bin 3008 -> 3012 bytes assets/avatars/hair_mohawk_01.glb | Bin 2780 -> 2796 bytes assets/avatars/hair_ponytail_01.glb | Bin 3004 -> 3004 bytes assets/avatars/head_cyber_01.glb | Bin 2980 -> 2980 bytes scripts/avatar/avatar_customization_ui.gd | 13 ++++----- scripts/avatar/avatar_loader.gd | 32 ++++++++++++++++++++++ scripts/avatar/avatar_system.gd | 2 ++ scripts/avatar/avatar_visualizer.gd | 2 ++ scripts/tools/gen_placeholder_avatars.gd | 30 +++++++++++--------- 11 files changed, 60 insertions(+), 21 deletions(-) diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb index 36c55368a996eb929a80d7dc967135d12d7f0ac2..d515718a8f45d17c715d6f95ca86eca86c5d692c 100644 GIT binary patch delta 331 zcmX?di1ET9M&9(C5H}_U28Mt{28KTqd8Y|l7=VGHp{0?5rIDqM;p7cmQWMWx3Yl7h zfQga0xuvOzp^kx`!Q=o=#m%~m7bPYONJ>qXkWv*iHUuj&HZ}sPvoyCbo$M#2p`i;@ z3o+Kj$kNc%*u=!hLp zE4zgQ1A~*N-{kvBiko#5Ll}92cHG+!^jQNC*-w6;q%c`WQG|!|f`eXopa#EW(qP_H4gqOg};}2aDk5HOez=0bitB1poj5 delta 319 zcmca`i1ElFM&9(C5H}_U1_qZz28It4d8Y}P8-ReJfw_@|g|V4|nU3M)56n^%&snnQ z80Z;Hp2(!Q*^2R^grI>1SfPoLrJ<>@iHVWPWEm+vK@&qu5HK}2urM(*H8nSz94)0W zSzbm|Sl2+$&=f3bX=Y+%Y;I&|JbAU0m8zjRSkT1W)WqD<(8x>&sL%-A5tF&4ZHQC3 zIahiyE3<-v!e#-*8b;&%^&HI!m*#ZCzL{-WF 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 59d9111838069ca5d72bc250e8fd8a20635b11a3..8cc59c8326893ee7731dc62ed91d2dbf72db66ae 100644 GIT binary patch delta 487 zcmX>gengx%JtxGCiGhLP2sZ-*%S2xPdSgp4FflVWHMKCbG}kfEGcYxQ2$~xJ1b%HD&-28qMe zfW$%Uy-4aoY?ykOI7|&p9A-Y$oO-whkOa(7m?AqQ3(yS$SpZWHV%uPGC`=q`8C(ra X+-{u~#K*`6!PvHFzS(Te(aH<}=Ofdb delta 476 zcmX>ien6ZzJtxGCiGhLP05_1B$m?HkXbA=u76#^)2Ihu326_gj1`uFwVrpP&VPFOl zGcnBsNGKrUoPqV(&##4`RdA!^B}~ zVB#?IVdm5$OTY|;DY8Sd0No&%WiW9YEDl9>1U55)=4*m|46_*7P>=?g<3Qq@?KxVR E0arG~H2?qr diff --git a/assets/avatars/hair_mohawk_01.glb b/assets/avatars/hair_mohawk_01.glb index 0caea8ae3b1a61aa47b524de74a8f661041b13f3..026fd38d3c8cd56a064a64911e346073190176a8 100644 GIT binary patch delta 632 zcmca3`bLyDJtxGCiGhLP4HpB$kBPkgiKYe+U~Xb+U}|AtremOIXlMxr=EjER=9UJQ zmOwEh(^ws)+{`>Bt7u&VJp&9~KtW7xKtUtZ$%TvxlWmxUCUY|$@ie!vv@kF>FtRW- zFf#+2YhY|?YGP?_X>M+AY;0y|q+_UOfNYf|NQ;S?sj($cm9e?0jxJDvj_%|m zOfy+@bd*Yp67!045=%1kLCyoZ4&(=@w+u}z4GaxI<^siICtqb#uUELi%)sE}=?4r2 z28RAaUu_u}8tjnR`;gcu;%CqJ>;L}B)HfhYAhVGTMKK7; z0+3}e3y{?ir5EUMdxp(F*=IAxm&~iOpL{dGjwO4#`QCX|`z5Xf*i~jt-%cg*$^SW3 NC-ZReOjhCA0RXa~+~fcN delta 619 zcmaDOdPkHuJtxGCiGhLP4i^K%i;2Ac^~RPEU}#`!X>4F@tYe^OXaE6*CYA<)aFVkl6c>*eK#$*Om7I)$awV z2eNIU?0raVkT^^YE_3RUC6L+3hN2jRWC6%Bm<7mch|(*_bm*EjhCqtRww#vQCGMuwJPU}0fkZfRg{2(-n>7$Rh1W^8I|VQ6VS z`6ZivJ=gWcd*1|F*quFd#umu7g|hb|u|eW6H6U>idmoZ|5F4f*CJs{r6Nj0PY>q9^ zoO+N1%utvjkOVG+K;kg F0{~NL%47fl delta 479 zcmZ{fF$%&k6ox6Hh~O-a78jvvk|t>))QhMGaL_Asa#rZ1t4HYK(0UEe;OGs^Yx_%4 z%=GiU|9wgFK|VNb5>_2y4HKad0h@7EA!Rj_T1w*>i)?v};M{1Y^(OFl+r7V7O=s&N4 zade1NzBl@m(FaEd9R8*!cAkU=kfKC`eooDp<4WzU{84Fp| GJ+$7X*{omy diff --git a/assets/avatars/head_cyber_01.glb b/assets/avatars/head_cyber_01.glb index 508b2f04376c04f443e2e4a3331b43e0c38e627e..b6e5686ba72ea5febcf0b3ff36360ca02a3a4b51 100644 GIT binary patch delta 464 zcmZ1?zC?UNOueBc7#LZYn46dwndunl8JZe^fvKgLfr*)ki3L#1*bpLSWNcw#Xkls* ztD}^gnWtnGtqYPvGXf}vX$Dv!h9P;Aiy37m^Dqf*=49N+Drg8b%G}t{+}zT@(sJ@? zHq&~p>x=iznki`)v}2wfkZlKL??YmP#9?Yc;vn`uB=sOROg&5-rUoVsGauO;JD@rB zAPJbEFhw8iqSIRvzgYq9cX9wY%X6s8C)hNJj s2U!LZ2V)=`BnA@)(@1Jy;&xE=Kywt~8eoQkBtdL24P 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":