feat(client): add animation support to character compositor
Load UAL animation library onto the skeleton and play idle animation after character assembly. Adds play_animation() and stop_animation() public API. Searches all animation libraries for matching animation name with common idle variant fallbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ extends Node3D
|
||||
|
||||
const BASE_PATH := "res://assets/characters/"
|
||||
const SKELETON_PATH := BASE_PATH + "skeleton/armature.glb"
|
||||
const ANIM_LIBRARY_PATH := BASE_PATH + "animations/ual_standard.glb"
|
||||
const SKIN_TONE_DIR := BASE_PATH + "skin_tones/"
|
||||
const TOON_SHADER_PATH := BASE_PATH + "shaders/toon.gdshader"
|
||||
const TOON_MASKED_SHADER_PATH := BASE_PATH + "shaders/toon_masked.gdshader"
|
||||
@@ -92,6 +93,7 @@ const OUTLINE_SIZE_THRESHOLD: float = 0.1 # skip tiny meshes (eyes, eyebrows)
|
||||
|
||||
var _skeleton: Skeleton3D = null
|
||||
var _body_root: Node3D = null
|
||||
var _anim_player: AnimationPlayer = null
|
||||
|
||||
var _body_meshes: Array[MeshInstance3D] = []
|
||||
var _clothing_meshes: Array[MeshInstance3D] = []
|
||||
@@ -140,6 +142,7 @@ func load_descriptor(descriptor: CharacterVisualDescriptor) -> void:
|
||||
_load_clothing(descriptor)
|
||||
_load_accessories(descriptor)
|
||||
_rebuild_outlines()
|
||||
_load_animations()
|
||||
|
||||
|
||||
## Set character facing from a 2D direction vector or a named string direction.
|
||||
@@ -239,6 +242,13 @@ func _clear() -> void:
|
||||
_active_coverages.clear()
|
||||
_active_torso_variant = "full"
|
||||
|
||||
if _anim_player and is_instance_valid(_anim_player):
|
||||
_anim_player.stop()
|
||||
if _anim_player.get_parent():
|
||||
_anim_player.get_parent().remove_child(_anim_player)
|
||||
_anim_player.free()
|
||||
_anim_player = null
|
||||
|
||||
if is_instance_valid(_body_root) and _body_root.get_parent():
|
||||
_body_root.get_parent().remove_child(_body_root)
|
||||
_body_root.queue_free()
|
||||
@@ -589,6 +599,75 @@ func _rebuild_outlines() -> void:
|
||||
_outline_nodes.append(outline_mi)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Animation
|
||||
# =============================================================================
|
||||
|
||||
func _load_animations() -> void:
|
||||
if _skeleton == null:
|
||||
return
|
||||
var anim_scene: PackedScene = load(ANIM_LIBRARY_PATH) as PackedScene
|
||||
if anim_scene == null:
|
||||
push_warning("CharacterVisual: animation library not found at %s" % ANIM_LIBRARY_PATH)
|
||||
return
|
||||
var anim_root: Node = anim_scene.instantiate()
|
||||
# Find the AnimationPlayer in the imported GLB scene
|
||||
var source_player: AnimationPlayer = null
|
||||
for child in anim_root.get_children():
|
||||
if child is AnimationPlayer:
|
||||
source_player = child as AnimationPlayer
|
||||
break
|
||||
if source_player == null:
|
||||
# Try deeper — some GLB imports nest the player
|
||||
for child in anim_root.get_children():
|
||||
for grandchild in child.get_children():
|
||||
if grandchild is AnimationPlayer:
|
||||
source_player = grandchild as AnimationPlayer
|
||||
break
|
||||
if source_player:
|
||||
break
|
||||
if source_player == null:
|
||||
push_warning("CharacterVisual: no AnimationPlayer found in animation library")
|
||||
anim_root.queue_free()
|
||||
return
|
||||
# Create our own AnimationPlayer on the skeleton
|
||||
_anim_player = AnimationPlayer.new()
|
||||
_anim_player.name = "AnimPlayer"
|
||||
_skeleton.add_child(_anim_player)
|
||||
# Copy animation libraries from the source player
|
||||
for lib_name in source_player.get_animation_library_list():
|
||||
var lib: AnimationLibrary = source_player.get_animation_library(lib_name)
|
||||
_anim_player.add_animation_library(lib_name, lib.duplicate())
|
||||
anim_root.queue_free()
|
||||
# Play idle if available
|
||||
play_animation("idle")
|
||||
|
||||
|
||||
## Play a named animation. Searches all libraries for a matching name.
|
||||
func play_animation(anim_name: String) -> void:
|
||||
if _anim_player == null:
|
||||
return
|
||||
# Search across all libraries for the animation
|
||||
for lib_name in _anim_player.get_animation_library_list():
|
||||
var lib: AnimationLibrary = _anim_player.get_animation_library(lib_name)
|
||||
if lib.has_animation(anim_name):
|
||||
var full_name: String = lib_name + "/" + anim_name if lib_name != "" else anim_name
|
||||
_anim_player.play(full_name)
|
||||
return
|
||||
# Try common idle variants
|
||||
for variant in ["Idle", "idle_01", "Idle_01", "breathing_idle", "Breathing_Idle"]:
|
||||
if anim_name == "idle" and variant != anim_name:
|
||||
play_animation(variant)
|
||||
return
|
||||
push_warning("CharacterVisual: animation '%s' not found in any library" % anim_name)
|
||||
|
||||
|
||||
## Stop all animations and return to rest pose.
|
||||
func stop_animation() -> void:
|
||||
if _anim_player:
|
||||
_anim_player.stop()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Static helpers
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user