rigging and animation
This commit is contained in:
parent
b85f225789
commit
cc027a2517
11 changed files with 2120 additions and 38 deletions
275
tests/demo/screens/skeletal_animation_demo.py
Normal file
275
tests/demo/screens/skeletal_animation_demo.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# skeletal_animation_demo.py - 3D Skeletal Animation Demo Screen
|
||||
# Demonstrates Entity3D animation with real animated glTF models
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
DEMO_NAME = "3D Skeletal Animation"
|
||||
DEMO_DESCRIPTION = """Entity3D Animation API with real skeletal models"""
|
||||
|
||||
# Create demo scene
|
||||
scene = mcrfpy.Scene("skeletal_animation_demo")
|
||||
|
||||
# Dark background frame
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||||
scene.children.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Skeletal Animation Demo", pos=(20, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
scene.children.append(title)
|
||||
|
||||
# Create the 3D viewport
|
||||
viewport = mcrfpy.Viewport3D(
|
||||
pos=(50, 50),
|
||||
size=(600, 500),
|
||||
render_resolution=(600, 500),
|
||||
fov=60.0,
|
||||
camera_pos=(0.0, 2.0, 5.0),
|
||||
camera_target=(0.0, 1.0, 0.0),
|
||||
bg_color=mcrfpy.Color(30, 30, 50)
|
||||
)
|
||||
scene.children.append(viewport)
|
||||
|
||||
# Set up navigation grid
|
||||
GRID_SIZE = 16
|
||||
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
|
||||
|
||||
# Build a simple flat floor
|
||||
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||
hm.normalize(0.0, 0.0)
|
||||
viewport.apply_heightmap(hm, 0.0)
|
||||
viewport.build_terrain(
|
||||
layer_name="floor",
|
||||
heightmap=hm,
|
||||
y_scale=0.0,
|
||||
cell_size=1.0
|
||||
)
|
||||
|
||||
# Apply floor colors (dark gray)
|
||||
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||
for y in range(GRID_SIZE):
|
||||
for x in range(GRID_SIZE):
|
||||
checker = ((x + y) % 2) * 0.1 + 0.15
|
||||
r_map[x, y] = checker
|
||||
g_map[x, y] = checker
|
||||
b_map[x, y] = checker + 0.05
|
||||
viewport.apply_terrain_colors("floor", r_map, g_map, b_map)
|
||||
|
||||
# Load animated models
|
||||
animated_entity = None
|
||||
model_info = "No animated model"
|
||||
|
||||
# Try to load CesiumMan (humanoid with walk animation)
|
||||
try:
|
||||
model = mcrfpy.Model3D("../assets/models/CesiumMan.glb")
|
||||
if model.has_skeleton:
|
||||
animated_entity = mcrfpy.Entity3D(pos=(8, 8), scale=1.0, color=mcrfpy.Color(200, 180, 150))
|
||||
animated_entity.model = model
|
||||
viewport.entities.append(animated_entity)
|
||||
|
||||
# Set up animation
|
||||
clips = model.animation_clips
|
||||
if clips:
|
||||
animated_entity.anim_clip = clips[0]
|
||||
animated_entity.anim_loop = True
|
||||
animated_entity.anim_speed = 1.0
|
||||
|
||||
model_info = f"CesiumMan: {model.bone_count} bones, {model.vertex_count} verts"
|
||||
print(f"Loaded {model_info}")
|
||||
print(f"Animation clips: {clips}")
|
||||
except Exception as e:
|
||||
print(f"Failed to load CesiumMan: {e}")
|
||||
|
||||
# Also try RiggedSimple as a second model
|
||||
try:
|
||||
model2 = mcrfpy.Model3D("../assets/models/RiggedSimple.glb")
|
||||
if model2.has_skeleton:
|
||||
entity2 = mcrfpy.Entity3D(pos=(10, 8), scale=0.5, color=mcrfpy.Color(100, 200, 255))
|
||||
entity2.model = model2
|
||||
viewport.entities.append(entity2)
|
||||
|
||||
clips = model2.animation_clips
|
||||
if clips:
|
||||
entity2.anim_clip = clips[0]
|
||||
entity2.anim_loop = True
|
||||
entity2.anim_speed = 1.5
|
||||
|
||||
print(f"Loaded RiggedSimple: {model2.bone_count} bones")
|
||||
except Exception as e:
|
||||
print(f"Failed to load RiggedSimple: {e}")
|
||||
|
||||
# Info panel on the right
|
||||
info_panel = mcrfpy.Frame(pos=(670, 50), size=(330, 500),
|
||||
fill_color=mcrfpy.Color(30, 30, 40),
|
||||
outline_color=mcrfpy.Color(80, 80, 100),
|
||||
outline=2.0)
|
||||
scene.children.append(info_panel)
|
||||
|
||||
# Panel title
|
||||
panel_title = mcrfpy.Caption(text="Animation Properties", pos=(690, 60))
|
||||
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
|
||||
scene.children.append(panel_title)
|
||||
|
||||
# Status labels (will be updated by timer)
|
||||
status_labels = []
|
||||
y_offset = 90
|
||||
|
||||
label_texts = [
|
||||
"Model: loading...",
|
||||
"anim_clip: ",
|
||||
"anim_time: 0.00",
|
||||
"anim_speed: 1.00",
|
||||
"anim_loop: True",
|
||||
"anim_paused: False",
|
||||
"anim_frame: 0",
|
||||
]
|
||||
|
||||
for text in label_texts:
|
||||
label = mcrfpy.Caption(text=text, pos=(690, y_offset))
|
||||
label.fill_color = mcrfpy.Color(150, 200, 150)
|
||||
scene.children.append(label)
|
||||
status_labels.append(label)
|
||||
y_offset += 25
|
||||
|
||||
# Set initial model info
|
||||
status_labels[0].text = f"Model: {model_info}"
|
||||
|
||||
# Controls section
|
||||
y_offset += 20
|
||||
controls_title = mcrfpy.Caption(text="Controls:", pos=(690, y_offset))
|
||||
controls_title.fill_color = mcrfpy.Color(255, 255, 200)
|
||||
scene.children.append(controls_title)
|
||||
y_offset += 25
|
||||
|
||||
controls = [
|
||||
"[SPACE] Toggle pause",
|
||||
"[L] Toggle loop",
|
||||
"[+/-] Adjust speed",
|
||||
"[R] Reset time",
|
||||
"[1-3] Camera presets",
|
||||
]
|
||||
|
||||
for ctrl in controls:
|
||||
cap = mcrfpy.Caption(text=ctrl, pos=(690, y_offset))
|
||||
cap.fill_color = mcrfpy.Color(180, 180, 150)
|
||||
scene.children.append(cap)
|
||||
y_offset += 20
|
||||
|
||||
# Auto-animate section
|
||||
y_offset += 20
|
||||
auto_title = mcrfpy.Caption(text="Auto-Animate:", pos=(690, y_offset))
|
||||
auto_title.fill_color = mcrfpy.Color(255, 200, 200)
|
||||
scene.children.append(auto_title)
|
||||
y_offset += 25
|
||||
|
||||
auto_labels = []
|
||||
auto_texts = [
|
||||
"auto_animate: True",
|
||||
"walk_clip: 'walk'",
|
||||
"idle_clip: 'idle'",
|
||||
]
|
||||
|
||||
for text in auto_texts:
|
||||
cap = mcrfpy.Caption(text=text, pos=(690, y_offset))
|
||||
cap.fill_color = mcrfpy.Color(180, 160, 160)
|
||||
scene.children.append(cap)
|
||||
auto_labels.append(cap)
|
||||
y_offset += 20
|
||||
|
||||
# Instructions at bottom
|
||||
status = mcrfpy.Caption(text="Status: Animation playing", pos=(20, 570))
|
||||
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
scene.children.append(status)
|
||||
|
||||
# Camera presets
|
||||
camera_presets = [
|
||||
((0.0, 2.0, 5.0), (0.0, 1.0, 0.0), "Front view"),
|
||||
((5.0, 3.0, 0.0), (0.0, 1.0, 0.0), "Side view"),
|
||||
((0.0, 6.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"),
|
||||
]
|
||||
|
||||
# Update function - updates display and entity rotation
|
||||
def update(timer, runtime):
|
||||
if animated_entity:
|
||||
# Update status display
|
||||
status_labels[1].text = f"anim_clip: '{animated_entity.anim_clip}'"
|
||||
status_labels[2].text = f"anim_time: {animated_entity.anim_time:.2f}"
|
||||
status_labels[3].text = f"anim_speed: {animated_entity.anim_speed:.2f}"
|
||||
status_labels[4].text = f"anim_loop: {animated_entity.anim_loop}"
|
||||
status_labels[5].text = f"anim_paused: {animated_entity.anim_paused}"
|
||||
status_labels[6].text = f"anim_frame: {animated_entity.anim_frame}"
|
||||
|
||||
auto_labels[0].text = f"auto_animate: {animated_entity.auto_animate}"
|
||||
auto_labels[1].text = f"walk_clip: '{animated_entity.walk_clip}'"
|
||||
auto_labels[2].text = f"idle_clip: '{animated_entity.idle_clip}'"
|
||||
|
||||
# Key handler
|
||||
def on_key(key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if animated_entity:
|
||||
if key == mcrfpy.Key.SPACE:
|
||||
animated_entity.anim_paused = not animated_entity.anim_paused
|
||||
status.text = f"Status: {'Paused' if animated_entity.anim_paused else 'Playing'}"
|
||||
|
||||
elif key == mcrfpy.Key.L:
|
||||
animated_entity.anim_loop = not animated_entity.anim_loop
|
||||
status.text = f"Status: Loop {'ON' if animated_entity.anim_loop else 'OFF'}"
|
||||
|
||||
elif key == mcrfpy.Key.EQUAL or key == mcrfpy.Key.ADD:
|
||||
animated_entity.anim_speed = min(animated_entity.anim_speed + 0.25, 4.0)
|
||||
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
|
||||
|
||||
elif key == mcrfpy.Key.HYPHEN or key == mcrfpy.Key.SUBTRACT:
|
||||
animated_entity.anim_speed = max(animated_entity.anim_speed - 0.25, 0.0)
|
||||
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
|
||||
|
||||
elif key == mcrfpy.Key.R:
|
||||
animated_entity.anim_time = 0.0
|
||||
status.text = "Status: Animation reset"
|
||||
|
||||
# Camera presets
|
||||
if key == mcrfpy.Key.NUM_1:
|
||||
pos, target, name = camera_presets[0]
|
||||
viewport.camera_pos = pos
|
||||
viewport.camera_target = target
|
||||
status.text = f"Camera: {name}"
|
||||
|
||||
elif key == mcrfpy.Key.NUM_2:
|
||||
pos, target, name = camera_presets[1]
|
||||
viewport.camera_pos = pos
|
||||
viewport.camera_target = target
|
||||
status.text = f"Camera: {name}"
|
||||
|
||||
elif key == mcrfpy.Key.NUM_3:
|
||||
pos, target, name = camera_presets[2]
|
||||
viewport.camera_pos = pos
|
||||
viewport.camera_target = target
|
||||
status.text = f"Camera: {name}"
|
||||
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
|
||||
# Set up scene
|
||||
scene.on_key = on_key
|
||||
|
||||
# Create timer for updates
|
||||
timer = mcrfpy.Timer("anim_update", update, 16)
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
print()
|
||||
print("Skeletal Animation Demo loaded!")
|
||||
print("Controls:")
|
||||
print(" [Space] Toggle pause")
|
||||
print(" [L] Toggle loop")
|
||||
print(" [+/-] Adjust speed")
|
||||
print(" [R] Reset time")
|
||||
print(" [1-3] Camera presets")
|
||||
print(" [ESC] Quit")
|
||||
Loading…
Add table
Add a link
Reference in a new issue