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