Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
- Added AnimationNameMapper class to handle conversion between different animation naming schemes (legacy, artist, hierarchical, semantic). - Included methods for initialization, pattern matching, conversion, and validation of animation names. - Developed comprehensive unit tests for the animation name converter and demo pages using Playwright. - Created a Vite configuration for the demo application, including asset handling and optimization settings. - Enhanced the demo with features for batch conversion, performance metrics, and responsive design.
269 lines
9.0 KiB
Python
269 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
Blender Animation Processor
|
|
Processes Blender animation files and exports them with proper naming schemes
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional
|
|
|
|
# Blender script template for processing animations
|
|
BLENDER_SCRIPT_TEMPLATE = """
|
|
import bpy
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
|
|
def process_animation_file(filepath, output_dir, naming_scheme='artist'):
|
|
\"\"\"Process a single Blender file and export animations\"\"\"
|
|
|
|
# Clear existing scene
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
# Open the file
|
|
bpy.ops.wm.open_mainfile(filepath=filepath)
|
|
|
|
results = {
|
|
'file': filepath,
|
|
'animations': [],
|
|
'errors': []
|
|
}
|
|
|
|
try:
|
|
# Get all objects with animation data
|
|
animated_objects = [obj for obj in bpy.data.objects if obj.animation_data and obj.animation_data.action]
|
|
|
|
if not animated_objects:
|
|
results['errors'].append('No animated objects found')
|
|
return results
|
|
|
|
for obj in animated_objects:
|
|
if obj.animation_data and obj.animation_data.action:
|
|
action = obj.animation_data.action
|
|
|
|
# Extract animation info
|
|
anim_info = {
|
|
'object': obj.name,
|
|
'action': action.name,
|
|
'frame_start': int(action.frame_range[0]),
|
|
'frame_end': int(action.frame_range[1]),
|
|
'duration': action.frame_range[1] - action.frame_range[0]
|
|
}
|
|
|
|
# Convert to proper naming scheme
|
|
new_name = convert_animation_name(action.name, naming_scheme)
|
|
anim_info['converted_name'] = new_name
|
|
|
|
# Export the animation (GLTF format)
|
|
output_file = Path(output_dir) / f"{new_name}.gltf"
|
|
|
|
# Select only this object
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
|
|
# Export GLTF with animation
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=str(output_file),
|
|
export_selected=True,
|
|
export_animations=True,
|
|
export_animation_mode='ACTIONS',
|
|
export_nla_strips=False,
|
|
export_frame_range=True,
|
|
export_frame_step=1,
|
|
export_custom_properties=True
|
|
)
|
|
|
|
anim_info['exported_file'] = str(output_file)
|
|
results['animations'].append(anim_info)
|
|
|
|
print(f"Exported animation: {action.name} -> {new_name}")
|
|
|
|
except Exception as e:
|
|
results['errors'].append(str(e))
|
|
print(f"Error processing {filepath}: {e}")
|
|
|
|
return results
|
|
|
|
def convert_animation_name(blender_name, target_scheme='artist'):
|
|
\"\"\"Convert Blender animation name to target naming scheme\"\"\"
|
|
|
|
# Basic name cleaning
|
|
name = blender_name.strip().replace(' ', '_')
|
|
|
|
# Remove common Blender prefixes/suffixes
|
|
name = name.replace('Action', '').replace('action', '')
|
|
name = name.replace('.001', '').replace('.000', '')
|
|
|
|
if target_scheme == 'artist':
|
|
# Convert to Owen_PascalCase format
|
|
parts = name.split('_')
|
|
pascal_parts = [part.capitalize() for part in parts if part]
|
|
return f"Owen_{''.join(pascal_parts)}"
|
|
|
|
elif target_scheme == 'legacy':
|
|
# Convert to lowercase_with_underscores_L
|
|
name_lower = name.lower()
|
|
# Add suffix based on animation type (default to L for Loop)
|
|
if not name_lower.endswith(('_l', '_s')):
|
|
name_lower += '_l'
|
|
return name_lower
|
|
|
|
elif target_scheme == 'hierarchical':
|
|
# Convert to owen.category.subcategory
|
|
parts = name.lower().split('_')
|
|
return f"owen.state.{'.'.join(parts)}.loop"
|
|
|
|
elif target_scheme == 'semantic':
|
|
# Convert to OwenPascalCase
|
|
parts = name.split('_')
|
|
pascal_parts = [part.capitalize() for part in parts if part]
|
|
return f"Owen{''.join(pascal_parts)}Loop"
|
|
|
|
return name
|
|
|
|
# Main processing
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
if len(sys.argv) < 4:
|
|
print("Usage: blender --background --python script.py input_dir output_dir naming_scheme")
|
|
sys.exit(1)
|
|
|
|
input_dir = sys.argv[-3]
|
|
output_dir = sys.argv[-2]
|
|
naming_scheme = sys.argv[-1]
|
|
|
|
print(f"Processing animations from {input_dir} to {output_dir} with {naming_scheme} scheme")
|
|
|
|
# Create output directory
|
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Process all .blend files in input directory
|
|
blend_files = list(Path(input_dir).glob('*.blend'))
|
|
|
|
all_results = {
|
|
'processed_files': [],
|
|
'total_animations': 0,
|
|
'total_files': len(blend_files),
|
|
'naming_scheme': naming_scheme
|
|
}
|
|
|
|
for blend_file in blend_files:
|
|
print(f"Processing: {blend_file}")
|
|
result = process_animation_file(str(blend_file), output_dir, naming_scheme)
|
|
all_results['processed_files'].append(result)
|
|
all_results['total_animations'] += len(result['animations'])
|
|
|
|
# Save processing report
|
|
report_file = Path(output_dir) / 'processing_report.json'
|
|
with open(report_file, 'w') as f:
|
|
json.dump(all_results, f, indent=2)
|
|
|
|
print(f"Processing complete. Processed {all_results['total_animations']} animations from {all_results['total_files']} files.")
|
|
print(f"Report saved to: {report_file}")
|
|
"""
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Process Blender animation files')
|
|
parser.add_argument('--input-dir', required=True, help='Directory containing .blend files')
|
|
parser.add_argument('--output-dir', required=True, help='Directory to export processed animations')
|
|
parser.add_argument('--naming-scheme', default='artist', choices=['legacy', 'artist', 'hierarchical', 'semantic'],
|
|
help='Target naming scheme for animations')
|
|
parser.add_argument('--blender-path', default='blender', help='Path to Blender executable')
|
|
parser.add_argument('--dry-run', action='store_true', help='Show what would be processed without actually doing it')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate input directory
|
|
input_path = Path(args.input_dir)
|
|
if not input_path.exists():
|
|
print(f"Error: Input directory '{args.input_dir}' does not exist")
|
|
sys.exit(1)
|
|
|
|
# Find .blend files
|
|
blend_files = list(input_path.glob('*.blend'))
|
|
if not blend_files:
|
|
print(f"Warning: No .blend files found in '{args.input_dir}'")
|
|
return
|
|
|
|
print(f"Found {len(blend_files)} .blend files to process:")
|
|
for blend_file in blend_files:
|
|
print(f" • {blend_file.name}")
|
|
|
|
if args.dry_run:
|
|
print(f"\nDry run complete. Would process {len(blend_files)} files with {args.naming_scheme} scheme.")
|
|
return
|
|
|
|
# Create output directory
|
|
output_path = Path(args.output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create temporary Blender script
|
|
script_path = output_path / 'temp_blender_script.py'
|
|
with open(script_path, 'w') as f:
|
|
f.write(BLENDER_SCRIPT_TEMPLATE)
|
|
|
|
try:
|
|
# Run Blender with the script
|
|
print(f"\nProcessing animations with Blender...")
|
|
print(f"Input: {args.input_dir}")
|
|
print(f"Output: {args.output_dir}")
|
|
print(f"Scheme: {args.naming_scheme}")
|
|
|
|
cmd = [
|
|
args.blender_path,
|
|
'--background',
|
|
'--python', str(script_path),
|
|
'--',
|
|
args.input_dir,
|
|
args.output_dir,
|
|
args.naming_scheme
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
print("✅ Blender processing completed successfully!")
|
|
print(result.stdout)
|
|
else:
|
|
print("❌ Blender processing failed!")
|
|
print("STDOUT:", result.stdout)
|
|
print("STDERR:", result.stderr)
|
|
sys.exit(1)
|
|
|
|
# Load and display the processing report
|
|
report_file = output_path / 'processing_report.json'
|
|
if report_file.exists():
|
|
with open(report_file, 'r') as f:
|
|
report = json.load(f)
|
|
|
|
print(f"\n📊 Processing Summary:")
|
|
print(f"Files processed: {report['total_files']}")
|
|
print(f"Animations exported: {report['total_animations']}")
|
|
print(f"Naming scheme: {report['naming_scheme']}")
|
|
|
|
# Show any errors
|
|
errors = []
|
|
for file_result in report['processed_files']:
|
|
errors.extend(file_result.get('errors', []))
|
|
|
|
if errors:
|
|
print(f"\n⚠️ Errors encountered:")
|
|
for error in errors:
|
|
print(f" • {error}")
|
|
|
|
finally:
|
|
# Clean up temporary script
|
|
if script_path.exists():
|
|
script_path.unlink()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|