fix: correct skin deformation and scale for GLB character models

The Miku model T-posed because the previous _update_skin() used
set_bone_pose(i, get_bone_pose(i)) which is a no-op when the pose
is unchanged. Replace with skeleton.notification(NOTIFICATION_UPDATE_SKELETON)
to force the Skeleton3D to recompute bone transforms and notify the
MeshInstance to update vertex buffers.

Also fix scale: the GLB is ~1.2m tall (not 1.6m as documented), so
set scale_factor=1.25 to fit the 1.8m player capsule, and add
position_y_offset=-0.2 to align the model's feet with the player origin.

Co-Authored-By: Hermes Agent <[email protected]>
This commit is contained in:
2026-06-25 00:56:25 -04:00
parent c4dc4ca77c
commit 43b870465b
3 changed files with 23 additions and 15 deletions
+19 -14
View File
@@ -5,9 +5,13 @@ class_name SkinnedPlayerModel
## Replaces the procedural HumanoidModel for characters with custom skins.
@export var model_path: String = ""
## Scale factor applied to the loaded scene. For the Miku GLB, 1.0 is correct (~1.6m).
## Scale factor applied to the loaded scene. The current Miku GLB is ~1.2m tall;
## set to 1.25 to reach ~1.5m (fits inside the 1.8m player capsule).
## For models exported in centimeters (Mixamo/Blender default), set to 0.01.
@export var scale_factor: float = 1.0
@export var scale_factor: float = 1.25
## Vertical offset to align the model's visual feet with the player origin.
## The Miku GLB has feet at Y=0.2 in rest pose; negative offset lowers them.
@export var position_y_offset: float = -0.2
@export var first_person_mode: bool = false # Hide head/torso for FPS view
## Bones per second for locomotion animations created at runtime.
@@ -56,10 +60,11 @@ func load_model(path: String) -> void:
add_child(scene)
print("SkinnedPlayerModel: scene added: %s" % scene.name)
# Apply model scale (the Miku GLB is already in meters, ~1.6m tall)
# Apply model scale (the Miku GLB is ~1.2m; scale_factor adjusts to target height)
# For models exported in centimeters (common from Mixamo/Blender), set scale_factor to 0.01.
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
print("SkinnedPlayerModel: applied scale %.4f" % scale_factor)
scene.position = Vector3(0, position_y_offset * scale_factor, 0)
print("SkinnedPlayerModel: applied scale %.4f, y_offset %.4f" % [scale_factor, position_y_offset])
# Find mesh instance for first-person mode
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
@@ -122,6 +127,10 @@ func load_model(path: String) -> void:
# Ensure fallback animations exist for states the movement system needs
_ensure_locomotion_animations(animation_player)
# Force initial skin update so the first rendered frame shows the
# correct animated pose instead of T-pose.
_update_skin()
else:
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
@@ -303,16 +312,12 @@ func _add_animation_to_player(ap: AnimationPlayer, anim_name: String, anim: Anim
print("SkinnedPlayerModel: added '%s' (%d tracks)" % [anim_name, anim.get_track_count()])
func _update_skin() -> void:
## Force skeleton skin update by re-applying current bone poses.
## GLTF runtime loaded models sometimes need this because the AnimationPlayer's
## internal bone pose updates don't always trigger the visual pipeline.
# Re-apply current bone poses to ensure dirty flags are set.
# This triggers NOTIFICATION_UPDATE_SKELETON on the Skeleton3D.
for i in range(skeleton.get_bone_count()):
skeleton.set_bone_pose(i, skeleton.get_bone_pose(i))
# Also notify the MeshInstance that its skeleton changed
if _mesh_instance:
_mesh_instance.skeleton = _mesh_instance.skeleton
## Force skeleton skin update by notifying Skeleton3D that poses changed.
## GLTF runtime loaded models need this because AnimationPlayer's internal
## bone pose updates don't always trigger the visual pipeline's dirty flags.
## NOTIFICATION_UPDATE_SKELETON causes the Skeleton3D to recompute its
## bone transforms and notify attached MeshInstances to update vertex buffers.
skeleton.notification(NOTIFICATION_UPDATE_SKELETON)
func _setup_first_person() -> void:
# In first person, hide the head and upper body so only arms/hands/legs show