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:
@@ -5,9 +5,13 @@ class_name SkinnedPlayerModel
|
|||||||
## Replaces the procedural HumanoidModel for characters with custom skins.
|
## Replaces the procedural HumanoidModel for characters with custom skins.
|
||||||
|
|
||||||
@export var model_path: String = ""
|
@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.
|
## 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
|
@export var first_person_mode: bool = false # Hide head/torso for FPS view
|
||||||
|
|
||||||
## Bones per second for locomotion animations created at runtime.
|
## Bones per second for locomotion animations created at runtime.
|
||||||
@@ -56,10 +60,11 @@ 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)
|
||||||
|
|
||||||
# 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.
|
# 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)
|
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
|
# Find mesh instance for first-person mode
|
||||||
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
|
_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 fallback animations exist for states the movement system needs
|
||||||
_ensure_locomotion_animations(animation_player)
|
_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:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
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()])
|
print("SkinnedPlayerModel: added '%s' (%d tracks)" % [anim_name, anim.get_track_count()])
|
||||||
|
|
||||||
func _update_skin() -> void:
|
func _update_skin() -> void:
|
||||||
## Force skeleton skin update by re-applying current bone poses.
|
## Force skeleton skin update by notifying Skeleton3D that poses changed.
|
||||||
## GLTF runtime loaded models sometimes need this because the AnimationPlayer's
|
## GLTF runtime loaded models need this because AnimationPlayer's internal
|
||||||
## internal bone pose updates don't always trigger the visual pipeline.
|
## bone pose updates don't always trigger the visual pipeline's dirty flags.
|
||||||
# Re-apply current bone poses to ensure dirty flags are set.
|
## NOTIFICATION_UPDATE_SKELETON causes the Skeleton3D to recompute its
|
||||||
# This triggers NOTIFICATION_UPDATE_SKELETON on the Skeleton3D.
|
## bone transforms and notify attached MeshInstances to update vertex buffers.
|
||||||
for i in range(skeleton.get_bone_count()):
|
skeleton.notification(NOTIFICATION_UPDATE_SKELETON)
|
||||||
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
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -433,7 +433,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
var skinned = load("res://characters/skinned_player_model.gd").new()
|
var skinned = load("res://characters/skinned_player_model.gd").new()
|
||||||
skinned.name = "SkinnedModel"
|
skinned.name = "SkinnedModel"
|
||||||
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
skinned.scale_factor = 1.0 # Already scaled to 1.8m in Blender
|
skinned.scale_factor = 1.25 # GLB is ~1.2m, scale to ~1.5m for player capsule
|
||||||
|
skinned.position_y_offset = -0.2 # Align feet with player origin
|
||||||
skinned.position = Vector3.ZERO # Model origin = player origin
|
skinned.position = Vector3.ZERO # Model origin = player origin
|
||||||
player.add_child(skinned)
|
player.add_child(skinned)
|
||||||
print("TestLevelBuilder: added SkinnedModel for player 1")
|
print("TestLevelBuilder: added SkinnedModel for player 1")
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
var skinned = load("res://characters/skinned_player_model.gd").new()
|
var skinned = load("res://characters/skinned_player_model.gd").new()
|
||||||
skinned.name = "SkinnedModel"
|
skinned.name = "SkinnedModel"
|
||||||
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
|
skinned.scale_factor = 1.25
|
||||||
|
skinned.position_y_offset = -0.2
|
||||||
skinned.position = Vector3(0, 0.0, 0)
|
skinned.position = Vector3(0, 0.0, 0)
|
||||||
skinned.first_person_mode = true
|
skinned.first_person_mode = true
|
||||||
player.add_child(skinned)
|
player.add_child(skinned)
|
||||||
|
|||||||
Reference in New Issue
Block a user