EG/core/terrain_manager.py
Hector 78dae899cf 3d_image和2d_image
根据高度图创建地形
2025-08-28 15:45:32 +08:00

473 lines
19 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
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
class TerrainManager:
"""地形管理类"""
def __init__(self, world):
self.world = world
self.terrains = []
# core/terrain_manager.py
def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
"""从高度图创建地形"""
# 检查文件是否存在
if not os.path.exists(heightmap_path):
print(f"错误: 高度图文件 {heightmap_path} 不存在")
return None
try:
# 创建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
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(self.world.render)
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)
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功从 {heightmap_path} 创建地形")
return terrain_info
except Exception as e:
print(f"创建地形时出错: {e}")
import traceback
traceback.print_exc()
return None
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
"""创建平面地形"""
try:
# 确保分辨率是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
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(self.world.render)
# 为地形添加碰撞体
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)
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}")
return terrain_info
except Exception as e:
print(f"创建平面地形时出错: {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"✓ 重新设置了地形碰撞体")
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
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():
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 = 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