600 lines
21 KiB
Python
600 lines
21 KiB
Python
"""
|
||
骨骼动画系统插件 - 动画优化和压缩模块
|
||
提供动画压缩、优化、缓存等功能
|
||
"""
|
||
|
||
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("动画优化和压缩模块加载完成") |