rewrite skinned_player_model.gd from scratch with clean indentation
This commit is contained in:
+116
-248
@@ -5,26 +5,22 @@ 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. 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.25
|
@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 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
|
||||||
|
|
||||||
## Bones per second for locomotion animations created at runtime.
|
|
||||||
@export var locomotion_cycle_speed: float = 4.0
|
@export var locomotion_cycle_speed: float = 4.0
|
||||||
|
|
||||||
var skeleton: Skeleton3D
|
var skeleton: Skeleton3D
|
||||||
var animation_player: AnimationPlayer
|
var animation_player: AnimationPlayer
|
||||||
var _anim_debug_timer: float = 0.0
|
var _anim_debug_timer: float = 0.0
|
||||||
var _mesh_instance: MeshInstance3D
|
var _mesh_instance: MeshInstance3D
|
||||||
var _bone_cache: Dictionary = {} # bone_name -> index
|
var _bone_cache: Dictionary = {}
|
||||||
|
var _bone_indices: Dictionary = {}
|
||||||
## Bone indices for procedural animation (cached at load time)
|
var _code_driven_mode: bool = false
|
||||||
var _bone_indices: Dictionary = {} # bone_name -> index
|
var _current_anim: String = ""
|
||||||
|
var _anim_time: float = 0.0
|
||||||
|
var _bone_idxs: Dictionary = {}
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if model_path != "":
|
if model_path != "":
|
||||||
@@ -35,100 +31,85 @@ func load_model(path: String) -> void:
|
|||||||
child.queue_free()
|
child.queue_free()
|
||||||
_bone_cache.clear()
|
_bone_cache.clear()
|
||||||
_bone_indices.clear()
|
_bone_indices.clear()
|
||||||
|
_code_driven_mode = false
|
||||||
|
_anim_time = 0.0
|
||||||
|
_current_anim = ""
|
||||||
|
_bone_idxs.clear()
|
||||||
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
||||||
|
|
||||||
# Read GLB file as bytes
|
|
||||||
if not FileAccess.file_exists(path):
|
if not FileAccess.file_exists(path):
|
||||||
print("SkinnedPlayerModel: file not found: %s" % path)
|
print("SkinnedPlayerModel: file not found: %s" % path)
|
||||||
return
|
return
|
||||||
|
|
||||||
var file = FileAccess.open(path, FileAccess.READ)
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
var bytes = file.get_buffer(file.get_length())
|
var bytes = file.get_buffer(file.get_length())
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
# Parse with GLTFDocument (works at runtime without .import files)
|
|
||||||
var gltf = GLTFDocument.new()
|
var gltf = GLTFDocument.new()
|
||||||
var state = GLTFState.new()
|
var state = GLTFState.new()
|
||||||
var err = gltf.append_from_buffer(bytes, "", state)
|
var err = gltf.append_from_buffer(bytes, "", state)
|
||||||
if err != OK:
|
if err != OK:
|
||||||
print("SkinnedPlayerModel: GLTFDocument parse failed: %d" % err)
|
print("SkinnedPlayerModel: GLTFDocument parse failed: %d" % err)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("SkinnedPlayerModel: parsed GLB, generating scene...")
|
print("SkinnedPlayerModel: parsed GLB, generating scene...")
|
||||||
var scene = gltf.generate_scene(state, 30, false, false)
|
var scene = gltf.generate_scene(state, 30, false, false)
|
||||||
if not scene:
|
if not scene:
|
||||||
print("SkinnedPlayerModel: failed to generate scene")
|
print("SkinnedPlayerModel: failed to generate scene")
|
||||||
return
|
return
|
||||||
|
|
||||||
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 ~1.2m; scale_factor adjusts to target height)
|
|
||||||
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
||||||
scene.position = Vector3(0, position_y_offset * scale_factor, 0)
|
scene.position = Vector3(0, position_y_offset * scale_factor, 0)
|
||||||
print("SkinnedPlayerModel: applied scale %.4f, y_offset %.4f" % [scale_factor, position_y_offset])
|
print("SkinnedPlayerModel: applied scale %.4f, y_offset %.4f" % [scale_factor, position_y_offset])
|
||||||
|
|
||||||
# Find skeleton FIRST (needed for mesh binding fix below)
|
|
||||||
skeleton = _find_skeleton(scene)
|
skeleton = _find_skeleton(scene)
|
||||||
if skeleton:
|
if skeleton:
|
||||||
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
||||||
# Cache bone indices for fast lookups
|
for i in range(skeleton.get_bone_count()):
|
||||||
for i in range(skeleton.get_bone_count()):
|
_bone_cache[skeleton.get_bone_name(i)] = i
|
||||||
_bone_cache[skeleton.get_bone_name(i)] = i
|
_bone_indices[skeleton.get_bone_name(i)] = i
|
||||||
_bone_indices[skeleton.get_bone_name(i)] = i
|
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||||
|
|
||||||
# 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)
|
||||||
if not _mesh_instance:
|
if not _mesh_instance:
|
||||||
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
||||||
if mesh_instances.size() > 0:
|
if mesh_instances.size() > 0:
|
||||||
_mesh_instance = mesh_instances[0]
|
_mesh_instance = mesh_instances[0]
|
||||||
|
|
||||||
# Fix skeleton binding
|
|
||||||
if _mesh_instance and skeleton:
|
if _mesh_instance and skeleton:
|
||||||
_mesh_instance.skeleton = NodePath("..")
|
_mesh_instance.skeleton = NodePath("..")
|
||||||
print("SkinnedPlayerModel: fixed mesh skeleton binding -> '%s'" % _mesh_instance.skeleton)
|
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()
|
||||||
|
|
||||||
# Print tree for debugging
|
|
||||||
print("SkinnedPlayerModel: scene tree:")
|
print("SkinnedPlayerModel: scene tree:")
|
||||||
_print_tree(scene, 0)
|
_print_tree(scene, 0)
|
||||||
|
|
||||||
# Find AnimationPlayer
|
|
||||||
animation_player = _find_animation_player(scene)
|
animation_player = _find_animation_player(scene)
|
||||||
if animation_player:
|
if animation_player:
|
||||||
animation_player.process_mode = Node.PROCESS_MODE_ALWAYS
|
animation_player.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
animation_player.active = true
|
animation_player.active = true
|
||||||
print("SkinnedPlayerModel: AP process_mode=%d active=%s" % [animation_player.process_mode, animation_player.active])
|
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:" % anim_list.size())
|
||||||
for a_name in anim_list:
|
for a_name in anim_list:
|
||||||
var a = animation_player.get_animation(a_name)
|
var a = animation_player.get_animation(a_name)
|
||||||
print(" - %s (%.2fs, %d tracks)" % [a_name, a.length, a.get_track_count()])
|
print(" - %s (%.2fs, %d tracks)" % [a_name, a.length, a.get_track_count()])
|
||||||
# Auto-play idle
|
if animation_player.has_animation("Idle"):
|
||||||
if animation_player.has_animation("idle"):
|
animation_player.play("Idle")
|
||||||
animation_player.play("idle")
|
print("SkinnedPlayerModel: playing 'Idle'")
|
||||||
print("SkinnedPlayerModel: playing 'idle'")
|
elif anim_list.size() > 0:
|
||||||
elif animation_player.has_animation("Idle"):
|
animation_player.play(anim_list[0])
|
||||||
animation_player.play("Idle")
|
_verify_animations(animation_player)
|
||||||
print("SkinnedPlayerModel: playing 'Idle'")
|
|
||||||
elif anim_list.size() > 0:
|
|
||||||
animation_player.play(anim_list[0])
|
|
||||||
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
|
|
||||||
|
|
||||||
# Skip the animation creation - the GLB already has proper animations
|
|
||||||
# Just verify they have real motion
|
|
||||||
_verify_animations(animation_player)
|
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||||
|
|
||||||
func _verify_animations(ap: AnimationPlayer) -> void:
|
func _verify_animations(ap: AnimationPlayer) -> void:
|
||||||
## Verifies that animations have real motion (not stripped by exporter).
|
|
||||||
## If all animations are stripped, enables code-driven animation.
|
|
||||||
var has_real_anim := false
|
var has_real_anim := false
|
||||||
for a_name in ap.get_animation_list():
|
for a_name in ap.get_animation_list():
|
||||||
var anim = ap.get_animation(a_name)
|
var anim = ap.get_animation(a_name)
|
||||||
@@ -146,71 +127,20 @@ func _verify_animations(ap: AnimationPlayer) -> void:
|
|||||||
max_delta = max(max_delta, v.angle_to(prev))
|
max_delta = max(max_delta, v.angle_to(prev))
|
||||||
if max_delta > 0.05:
|
if max_delta > 0.05:
|
||||||
has_real_anim = true
|
has_real_anim = true
|
||||||
print(" Animation '%s' has real motion (delta=%.3f)" % [a_name, max_delta])
|
print(" OK: '%s' (delta=%.3f)" % [a_name, max_delta])
|
||||||
|
else:
|
||||||
|
print(" STRIPPED: '%s' (delta=%.4f)" % [a_name, max_delta])
|
||||||
if not has_real_anim:
|
if not has_real_anim:
|
||||||
print("SkinnedPlayerModel: All GLB animations are stripped, using code-driven mode")
|
print("SkinnedPlayerModel: All GLB anims stripped, using code-driven mode")
|
||||||
_enable_code_driven_anim()
|
_enable_code_driven_anim()
|
||||||
|
|
||||||
func _enable_code_driven_anim() -> void:
|
func _enable_code_driven_anim() -> void:
|
||||||
## Enables code-driven animation when GLB animations are stripped.
|
|
||||||
_code_driven_mode = true
|
_code_driven_mode = true
|
||||||
## GLB skeletons. Animation resources are kept for reference but the
|
|
||||||
## actual animation is done code-driven in _process().
|
|
||||||
pass
|
|
||||||
|
|
||||||
func _create_walk_anim() -> Animation:
|
|
||||||
return Animation.new()
|
|
||||||
|
|
||||||
func _create_run_anim() -> Animation:
|
|
||||||
return Animation.new()
|
|
||||||
|
|
||||||
func _create_jump_anim() -> Animation:
|
|
||||||
return Animation.new()
|
|
||||||
|
|
||||||
func _create_idle_anim() -> Animation:
|
|
||||||
return Animation.new()
|
|
||||||
|
|
||||||
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
|
||||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
|
||||||
## Checks the maximum delta between any two keyframe values on any track.
|
|
||||||
if not ap.has_animation(anim_name):
|
|
||||||
return true
|
|
||||||
var anim = ap.get_animation(anim_name)
|
|
||||||
if anim.get_track_count() == 0:
|
|
||||||
return true
|
|
||||||
# Check max delta across all tracks
|
|
||||||
var max_delta: float = 0.0
|
|
||||||
for t in range(anim.get_track_count()):
|
|
||||||
var key_count = anim.track_get_key_count(t)
|
|
||||||
if key_count < 2:
|
|
||||||
continue
|
|
||||||
for i in range(1, key_count):
|
|
||||||
var v = anim.track_get_key_value(t, i)
|
|
||||||
var prev = anim.track_get_key_value(t, i - 1)
|
|
||||||
if v is Vector3 and prev is Vector3:
|
|
||||||
max_delta = max(max_delta, v.distance_to(prev))
|
|
||||||
elif v is Quaternion and prev is Quaternion:
|
|
||||||
max_delta = max(max_delta, v.angle_to(prev))
|
|
||||||
# Threshold: < 0.005 units of motion
|
|
||||||
return max_delta < 0.005
|
|
||||||
|
|
||||||
func _get_bone_name(bone_name: String) -> String:
|
|
||||||
## Returns the bone name as stored in the skeleton (uses cache).
|
|
||||||
if _bone_cache.has(bone_name):
|
|
||||||
return bone_name
|
|
||||||
return bone_name
|
|
||||||
|
|
||||||
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
|
|
||||||
# This creates the classic FPS arms-only view
|
|
||||||
if not _mesh_instance:
|
if not _mesh_instance:
|
||||||
return
|
return
|
||||||
# The mesh has multiple sub-meshes (body parts). We need to hide the ones
|
|
||||||
# that are above the chest (head, torso, skirt, etc.)
|
|
||||||
# For now, we hide the entire mesh and rely on the arms being separate
|
|
||||||
# A more sophisticated approach would hide specific bones
|
|
||||||
_mesh_instance.visible = false
|
_mesh_instance.visible = false
|
||||||
print("SkinnedPlayerModel: mesh hidden for first-person (arms-only view needs separate arms model)")
|
|
||||||
|
|
||||||
func _print_tree(node: Node, depth: int) -> void:
|
func _print_tree(node: Node, depth: int) -> void:
|
||||||
var indent = " ".repeat(depth)
|
var indent = " ".repeat(depth)
|
||||||
@@ -241,28 +171,10 @@ func _find_animation_player(node: Node) -> AnimationPlayer:
|
|||||||
return result
|
return result
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func play_animation(anim_name: String) -> void:
|
|
||||||
"""Play an animation by name. Silently skips if the animation doesn't exist."""
|
|
||||||
if animation_player and animation_player.has_animation(anim_name):
|
|
||||||
if animation_player.current_animation != anim_name or not animation_player.is_playing():
|
|
||||||
animation_player.play(anim_name)
|
|
||||||
return
|
|
||||||
# Animation missing — already covered by _ensure_locomotion_animations()
|
|
||||||
|
|
||||||
var _missing_anim_warned: Array = []
|
|
||||||
|
|
||||||
func _warn_missing(anim_name: String) -> void:
|
|
||||||
## Warn once per missing animation, not every frame.
|
|
||||||
if _missing_anim_warned.has(anim_name):
|
|
||||||
return
|
|
||||||
_missing_anim_warned.append(anim_name)
|
|
||||||
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not skeleton:
|
if not skeleton:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get movement state from parent PlayerMovementController
|
|
||||||
var parent = get_parent()
|
var parent = get_parent()
|
||||||
if not parent:
|
if not parent:
|
||||||
return
|
return
|
||||||
@@ -281,7 +193,6 @@ func _process(delta: float) -> void:
|
|||||||
if parent and "is_dead" in parent:
|
if parent and "is_dead" in parent:
|
||||||
is_dead = parent.is_dead
|
is_dead = parent.is_dead
|
||||||
|
|
||||||
# Determine target animation
|
|
||||||
var anim_name = "Idle"
|
var anim_name = "Idle"
|
||||||
if is_dead:
|
if is_dead:
|
||||||
anim_name = "Death"
|
anim_name = "Death"
|
||||||
@@ -304,152 +215,109 @@ func _process(delta: float) -> void:
|
|||||||
_:
|
_:
|
||||||
anim_name = "Idle"
|
anim_name = "Idle"
|
||||||
|
|
||||||
# Switch animation if needed (AP for reference timing, code for actual poses)
|
|
||||||
if animation_player and (animation_player.current_animation != anim_name or not animation_player.is_playing()):
|
if animation_player and (animation_player.current_animation != anim_name or not animation_player.is_playing()):
|
||||||
if animation_player.has_animation(anim_name):
|
if animation_player.has_animation(anim_name):
|
||||||
animation_player.play(anim_name)
|
animation_player.play(anim_name)
|
||||||
|
|
||||||
# Update animation time
|
|
||||||
_anim_time += delta
|
_anim_time += delta
|
||||||
_current_anim = anim_name
|
_current_anim = anim_name
|
||||||
|
|
||||||
# Apply code-driven animation poses directly to skeleton
|
if _code_driven_mode:
|
||||||
_apply_animation_by_code(anim_name, _anim_time)
|
_apply_code_animation(anim_name, _anim_time)
|
||||||
|
else:
|
||||||
|
# GLB animations are applied by AP, just force skeleton update
|
||||||
|
skeleton.force_update_all_bone_transforms()
|
||||||
|
|
||||||
# Debug
|
|
||||||
_anim_debug_timer += delta
|
_anim_debug_timer += delta
|
||||||
if _anim_debug_timer > 2.0:
|
if _anim_debug_timer > 2.0:
|
||||||
_anim_debug_timer = 0.0
|
_anim_debug_timer = 0.0
|
||||||
if animation_player and animation_player.is_playing():
|
print("SkinnedPlayerModel: '%s' t=%.1f mode=%s" % [anim_name, _anim_time, "code" if _code_driven_mode else "AP"])
|
||||||
print("SkinnedPlayerModel: '%s' (pos: %.2f)" % [anim_name, _anim_time])
|
|
||||||
|
|
||||||
|
func _apply_code_animation(anim: String, time: float) -> void:
|
||||||
# Code-driven animation: applies bone rotations directly each frame.
|
|
||||||
var _current_anim: String = ""
|
|
||||||
var _anim_time: float = 0.0
|
|
||||||
|
|
||||||
## Bone indices for animation (cached at load time)
|
|
||||||
var _bone_idxs: Dictionary = {} # bone_name -> index
|
|
||||||
|
|
||||||
## Per-bone animation rot_x values for each bone (radians offset from rest)
|
|
||||||
func _apply_animation_by_code(anim_name: String, time: float) -> void:
|
|
||||||
if not skeleton:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ensure bone indices are cached
|
|
||||||
if _bone_idxs.is_empty():
|
if _bone_idxs.is_empty():
|
||||||
_cache_bone_indices()
|
_cache_bone_indices()
|
||||||
|
|
||||||
# Only override bones that we have animation data for
|
|
||||||
for bone_name in _bone_idxs:
|
for bone_name in _bone_idxs:
|
||||||
var bone_idx: int = _bone_idxs[bone_name]
|
var idx: int = _bone_idxs[bone_name]
|
||||||
var offset: Vector3 = _get_bone_anim_offset(anim_name, bone_idx, time)
|
var offset := _get_offset(anim, idx, time)
|
||||||
if offset == Vector3.ZERO:
|
if offset == Vector3.ZERO:
|
||||||
|
# Reset this bone to rest (clear any previous override)
|
||||||
|
skeleton.set_bone_global_pose_override(idx, Transform3D(), 0.0, true)
|
||||||
continue
|
continue
|
||||||
|
var rest: Transform3D = skeleton.get_bone_rest(idx)
|
||||||
var rest: Transform3D = skeleton.get_bone_rest(bone_idx)
|
var q := Quaternion.from_euler(offset)
|
||||||
var offset_basis: Basis = Basis(Quaternion.from_euler(offset))
|
var new_basis: Basis = Basis(q) * rest.basis
|
||||||
var new_basis: Basis = offset_basis * rest.basis
|
skeleton.set_bone_global_pose_override(idx, Transform3D(new_basis, rest.origin), 1.0, true)
|
||||||
skeleton.set_bone_global_pose_override(bone_idx, Transform3D(new_basis, rest.origin), 1.0, true)
|
|
||||||
|
|
||||||
|
|
||||||
func _cache_bone_indices() -> void:
|
func _cache_bone_indices() -> void:
|
||||||
if not skeleton:
|
|
||||||
return
|
|
||||||
_bone_idxs.clear()
|
_bone_idxs.clear()
|
||||||
for i in range(skeleton.get_bone_count()):
|
for i in range(skeleton.get_bone_count()):
|
||||||
_bone_idxs[skeleton.get_bone_name(i)] = i
|
_bone_idxs[skeleton.get_bone_name(i)] = i
|
||||||
|
|
||||||
|
func _get_offset(anim: String, bone_idx: int, time: float) -> Vector3:
|
||||||
## Returns the bone rotation offset (euler radians) for a given bone at a given time.
|
|
||||||
func _get_bone_anim_offset(anim_name: String, bone_idx: int, time: float) -> Vector3:
|
|
||||||
if not skeleton:
|
if not skeleton:
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|
||||||
var bone_name: String = ""
|
var bone_name: String = ""
|
||||||
for k in _bone_idxs:
|
for k in _bone_idxs:
|
||||||
if _bone_idxs[k] == bone_idx:
|
if _bone_idxs[k] == bone_idx:
|
||||||
bone_name = k
|
bone_name = k
|
||||||
break
|
break
|
||||||
|
match anim:
|
||||||
if anim_name == "Walk":
|
"Walk": return _walk_offset(bone_name, time)
|
||||||
return _walk_offset(bone_name, time)
|
"Run": return _run_offset(bone_name, time)
|
||||||
elif anim_name == "Run":
|
"Jump": return _jump_offset(bone_name, time)
|
||||||
return _run_offset(bone_name, time)
|
"Idle": return _idle_offset(bone_name, time)
|
||||||
elif anim_name == "Jump":
|
"Crouch": return _crouch_offset(bone_name, time)
|
||||||
return _jump_offset(bone_name, time)
|
|
||||||
elif anim_name == "Idle":
|
|
||||||
return _idle_offset(bone_name, time)
|
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
func _walk_offset(bone: String, time: float) -> Vector3:
|
||||||
|
var c: float = 1.0 / locomotion_cycle_speed
|
||||||
|
var t: float = fmod(time, c) / c * 6.28318
|
||||||
|
match bone:
|
||||||
|
"LeftUpperLeg": return Vector3(sin(t) * 0.8, 0, 0)
|
||||||
|
"RightUpperLeg": return Vector3(sin(t + 3.14159) * 0.8, 0, 0)
|
||||||
|
"LeftUpperArm": return Vector3(sin(t + 3.14159) * 0.6, 0, 0)
|
||||||
|
"RightUpperArm": return Vector3(sin(t) * 0.6, 0, 0)
|
||||||
|
"Spine": return Vector3(0, 0, sin(t) * 0.15)
|
||||||
|
"Chest": return Vector3(0, 0, sin(t) * 0.1)
|
||||||
|
"Hips": return Vector3(0, sin(t) * 0.05, 0)
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
func _walk_offset(bone_name: String, time: float) -> Vector3:
|
func _run_offset(bone: String, time: float) -> Vector3:
|
||||||
var cycle: float = 1.0 / locomotion_cycle_speed
|
var c: float = 1.0 / locomotion_cycle_speed * 1.8
|
||||||
var t: float = fmod(time, cycle) / cycle # Normalize to 0-1
|
var t: float = fmod(time, c) / c * 6.28318
|
||||||
var phase: float = t * 6.28318 # 2*pi
|
match bone:
|
||||||
match bone_name:
|
"LeftUpperLeg": return Vector3(sin(t) * 1.0, 0, 0)
|
||||||
"LeftUpperLeg":
|
"RightUpperLeg": return Vector3(sin(t + 3.14159) * 1.0, 0, 0)
|
||||||
return Vector3(sin(phase) * 0.6, 0, 0)
|
"LeftUpperArm": return Vector3(sin(t + 3.14159) * 0.8, 0, 0)
|
||||||
"RightUpperLeg":
|
"RightUpperArm": return Vector3(sin(t) * 0.8, 0, 0)
|
||||||
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
"Spine": return Vector3(0, 0, sin(t) * 0.2)
|
||||||
"LeftUpperArm":
|
"Hips": return Vector3(0, sin(t) * 0.1, 0)
|
||||||
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
return Vector3.ZERO
|
||||||
"RightUpperArm":
|
|
||||||
return Vector3(sin(phase) * 0.4, 0, 0)
|
|
||||||
"Spine":
|
|
||||||
return Vector3(0, sin(phase) * 0.1, 0)
|
|
||||||
_:
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|
||||||
func _run_offset(bone_name: String, time: float) -> Vector3:
|
func _jump_offset(bone: String, time: float) -> Vector3:
|
||||||
var cycle: float = 1.0 / locomotion_cycle_speed * 1.8
|
var c: float = 0.83
|
||||||
var t: float = fmod(time, cycle) / cycle
|
var t: float = fmod(time, c) / c
|
||||||
var phase: float = t * 6.28318
|
var peak: float = max(0.0, 1.0 - abs(t - 0.5) * 3.0)
|
||||||
match bone_name:
|
|
||||||
"LeftUpperLeg":
|
|
||||||
return Vector3(sin(phase) * 0.6, 0, 0)
|
|
||||||
"RightUpperLeg":
|
|
||||||
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
|
||||||
"LeftUpperArm":
|
|
||||||
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
|
||||||
"RightUpperArm":
|
|
||||||
return Vector3(sin(phase) * 0.4, 0, 0)
|
|
||||||
_:
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|
||||||
func _jump_offset(bone_name: String, time: float) -> Vector3:
|
|
||||||
var cycle: float = 0.83
|
|
||||||
var t: float = fmod(time, cycle) / cycle
|
|
||||||
var phase: float = t * 6.28318
|
|
||||||
# Jump pose: arms raised, knees bent, brief hop
|
|
||||||
var peak: float = 1.0 - abs(t - 0.5) * 2.0
|
|
||||||
peak = clamp(peak, 0.0, 1.0)
|
peak = clamp(peak, 0.0, 1.0)
|
||||||
match bone_name:
|
match bone:
|
||||||
"LeftUpperArm":
|
"LeftUpperArm": return Vector3(-2.5 * peak, 0, -0.3 * peak)
|
||||||
return Vector3(-1.2 * peak, 0, -0.3 * peak)
|
"RightUpperArm": return Vector3(-2.5 * peak, 0, 0.3 * peak)
|
||||||
"RightUpperArm":
|
"LeftUpperLeg": return Vector3(-0.5 * peak, 0, 0)
|
||||||
return Vector3(-1.2 * peak, 0, 0.3 * peak)
|
"RightUpperLeg": return Vector3(-0.5 * peak, 0, 0)
|
||||||
"LeftUpperLeg":
|
"Spine": return Vector3(0.2 * peak, 0, 0)
|
||||||
return Vector3(-0.3 * peak, 0, 0)
|
"Hips": return Vector3(0, 0.4 * peak, 0)
|
||||||
"RightUpperLeg":
|
return Vector3.ZERO
|
||||||
return Vector3(-0.3 * peak, 0, 0)
|
|
||||||
"Spine":
|
|
||||||
return Vector3(0.1 * peak, 0, 0)
|
|
||||||
_:
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|
||||||
func _idle_offset(bone_name: String, time: float) -> Vector3:
|
func _idle_offset(bone: String, time: float) -> Vector3:
|
||||||
var t = time * 0.5 # Slow breathing
|
var t: float = time * 0.5
|
||||||
match bone_name:
|
match bone:
|
||||||
"Spine":
|
"Spine": return Vector3(0, sin(t) * 0.05, 0)
|
||||||
return Vector3(0, sin(t) * 0.005, 0)
|
"Head": return Vector3(sin(t * 0.7) * 0.05, sin(t * 0.5) * 0.05, 0)
|
||||||
"Head":
|
"LeftUpperArm": return Vector3(sin(t) * 0.05, 0, sin(t * 0.5) * 0.15)
|
||||||
return Vector3(sin(t * 0.7) * 0.02, sin(t * 0.5) * 0.02, 0)
|
"RightUpperArm": return Vector3(sin(t) * 0.05, 0, -sin(t * 0.5) * 0.15)
|
||||||
"LeftUpperArm":
|
"Hips": return Vector3(0, sin(t) * 0.03, 0)
|
||||||
return Vector3(sin(t) * 0.02, 0, sin(t * 0.5) * 0.05)
|
return Vector3.ZERO
|
||||||
"RightUpperArm":
|
|
||||||
return Vector3(sin(t) * 0.02, 0, -sin(t * 0.5) * 0.05)
|
func _crouch_offset(bone: String, time: float) -> Vector3:
|
||||||
"Hips":
|
# Static crouch pose if not animated by AP
|
||||||
return Vector3(0, sin(t) * 0.005, 0)
|
return Vector3.ZERO
|
||||||
_:
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|||||||
Reference in New Issue
Block a user