# core/terrain_manager.py import time import urllib from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight import os from scene import util class TerrainManager: """地形管理类""" def __init__(self, world): self.world = world self.terrains = [] # core/terrain_manager.py def _get_tree_widget(self): """安全获取树形控件""" try: if (hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget')): return self.world.interface_manager.treeWidget except AttributeError: pass return None def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): """从高度图创建地形""" # 检查文件是否存在 if not os.path.exists(heightmap_path): print(f"错误: 高度图文件 {heightmap_path} 不存在") return None try: print(f"🔆 开始创建高度图地形") # 获取树形控件 tree_widget = self._get_tree_widget() target_parents = None if tree_widget: target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: if hasattr(self.world, 'render'): target_parents = [(None, self.world.render)] else: print("❌ 没有找到有效的父节点") return None created_terrains = [] try: parent_item, parent_node = target_parents[0] # 创建GeoMipTerrain对象 terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" terrain = GeoMipTerrain(terrain_name) # 加载高度图 height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) # 检查并调整图像尺寸为2的幂次方加1 width, height = height_image.getXSize(), height_image.getYSize() print(f"原始图像尺寸: {width}x{height}") # 找到最接近的有效尺寸 valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] target_size = 129 # 默认尺寸 # 选择最接近的尺寸 max_dim = max(width, height) for size in valid_sizes: if size >= max_dim: target_size = size break else: target_size = valid_sizes[-1] # 使用最大尺寸 # 如果需要,调整图像尺寸 if width != target_size or height != target_size: print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") # 使用正确的图像缩放方法 resized_image = PNMImage(target_size, target_size) resized_image.quickFilterFrom(height_image) height_image = resized_image # 使用正确的方法设置高度图 terrain.setHeightfield(height_image) # 设置地形参数 terrain.setBruteforce(True) # 使用LOD terrain.setBlockSize(32) # terrain.setNearFarThreshold(50.0,200.0) # 生成地形 terrain.generate() # 获取地形节点 terrain_node = terrain.getRoot() if terrain_node.isEmpty(): print("错误:无法生成有效的地形节点") return None terrain_node.setTag("is_scene_element", "1") terrain_node.setTag("tree_item_type", "TERRAIN_NODE") node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" terrain_node.setName(node_name) center_offset = (target_size - 1) / 2 terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) # 设置缩放 terrain_node.setScale(scale[0], scale[1], scale[2]) # 将地形添加到场景中 terrain_node.reparentTo(parent_node) from panda3d.core import BitMask32 # 设置地形节点的碰撞掩码 terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 # 为地形的所有子节点也设置碰撞掩码 for child in terrain_node.getChildren(): child.setCollideMask(BitMask32.bit(2)) # 添加材质 self._applyTerrainMaterial(terrain_node) terrain_node.setPythonTag("selectable", True) # 保存地形信息(包括高度图的副本) terrain_info = { 'terrain': terrain, 'node': terrain_node, 'heightmap': heightmap_path, 'heightfield': height_image, # 保存高度图副本 'scale': scale, 'name': node_name } self.terrains.append(terrain_info) parent_name = parent_item.text(0) if parent_item else "root" print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}") # 在Qt树形控件中添加对应节点 qt_item = None if tree_widget and parent_item: qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") if qt_item: created_terrains.append((terrain_node, qt_item)) else: created_terrains.append((terrain_node, None)) print("⚠️ Qt树节点添加跳过或失败,但Panda3D对象已创建") # 添加到场景管理器(ImGui环境使用) if hasattr(self.world, 'scene_manager') and self.world.scene_manager: if not hasattr(self.world.scene_manager, 'models'): self.world.scene_manager.models = [] if terrain_node not in self.world.scene_manager.models: self.world.scene_manager.models.append(terrain_node) # 更新场景树 if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() except Exception as e: parent_name = parent_item.text(0) if parent_item else "root" print(f"❌ 为 {parent_name} 创建高度图地形失败: {str(e)}") return None # 处理创建结果 if not created_terrains: print("❌ 没有成功创建任何高度图地形") return None # 选中最后创建的光源 if created_terrains: last_light_np, last_qt_item = created_terrains[-1] if last_qt_item: tree_widget.setCurrentItem(last_qt_item) # 更新选择和属性面板 tree_widget.update_selection_and_properties(last_light_np, last_qt_item) print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形") # 返回值处理 if len(created_terrains) == 1: return created_terrains[0][0] # 单个光源返回NodePath else: return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: print(f"❌ 创建高度图地形过程失败: {str(e)}") import traceback traceback.print_exc() return None def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): """创建平面地形""" try: print(f"🔆 开始创建平面地形") # 获取树形控件 tree_widget = self._get_tree_widget() target_parents = None if tree_widget: target_parents = tree_widget.get_target_parents_for_creation() if not target_parents: if hasattr(self.world, 'render'): target_parents = [(None, self.world.render)] else: print("❌ 没有找到有效的父节点") return None created_terrains = [] try: parent_item, parent_node = target_parents[0] # 确保分辨率是2的幂次方加1 (如129, 257, 513等) valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] if resolution not in valid_resolutions: # 找到最接近的有效分辨率 closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") resolution = closest_res # 创建GeoMipTerrain对象 terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" terrain = GeoMipTerrain(terrain_name) # 创建一个平面高度图,尺寸必须是2^n+1 height_image = PNMImage(resolution, resolution) height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) # 使用正确的方法设置高度图 terrain.setHeightfield(height_image) terrain.setBruteforce(True) # 设置LOD # 设置地形参数 terrain.setBlockSize(32) # 设置块大小 # 生成地形 terrain.generate() # 获取地形节点 terrain_node = terrain.getRoot() if terrain_node.isEmpty(): print("错误:无法生成有效的平面地形节点") return None terrain_node.setTag("is_scene_element", "1") terrain_node.setTag("tree_item_type", "TERRAIN_NODE") node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" terrain_node.setName(node_name) center_offset = (resolution - 1) / 2 terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) # 将地形添加到场景中 terrain_node.reparentTo(parent_node) # 为地形添加碰撞体 from panda3d.core import BitMask32 # 设置地形节点的碰撞掩码 terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 # 为地形的所有子节点也设置碰撞掩码 for child in terrain_node.getChildren(): child.setCollideMask(BitMask32.bit(2)) # 添加材质 self._applyTerrainMaterial(terrain_node) terrain_node.setPythonTag("selectable", True) # 保存地形信息(包括高度图) terrain_info = { 'terrain': terrain, 'node': terrain_node, 'heightmap': None, 'heightfield': height_image, # 保存高度图 'scale': (size[0], size[1], 50), 'name': node_name } self.terrains.append(terrain_info) parent_name = parent_item.text(0) if parent_item else "root" print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}") # 在Qt树形控件中添加对应节点 qt_item = None if tree_widget and parent_item: qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") if qt_item: created_terrains.append((terrain_node, qt_item)) else: created_terrains.append((terrain_node, None)) print("⚠️ Qt树节点添加跳过或失败,但Panda3D对象已创建") # 添加到场景管理器(ImGui环境使用) if hasattr(self.world, 'scene_manager') and self.world.scene_manager: if not hasattr(self.world.scene_manager, 'models'): self.world.scene_manager.models = [] if terrain_node not in self.world.scene_manager.models: self.world.scene_manager.models.append(terrain_node) # 更新场景树 if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() except Exception as e: parent_name = parent_item.text(0) if parent_item else "root" print(f"❌ 为 {parent_name} 创建平面地形失败: {str(e)}") return None # 处理创建结果 if not created_terrains: print("❌ 没有成功创建任何平面地形") return None # 选中最后创建的光源 if created_terrains: last_light_np, last_qt_item = created_terrains[-1] if last_qt_item: tree_widget.setCurrentItem(last_qt_item) # 更新选择和属性面板 tree_widget.update_selection_and_properties(last_light_np, last_qt_item) print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形") # 返回值处理 if len(created_terrains) == 1: return created_terrains[0][0] # 单个光源返回NodePath else: return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: print(f"❌ 创建平面地形过程失败: {str(e)}") import traceback traceback.print_exc() return None def _applyTerrainMaterial(self, terrain_node): """为地形应用材质""" try: # 创建材质 material = Material() material.setAmbient((0.5, 0.5, 0.5, 1.0)) # 环境光反射 material.setDiffuse((1, 1, 1, 1.0)) # 漫反射(绿色调) material.setSpecular((0.2, 0.2, 0.2, 1.0)) # 镜面反射 material.setShininess(5.0) # 高光强度 # 应用材质到地形 terrain_node.setMaterial(material) # 启用材质 terrain_node.setAttrib(ColorAttrib.makeDefault()) print("✓ 地形材质已应用") except Exception as e: print(f"应用地形材质时出错: {e}") def updateTerrain(self): """更新所有地形的LOD""" for terrain_info in self.terrains: terrain = terrain_info['terrain'] if hasattr(terrain, 'update'): terrain.update() def setTerrainLODThreshold(self, terrain_info, threshold): """设置地形LOD阈值""" if terrain_info in self.terrains: terrain = terrain_info['terrain'] # GeoMipTerrain可能没有setNearFarThreshold方法,使用其他方式设置LOD print("注意: GeoMipTerrain可能不支持直接设置LOD阈值") def getTerrainHeight(self, terrain_info, x, y): """获取地形上指定点的高度""" if terrain_info in self.terrains: terrain = terrain_info['terrain'] if hasattr(terrain, 'getElevation'): return terrain.getElevation(x, y) return 0 # core/terrain_manager.py def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): """修改地形高度""" if terrain_info not in self.terrains: return False try: terrain = terrain_info['terrain'] terrain_node = terrain_info['node'] # 获取地形节点 # 直接使用我们保存的高度图数据 if 'heightfield' not in terrain_info: print("地形信息中没有保存高度图数据") return False heightmap = terrain_info['heightfield'] if not heightmap: print("无法获取地形高度图数据") return False # 获取高度图尺寸 width = heightmap.getXSize() height = heightmap.getYSize() # 获取地形节点信息 terrain_pos = terrain_node.getPos() terrain_scale = terrain_node.getScale() # 正确的坐标转换方法 # 1. 将世界坐标转换为相对于地形中心的坐标 relative_x = x - terrain_pos.getX() relative_y = y - terrain_pos.getY() # 2. 考虑地形的缩放 scaled_x = relative_x / terrain_scale.getX() scaled_y = relative_y / terrain_scale.getY() # 3. 转换为高度图坐标 center_offset = (width - 1) / 2 center_x = int(scaled_x + center_offset) center_y = int(scaled_y + center_offset) print(f"坐标转换详情:") print(f" 世界坐标: ({x:.2f}, {y:.2f})") print(f" 地形位置: ({terrain_pos.getX():.2f}, {terrain_pos.getY():.2f})") print(f" 地形缩放: ({terrain_scale.getX():.2f}, {terrain_scale.getY():.2f})") print(f" 相对坐标: ({relative_x:.2f}, {relative_y:.2f})") print(f" 缩放坐标: ({scaled_x:.2f}, {scaled_y:.2f})") print(f" 中心偏移: {center_offset}") print(f" 高度图坐标: ({center_x}, {center_y})") print(f" 高度图尺寸: {width} x {height}") # 检查中心点是否在有效范围内 if not (0 <= center_x < width and 0 <= center_y < height): print(f"❌ 中心点坐标超出范围: ({center_x}, {center_y})") print(f"❌ 有效范围: (0-{width - 1}, 0-{height - 1})") return False # 修改半径范围内的点 modified = False # 使用正确的半径计算(考虑地形缩放) effective_radius = max(1, int(radius / max(terrain_scale.getX(), terrain_scale.getY()))) print(f"有效编辑半径: {effective_radius}") # 限制编辑半径不超过地形尺寸的1/4,避免性能问题 effective_radius = min(effective_radius, width // 4, height // 4) for dx in range(-effective_radius, effective_radius + 1): for dy in range(-effective_radius, effective_radius + 1): # 检查距离 distance = (dx * dx + dy * dy) ** 0.5 if distance <= effective_radius: # 计算影响权重 weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 # 计算目标点坐标 target_x = center_x + dx target_y = center_y + dy # 检查边界 if 0 <= target_x < width and 0 <= target_y < height: # 获取当前高度 current_height = heightmap.getRed(target_x, target_y) # 根据操作类型修改高度 if operation == "add": new_height = min(1.0, current_height + strength * weight * 0.01) elif operation == "subtract": new_height = max(0.0, current_height - strength * weight * 0.01) elif operation == "set": # 平坦化操作,设置为中等高度 new_height = 0.5 else: new_height = current_height # 设置新高度 heightmap.setRed(target_x, target_y, new_height) heightmap.setGreen(target_x, target_y, new_height) heightmap.setBlue(target_x, target_y, new_height) modified = True # 如果有修改,则重新生成地形 if modified: # 重新设置高度图并生成地形 terrain.setHeightfield(heightmap) terrain.generate() # 关键修复:重新创建碰撞体,因为在生成地形时会丢失原有碰撞体 from panda3d.core import BitMask32, CollisionNode, CollisionPlane, Plane, Point3 # 移除旧的地形碰撞体(如果存在) for child in terrain_node.getChildren(): if isinstance(child.node(), CollisionNode) and child.getName().startswith("terrain_collision_"): child.removeNode() # 创建新的地形碰撞体 # 使用更简单的方法:直接给地形节点添加碰撞体 terrain_node.setCollideMask(BitMask32.bit(2)) # 确保地形节点本身具有碰撞掩码 # 为地形的每个子节点也设置碰撞掩码 for child in terrain_node.getChildren(): child.setCollideMask(BitMask32.bit(2)) print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") print(f"✓ 重新设置了地形碰撞体") return modified except Exception as e: print(f"修改地形高度时出错: {e}") import traceback traceback.print_exc() return False def deleteTerrain(self, terrain_info): """删除地形""" if terrain_info in self.terrains: # 从场景中移除地形节点 terrain_node = terrain_info['node'] if terrain_node and not terrain_node.isEmpty(): tree_widget = self._get_tree_widget() if tree_widget: tree_widget.delete_items(tree_widget.selectedItems()) else: terrain_node.removeNode() # 从列表中移除 self.terrains.remove(terrain_info) # 更新场景树 (如果是使用了ImGui界面或兼容界面) if hasattr(self.world, 'scene_manager'): if hasattr(self.world.scene_manager, 'updateSceneTree'): self.world.scene_manager.updateSceneTree() if hasattr(self.world.scene_manager, 'models') and terrain_node in self.world.scene_manager.models: self.world.scene_manager.models.remove(terrain_node) print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") import gc gc.collect() return True return False def getTerrainByName(self, name): """根据名称查找地形""" for terrain_info in self.terrains: if terrain_info.get('name') == name: return terrain_info return None def getAllTerrainNames(self): """获取所有地形名称""" return [terrain_info.get('name', f'Terrain_{i}') for i, terrain_info in enumerate(self.terrains)] def setTerrainColor(self, terrain_info, color): """设置地形颜色""" try: if terrain_info in self.terrains: terrain_node = terrain_info['node'] if terrain_node: # 创建新材质 material = Material() material.setAmbient((color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, 1.0)) material.setDiffuse((color[0], color[1], color[2], 1.0)) material.setSpecular((0.2, 0.2, 0.2, 1.0)) material.setShininess(5.0) # 应用材质 terrain_node.setMaterial(material) print(f"✓ 地形颜色已更新: {color}") return True return False except Exception as e: print(f"设置地形颜色时出错: {e}") return False def setTerrainTexture(self, terrain_info, texture_path): """为地形设置纹理""" try: if terrain_info in self.terrains: terrain_node = terrain_info['node'] if terrain_node and os.path.exists(texture_path): texture_path = util.normalize_model_path(texture_path) # 加载纹理 texture = self.world.loader.loadTexture(texture_path) if texture: # 应用纹理 terrain_node.setTexture(texture, 1) print(f"✓ 地形纹理已应用: {texture_path}") return True else: print(f"✗ 无法加载纹理: {texture_path}") else: print(f"✗ 纹理文件不存在: {texture_path}") return False except Exception as e: print(f"应用地形纹理时出错: {e}") return False def saveTerrainData(self, terrain_info, filename): """保存地形数据到文件""" try: terrain_node = terrain_info['node'] heightfield = terrain_info['heightfield'] # 保存高度图到文件 if heightfield: # 创建保存路径 terrain_dir = os.path.join(os.path.dirname(filename), "terrains") if not os.path.exists(terrain_dir): os.makedirs(terrain_dir) # 生成唯一的地形文件名 terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png" terrain_path = os.path.join(terrain_dir, terrain_filename) # 保存高度图 heightfield.write(Filename.fromOsSpecific(terrain_path)) # 保存地形信息到标签 terrain_node.setTag("terrain_heightmap_path", terrain_path) terrain_node.setTag("terrain_scale", str(terrain_info['scale'])) print(f"✓ 地形数据已保存: {terrain_path}") return terrain_path return None except Exception as e: print(f"保存地形数据时出错: {e}") return None def _recreateTerrain(self, terrain_node): """重新创建地形""" try: print(f"重新创建地形: {terrain_node.getName()}") # 获取保存的地形信息 heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag( "terrain_heightmap_path") else None scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)" # 解析缩放信息 try: scale_str = scale_str.strip("()") scale_values = [float(x.strip()) for x in scale_str.split(",")] scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1) except: scale = (1, 1, 1) # 恢复位置信息 if terrain_node.hasTag("transform_pos"): try: pos_str = terrain_node.getTag("transform_pos") pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') pos_values = [float(x.strip()) for x in pos_str.split(',')] if len(pos_values) >= 3: terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2]) except Exception as e: print(f"恢复地形位置失败: {e}") # 如果有高度图路径,重新创建地形 if heightmap_path and os.path.exists(heightmap_path): # 使用现有方法重新创建地形 new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale) if new_terrain: # 删除旧的地形节点 if not terrain_node.isEmpty(): terrain_node.removeNode() return new_terrain # 如果没有高度图或创建失败,创建一个平面地形作为替代 print("使用平面地形作为替代") new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129) if new_terrain: # 删除旧的地形节点 if not terrain_node.isEmpty(): terrain_node.removeNode() return new_terrain return terrain_node except Exception as e: print(f"重新创建地形失败: {e}") return terrain_node