From 181ba6ae02ab93f2156e1d2b0d15f0cf594687e6 Mon Sep 17 00:00:00 2001 From: daddy Date: Sun, 6 Sep 2026 15:18:26 +0000 Subject: [PATCH] Avatar customization: drag-to-rotate preview, fill the dead space, seam fix - 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 Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ --- assets/avatars/base_human_male_01.glb | Bin 24872 -> 24900 bytes scripts/avatar/avatar_customization_ui.gd | 73 ++++++++++++++++++---- scripts/tools/gen_placeholder_avatars.gd | 21 ++++--- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb index c4465cd18d3e92a22e3627a868f66a15e304a7f7..36c55368a996eb929a80d7dc967135d12d7f0ac2 100644 GIT binary patch literal 24900 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 X!Q5tT1C!sjb1!{IZ){8d?@ay=4!?2D literal 24872 zcmeGkOK==VbsY#F2MB}&2p=ORd=eNh-90@&5v?pcvTP$uiKN7UZOlrVl{S%fmEE-y z$K@&=@Ff=)Rb1dqam*DLh1aQ2TqvL_RZzZg$%Tu#!kPDarf1&H%&xSvHj%4#Q!BrH zdjC)N>z-9MwpLC)QYw|6t(HoEpD2~iEH5rxGpcKALA%{-wT;R1*NoN6wOY`6r5?Ot zOfu70Yi{o}8$qM9a&;##CT%VpL|$%mj7cF)W4rn$u<mWmM6JqyMDwj zT!rS$6`p4~%rQaCMNm_36i{=Nl{0#dipb7TaHHAUu5Q)e3f7IuT6L=(m_|o=YMh-} zk;ZkiHzIs+L^$(wNLbmmP0+=Ce0j_lj?FC>{1jg3JLkRtv>&?%<^qrU0bmYUe`8;n zJz-^x75g^!C@_Ju87VY!}bF)ujE&=!UX=t%<5GE@IRE+)9=TU=U>Efv>Zkp*M3 zk+t@1Y-CV>qp)Cx3>8#zw$fFyRi%@>a)=847iTT`JWyxWV>?f0(p^+%RI*Wv#i}t9 zr0w={J8Oc{mTvHTx0xHO9UR}~6EpKOOPOMH2YYLQDdAee%2I|s<1yD~E@u)_ZCeWG zz+z!B;ab>7AyQ1 z3`}IU$$0Jxopf2^+X3h@*dqom35l@}q|NqN)f4T4r}~V0F=AkTavE)q^*vGg;2JG= zAx;`CF~Q|=9T(cm_#VqB*}7SoyWw``!YJrEu8SkB7w$S-at9_SC>Pv`(xY!|Bm6Q? zh{J8wS3TRA7q&HbY}=B)Yk`n~nap;Y*khF@&LyKyxK<{g2kM0C0kf*@b^wQnYsN;< z2wK%nvt>*gbItW;XJWRoQEvnjS7g~Ki*mbDUEK=G&04L#ReWs#A2c>Po5m!E z2_YH%v%eNT^cXTM)@sTNWC$_KomPDvPD;4!cdD%o^bxFe#2|Ta+Ub=iJQz=uhFBiR z-?zMsebROyt!Qy9ZwUKvR_bxr_Ux3qusrYEx{S9D9QIPy4dX2yx@a;y##8LpfZ%7G z^Ee)Ci^j@^2Jf}TPRA;3-JsIQSm$${j3SxjGIC89u5UXjXNSmS##i6fxrmKRWnz%8 zx`niDoCz;w&7eej+LMIe!o{p;CShGpl|APi4eAj%gnwBBg{6It<26KE#$6LT4F0AZ zPQ`Cf_;dWV+|jTcGhjk(1a7FRB{>y3@A;8LsE>=;0{UF`%d*ug?c zJFBg;L8rR4RbN|bUf$Sjz$F}RhpW|gaH6@@Y@MvGsZH@bEBl;F=1P~j%;EyJS-97- zJmCtMC_EQV#E4=R0~!u{qJ+T$b|{#%P-~<_i^B^8qop+_*l2QaVXW}D6#P1_nZp># zm<5}%Z~5GD9AA2_k5+LQbv*cB`!K^;5--3cgTQ&PhfCX*KI8v)Lm_aSyRPdww&ytx z)HsY^3{7!u30oUyuwP?DT?;NKVZH)y7~Iw2MZ^IJX?~y?lb-Lu%^hTr`S59EV^#6A zH-o5NTlMXFr+y{+Sna@|vwFFMAFml_7Edp%EMH>ABs9>%;?mifc^G?4cllAQ3{6cM=2PpmEv(P(Tb<=SS=n~sb057FBp~RMI-ih z7B@7lw>Q4vdaQQ80R{PzeI&B**@6^4oJgdEK~{Skg$j9Bnmc{iVcWo$4P%^y z6C{kXDfTnfH~O%$FlAD>uLIv)XTMCmGPz6A8z~Ltd%l*izQQNB@K}-AF#W0rqnY|TiQ7=#7J~I9l&LUhat1fslJEr zKfQ1Z(rhZasuuF>1Qnx%gS4B{GjcqXl=~Er?Vt^}U@&S#2|M0gzY1|~zlM+D)sd&Y z2Df&KL_y)upvMQu6C@j{Lwvn~7jeOHnDWk6VQf^B9!P(+S#NZZgqy;&OljJtW9t1* zZOOufaSDbgSS?{g;jRfc)Hp^k6Yj2sFok8RhZ2#UaItuOVq(HORw^Aoy#U{I*qw$S zsnQn7j7QE6Ur#Y4?0KUuho7#5`U@m?nwNvPyKl$eu&+>BgC(C z3y9RO{Ob06G!ed_I^{8OXwPaVG79VPz1{?*fW{{7Oe z!tqD`8~1)OBL7Q;`N4mPyQ)7(*6{rU_D@It7v_)rAAw)_Ux@TPwoHUtzULeeenC~=Pul=09>K) zJ9CGQMfe^XiYKtp`myJcd5)An98i6%p6`t6Zw+Xt8GAD?H8q?c!Vjv zh7~>0tMQ7S;)(rQmy%EMgg%c3b!mL}=l9-+{y#PiIaL2r`0(97DtwxT;x&wX5igag z;D`o#q?d-`HH^Bb%p91?R5s`E_Siu661YPAqP9_dCMMcP{8#IxGShf%KhdS+Q@obb zhbHX{tW{yYfL_^+xkFgXgx~kjP`ri-FUrvL3MTew`5C;XL0&2|2d2E#PKsA>3`{P5dher;5=PgV9`r;S9^yR0&^0luXnfUrQo-w}p?QebOxo5v?e(&h_%kY=smM!5M%Ki@~pRY_! z&m24c!im|rlc&y{{^8mAg%=lJT3%Xt<>hlf{?Sh^oWFSKr$1Y*t_AD0&5inNuhwe+ z!e?RA@GPnVFg{m`;af-kq~RVKiYKtp`myJcd5)An98i6%p6`t6Zw+X zt8GAD?H8q?c!Vjvh7~>0tMQ7S;)(rQmy%EMgg%c3b!mJzy4M`vEyri?@Vpt#>@fE| z;Lr4o_ORY9qYr7@sbBkn;|)Wf_YqMP9-lpR>~_Ptj_(=p|08pd-l}fm+^d|A&syVn zkO=HCJMCk(DcybMd3gT;pK-(Y%qkSVb!0aD-a?9o;x$ZoyF{jfBbsiShT=7hyi{fm zOl2yYb9j4fpn3^hp?3DcXJQh?NBN@lQeK29UfWM}DZLc0>HE;6eZkKvU-&PDwM?*~ zhlb)cOnA{3O|M{LkCvaoYZ~OGGIL7{s0--o8xmvH|d&yeF866fV&-;cmqZ~Xn1aE^Y^XO6UdI-eKKU$_Po z&;R}6r@jme$5ThqbBCH=)vS$u5YFBTHwrv{UfC~?<9zM)odH#gVI77wT?Bpt;(U5aV}m!bm3pU3f_5g6A4jmEfN?sGlt;|^p$&v)K>@|~&wKKsvU_&d6;Wmy#p zAGvW);nOq}uVKxLe1g3)G*RSa1RZ|6BzEbV!d#u*9%jb=oi|Y=G8Rl zOO9UZqw 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 # --------------------------------------------------------------------------- diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index 546b0d5..fa06ac5 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -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]