EG/core/terrain_manager.py
Hector 0ec780792e Merge remote-tracking branch 'origin/main_ch_eg' into addRender
# Conflicts:
#	RenderPipelineFile/config/daytime.yaml
#	scene/scene_manager.py
#	ui/main_window.py
2025-09-15 16:56:28 +08:00

661 lines
27 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.

# 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()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
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)
print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
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对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {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()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
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_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)
print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
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对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {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())
# terrain_node.removeNode()
#
# # 从列表中移除
# self.terrains.remove(terrain_info)
# # 更新场景树
# if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
# self.world.scene_manager.updateSceneTree()
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