digiPal / models /rigging_processor.py
BladeSzaSza's picture
new design
fe24641
import numpy as np
import trimesh
from typing import Union, Dict, List, Tuple, Optional
import tempfile
from pathlib import Path
class UniRigProcessor:
"""Automatic rigging for 3D models using simplified UniRig approach"""
def __init__(self, device: str = "cuda"):
self.device = device
self.model = None
# Rigging parameters
self.bone_detection_threshold = 0.1
self.max_bones = 20
self.min_bones = 5
# Animation presets for monsters
self.animation_presets = {
'idle': self._create_idle_animation,
'walk': self._create_walk_animation,
'attack': self._create_attack_animation,
'happy': self._create_happy_animation
}
def load_model(self):
"""Load rigging model (placeholder for actual implementation)"""
# In production, this would load the actual UniRig model
# For now, we'll use procedural rigging
self.model = "procedural"
def rig_mesh(self,
mesh: Union[str, trimesh.Trimesh],
mesh_type: str = "monster") -> Dict[str, any]:
"""Add rigging to a 3D mesh"""
try:
# Load mesh if path provided
if isinstance(mesh, str):
mesh = trimesh.load(mesh)
# Ensure model is loaded
if self.model is None:
self.load_model()
# Analyze mesh structure
mesh_analysis = self._analyze_mesh(mesh)
# Generate skeleton
skeleton = self._generate_skeleton(mesh, mesh_analysis)
# Compute bone weights
weights = self._compute_bone_weights(mesh, skeleton)
# Create rigged model
rigged_model = {
'mesh': mesh,
'skeleton': skeleton,
'weights': weights,
'animations': self._create_default_animations(skeleton),
'metadata': {
'mesh_type': mesh_type,
'bone_count': len(skeleton['bones']),
'vertex_count': len(mesh.vertices)
}
}
# Save rigged model
output_path = self._save_rigged_model(rigged_model)
return output_path
except Exception as e:
print(f"Rigging error: {e}")
# Return original mesh if rigging fails
return self._save_mesh_without_rigging(mesh)
def _analyze_mesh(self, mesh: trimesh.Trimesh) -> Dict[str, any]:
"""Analyze mesh structure for rigging"""
# Get mesh bounds and center
bounds = mesh.bounds
center = mesh.centroid
# Analyze mesh topology
analysis = {
'bounds': bounds,
'center': center,
'height': bounds[1][2] - bounds[0][2],
'width': bounds[1][0] - bounds[0][0],
'depth': bounds[1][1] - bounds[0][1],
'is_symmetric': self._check_symmetry(mesh),
'detected_limbs': self._detect_limbs(mesh),
'mesh_type': self._classify_mesh_type(mesh)
}
return analysis
def _check_symmetry(self, mesh: trimesh.Trimesh) -> bool:
"""Check if mesh is roughly symmetric"""
# Simple check: compare left and right halves
vertices = mesh.vertices
center_x = mesh.centroid[0]
left_verts = vertices[vertices[:, 0] < center_x]
right_verts = vertices[vertices[:, 0] > center_x]
# Check if similar number of vertices on each side
ratio = len(left_verts) / (len(right_verts) + 1)
return 0.8 < ratio < 1.2
def _detect_limbs(self, mesh: trimesh.Trimesh) -> List[Dict]:
"""Detect potential limbs in the mesh"""
# Simplified limb detection using vertex clustering
from sklearn.cluster import DBSCAN
limbs = []
try:
# Cluster vertices to find distinct parts
clustering = DBSCAN(eps=0.1, min_samples=10).fit(mesh.vertices)
# Analyze each cluster
for label in set(clustering.labels_):
if label == -1: # Noise
continue
cluster_verts = mesh.vertices[clustering.labels_ == label]
# Check if cluster could be a limb
cluster_bounds = np.array([cluster_verts.min(axis=0), cluster_verts.max(axis=0)])
dimensions = cluster_bounds[1] - cluster_bounds[0]
# Limbs are typically elongated
if max(dimensions) / (min(dimensions) + 0.001) > 2:
limbs.append({
'center': cluster_verts.mean(axis=0),
'direction': dimensions,
'size': len(cluster_verts)
})
except:
# Fallback if clustering fails
pass
return limbs
def _classify_mesh_type(self, mesh: trimesh.Trimesh) -> str:
"""Classify the type of creature mesh"""
analysis = {
'height': mesh.bounds[1][2] - mesh.bounds[0][2],
'width': mesh.bounds[1][0] - mesh.bounds[0][0],
'depth': mesh.bounds[1][1] - mesh.bounds[0][1]
}
# Simple classification based on proportions
aspect_ratio = analysis['height'] / max(analysis['width'], analysis['depth'])
if aspect_ratio > 1.5:
return 'bipedal' # Tall creatures
elif aspect_ratio < 0.7:
return 'quadruped' # Wide creatures
else:
return 'hybrid' # Mixed proportions
def _generate_skeleton(self, mesh: trimesh.Trimesh, analysis: Dict) -> Dict:
"""Generate skeleton for the mesh"""
skeleton = {
'bones': [],
'hierarchy': {},
'bind_poses': []
}
# Create root bone at center
root_pos = analysis['center']
root_bone = {
'id': 0,
'name': 'root',
'position': root_pos,
'parent': -1,
'children': []
}
skeleton['bones'].append(root_bone)
# Generate bones based on mesh type
mesh_type = analysis['mesh_type']
if mesh_type == 'bipedal':
skeleton = self._generate_bipedal_skeleton(mesh, skeleton, analysis)
elif mesh_type == 'quadruped':
skeleton = self._generate_quadruped_skeleton(mesh, skeleton, analysis)
else:
skeleton = self._generate_hybrid_skeleton(mesh, skeleton, analysis)
# Build hierarchy
for bone in skeleton['bones']:
if bone['parent'] >= 0:
skeleton['bones'][bone['parent']]['children'].append(bone['id'])
return skeleton
def _generate_bipedal_skeleton(self, mesh: trimesh.Trimesh, skeleton: Dict, analysis: Dict) -> Dict:
"""Generate skeleton for bipedal creature"""
bounds = analysis['bounds']
center = analysis['center']
height = analysis['height']
# Spine bones
spine_positions = [
center + [0, 0, -height * 0.4], # Hips
center + [0, 0, 0], # Chest
center + [0, 0, height * 0.3] # Head
]
parent_id = 0
for i, pos in enumerate(spine_positions):
bone = {
'id': len(skeleton['bones']),
'name': ['hips', 'chest', 'head'][i],
'position': pos,
'parent': parent_id,
'children': []
}
skeleton['bones'].append(bone)
parent_id = bone['id']
# Add limbs
chest_id = skeleton['bones'][2]['id'] # Chest bone
hips_id = skeleton['bones'][1]['id'] # Hips bone
# Arms
arm_offset = analysis['width'] * 0.4
for side, offset in [('left', -arm_offset), ('right', arm_offset)]:
shoulder_pos = skeleton['bones'][chest_id]['position'] + [offset, 0, 0]
elbow_pos = shoulder_pos + [offset * 0.5, 0, -height * 0.2]
# Shoulder
shoulder = {
'id': len(skeleton['bones']),
'name': f'{side}_shoulder',
'position': shoulder_pos,
'parent': chest_id,
'children': []
}
skeleton['bones'].append(shoulder)
# Elbow/Hand
hand = {
'id': len(skeleton['bones']),
'name': f'{side}_hand',
'position': elbow_pos,
'parent': shoulder['id'],
'children': []
}
skeleton['bones'].append(hand)
# Legs
for side, offset in [('left', -arm_offset * 0.5), ('right', arm_offset * 0.5)]:
hip_pos = skeleton['bones'][hips_id]['position'] + [offset, 0, 0]
foot_pos = hip_pos + [0, 0, -height * 0.4]
# Leg
leg = {
'id': len(skeleton['bones']),
'name': f'{side}_leg',
'position': hip_pos,
'parent': hips_id,
'children': []
}
skeleton['bones'].append(leg)
# Foot
foot = {
'id': len(skeleton['bones']),
'name': f'{side}_foot',
'position': foot_pos,
'parent': leg['id'],
'children': []
}
skeleton['bones'].append(foot)
return skeleton
def _generate_quadruped_skeleton(self, mesh: trimesh.Trimesh, skeleton: Dict, analysis: Dict) -> Dict:
"""Generate skeleton for quadruped creature"""
# Similar to bipedal but with 4 legs and horizontal spine
center = analysis['center']
width = analysis['width']
depth = analysis['depth']
# Spine (horizontal)
spine_positions = [
center + [-width * 0.3, 0, 0], # Tail
center, # Body
center + [width * 0.3, 0, 0] # Head
]
parent_id = 0
for i, pos in enumerate(spine_positions):
bone = {
'id': len(skeleton['bones']),
'name': ['tail', 'body', 'head'][i],
'position': pos,
'parent': parent_id,
'children': []
}
skeleton['bones'].append(bone)
parent_id = bone['id'] if i < 2 else skeleton['bones'][1]['id']
# Add 4 legs
body_id = skeleton['bones'][1]['id']
for front_back, x_offset in [('front', width * 0.2), ('back', -width * 0.2)]:
for side, z_offset in [('left', -depth * 0.3), ('right', depth * 0.3)]:
leg_pos = skeleton['bones'][body_id]['position'] + [x_offset, -analysis['height'] * 0.3, z_offset]
leg = {
'id': len(skeleton['bones']),
'name': f'{front_back}_{side}_leg',
'position': leg_pos,
'parent': body_id,
'children': []
}
skeleton['bones'].append(leg)
return skeleton
def _generate_hybrid_skeleton(self, mesh: trimesh.Trimesh, skeleton: Dict, analysis: Dict) -> Dict:
"""Generate skeleton for hybrid creature"""
# Mix of bipedal and quadruped features
# For simplicity, use bipedal as base
return self._generate_bipedal_skeleton(mesh, skeleton, analysis)
def _compute_bone_weights(self, mesh: trimesh.Trimesh, skeleton: Dict) -> np.ndarray:
"""Compute bone weights for vertices"""
num_vertices = len(mesh.vertices)
num_bones = len(skeleton['bones'])
# Initialize weights matrix
weights = np.zeros((num_vertices, num_bones))
# For each vertex, compute influence from each bone
for v_idx, vertex in enumerate(mesh.vertices):
total_weight = 0
for b_idx, bone in enumerate(skeleton['bones']):
# Distance-based weight
distance = np.linalg.norm(vertex - bone['position'])
# Inverse distance weight with falloff
weight = 1.0 / (distance + 0.1)
weights[v_idx, b_idx] = weight
total_weight += weight
# Normalize weights
if total_weight > 0:
weights[v_idx] /= total_weight
# Keep only top 4 influences per vertex (standard for game engines)
top_4 = np.argsort(weights[v_idx])[-4:]
mask = np.zeros(num_bones, dtype=bool)
mask[top_4] = True
weights[v_idx, ~mask] = 0
# Re-normalize
if weights[v_idx].sum() > 0:
weights[v_idx] /= weights[v_idx].sum()
return weights
def _create_default_animations(self, skeleton: Dict) -> Dict[str, List]:
"""Create default animations for the skeleton"""
animations = {}
# Create basic animation sets
for anim_name, anim_func in self.animation_presets.items():
animations[anim_name] = anim_func(skeleton)
return animations
def _create_idle_animation(self, skeleton: Dict) -> List[Dict]:
"""Create idle animation keyframes"""
keyframes = []
# Simple breathing/bobbing motion
for t in np.linspace(0, 2 * np.pi, 30):
frame = {
'time': t / (2 * np.pi),
'bones': {}
}
# Subtle movement for each bone
for bone in skeleton['bones']:
if 'chest' in bone['name'] or 'body' in bone['name']:
# Breathing motion
offset = np.sin(t) * 0.02
frame['bones'][bone['id']] = {
'position': bone['position'] + [0, offset, 0],
'rotation': [0, 0, 0, 1] # Quaternion
}
else:
# No movement
frame['bones'][bone['id']] = {
'position': bone['position'],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
return keyframes
def _create_walk_animation(self, skeleton: Dict) -> List[Dict]:
"""Create walk animation keyframes"""
# Simplified walk cycle
keyframes = []
for t in np.linspace(0, 2 * np.pi, 60):
frame = {
'time': t / (2 * np.pi),
'bones': {}
}
# Animate legs with sine waves
for bone in skeleton['bones']:
if 'leg' in bone['name'] or 'foot' in bone['name']:
# Alternating leg movement
phase = 0 if 'left' in bone['name'] else np.pi
offset = np.sin(t + phase) * 0.1
frame['bones'][bone['id']] = {
'position': bone['position'] + [offset, 0, 0],
'rotation': [0, 0, 0, 1]
}
else:
frame['bones'][bone['id']] = {
'position': bone['position'],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
return keyframes
def _create_attack_animation(self, skeleton: Dict) -> List[Dict]:
"""Create attack animation keyframes"""
# Quick strike motion
keyframes = []
# Wind up
for t in np.linspace(0, 0.3, 10):
frame = {'time': t, 'bones': {}}
for bone in skeleton['bones']:
frame['bones'][bone['id']] = {
'position': bone['position'],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
# Strike
for t in np.linspace(0.3, 0.5, 5):
frame = {'time': t, 'bones': {}}
for bone in skeleton['bones']:
if 'hand' in bone['name'] or 'head' in bone['name']:
# Forward motion
offset = (t - 0.3) * 0.5
frame['bones'][bone['id']] = {
'position': bone['position'] + [offset, 0, 0],
'rotation': [0, 0, 0, 1]
}
else:
frame['bones'][bone['id']] = {
'position': bone['position'],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
# Return
for t in np.linspace(0.5, 1.0, 10):
frame = {'time': t, 'bones': {}}
for bone in skeleton['bones']:
frame['bones'][bone['id']] = {
'position': bone['position'],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
return keyframes
def _create_happy_animation(self, skeleton: Dict) -> List[Dict]:
"""Create happy/excited animation keyframes"""
# Jumping or bouncing motion
keyframes = []
for t in np.linspace(0, 2 * np.pi, 40):
frame = {
'time': t / (2 * np.pi),
'bones': {}
}
# Bouncing motion
bounce = abs(np.sin(t * 2)) * 0.1
for bone in skeleton['bones']:
frame['bones'][bone['id']] = {
'position': bone['position'] + [0, bounce, 0],
'rotation': [0, 0, 0, 1]
}
keyframes.append(frame)
return keyframes
def _save_rigged_model(self, rigged_model: Dict) -> str:
"""Save rigged model to file"""
# Create temporary file
with tempfile.NamedTemporaryFile(suffix='.glb', delete=False) as tmp:
output_path = tmp.name
# In production, this would export the rigged model with animations
# For now, just save the mesh
rigged_model['mesh'].export(output_path)
return output_path
def _save_mesh_without_rigging(self, mesh: Union[str, trimesh.Trimesh]) -> str:
"""Save mesh without rigging as fallback"""
if isinstance(mesh, str):
return mesh
with tempfile.NamedTemporaryFile(suffix='.glb', delete=False) as tmp:
output_path = tmp.name
mesh.export(output_path)
return output_path
def to(self, device: str):
"""Move model to specified device (compatibility method)"""
self.device = device