fix: force bone pose update for GLTF runtime skin deformation

GLTF runtime loaded models may not trigger automatic MeshInstance
vertex deformation when the AnimationPlayer updates bone poses.
Added throttled _update_skin() that re-applies current bone poses
to trigger NOTIFICATION_UPDATE_SKELETON, ensuring the renderer
sees the updated vertex positions.
This commit is contained in:
2026-06-25 00:26:34 -04:00
parent fead2cc055
commit 73831cb418
+36 -2
View File
@@ -56,7 +56,7 @@ func load_model(path: String) -> void:
add_child(scene) add_child(scene)
print("SkinnedPlayerModel: scene added: %s" % scene.name) print("SkinnedPlayerModel: scene added: %s" % scene.name)
# Model scale the Miku GLB is already exported at meter scale (~1.6m tall). # Apply model scale (the Miku GLB is already in meters, ~1.6m tall)
# For models exported in centimeters (common from Mixamo/Blender), set scale_factor to 0.01. # For models exported in centimeters (common from Mixamo/Blender), set scale_factor to 0.01.
scene.scale = Vector3(scale_factor, scale_factor, scale_factor) scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
print("SkinnedPlayerModel: applied scale %.4f" % scale_factor) print("SkinnedPlayerModel: applied scale %.4f" % scale_factor)
@@ -68,6 +68,18 @@ func load_model(path: String) -> void:
if mesh_instances.size() > 0: if mesh_instances.size() > 0:
_mesh_instance = mesh_instances[0] _mesh_instance = mesh_instances[0]
# Fix skeleton binding: GLTF imports set MeshInstance.skeleton to ".."
# which should work for a direct parent, but in our case the MeshInstance
# is a child of Skeleton3D which is a child of MikuRig. The skeleton
# property needs to point to the Skeleton3D node for skin deformation.
# Set it explicitly to the skeleton's path relative to MeshInstance.
if _mesh_instance and skeleton:
# Manually compute relative path since get_relative_path_to is not
# exposed for MeshInstance3D in Godot 4.2.
# MeshInstance is child of Skeleton3D, so ".." points to Skeleton3D.
_mesh_instance.skeleton = NodePath("..")
print("SkinnedPlayerModel: fixed mesh skeleton binding -> '%s'" % _mesh_instance.skeleton)
if first_person_mode and _mesh_instance: if first_person_mode and _mesh_instance:
_setup_first_person() _setup_first_person()
@@ -88,6 +100,10 @@ func load_model(path: String) -> void:
# Find AnimationPlayer # Find AnimationPlayer
animation_player = _find_animation_player(scene) animation_player = _find_animation_player(scene)
if animation_player: if animation_player:
# Ensure AnimationPlayer processes even if parent has process_mode disabled
animation_player.process_mode = Node.PROCESS_MODE_INHERIT
animation_player.active = true
print("SkinnedPlayerModel: AP process_mode=%d active=%s" % [animation_player.process_mode, animation_player.active])
var anim_list = animation_player.get_animation_list() var anim_list = animation_player.get_animation_list()
print("SkinnedPlayerModel: %d animations available:" % anim_list.size()) print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
for anim_name in anim_list: for anim_name in anim_list:
@@ -286,12 +302,20 @@ func _add_animation_to_player(ap: AnimationPlayer, anim_name: String, anim: Anim
lib.add_animation(anim_name, anim) lib.add_animation(anim_name, anim)
print("SkinnedPlayerModel: added '%s' (%d tracks)" % [anim_name, anim.get_track_count()]) print("SkinnedPlayerModel: added '%s' (%d tracks)" % [anim_name, anim.get_track_count()])
func _update_skin() -> void:
## Force skeleton skin update by notifying the Skeleton3D that bones changed.
## 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))
func _setup_first_person() -> void: func _setup_first_person() -> void:
# In first person, hide the head and upper body so only arms/hands/legs show # In first person, hide the head and upper body so only arms/hands/legs show
# This creates the classic FPS arms-only view # This creates the classic FPS arms-only view
if not _mesh_instance: if not _mesh_instance:
return return
print("SkinnedPlayerModel: setting up first-person mode")
# The mesh has multiple sub-meshes (body parts). We need to hide the ones # The mesh has multiple sub-meshes (body parts). We need to hide the ones
# that are above the chest (head, torso, skirt, etc.) # that are above the chest (head, torso, skirt, etc.)
# For now, we hide the entire mesh and rely on the arms being separate # For now, we hide the entire mesh and rely on the arms being separate
@@ -338,6 +362,7 @@ func play_animation(anim_name: String) -> void:
var _last_played: String = "" var _last_played: String = ""
var _missing_anim_warned: Array = [] var _missing_anim_warned: Array = []
var _skin_update_acc: float = 0.0
func _warn_missing(anim_name: String) -> void: func _warn_missing(anim_name: String) -> void:
## Warn once per missing animation, not every frame. ## Warn once per missing animation, not every frame.
@@ -359,6 +384,15 @@ func _process(delta: float) -> void:
elif _last_played == "": elif _last_played == "":
print("SkinnedPlayerModel: NOT playing") print("SkinnedPlayerModel: NOT playing")
# Force skeleton skin update — GLTF runtime loaded models need explicit
# bone pose re-application to trigger MeshInstance vertex deformation.
# Throttled to ~30fps to avoid per-frame overhead.
if skeleton and animation_player.is_playing():
_skin_update_acc += delta
if _skin_update_acc > 0.033:
_skin_update_acc = 0.0
_update_skin()
# Get movement state from parent PlayerMovementController # Get movement state from parent PlayerMovementController
var parent = get_parent() var parent = get_parent()
if not parent: if not parent: