EG/plugins/user/skeletal_animation_system/animation_optimization.py
2025-10-30 11:46:41 +08:00

600 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
骨骼动画系统插件 - 动画优化和压缩模块
提供动画压缩、优化、缓存等功能
"""
from panda3d.core import *
from direct.actor.Actor import Actor
import math
import json
class AnimationCompression:
"""
动画压缩系统
支持关键帧压缩、插值优化等技术
"""
def __init__(self):
self.compression_settings = {
'position_threshold': 0.01, # 位置阈值
'rotation_threshold': 0.1, # 旋转阈值(度)
'scale_threshold': 0.01, # 缩放阈值
'enable_quantization': True, # 是否启用量化
'quantization_bits': 16 # 量化位数
}
def compress_animation(self, actor, anim_name, output_path=None):
"""
压缩指定动画
:param actor: Actor对象
:param anim_name: 动画名称
:param output_path: 输出路径
:return: 压缩后的动画数据
"""
if anim_name not in actor.getAnimNames():
print(f"错误: 动画 {anim_name} 不存在")
return None
print(f"开始压缩动画: {anim_name}")
# 获取动画控制
anim_control = actor.getAnimControl(anim_name)
if not anim_control:
print(f"错误: 无法获取动画控制 {anim_name}")
return None
# 获取动画数据
num_frames = anim_control.getNumFrames()
frame_rate = anim_control.getFrameRate()
print(f" 动画信息: {num_frames} 帧, {frame_rate} FPS")
# 压缩数据结构
compressed_data = {
'name': anim_name,
'frame_rate': frame_rate,
'duration': num_frames / frame_rate if frame_rate > 0 else 0,
'channels': {}
}
# 获取骨骼列表
channels = self._extract_animation_channels(actor, anim_name, num_frames)
# 压缩每个通道
for bone_name, channel_data in channels.items():
compressed_channel = self._compress_channel(channel_data)
compressed_data['channels'][bone_name] = compressed_channel
# 保存压缩数据
if output_path:
self._save_compressed_animation(compressed_data, output_path)
compression_ratio = self._calculate_compression_ratio(channels, compressed_data)
print(f" 压缩完成,压缩比: {compression_ratio:.2f}:1")
return compressed_data
def _extract_animation_channels(self, actor, anim_name, num_frames):
"""
提取动画通道数据
:param actor: Actor对象
:param anim_name: 动画名称
:param num_frames: 帧数
:return: 通道数据字典
"""
channels = {}
# 获取所有骨骼
bundle = actor.getPartBundleDict().get('modelRoot')
if not bundle:
return channels
# 遍历骨骼
for i in range(bundle.getNumChildren()):
child = bundle.getChild(i)
if hasattr(child, 'getName'):
bone_name = child.getName()
channels[bone_name] = self._extract_bone_channel(actor, bone_name, anim_name, num_frames)
return channels
def _extract_bone_channel(self, actor, bone_name, anim_name, num_frames):
"""
提取单个骨骼的动画通道
:param actor: Actor对象
:param bone_name: 骨骼名称
:param anim_name: 动画名称
:param num_frames: 帧数
:return: 骨骼动画数据
"""
channel_data = {
'positions': [],
'rotations': [],
'scales': []
}
# 为每一帧提取数据
for frame in range(num_frames):
# 设置到指定帧
actor.pose(anim_name, frame)
# 获取骨骼变换
joint = actor.exposeJoint(None, "modelRoot", bone_name)
if joint:
pos = joint.getPos()
hpr = joint.getHpr()
scale = joint.getScale()
channel_data['positions'].append((pos.x, pos.y, pos.z))
channel_data['rotations'].append((hpr.x, hpr.y, hpr.z))
channel_data['scales'].append((scale.x, scale.y, scale.z))
return channel_data
def _compress_channel(self, channel_data):
"""
压缩单个通道
:param channel_data: 通道数据
:return: 压缩后的通道数据
"""
compressed_channel = {
'positions': self._compress_keyframes(channel_data['positions'], 'position'),
'rotations': self._compress_keyframes(channel_data['rotations'], 'rotation'),
'scales': self._compress_keyframes(channel_data['scales'], 'scale')
}
return compressed_channel
def _compress_keyframes(self, keyframes, data_type):
"""
压缩关键帧数据
:param keyframes: 关键帧数据
:param data_type: 数据类型 ('position', 'rotation', 'scale')
:return: 压缩后的关键帧
"""
if not keyframes:
return []
# 根据数据类型获取阈值
if data_type == 'position':
threshold = self.compression_settings['position_threshold']
elif data_type == 'rotation':
threshold = self.compression_settings['rotation_threshold']
else: # scale
threshold = self.compression_settings['scale_threshold']
compressed_keyframes = []
compressed_keyframes.append((0, keyframes[0])) # 第一帧总是保留
# 关键帧简化算法
last_keyframe = keyframes[0]
for i in range(1, len(keyframes)):
current_keyframe = keyframes[i]
# 检查是否需要保留这一帧
if self._needs_keyframe(last_keyframe, current_keyframe, threshold, data_type):
compressed_keyframes.append((i, current_keyframe))
last_keyframe = current_keyframe
# 总是保留最后一帧
if len(keyframes) > 1 and (len(compressed_keyframes) == 0 or compressed_keyframes[-1][0] != len(keyframes) - 1):
compressed_keyframes.append((len(keyframes) - 1, keyframes[-1]))
# 应用量化
if self.compression_settings['enable_quantization']:
compressed_keyframes = self._quantize_keyframes(compressed_keyframes, data_type)
return compressed_keyframes
def _needs_keyframe(self, last_frame, current_frame, threshold, data_type):
"""
判断是否需要保留关键帧
:param last_frame: 上一帧数据
:param current_frame: 当前帧数据
:param threshold: 阈值
:param data_type: 数据类型
:return: 是否需要保留
"""
if data_type == 'rotation':
# 旋转使用角度差
diff_x = abs(current_frame[0] - last_frame[0])
diff_y = abs(current_frame[1] - last_frame[1])
diff_z = abs(current_frame[2] - last_frame[2])
return diff_x > threshold or diff_y > threshold or diff_z > threshold
else:
# 位置和缩放使用欧几里得距离
diff_x = abs(current_frame[0] - last_frame[0])
diff_y = abs(current_frame[1] - last_frame[1])
diff_z = abs(current_frame[2] - last_frame[2])
distance = math.sqrt(diff_x**2 + diff_y**2 + diff_z**2)
return distance > threshold
def _quantize_keyframes(self, keyframes, data_type):
"""
量化关键帧数据
:param keyframes: 关键帧数据
:param data_type: 数据类型
:return: 量化后的关键帧
"""
bits = self.compression_settings['quantization_bits']
max_value = 2**(bits - 1) - 1
min_value = -2**(bits - 1)
quantized_keyframes = []
for frame_index, frame_data in keyframes:
quantized_data = []
for value in frame_data:
# 简化量化过程
quantized_value = max(min_value, min(max_value, int(value * (max_value / 10.0))))
quantized_data.append(quantized_value)
quantized_keyframes.append((frame_index, tuple(quantized_data)))
return quantized_keyframes
def _calculate_compression_ratio(self, original_data, compressed_data):
"""
计算压缩比
:param original_data: 原始数据
:param compressed_data: 压缩数据
:return: 压缩比
"""
# 简化的压缩比计算
original_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales'])
for channel in original_data.values())
compressed_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales'])
for channel in compressed_data['channels'].values())
if compressed_size == 0:
return 1.0
return original_size / compressed_size
def _save_compressed_animation(self, compressed_data, output_path):
"""
保存压缩动画数据
:param compressed_data: 压缩数据
:param output_path: 输出路径
"""
try:
with open(output_path, 'w') as f:
json.dump(compressed_data, f, indent=2)
print(f"压缩动画已保存到: {output_path}")
except Exception as e:
print(f"保存压缩动画失败: {e}")
class AnimationCaching:
"""
动画缓存系统
提供动画数据缓存以提高性能
"""
def __init__(self, max_cache_size=100):
self.max_cache_size = max_cache_size
self.cache = {} # {anim_key: anim_data}
self.access_order = [] # 访问顺序用于LRU
self.cache_stats = {
'hits': 0,
'misses': 0,
'evictions': 0
}
def get_cached_animation(self, actor_id, anim_name):
"""
获取缓存的动画数据
:param actor_id: Actor ID
:param anim_name: 动画名称
:return: 动画数据或None
"""
cache_key = f"{actor_id}:{anim_name}"
if cache_key in self.cache:
# 缓存命中
self.cache_stats['hits'] += 1
# 更新访问顺序
if cache_key in self.access_order:
self.access_order.remove(cache_key)
self.access_order.append(cache_key)
print(f"动画缓存命中: {cache_key}")
return self.cache[cache_key]
else:
# 缓存未命中
self.cache_stats['misses'] += 1
print(f"动画缓存未命中: {cache_key}")
return None
def cache_animation(self, actor_id, anim_name, anim_data):
"""
缓存动画数据
:param actor_id: Actor ID
:param anim_name: 动画名称
:param anim_data: 动画数据
"""
cache_key = f"{actor_id}:{anim_name}"
# 如果缓存已满,移除最久未使用的项
if len(self.cache) >= self.max_cache_size:
if self.access_order:
oldest_key = self.access_order.pop(0)
del self.cache[oldest_key]
self.cache_stats['evictions'] += 1
print(f"动画缓存已满,移除最旧项: {oldest_key}")
# 添加新项
self.cache[cache_key] = anim_data
self.access_order.append(cache_key)
print(f"动画已缓存: {cache_key}")
def invalidate_cache(self, actor_id=None, anim_name=None):
"""
使缓存失效
:param actor_id: Actor ID如果为None则匹配所有
:param anim_name: 动画名称如果为None则匹配所有
"""
if actor_id is None and anim_name is None:
# 清空所有缓存
self.cache.clear()
self.access_order.clear()
print("所有动画缓存已清空")
return
# 构建匹配模式
if actor_id and anim_name:
pattern = f"{actor_id}:{anim_name}"
keys_to_remove = [k for k in self.cache.keys() if k == pattern]
elif actor_id:
pattern = f"{actor_id}:"
keys_to_remove = [k for k in self.cache.keys() if k.startswith(pattern)]
else: # anim_name only
keys_to_remove = [k for k in self.cache.keys() if k.endswith(f":{anim_name}")]
# 移除匹配项
for key in keys_to_remove:
if key in self.cache:
del self.cache[key]
if key in self.access_order:
self.access_order.remove(key)
print(f"已使 {len(keys_to_remove)} 项缓存失效")
def get_cache_stats(self):
"""
获取缓存统计信息
:return: 统计信息字典
"""
total_requests = self.cache_stats['hits'] + self.cache_stats['misses']
hit_rate = self.cache_stats['hits'] / total_requests if total_requests > 0 else 0
return {
'cache_size': len(self.cache),
'max_size': self.max_cache_size,
'hits': self.cache_stats['hits'],
'misses': self.cache_stats['misses'],
'evictions': self.cache_stats['evictions'],
'hit_rate': hit_rate,
'total_requests': total_requests
}
def clear_stats(self):
"""
清空统计信息
"""
self.cache_stats = {
'hits': 0,
'misses': 0,
'evictions': 0
}
class AnimationLOD:
"""
动画细节层次系统
根据距离和重要性调整动画质量
"""
def __init__(self, world):
self.world = world
self.lod_settings = {
'distance_lod': True,
'distance_thresholds': [10, 20, 50], # 距离阈值
'lod_levels': [0, 1, 2, 3], # LOD级别
'performance_lod': True,
'fps_threshold': 30, # FPS阈值
'automatic_lod': True
}
self.actor_lod_levels = {} # {actor_id: lod_level}
self.performance_monitor = {
'last_fps': 60,
'frame_times': []
}
def update_lod_levels(self, camera_pos):
"""
更新所有Actor的LOD级别
:param camera_pos: 摄像机位置
"""
if not self.lod_settings['automatic_lod']:
return
# 获取所有Actor
actors = self.world.render.findAllMatches("**/+ActorNode")
for actor_np in actors:
actor = actor_np.node()
if not isinstance(actor, Actor):
continue
actor_id = actor_np.getName()
# 计算距离LOD
distance_lod = 0
if self.lod_settings['distance_lod']:
distance = (actor_np.getPos() - camera_pos).length()
thresholds = self.lod_settings['distance_thresholds']
for i, threshold in enumerate(thresholds):
if distance > threshold:
distance_lod = i + 1
# 计算性能LOD
performance_lod = 0
if self.lod_settings['performance_lod']:
if self.performance_monitor['last_fps'] < self.lod_settings['fps_threshold']:
performance_lod = 1
# 确定最终LOD级别
final_lod = max(distance_lod, performance_lod)
self.actor_lod_levels[actor_id] = final_lod
# 应用LOD设置
self._apply_lod_settings(actor, actor_id, final_lod)
def _apply_lod_settings(self, actor, actor_id, lod_level):
"""
应用LOD设置到Actor
:param actor: Actor对象
:param actor_id: Actor ID
:param lod_level: LOD级别
"""
# LOD级别说明:
# 0 - 最高质量 (全帧率,全骨骼)
# 1 - 中等质量 (半帧率,全骨骼)
# 2 - 低质量 (1/4帧率简化骨骼)
# 3 - 最低质量 (1/10帧率极简骨骼)
if lod_level == 0:
# 最高质量 - 不做调整
pass
elif lod_level == 1:
# 中等质量 - 降低播放速度
for anim_name in actor.getAnimNames():
current_rate = actor.getPlayRate(anim_name)
actor.setPlayRate(current_rate * 0.5, anim_name)
elif lod_level == 2:
# 低质量 - 进一步降低播放速度
for anim_name in actor.getAnimNames():
current_rate = actor.getPlayRate(anim_name)
actor.setPlayRate(current_rate * 0.25, anim_name)
elif lod_level == 3:
# 最低质量 - 大幅降低播放速度
for anim_name in actor.getAnimNames():
current_rate = actor.getPlayRate(anim_name)
actor.setPlayRate(current_rate * 0.1, anim_name)
print(f"Actor {actor_id} LOD级别设置为: {lod_level}")
def set_actor_importance(self, actor_id, importance):
"""
设置Actor重要性影响LOD计算
:param actor_id: Actor ID
:param importance: 重要性 (0.0 - 1.0)
"""
# 重要性高的Actor会获得更好的LOD级别
# 这里可以实现更复杂的逻辑
pass
def update_performance_stats(self, current_fps, frame_time):
"""
更新性能统计
:param current_fps: 当前FPS
:param frame_time: 帧时间
"""
self.performance_monitor['last_fps'] = current_fps
self.performance_monitor['frame_times'].append(frame_time)
# 保持最近30帧的统计数据
if len(self.performance_monitor['frame_times']) > 30:
self.performance_monitor['frame_times'] = self.performance_monitor['frame_times'][-30:]
def get_lod_stats(self):
"""
获取LOD统计信息
:return: 统计信息
"""
lod_counts = {}
for lod_level in self.actor_lod_levels.values():
if lod_level not in lod_counts:
lod_counts[lod_level] = 0
lod_counts[lod_level] += 1
return {
'actor_lod_distribution': lod_counts,
'current_fps': self.performance_monitor['last_fps'],
'average_frame_time': sum(self.performance_monitor['frame_times']) / len(self.performance_monitor['frame_times'])
if self.performance_monitor['frame_times'] else 0
}
# 使用示例
def example_compression_usage(actor):
"""
动画压缩使用示例
"""
print("=== 动画压缩使用示例 ===")
compressor = AnimationCompression()
# 压缩所有动画
for anim_name in actor.getAnimNames():
print(f"压缩动画: {anim_name}")
compressed_data = compressor.compress_animation(actor, anim_name)
if compressed_data:
print(f" 压缩成功: {len(compressed_data['channels'])} 个骨骼通道")
print("动画压缩示例完成")
def example_caching_usage(world):
"""
动画缓存使用示例
"""
print("=== 动画缓存使用示例 ===")
cache = AnimationCaching(max_cache_size=50)
# 模拟缓存一些动画数据
dummy_data = {'frames': 100, 'channels': 20}
cache.cache_animation("actor_1", "walk", dummy_data)
cache.cache_animation("actor_1", "run", dummy_data)
cache.cache_animation("actor_2", "idle", dummy_data)
# 尝试获取缓存数据
data = cache.get_cached_animation("actor_1", "walk")
if data:
print(" 成功获取缓存数据")
# 查看缓存统计
stats = cache.get_cache_stats()
print(f" 缓存统计: {stats}")
print("动画缓存示例完成")
def example_lod_usage(world):
"""
动画LOD使用示例
"""
print("=== 动画LOD使用示例 ===")
lod_system = AnimationLOD(world)
# 模拟更新LOD级别
camera_pos = Point3(0, 0, 0)
lod_system.update_lod_levels(camera_pos)
# 更新性能统计
lod_system.update_performance_stats(45, 0.022) # 45 FPS, 22ms per frame
# 获取LOD统计
stats = lod_system.get_lod_stats()
print(f" LOD统计: {stats}")
print("动画LOD示例完成")
if __name__ == "__main__":
print("动画优化和压缩模块加载完成")