EG/plugins/user/terrain_editor/io/terrain_io.py
2025-10-30 11:46:41 +08:00

494 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.

"""
地形导入导出模块
提供完整的地形数据导入导出功能,支持多种格式和批量操作
"""
import os
import json
import math
import numpy as np
from datetime import datetime
from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, PNMFileType
class TerrainIO:
"""
地形导入导出类
提供完整的地形数据导入导出功能
"""
def __init__(self, world):
self.world = world
self.supported_formats = ['.png', '.jpg', '.jpeg', '.bmp', '.tga', '.raw', '.ter']
def import_heightmap(self, heightmap_path, scale=(1, 1, 1), resolution=None):
"""
从高度图导入地形
支持多种图像格式的高度图
"""
if not os.path.exists(heightmap_path):
print(f"高度图文件不存在: {heightmap_path}")
return None
try:
# 检查文件格式
file_ext = os.path.splitext(heightmap_path)[1].lower()
if file_ext not in self.supported_formats:
print(f"不支持的高度图格式: {file_ext}")
return None
# 使用现有的地形管理器创建地形
if hasattr(self.world, 'terrain_manager'):
terrain = self.world.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale)
return terrain
else:
print("地形管理器不可用")
return None
except Exception as e:
print(f"导入高度图时出错: {e}")
return None
def export_heightmap(self, terrain_info, output_path, format='png', bit_depth=8):
"""
导出地形为高度图
支持多种格式和位深度
"""
if not terrain_info:
print("地形信息无效")
return False
try:
# 从地形信息中获取高度图数据
if 'heightfield' in terrain_info:
heightfield = terrain_info['heightfield']
if heightfield:
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 根据位深度调整高度图
if bit_depth == 16:
# 转换为16位
temp_image = PNMImage(heightfield.getXSize(), heightfield.getYSize(), 1) # 灰度图
temp_image.setMaxval(65535) # 16位最大值
for x in range(heightfield.getXSize()):
for y in range(heightfield.getYSize()):
# 将0-1范围的值转换为0-65535范围
value = int(heightfield.getRed(x, y) * 65535)
temp_image.setGray(x, y, value)
heightfield = temp_image
# 获取文件扩展名
file_ext = os.path.splitext(output_path)[1].lower()
if not file_ext:
output_path += f".{format}"
# 写入高度图文件
success = heightfield.write(Filename.fromOsSpecific(output_path))
if success:
print(f"高度图导出成功: {output_path} (位深度: {bit_depth})")
return True
else:
print(f"高度图导出失败: {output_path}")
return False
else:
print("地形中没有高度图数据")
return False
else:
print("地形信息中没有高度图数据")
return False
except Exception as e:
print(f"导出高度图时出错: {e}")
return False
def import_raw_heightmap(self, raw_path, width, height, bit_depth=8, scale=(1, 1, 1)):
"""
导入RAW格式的高度图
"""
try:
if not os.path.exists(raw_path):
print(f"RAW文件不存在: {raw_path}")
return None
# 读取RAW数据
with open(raw_path, 'rb') as f:
if bit_depth == 8:
data = np.fromfile(f, dtype=np.uint8)
elif bit_depth == 16:
data = np.fromfile(f, dtype=np.uint16)
else:
print(f"不支持的位深度: {bit_depth}")
return None
# 检查数据大小
if len(data) != width * height:
print(f"RAW数据大小不匹配: 期望 {width * height}, 实际 {len(data)}")
return None
# 创建PNM图像
heightfield = PNMImage(width, height)
# 填充高度数据
if bit_depth == 8:
max_val = 255.0
else: # 16位
max_val = 65535.0
for y in range(height):
for x in range(width):
# RAW文件通常是行优先存储
index = y * width + x
value = data[index] / max_val
heightfield.setRed(x, y, value)
heightfield.setGreen(x, y, value)
heightfield.setBlue(x, y, value)
# 保存到临时文件
temp_path = f"/tmp/temp_heightmap_{int(datetime.now().timestamp())}.png"
heightfield.write(Filename.fromOsSpecific(temp_path))
# 使用现有方法创建地形
return self.import_heightmap(temp_path, scale)
except Exception as e:
print(f"导入RAW高度图时出错: {e}")
return None
def export_raw_heightmap(self, terrain_info, output_path, bit_depth=16):
"""
导出地形为RAW格式的高度图
"""
if not terrain_info or 'heightfield' not in terrain_info:
print("地形信息无效或没有高度图数据")
return False
try:
heightfield = terrain_info['heightfield']
width = heightfield.getXSize()
height = heightfield.getYSize()
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 创建数据数组
if bit_depth == 8:
data = np.zeros(width * height, dtype=np.uint8)
max_val = 255.0
elif bit_depth == 16:
data = np.zeros(width * height, dtype=np.uint16)
max_val = 65535.0
else:
print(f"不支持的位深度: {bit_depth}")
return False
# 填充数据
for y in range(height):
for x in range(width):
value = heightfield.getRed(x, y) * max_val
index = y * width + x
data[index] = int(value)
# 写入RAW文件
with open(output_path, 'wb') as f:
data.tofile(f)
print(f"RAW高度图导出成功: {output_path} ({width}x{height}, {bit_depth}位)")
return True
except Exception as e:
print(f"导出RAW高度图时出错: {e}")
return False
def import_terrain_data(self, terrain_data_path):
"""
导入地形数据文件JSON格式
"""
try:
if not os.path.exists(terrain_data_path):
print(f"地形数据文件不存在: {terrain_data_path}")
return None
with open(terrain_data_path, 'r', encoding='utf-8') as f:
terrain_data = json.load(f)
# 解析地形数据并创建地形
terrain_type = terrain_data.get('type', 'heightmap')
if terrain_type == 'heightmap' and 'heightmap_path' in terrain_data:
heightmap_path = terrain_data['heightmap_path']
# 检查高度图路径是否为相对路径
if not os.path.isabs(heightmap_path):
# 转换为绝对路径
data_dir = os.path.dirname(terrain_data_path)
heightmap_path = os.path.join(data_dir, heightmap_path)
scale = tuple(terrain_data.get('scale', (1, 1, 1)))
resolution = terrain_data.get('resolution')
return self.import_heightmap(heightmap_path, scale, resolution)
elif terrain_type == 'procedural':
# 创建程序化地形
return self._create_procedural_terrain(terrain_data)
else:
print(f"不支持的地形类型: {terrain_type}")
return None
except Exception as e:
print(f"导入地形数据时出错: {e}")
return None
def export_terrain_data(self, terrain_info, output_path, include_heightmap=True):
"""
导出地形数据文件JSON格式
"""
if not terrain_info:
print("地形信息无效")
return False
try:
# 收集地形信息
terrain_data = {
'name': terrain_info.get('name', f'terrain_{int(datetime.now().timestamp())}'),
'scale': terrain_info.get('scale', (1, 1, 1)),
'type': 'heightmap' if terrain_info.get('heightmap') else 'procedural',
'created_at': datetime.now().isoformat(),
'resolution': {
'width': terrain_info['heightfield'].getXSize() if terrain_info.get('heightfield') else 0,
'height': terrain_info['heightfield'].getYSize() if terrain_info.get('heightfield') else 0
},
'metadata': terrain_info.get('metadata', {})
}
# 添加高度图信息
if terrain_info.get('heightmap'):
heightmap_path = terrain_info['heightmap']
terrain_data['heightmap_path'] = os.path.basename(heightmap_path)
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 写入地形数据文件
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(terrain_data, f, indent=2, ensure_ascii=False)
print(f"地形数据导出成功: {output_path}")
# 如果需要,也导出高度图
if include_heightmap and terrain_info.get('heightmap'):
heightmap_output = output_path.replace('.json', '_heightmap.png')
return self.export_heightmap(terrain_info, heightmap_output)
return True
except Exception as e:
print(f"导出地形数据时出错: {e}")
return False
def batch_import(self, import_configs):
"""
批量导入地形
import_configs: 导入配置列表,每个配置包含路径和参数
"""
imported_terrains = []
for config in import_configs:
path = config.get('path')
if not path:
continue
# 根据文件扩展名确定导入方法
file_ext = os.path.splitext(path)[1].lower()
if file_ext in ['.json']:
# 导入地形数据文件
terrain = self.import_terrain_data(path)
elif file_ext in ['.raw']:
# 导入RAW格式
width = config.get('width', 129)
height = config.get('height', 129)
bit_depth = config.get('bit_depth', 8)
scale = config.get('scale', (1, 1, 1))
terrain = self.import_raw_heightmap(path, width, height, bit_depth, scale)
else:
# 导入图像格式高度图
scale = config.get('scale', (1, 1, 1))
resolution = config.get('resolution')
terrain = self.import_heightmap(path, scale, resolution)
if terrain:
imported_terrains.append(terrain)
print(f"批量导入完成,成功导入 {len(imported_terrains)} 个地形")
return imported_terrains
def batch_export(self, terrain_infos, output_dir, export_format='json', include_heightmap=True):
"""
批量导出地形
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
exported_count = 0
for i, terrain_info in enumerate(terrain_infos):
if export_format.lower() == 'json':
output_path = os.path.join(output_dir, f"terrain_{i}_data.json")
if self.export_terrain_data(terrain_info, output_path, include_heightmap):
exported_count += 1
else:
# 导出为高度图
output_path = os.path.join(output_dir, f"terrain_{i}_heightmap.png")
if self.export_heightmap(terrain_info, output_path):
exported_count += 1
print(f"批量导出完成,成功导出 {exported_count} 个地形")
return exported_count
def _create_procedural_terrain(self, terrain_data):
"""
创建程序化地形
"""
try:
if not hasattr(self.world, 'terrain_manager'):
print("地形管理器不可用")
return None
# 获取参数
scale = tuple(terrain_data.get('scale', (1, 1, 1)))
resolution = terrain_data.get('resolution', 129)
algorithm = terrain_data.get('algorithm', 'perlin')
# 创建平面地形作为基础
terrain = self.world.terrain_manager.createFlatTerrain(
size=(scale[0], scale[1]),
resolution=resolution
)
if terrain:
# 应用程序化算法修改高度
self._apply_procedural_algorithm(terrain, algorithm, terrain_data)
return terrain
except Exception as e:
print(f"创建程序化地形时出错: {e}")
return None
def _apply_procedural_algorithm(self, terrain, algorithm, params):
"""
应用程序化算法生成地形
"""
print(f"应用程序化算法: {algorithm}")
# 这里可以实现各种程序化地形生成算法
# 如Perlin噪声、Voronoi图、分形等
def import_terragen_terrain(self, ter_path, scale=(1, 1, 1)):
"""
导入Terragen格式的地形文件
"""
try:
if not os.path.exists(ter_path):
print(f"Terragen文件不存在: {ter_path}")
return None
# 解析Terragen文件格式
terrain_data = self._parse_terragen_file(ter_path)
if not terrain_data:
return None
# 创建高度图
width = terrain_data['width']
height = terrain_data['height']
heightfield = PNMImage(width, height)
# 填充高度数据
heights = terrain_data['heights']
for y in range(height):
for x in range(width):
index = y * width + x
if index < len(heights):
# 将高度值归一化到0-1范围
normalized_height = (heights[index] - terrain_data['min_height']) / (terrain_data['max_height'] - terrain_data['min_height'])
heightfield.setRed(x, y, normalized_height)
heightfield.setGreen(x, y, normalized_height)
heightfield.setBlue(x, y, normalized_height)
# 保存到临时文件
temp_path = f"/tmp/terragen_heightmap_{int(datetime.now().timestamp())}.png"
heightfield.write(Filename.fromOsSpecific(temp_path))
# 使用现有方法创建地形
return self.import_heightmap(temp_path, scale)
except Exception as e:
print(f"导入Terragen地形时出错: {e}")
return None
def _parse_terragen_file(self, ter_path):
"""
解析Terragen文件
"""
try:
with open(ter_path, 'rb') as f:
# 读取文件头
header = f.read(16)
if header[:8] != b'TERRAGEN':
print("无效的Terragen文件")
return None
# 解析版本和参数
version = int.from_bytes(header[8:12], byteorder='little')
width = int.from_bytes(header[12:16], byteorder='little')
# 读取高度数据
heights = []
min_height = float('inf')
max_height = float('-inf')
while True:
chunk_header = f.read(4)
if not chunk_header:
break
chunk_type = chunk_header
chunk_size = int.from_bytes(f.read(4), byteorder='little')
if chunk_type == b'ALTW':
# 高度数据块
for _ in range(chunk_size // 2):
height = int.from_bytes(f.read(2), byteorder='little', signed=True)
heights.append(height)
min_height = min(min_height, height)
max_height = max(max_height, height)
else:
# 跳过其他块
f.seek(chunk_size, 1)
return {
'width': width,
'height': width, # Terragen通常是方形
'heights': heights,
'min_height': min_height,
'max_height': max_height
}
except Exception as e:
print(f"解析Terragen文件时出错: {e}")
return None
def export_terrain_mesh(self, terrain_info, output_path, format='obj'):
"""
导出地形为网格文件OBJ等格式
"""
print(f"导出地形网格为 {format} 格式")
# 这里可以实现将地形导出为网格文件的功能
return False