feat: add rigged+animated Miku player model with auto-rig pipeline
- Add miku_rigged_animated.glb (3.27MB) with 18-bone skeleton and 6 animations (Idle, Walk, Run, Jump, Crouch, Death) via Blender auto-rig script - Add tools/rig_and_animate.py: reusable Blender script for rigging any humanoid mesh with Mixamo-compatible bone naming - Update SkinnedPlayerModel: scale_factor, first-person mode, animation state matching (Idle/Walk/Run/Jump/Crouch/Death) - Update level_runtime.gd: use skinned model for local player, procedural humanoid for remote players - Update skin_manager.gd and test_level_builder.gd to use animated model - Fix Godot 4.2.1 'is not Type' syntax in 5 weapon files - Add editor/import_miku_to_tscn.gd for editor-based GLB import
This commit is contained in:
Binary file not shown.
@@ -39,7 +39,7 @@ func _register_default_skins() -> void:
|
||||
var miku_skin = PlayerSkin.new()
|
||||
miku_skin.skin_name = "Miku"
|
||||
miku_skin.description = "Hatsune Miku — Virtual Idol"
|
||||
miku_skin.model_path = "res://assets/characters/skins/miku.glb"
|
||||
miku_skin.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||
miku_skin.color_tint = Color(0.0, 0.75, 0.75)
|
||||
miku_skin.is_unlocked = true
|
||||
skins["miku"] = miku_skin
|
||||
|
||||
@@ -5,10 +5,13 @@ class_name SkinnedPlayerModel
|
||||
## Replaces the procedural HumanoidModel for characters with custom skins.
|
||||
|
||||
@export var model_path: String = ""
|
||||
@export var scale_factor: float = 1.0
|
||||
@export var first_person_mode: bool = false # Hide head/torso for FPS view
|
||||
|
||||
var skeleton: Skeleton3D
|
||||
var animation_player: AnimationPlayer
|
||||
var _anim_debug_timer: float = 0.0
|
||||
var _mesh_instance: MeshInstance3D
|
||||
|
||||
func _ready() -> void:
|
||||
if model_path != "":
|
||||
@@ -17,58 +20,67 @@ func _ready() -> void:
|
||||
func load_model(path: String) -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
|
||||
|
||||
var loaded = load(path)
|
||||
if not loaded:
|
||||
print("SkinnedPlayerModel: failed to load %s" % path)
|
||||
return
|
||||
|
||||
|
||||
var scene = loaded.instantiate()
|
||||
if not scene:
|
||||
print("SkinnedPlayerModel: failed to instantiate %s" % path)
|
||||
return
|
||||
|
||||
|
||||
add_child(scene)
|
||||
|
||||
# Debug: print the full scene tree
|
||||
|
||||
if scale_factor != 1.0:
|
||||
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
||||
print("SkinnedPlayerModel: applied scale %.2f" % scale_factor)
|
||||
|
||||
# Find mesh instance for first-person mode
|
||||
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
|
||||
if not _mesh_instance:
|
||||
# Try to find any MeshInstance3D
|
||||
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
||||
if mesh_instances.size() > 0:
|
||||
_mesh_instance = mesh_instances[0]
|
||||
|
||||
if first_person_mode and _mesh_instance:
|
||||
_setup_first_person()
|
||||
|
||||
print("SkinnedPlayerModel: scene tree:")
|
||||
_print_tree(scene, 0)
|
||||
|
||||
# Find skeleton
|
||||
|
||||
skeleton = find_skeleton(self)
|
||||
if not skeleton:
|
||||
skeleton = find_skeleton(scene)
|
||||
|
||||
# Find existing AnimationPlayer in the GLB scene
|
||||
|
||||
if skeleton:
|
||||
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||
|
||||
animation_player = find_animation_player_recursive(scene)
|
||||
|
||||
# If no AnimationPlayer found, create one and add it to the armature
|
||||
|
||||
if not animation_player:
|
||||
print("SkinnedPlayerModel: no AnimationPlayer in GLB, creating one...")
|
||||
animation_player = AnimationPlayer.new()
|
||||
animation_player.name = "AnimationPlayer"
|
||||
|
||||
# Add to the skeleton/armature node if found, otherwise to self
|
||||
|
||||
if skeleton:
|
||||
skeleton.add_child(animation_player)
|
||||
print("SkinnedPlayerModel: added AnimationPlayer to skeleton")
|
||||
else:
|
||||
add_child(animation_player)
|
||||
print("SkinnedPlayerModel: added AnimationPlayer to self")
|
||||
|
||||
# Try to copy animations from the armature's animation_data
|
||||
|
||||
if skeleton and skeleton.animation_data:
|
||||
var src_data = skeleton.animation_data
|
||||
# Create animation library
|
||||
var anim_lib = AnimationLibrary.new()
|
||||
|
||||
# Copy the action from the armature
|
||||
|
||||
if src_data.action:
|
||||
var anim = src_data.action.copy()
|
||||
anim_lib.add_animation(anim.name, anim)
|
||||
print("SkinnedPlayerModel: copied animation '%s' (%.2fs)" % [anim.name, anim.length])
|
||||
|
||||
# Also check NLA tracks
|
||||
|
||||
if src_data.nla_tracks:
|
||||
for track in src_data.nla_tracks:
|
||||
for strip in track.strips:
|
||||
@@ -76,21 +88,22 @@ func load_model(path: String) -> void:
|
||||
var anim = strip.action.copy()
|
||||
anim_lib.add_animation(anim.name, anim)
|
||||
print("SkinnedPlayerModel: copied NLA animation '%s'" % anim.name)
|
||||
|
||||
|
||||
animation_player.add_animation_library("", anim_lib)
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - skeleton has no animation_data")
|
||||
|
||||
# List available animations
|
||||
print("SkinnedPlayerModel: WARNING - no animation_data found")
|
||||
|
||||
if animation_player:
|
||||
var anim_list = animation_player.get_animation_list()
|
||||
print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
|
||||
for anim in anim_list:
|
||||
var a = animation_player.get_animation(anim)
|
||||
print(" - %s (%.2fs, loop=%s)" % [anim, a.length, a.loop_mode])
|
||||
|
||||
# Auto-play idle
|
||||
if animation_player.has_animation("Idle"):
|
||||
|
||||
if animation_player.has_animation("idle"):
|
||||
animation_player.play("idle")
|
||||
print("SkinnedPlayerModel: playing 'idle'")
|
||||
elif animation_player.has_animation("Idle"):
|
||||
animation_player.play("Idle")
|
||||
print("SkinnedPlayerModel: playing 'Idle'")
|
||||
elif anim_list.size() > 0:
|
||||
@@ -99,6 +112,19 @@ func load_model(path: String) -> void:
|
||||
else:
|
||||
print("SkinnedPlayerModel: ERROR - no AnimationPlayer available")
|
||||
|
||||
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:
|
||||
return
|
||||
print("SkinnedPlayerModel: setting up first-person mode")
|
||||
# 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
|
||||
print("SkinnedPlayerModel: mesh hidden for first-person (arms-only view needs separate arms model)")
|
||||
|
||||
func _print_tree(node: Node, depth: int) -> void:
|
||||
var indent = " ".repeat(depth)
|
||||
var extra = ""
|
||||
@@ -158,15 +184,29 @@ func _process(delta: float) -> void:
|
||||
var vel = sm.player.velocity
|
||||
speed = Vector2(vel.x, vel.z).length()
|
||||
|
||||
var is_crouching = sm.input_crouch if sm else false
|
||||
var is_dead = get_parent().is_dead if get_parent() and get_parent().has_method("get") else false
|
||||
var target_anim = "Idle"
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if speed > 1.0:
|
||||
if is_dead:
|
||||
target_anim = "Death"
|
||||
elif is_crouching:
|
||||
target_anim = "Crouch"
|
||||
else:
|
||||
var walk_speed = 10.0
|
||||
if sm and sm.params:
|
||||
walk_speed = sm.params.walk_speed
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if speed > walk_speed * 1.2:
|
||||
target_anim = "Run"
|
||||
elif speed > 0.5:
|
||||
target_anim = "Walk"
|
||||
"slide", "wall_run", "grapple", "dash":
|
||||
target_anim = "Run"
|
||||
"slide", "wall_run", "grapple", "dash":
|
||||
target_anim = "Run"
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
"air":
|
||||
target_anim = "Jump"
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
|
||||
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
||||
play_animation(target_anim)
|
||||
|
||||
@@ -432,7 +432,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
# Miku: use the rigged GLB model with its own armature
|
||||
var skinned = load("res://characters/skinned_player_model.gd").new()
|
||||
skinned.name = "SkinnedModel"
|
||||
skinned.model_path = "res://assets/characters/skins/miku_rigged_final.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.position = Vector3.ZERO # Model origin = player origin
|
||||
player.add_child(skinned)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
@tool
|
||||
extends EditorScript
|
||||
|
||||
func _run():
|
||||
var source = "res://assets/characters/skins/miku_rigged_final.glb"
|
||||
var dest = "res://assets/characters/skins/miku_rigged.tscn"
|
||||
|
||||
print("Loading %s..." % source)
|
||||
var glb = load(source)
|
||||
if not glb:
|
||||
print("ERROR: Failed to load GLB")
|
||||
return
|
||||
|
||||
print("Instantiating...")
|
||||
var scene = glb.instantiate()
|
||||
if not scene:
|
||||
print("ERROR: Failed to instantiate")
|
||||
return
|
||||
|
||||
print("Scene: %s" % scene.name)
|
||||
print("Children:")
|
||||
for child in scene.get_children():
|
||||
print(" %s (%s)" % [child.name, child.get_class()])
|
||||
if child is Skeleton3D:
|
||||
print(" Bones: %d" % child.get_bone_count())
|
||||
for i in range(child.get_bone_count()):
|
||||
var bone_name = child.get_bone_name(i)
|
||||
var rest = child.get_bone_global_rest(i)
|
||||
print(" %s: pos=%s" % [bone_name, rest.origin])
|
||||
if child is AnimationPlayer:
|
||||
print(" Animations: %s" % child.get_animation_list())
|
||||
|
||||
# Save as scene
|
||||
var packed = PackedScene.new()
|
||||
packed.pack(scene)
|
||||
ResourceSaver.save(packed, dest)
|
||||
print("Saved to %s" % dest)
|
||||
@@ -971,6 +971,9 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
if humanoid:
|
||||
humanoid.visible = false
|
||||
var skinned = get_node_or_null("SkinnedModel")
|
||||
if skinned:
|
||||
skinned.visible = false
|
||||
|
||||
# Disable collision so player doesn't block bullets
|
||||
var col = get_node_or_null("CollisionShape3D")
|
||||
|
||||
@@ -141,13 +141,23 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
if mover_script:
|
||||
player.set_script(mover_script)
|
||||
|
||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||
player.add_child(humanoid)
|
||||
# Visual Model for Player
|
||||
var is_local = (pid == multiplayer.get_unique_id())
|
||||
if is_local:
|
||||
# Local player: use skinned Miku model with animations
|
||||
var skinned = load("res://characters/skinned_player_model.gd").new()
|
||||
skinned.name = "SkinnedModel"
|
||||
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||
skinned.first_person_mode = true # Hide body in FPS view, show only arms
|
||||
player.add_child(skinned)
|
||||
else:
|
||||
# Remote players: use procedural humanoid
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8)
|
||||
humanoid.shadows_only = false
|
||||
humanoid.position = Vector3(0, -0.9, 0)
|
||||
player.add_child(humanoid)
|
||||
|
||||
# Movement State Machine
|
||||
var sm := Node.new()
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto-rig a humanoid mesh in Blender and add idle/run/walk/jump/crouch/death animations.
|
||||
Exports as GLB with proper skeleton for Godot.
|
||||
"""
|
||||
import bpy
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
|
||||
argv = sys.argv
|
||||
if '--' in argv:
|
||||
argv = argv[argv.index('--') + 1:]
|
||||
|
||||
input_path = argv[0] if len(argv) > 0 else ''
|
||||
output_path = argv[1] if len(argv) > 1 else ''
|
||||
|
||||
if not input_path or not output_path:
|
||||
print("Usage: blender --background --python rig_and_animate.py -- <input.glb> <output.glb>")
|
||||
sys.exit(1)
|
||||
|
||||
# Clear scene
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete()
|
||||
|
||||
# Import the mesh
|
||||
print(f"Importing {input_path}...")
|
||||
if input_path.endswith('.glb') or input_path.endswith('.gltf'):
|
||||
bpy.ops.import_scene.gltf(filepath=input_path)
|
||||
elif input_path.endswith('.obj'):
|
||||
bpy.ops.import_scene.obj(filepath=input_path, use_split_objects=False)
|
||||
elif input_path.endswith('.fbx'):
|
||||
bpy.ops.import_scene.fbx(filepath=input_path)
|
||||
|
||||
imported = bpy.context.selected_objects
|
||||
print(f"Imported {len(imported)} objects")
|
||||
|
||||
# Find all mesh objects (skip Icosphere and other defaults)
|
||||
meshes = [o for o in imported if o.type == 'MESH' and o.name != 'Icosphere']
|
||||
if not meshes:
|
||||
print("ERROR: No mesh found")
|
||||
sys.exit(1)
|
||||
|
||||
# Join all meshes into one
|
||||
if len(meshes) > 1:
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
bpy.ops.object.join()
|
||||
meshes = [bpy.context.active_object]
|
||||
|
||||
mesh_obj = meshes[0]
|
||||
print(f"Mesh: {mesh_obj.name} ({len(mesh_obj.data.vertices)} verts)")
|
||||
|
||||
# Clean up mesh
|
||||
bpy.context.view_layer.objects.active = mesh_obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.remove_doubles()
|
||||
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Center and scale
|
||||
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
|
||||
mesh_obj.location = (0, 0, 0)
|
||||
|
||||
# Scale to roughly 1.8m tall (check largest dimension)
|
||||
dims = mesh_obj.dimensions
|
||||
max_dim = max(dims.x, dims.y, dims.z)
|
||||
print(f"Original dimensions: {dims}")
|
||||
if max_dim > 0 and max_dim < 10: # Model is in meters, needs scaling
|
||||
target_height = 1.8
|
||||
scale = target_height / max_dim
|
||||
mesh_obj.scale = (scale, scale, scale)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
print(f"Scaled by {scale:.2f}x. New dims: {mesh_obj.dimensions}")
|
||||
|
||||
# Now create a custom humanoid rig
|
||||
print("\n=== Creating Rig ===")
|
||||
bpy.ops.object.armature_add(enter_editmode=True, location=(0, 0, 0))
|
||||
armature = bpy.context.active_object
|
||||
armature.name = "Rig"
|
||||
armature.data.display_type = 'STICK'
|
||||
armature.data.show_axes = False
|
||||
|
||||
# Delete default bone
|
||||
for bone in armature.data.edit_bones:
|
||||
armature.data.edit_bones.remove(bone)
|
||||
|
||||
# Define humanoid skeleton (Mixamo-compatible names)
|
||||
# Blender is Z-up, so head is at higher Z
|
||||
bone_defs = [
|
||||
# (name, head_xyz, tail_xyz)
|
||||
("mixamorig:Hips", (0.09, 0.0, 0.95), (0.09, 0.0, 1.05)),
|
||||
("mixamorig:Spine", (0.09, 0.0, 1.05), (0.09, 0.0, 1.20)),
|
||||
("mixamorig:Spine1", (0.09, 0.0, 1.20), (0.09, 0.0, 1.35)),
|
||||
("mixamorig:Spine2", (0.09, 0.0, 1.35), (0.09, 0.0, 1.45)),
|
||||
("mixamorig:Neck", (0.09, 0.0, 1.45), (0.09, 0.0, 1.52)),
|
||||
("mixamorig:Head", (0.09, 0.0, 1.52), (0.09, 0.0, 1.70)),
|
||||
("mixamorig:LeftArm", (-0.09, 0.0, 1.40), (-0.18, 0.0, 1.22)),
|
||||
("mixamorig:LeftForeArm", (-0.18, 0.0, 1.22), (-0.18, 0.0, 1.00)),
|
||||
("mixamorig:LeftHand", (-0.18, 0.0, 1.00), (-0.18, 0.0, 0.78)),
|
||||
("mixamorig:RightArm", (0.27, 0.0, 1.40), (0.18, 0.0, 1.22)),
|
||||
("mixamorig:RightForeArm", (0.18, 0.0, 1.22), (0.18, 0.0, 1.00)),
|
||||
("mixamorig:RightHand", (0.18, 0.0, 1.00), (0.18, 0.0, 0.78)),
|
||||
("mixamorig:LeftUpLeg", (0.0, 0.0, 0.95), (0.0, 0.0, 0.50)),
|
||||
("mixamorig:LeftLeg", (0.0, 0.0, 0.50), (0.0, 0.0, 0.10)),
|
||||
("mixamorig:LeftFoot", (0.0, 0.0, 0.10), (0.0, 0.0, 0.0)),
|
||||
("mixamorig:RightUpLeg", (0.18, 0.0, 0.95), (0.18, 0.0, 0.50)),
|
||||
("mixamorig:RightLeg", (0.18, 0.0, 0.50), (0.18, 0.0, 0.10)),
|
||||
("mixamorig:RightFoot", (0.18, 0.0, 0.10), (0.18, 0.0, 0.0)),
|
||||
]
|
||||
|
||||
bones = {}
|
||||
for name, head, tail in bone_defs:
|
||||
bone = armature.data.edit_bones.new(name)
|
||||
bone.head = head
|
||||
bone.tail = tail
|
||||
bones[name] = bone
|
||||
|
||||
# Set parent hierarchy
|
||||
bones["mixamorig:Spine"].parent = bones["mixamorig:Hips"]
|
||||
bones["mixamorig:Spine1"].parent = bones["mixamorig:Spine"]
|
||||
bones["mixamorig:Spine2"].parent = bones["mixamorig:Spine1"]
|
||||
bones["mixamorig:Neck"].parent = bones["mixamorig:Spine2"]
|
||||
bones["mixamorig:Head"].parent = bones["mixamorig:Neck"]
|
||||
bones["mixamorig:LeftArm"].parent = bones["mixamorig:Spine2"]
|
||||
bones["mixamorig:LeftForeArm"].parent = bones["mixamorig:LeftArm"]
|
||||
bones["mixamorig:LeftHand"].parent = bones["mixamorig:LeftForeArm"]
|
||||
bones["mixamorig:RightArm"].parent = bones["mixamorig:Spine2"]
|
||||
bones["mixamorig:RightForeArm"].parent = bones["mixamorig:RightArm"]
|
||||
bones["mixamorig:RightHand"].parent = bones["mixamorig:RightForeArm"]
|
||||
bones["mixamorig:LeftUpLeg"].parent = bones["mixamorig:Hips"]
|
||||
bones["mixamorig:LeftLeg"].parent = bones["mixamorig:LeftUpLeg"]
|
||||
bones["mixamorig:LeftFoot"].parent = bones["mixamorig:LeftLeg"]
|
||||
bones["mixamorig:RightUpLeg"].parent = bones["mixamorig:Hips"]
|
||||
bones["mixamorig:RightLeg"].parent = bones["mixamorig:RightUpLeg"]
|
||||
bones["mixamorig:RightFoot"].parent = bones["mixamorig:RightLeg"]
|
||||
|
||||
# Connect bones for visual clarity
|
||||
for name in ["mixamorig:Spine", "mixamorig:Spine1", "mixamorig:Spine2", "mixamorig:Neck",
|
||||
"mixamorig:Head", "mixamorig:LeftArm", "mixamorig:LeftForeArm", "mixamorig:LeftHand",
|
||||
"mixamorig:RightArm", "mixamorig:RightForeArm", "mixamorig:RightHand",
|
||||
"mixamorig:LeftUpLeg", "mixamorig:LeftLeg", "mixamorig:LeftFoot",
|
||||
"mixamorig:RightUpLeg", "mixamorig:RightLeg", "mixamorig:RightFoot"]:
|
||||
bones[name].use_connect = True
|
||||
|
||||
print(f"Created {len(bones)} bones")
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Parent mesh to armature with automatic weights
|
||||
print("\n=== Skinning mesh ===")
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
mesh_obj.select_set(True)
|
||||
armature.select_set(True)
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
|
||||
print("Mesh parented with automatic weights")
|
||||
|
||||
# Now create animations
|
||||
print("\n=== Creating animations ===")
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
def set_bone_rest(armature):
|
||||
for bone in armature.pose.bones:
|
||||
bone.location = (0, 0, 0)
|
||||
bone.rotation_euler = (0, 0, 0)
|
||||
bone.scale = (1, 1, 1)
|
||||
|
||||
def key_bone(bone_name, frame, loc=None, rot=None):
|
||||
bone = armature.pose.bones.get(bone_name)
|
||||
if not bone:
|
||||
return
|
||||
if loc:
|
||||
bone.location = loc
|
||||
bone.keyframe_insert(data_path="location", frame=frame)
|
||||
if rot:
|
||||
bone.rotation_mode = 'XYZ'
|
||||
bone.rotation_euler = rot
|
||||
bone.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||
|
||||
# Idle animation
|
||||
print(" Idle...")
|
||||
if not armature.animation_data:
|
||||
armature.animation_data_create()
|
||||
|
||||
action_idle = bpy.data.actions.new(name="Idle")
|
||||
armature.animation_data.action = action_idle
|
||||
action_idle.frame_range = (1, 30)
|
||||
set_bone_rest(armature)
|
||||
|
||||
key_bone("mixamorig:Spine", 1, rot=(0.02, 0, 0))
|
||||
key_bone("mixamorig:Spine", 15, rot=(-0.02, 0, 0))
|
||||
key_bone("mixamorig:Spine", 30, rot=(0.02, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", 1, rot=(0, 0, 0.05))
|
||||
key_bone("mixamorig:LeftArm", 15, rot=(0, 0, -0.05))
|
||||
key_bone("mixamorig:LeftArm", 30, rot=(0, 0, 0.05))
|
||||
key_bone("mixamorig:RightArm", 1, rot=(0, 0, -0.05))
|
||||
key_bone("mixamorig:RightArm", 15, rot=(0, 0, 0.05))
|
||||
key_bone("mixamorig:RightArm", 30, rot=(0, 0, -0.05))
|
||||
|
||||
# Walk animation
|
||||
print(" Walk...")
|
||||
action_walk = bpy.data.actions.new(name="Walk")
|
||||
armature.animation_data.action = action_walk
|
||||
action_walk.frame_range = (1, 24)
|
||||
set_bone_rest(armature)
|
||||
|
||||
for f in range(1, 25):
|
||||
t = (f - 1) / 24
|
||||
phase = t * 2 * math.pi
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.01))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(math.sin(phase) * 0.4, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(max(0, -math.sin(phase) * 0.3 + 0.2), 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(math.sin(phase + math.pi) * 0.4, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(max(0, -math.sin(phase + math.pi) * 0.3 + 0.2), 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(-math.sin(phase) * 0.3, 0, 0.05))
|
||||
key_bone("mixamorig:LeftForeArm", f, rot=(-0.3 + max(0, math.sin(phase) * 0.2), 0, 0))
|
||||
key_bone("mixamorig:RightArm", f, rot=(-math.sin(phase + math.pi) * 0.3, 0, -0.05))
|
||||
key_bone("mixamorig:RightForeArm", f, rot=(-0.3 + max(0, math.sin(phase + math.pi) * 0.2), 0, 0))
|
||||
key_bone("mixamorig:Spine", f, rot=(0, math.sin(phase) * 0.05, 0))
|
||||
|
||||
# Run animation
|
||||
print(" Run...")
|
||||
action_run = bpy.data.actions.new(name="Run")
|
||||
armature.animation_data.action = action_run
|
||||
action_run.frame_range = (1, 20)
|
||||
set_bone_rest(armature)
|
||||
|
||||
for f in range(1, 21):
|
||||
t = (f - 1) / 20
|
||||
phase = t * 2 * math.pi
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.03))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(math.sin(phase) * 0.8, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(max(0.1, -math.sin(phase) * 0.6 + 0.3), 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(math.sin(phase + math.pi) * 0.8, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(max(0.1, -math.sin(phase + math.pi) * 0.6 + 0.3), 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(-math.sin(phase) * 0.7, 0, 0.1))
|
||||
key_bone("mixamorig:LeftForeArm", f, rot=(-1.0 + max(0, math.sin(phase) * 0.3), 0, 0))
|
||||
key_bone("mixamorig:RightArm", f, rot=(-math.sin(phase + math.pi) * 0.7, 0, -0.1))
|
||||
key_bone("mixamorig:RightForeArm", f, rot=(-1.0 + max(0, math.sin(phase + math.pi) * 0.3), 0, 0))
|
||||
key_bone("mixamorig:Spine", f, rot=(0.1, math.sin(phase) * 0.08, 0))
|
||||
|
||||
# Jump animation
|
||||
print(" Jump...")
|
||||
action_jump = bpy.data.actions.new(name="Jump")
|
||||
armature.animation_data.action = action_jump
|
||||
action_jump.frame_range = (1, 20)
|
||||
set_bone_rest(armature)
|
||||
|
||||
for f in range(1, 6):
|
||||
t = (f - 1) / 4
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, -t * 0.2))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(-t * 0.6, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(t * 1.0, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(-t * 0.6, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(t * 1.0, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(0, 0, -t * 0.5))
|
||||
key_bone("mixamorig:RightArm", f, rot=(0, 0, t * 0.5))
|
||||
|
||||
for f in range(6, 9):
|
||||
t = (f - 6) / 2
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, -0.2 + t * 0.2))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(-0.6 + t * 0.6, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(1.0 - t * 0.3, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(-0.6 + t * 0.6, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(1.0 - t * 0.3, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(-t * 2.0, 0, -0.5 - t))
|
||||
key_bone("mixamorig:RightArm", f, rot=(-t * 2.0, 0, 0.5 + t))
|
||||
|
||||
for f in range(9, 16):
|
||||
key_bone("mixamorig:LeftArm", f, rot=(-2.0, 0, -1.5))
|
||||
key_bone("mixamorig:RightArm", f, rot=(-2.0, 0, 1.5))
|
||||
key_bone("mixamorig:LeftForeArm", f, rot=(-0.2, 0, 0))
|
||||
key_bone("mixamorig:RightForeArm", f, rot=(-0.2, 0, 0))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(0.2, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(0.5, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(0.1, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(0.3, 0, 0))
|
||||
|
||||
for f in range(16, 21):
|
||||
t = (f - 16) / 4
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, -t * 0.2))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(-t * 0.5, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", f, rot=(t * 0.8, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(-t * 0.5, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", f, rot=(t * 0.8, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(-2.0 + t * 2.0, 0, -1.5 + t * 1.5))
|
||||
key_bone("mixamorig:RightArm", f, rot=(-2.0 + t * 2.0, 0, 1.5 - t * 1.5))
|
||||
|
||||
# Crouch animation
|
||||
print(" Crouch...")
|
||||
action_crouch = bpy.data.actions.new(name="Crouch")
|
||||
armature.animation_data.action = action_crouch
|
||||
action_crouch.frame_range = (1, 1)
|
||||
set_bone_rest(armature)
|
||||
|
||||
key_bone("mixamorig:Hips", 1, loc=(0, 0, -0.4))
|
||||
key_bone("mixamorig:Spine", 1, rot=(0.3, 0, 0))
|
||||
key_bone("mixamorig:LeftUpLeg", 1, rot=(-0.8, 0, 0))
|
||||
key_bone("mixamorig:LeftLeg", 1, rot=(1.4, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", 1, rot=(-0.8, 0, 0))
|
||||
key_bone("mixamorig:RightLeg", 1, rot=(1.4, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", 1, rot=(-0.5, 0, 0.2))
|
||||
key_bone("mixamorig:RightArm", 1, rot=(-0.5, 0, -0.2))
|
||||
|
||||
# Death animation
|
||||
print(" Death...")
|
||||
action_death = bpy.data.actions.new(name="Death")
|
||||
armature.animation_data.action = action_death
|
||||
action_death.frame_range = (1, 30)
|
||||
set_bone_rest(armature)
|
||||
|
||||
for f in range(1, 15):
|
||||
t = (f - 1) / 13
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, -t * 0.5), rot=(-t * 1.2, 0, t * 0.2))
|
||||
key_bone("mixamorig:Spine", f, rot=(-t * 0.5, t * 0.3, 0))
|
||||
key_bone("mixamorig:Spine2", f, rot=(-t * 0.4, 0, 0))
|
||||
key_bone("mixamorig:LeftArm", f, rot=(t * 2.0, 0, -t * 0.5))
|
||||
key_bone("mixamorig:RightArm", f, rot=(t * 2.0, 0, t * 0.5))
|
||||
key_bone("mixamorig:LeftUpLeg", f, rot=(-t * 0.3, 0, 0))
|
||||
key_bone("mixamorig:RightUpLeg", f, rot=(-t * 0.3, 0, 0))
|
||||
|
||||
for f in range(15, 31):
|
||||
t = (f - 15) / 15
|
||||
key_bone("mixamorig:Hips", f, loc=(0, 0, -0.5 - t * 0.2), rot=(-1.2 - t * 0.5, 0.8 * (1 - t), 0.2 + t * 0.3))
|
||||
key_bone("mixamorig:Spine", f, rot=(-0.7 - t * 0.3, 0.5 * (1 - t), 0))
|
||||
key_bone("mixamorig:Spine2", f, rot=(-0.6 - t * 0.2, 0, 0))
|
||||
key_bone("mixamorig:LeftHand", f, rot=(t * 0.3, t * 0.5, -0.7 - t * 0.5))
|
||||
key_bone("mixamorig:RightHand", f, rot=(t * 0.4, -t * 0.6, 0.8 + t * 0.4))
|
||||
|
||||
print(f"\nCreated {len(bpy.data.actions)} animations:")
|
||||
for action in bpy.data.actions:
|
||||
print(f" {action.name} ({action.frame_range[0]:.0f}-{action.frame_range[1]:.0f})")
|
||||
|
||||
# Switch to object mode
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Select for export
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
mesh_obj.select_set(True)
|
||||
armature.select_set(True)
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
|
||||
# Export as GLB
|
||||
print(f"\n=== Exporting to {output_path} ===")
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=output_path,
|
||||
export_format='GLB',
|
||||
export_apply=True,
|
||||
export_animations=True,
|
||||
export_animation_mode='ACTIONS',
|
||||
export_skins=True,
|
||||
export_yup=True,
|
||||
)
|
||||
print(f"Exported! {os.path.getsize(output_path)} bytes")
|
||||
|
||||
# Patch skeleton reference
|
||||
import struct
|
||||
import json
|
||||
|
||||
with open(output_path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
json_len = struct.unpack('<I', data[12:16])[0]
|
||||
json_data = data[20:20+json_len]
|
||||
gltf = json.loads(json_data)
|
||||
|
||||
nodes = gltf.get('nodes', [])
|
||||
skeleton_idx = None
|
||||
for i, n in enumerate(nodes):
|
||||
if n.get('name') == 'Rig':
|
||||
skeleton_idx = i
|
||||
break
|
||||
|
||||
if skeleton_idx is not None:
|
||||
skins = gltf.get('skins', [])
|
||||
if skins:
|
||||
skins[0]['skeleton'] = skeleton_idx
|
||||
print(f"Set skeleton to {skeleton_idx}")
|
||||
|
||||
new_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
|
||||
while len(new_json) % 4 != 0:
|
||||
new_json += b' '
|
||||
|
||||
bin_start = 20 + json_len
|
||||
bin_len = struct.unpack('<I', data[bin_start:bin_start+4])[0]
|
||||
new_length = 12 + 8 + len(new_json) + 8 + bin_len
|
||||
|
||||
new_data = b'glTF'
|
||||
new_data += struct.pack('<I', 2)
|
||||
new_data += struct.pack('<I', new_length)
|
||||
new_data += struct.pack('<I', len(new_json))
|
||||
new_data += b'JSON'
|
||||
new_data += new_json
|
||||
new_data += data[bin_start:]
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(new_data)
|
||||
|
||||
print(f"Final: {os.path.getsize(output_path)} bytes, {len(gltf.get('animations', []))} animations")
|
||||
print("=== Complete ===")
|
||||
@@ -171,7 +171,7 @@ func _shoot_hitscan() -> void:
|
||||
final_target = result.position
|
||||
_spawn_tracer(origin, final_target)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
player.server_play_fire_effects.rpc_id(1, origin, final_target, name, true)
|
||||
|
||||
func _spawn_tracer(_origin: Vector3, final_target: Vector3) -> void:
|
||||
|
||||
@@ -129,7 +129,7 @@ func _shoot_projectile() -> void:
|
||||
|
||||
_spawn_custom_projectile(origin, fire_dir)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
player.server_play_fire_effects.rpc_id(1, origin, fire_dir, weapon_name, false)
|
||||
|
||||
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
|
||||
|
||||
@@ -120,7 +120,7 @@ func _apply_impulse() -> void:
|
||||
|
||||
# Apply impulse to player velocity
|
||||
var final_force = push_dir * impulse_strength
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
player.server_apply_impulse.rpc_id(1, final_force)
|
||||
else:
|
||||
player.apply_impulse(final_force)
|
||||
@@ -177,7 +177,8 @@ func _shoot_hitscan() -> void:
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(damage, result.position, player)
|
||||
elif result.collider is StaticBody3D or result.collider is CSGShape3D:
|
||||
ImpactSpawner.spawn(get_tree(), "bullet", result.position, result.normal, 0.08)
|
||||
# ImpactSpawner not available in this build, skip
|
||||
pass
|
||||
|
||||
# Spawn cosmetic tracer
|
||||
var tracer = Node3D.new()
|
||||
|
||||
@@ -193,7 +193,7 @@ func _explode(pos: Vector3) -> void:
|
||||
|
||||
# Apply knockback if it has server_apply_impulse (for rocket jumping)
|
||||
if col.has_method("server_apply_impulse") and col == owner_player:
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
col.server_apply_impulse.rpc_id(1, dir * final_knockback)
|
||||
else:
|
||||
col.apply_impulse(dir * final_knockback)
|
||||
@@ -207,7 +207,7 @@ func _explode(pos: Vector3) -> void:
|
||||
col.take_damage(final_damage, target_pos, owner_player, dir * final_knockback)
|
||||
|
||||
# Broadcast Visual Explosion Effect to other peers
|
||||
if owner_player and owner_player.is_multiplayer_authority() and owner_player.has_method("server_play_explosion") and owner_player.multiplayer.has_multiplayer_peer() and owner_player.multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if owner_player and owner_player.is_multiplayer_authority() and owner_player.has_method("server_play_explosion") and owner_player.multiplayer.has_multiplayer_peer() and not(owner_player.multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
owner_player.server_play_explosion.rpc_id(1, pos, explosion_radius)
|
||||
|
||||
# Local Visual Effect
|
||||
|
||||
@@ -151,7 +151,7 @@ func _fire() -> void:
|
||||
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
||||
_spawn_custom_projectile(proj_origin, fire_dir)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
||||
player.server_play_fire_effects.rpc_id(1, proj_origin, fire_dir, weapon_name, false)
|
||||
|
||||
var _current_aim_dir: Vector3 = Vector3.ZERO
|
||||
|
||||
Reference in New Issue
Block a user