1
0
forked from Rowland/EG
EG/core/terrain_manager.py
Hector afb4a4bb50 3d_image和2d_image
根据高度图创建地形
2025-08-27 17:17:54 +08:00

379 lines
14 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
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)}"
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]
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(False) # 使用LOD
# 生成地形
terrain.generate()
# 获取地形节点
terrain_node = terrain.getRoot()
# 设置缩放
terrain_node.setScale(scale[0], scale[1], scale[2])
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
# 为地形节点设置名称,便于在场景树中识别
terrain_node.setName(f"Terrain_{os.path.basename(heightmap_path)}")
from panda3d.core import BitMask32
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 添加材质
self._applyTerrainMaterial(terrain_node)
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': f"Terrain_{os.path.basename(heightmap_path)}"
}
self.terrains.append(terrain_info)
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
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=(100, 100), 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)}"
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(False) # 使用LOD
# 设置地形参数
terrain.setBlockSize(32) # 设置块大小
# 生成地形
terrain.generate()
# 获取地形节点
terrain_node = terrain.getRoot()
# 设置缩放以达到指定大小
terrain_node.setScale(size[0], size[1], 50) # Z轴缩放更大以显示地形起伏
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
# 为地形节点设置名称,便于在场景树中识别
terrain_node.setName(f"FlatTerrain_{len(self.terrains)}")
# 为地形添加碰撞体
from panda3d.core import BitMask32
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 添加材质
self._applyTerrainMaterial(terrain_node)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': f"FlatTerrain_{len(self.terrains)}"
}
self.terrains.append(terrain_info)
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
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.makeDefaultColor())
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']
# 直接使用我们保存的高度图数据
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()
# 转换坐标到高度图空间
center_x = int((x / terrain_info['scale'][0]) * width + width / 2)
center_y = int((y / terrain_info['scale'][1]) * height + height / 2)
# 修改半径范围内的点
modified = False
for dx in range(-int(radius), int(radius) + 1):
for dy in range(-int(radius), int(radius) + 1):
# 检查距离
distance = (dx * dx + dy * dy) ** 0.5
if distance <= radius:
# 计算影响权重
weight = 1.0 - (distance / radius)
# 计算目标点坐标
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 = current_height + strength * weight * 0.1
elif operation == "subtract":
new_height = current_height - strength * weight * 0.1
elif operation == "set":
new_height = strength
else:
new_height = current_height
# 限制高度范围
new_height = max(0.0, min(1.0, new_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()
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}")
# 更新场景树
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:
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')}")
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