PyVista战场可视化实战(一):构建3D战场环境的基础

摘要

本文是"PyVista雷达电子对抗战场态势仿真"系列博客的第一篇,将全面介绍如何使用PyVista构建基础的3D战场可视化环境。内容涵盖PyVista核心概念、战场地形创建、作战单位模型设计以及场景集成,同时提供简化版和高保真版两种实现方案。通过完整的代码示例和深入的技术解析,帮助读者快速掌握3D战场可视化的核心技术。

1. 引言:为什么选择PyVista进行战场可视化?

在现代军事仿真和训练系统中,3D战场态势可视化扮演着至关重要的角色。传统的2D平面显示虽然简洁,但无法充分展现战场的高度信息、地形起伏、电磁环境等关键要素。PyVista作为基于VTK的Python可视化库,为战场3D可视化提供了强大的技术支撑。

1.1 PyVista在军事仿真中的优势

跨学科融合能力

PyVista的"领域无关"特性使其能够完美适应军事仿真的多学科需求。从地理信息系统(GIS)的地形处理,到计算流体力学(CFD)的电磁场模拟,再到计算机图形学的实时渲染,PyVista提供了统一的解决方案。

性能与易用性的平衡

相较于直接使用VTK的复杂接口,PyVista通过NumPy风格的数组操作和Pythonic的API设计,大幅降低了3D可视化的技术门槛。同时,基于C++的VTK后端保证了渲染性能,能够处理大规模战场数据。

开源生态的丰富资源

作为开源项目,PyVista可以无缝集成Python科学计算生态中的其他工具,如NumPy、SciPy、Pandas等,为战场数据处理和分析提供完整的工作流。

1.2 本系列博客的教学设计理念

双版本代码策略

为满足不同层次读者的需求,本系列采用"简化版+高保真版"的双版本代码设计:

  • 简化版:侧重算法原理和核心功能实现,代码简洁,依赖少,适合快速学习和原型开发

  • 高保真版:注重视觉效果和工程实践,包含优化技巧和高级特性,适合项目应用

渐进式学习路径

从基础的3D场景构建开始,逐步引入雷达探测、电子对抗、电磁态势等高级主题,确保读者能够循序渐进地掌握完整的技术栈。

2. 环境配置与基础概念

2.1 安装与依赖管理

核心依赖包

bash 复制代码
# 基础科学计算栈
pip install numpy scipy matplotlib pandas

# PyVista核心库
pip install pyvista

# 可选:用于地理数据处理的扩展库
pip install rasterio geopandas

# 可选:用于高保真版的高级功能
pip install pyvistaqt imageio

验证安装

bash 复制代码
import pyvista as pv
print(f"PyVista版本: {pv.__version__}")

# 测试基础功能
pl = pv.Plotter()
pl.add_mesh(pv.Sphere())
pl.show()

2.2 PyVista核心概念解析

数据集合(DataSet)体系

PyVista的核心是各种类型的DataSet,它们构成了3D可视化的基础数据结构:

关键概念说明

  • 点(Points):几何体的顶点坐标,N×3的NumPy数组

  • 单元(Cells):连接点构成的基本几何元素(三角形、四边形等)

  • 场数据(Field Data):与点或单元关联的物理量(如高程、温度、应力等)

3. 战场地形可视化

地形是战场环境的基础,准确的地形表示对于军事仿真至关重要。我们将从简化版到高保真版逐步构建地形可视化系统。

3.1 简化版地形:程序化生成

基础高度场生成

python 复制代码
import numpy as np
import pyvista as pv

class SimpleTerrain:
    """简化版地形生成器"""
    
    def __init__(self, size=100, resolution=50):
        self.size = size
        self.resolution = resolution
        self.x = np.linspace(-size/2, size/2, resolution)
        self.y = np.linspace(-size/2, size/2, resolution)
        self.xx, self.yy = np.meshgrid(self.x, self.y)
        
    def generate_hills(self, amplitude=10, frequency=0.1):
        """生成丘陵地形"""
        # 使用正弦函数组合创建自然起伏
        z1 = amplitude * np.sin(frequency * self.xx) * np.sin(frequency * self.yy)
        z2 = 0.5 * amplitude * np.sin(2*frequency * self.xx) * np.cos(2*frequency * self.yy)
        elevation = z1 + z2
        
        # 创建网格
        terrain = pv.StructuredGrid(self.xx, self.yy, elevation)
        terrain["elevation"] = elevation.ravel()
        return terrain
    
    def generate_valley(self, depth=15, width=20):
        """生成山谷地形"""
        # 创建基本平面
        elevation = np.zeros_like(self.xx)
        
        # 添加山谷
        distance_from_center = np.sqrt(self.xx**2 + self.yy**2)
        valley_profile = depth * np.exp(-(distance_from_center**2) / (2 * width**2))
        elevation -= valley_profile
        
        terrain = pv.StructuredGrid(self.xx, self.yy, elevation)
        terrain["elevation"] = elevation.ravel()
        return terrain

地形可视化与纹理映射

python 复制代码
def visualize_simple_terrain():
    """简化版地形可视化"""
    terrain_gen = SimpleTerrain(size=200, resolution=100)
    
    # 生成多种地形
    hills = terrain_gen.generate_hills(amplitude=15, frequency=0.05)
    valley = terrain_gen.generate_valley(depth=20, width=30)
    
    # 创建绘图窗口
    plotter = pv.Plotter(window_size=[1200, 800])
    
    # 添加丘陵地形
    plotter.add_mesh(hills, cmap='terrain', scalars='elevation', 
                    show_edges=False, lighting=True)
    
    # 设置相机位置
    plotter.camera_position = [(300, 300, 100), (0, 0, 0), (0, 0, 1)]
    
    # 添加标尺和背景
    plotter.add_axes()
    plotter.set_background('skyblue', top='white')
    
    plotter.show()

if __name__ == "__main__":
    visualize_simple_terrain()

3.2 高保真版地形:真实DEM数据集成

真实地理数据处理

python 复制代码
import rasterio
from rasterio.transform import from_origin
import pyvista as pv

class HighFidelityTerrain:
    """高保真版地形处理器"""
    
    def __init__(self):
        self.dem_data = None
        self.transform = None
        
    def load_dem_from_file(self, filepath):
        """从GeoTIFF文件加载DEM数据"""
        with rasterio.open(filepath) as dataset:
            self.dem_data = dataset.read(1)
            self.transform = dataset.transform
            
            # 创建坐标网格
            rows, cols = self.dem_data.shape
            x = np.arange(cols) * self.transform[0] + self.transform[2]
            y = np.arange(rows) * self.transform[4] + self.transform[5]
            xx, yy = np.meshgrid(x, y)
            
            return xx, yy, self.dem_data
    
    def create_realistic_terrain(self, dem_file=None, exaggeration=1.0):
        """创建真实感地形"""
        if dem_file:
            # 从文件加载真实DEM数据
            xx, yy, elevation = self.load_dem_from_file(dem_file)
        else:
            # 生成模拟的高分辨率地形
            size = 1000
            resolution = 500
            x = np.linspace(-size/2, size/2, resolution)
            y = np.linspace(-size/2, size/2, resolution)
            xx, yy = np.meshgrid(x, y)
            
            # 使用分形噪声生成更自然的地形
            elevation = self._generate_fractal_terrain(resolution, octaves=6)
            elevation = elevation * 100 * exaggeration  # 高程夸张系数
        
        # 应用高程夸张
        elevation = elevation * exaggeration
        
        # 创建高分辨率网格
        terrain = pv.StructuredGrid(xx, yy, elevation)
        terrain["elevation"] = elevation.ravel()
        
        return terrain
    
    def _generate_fractal_terrain(self, size, octaves=6):
        """生成分形地形噪声"""
        shape = (size, size)
        persistence = 0.5
        lacunarity = 2.0
        
        # 基础噪声层
        base = np.random.rand(*shape)
        terrain = np.zeros(shape)
        
        for octave in range(octaves):
            frequency = lacunarity ** octave
            amplitude = persistence ** octave
            
            # 创建更细密的噪声
            octave_noise = np.zeros(shape)
            for i in range(shape[0]):
                for j in range(shape[1]):
                    x_idx = int(i * frequency) % shape[0]
                    y_idx = int(j * frequency) % shape[1]
                    octave_noise[i, j] = base[x_idx, y_idx]
            
            terrain += octave_noise * amplitude
        
        # 标准化
        terrain = (terrain - terrain.min()) / (terrain.max() - terrain.min())
        return terrain
    
    def add_terrain_textures(self, terrain, texture_type='satellite'):
        """添加地形纹理"""
        if texture_type == 'satellite':
            # 模拟卫星影像纹理
            cmap = 'gist_earth'
        elif texture_type == 'elevation':
            cmap = 'terrain'
        elif texture_type == 'military':
            # 军事地图风格
            cmap = 'viridis'
        else:
            cmap = 'terrain'
            
        return cmap

高级地形渲染技术

python 复制代码
def create_advanced_terrain_scene():
    """创建高级地形场景"""
    terrain_gen = HighFidelityTerrain()
    
    # 生成高细节地形
    terrain = terrain_gen.create_realistic_terrain(exaggeration=1.5)
    
    # 创建高级绘图窗口
    plotter = pv.Plotter(window_size=[1600, 900], 
                         lighting='three lights',
                         multi_samples=4)  # 抗锯齿
    
    # 添加带光照的地形
    cmap = terrain_gen.add_terrain_textures(terrain, 'military')
    terrain_actor = plotter.add_mesh(terrain, cmap=cmap, scalars='elevation',
                                   smooth_shading=True, 
                                   specular=0.3,  # 高光强度
                                   specular_power=20,
                                   show_edges=False)
    
    # 添加等高线
    contours = terrain.contour(10, scalars='elevation')
    plotter.add_mesh(contours, color='black', line_width=2, opacity=0.5)
    
    # 高级相机控制
    plotter.camera_position = [(2000, 2000, 800), (0, 0, 100), (0, 0, 1)]
    plotter.camera_set = True
    
    # 添加指北针和比例尺
    plotter.add_axes_interactive()
    plotter.add_bounds_axes(terrain)
    
    # 设置天空盒背景
    plotter.set_background('linear_gradient', 
                          bottom='#1e3c72', 
                          top='#2a5298')
    
    return plotter, terrain

4. 作战单位模型设计

战场单位的可视化需要兼顾识别性和性能。我们将设计从简化的几何表示到高细节3D模型的全套方案。

4.1 简化版单位模型:参数化几何体

基础单位类设计

python 复制代码
class BattlefieldUnit:
    """战场单位基类"""
    
    def __init__(self, position, unit_id, unit_type):
        self.position = np.array(position, dtype=float)
        self.unit_id = unit_id
        self.unit_type = unit_type
        self.mesh = None
        self.color = None
        self.orientation = 0  # 朝向角度(度)
        
    def create_simplified_geometry(self, scale=1.0):
        """创建简化版几何体(由子类实现)"""
        raise NotImplementedError
        
    def update_position(self, new_position):
        """更新单位位置"""
        if self.mesh is not None:
            # 计算位移向量
            displacement = np.array(new_position) - self.position
            self.mesh.translate(displacement, inplace=True)
        self.position = np.array(new_position)
        
    def set_orientation(self, angle_degrees):
        """设置单位朝向"""
        if self.mesh is not None:
            # 绕Z轴旋转
            self.mesh.rotate_z(angle_degrees - self.orientation, inplace=True)
            self.orientation = angle_degrees

class RadarUnit(BattlefieldUnit):
    """雷达单位"""
    
    def __init__(self, position, unit_id, radar_range=100, beam_width=30):
        super().__init__(position, unit_id, 'radar')
        self.radar_range = radar_range
        self.beam_width = beam_width
        self.color = 'red'
        
    def create_simplified_geometry(self, scale=1.0):
        """创建简化雷达模型"""
        # 雷达基座 - 圆柱体
        base = pv.Cylinder(center=[0, 0, 2], direction=[0, 0, 1], 
                          radius=5*scale, height=4)
        
        # 雷达天线 - 抛物面简化表示
        antenna = pv.Cone(center=[0, 0, 8], direction=[1, 0, 0],
                         height=8*scale, radius=3*scale)
        
        # 合并几何体
        self.mesh = base.boolean_union(antenna)
        self.mesh.translate(self.position, inplace=True)
        
        return self.mesh

class AircraftUnit(BattlefieldUnit):
    """飞机单位"""
    
    def __init__(self, position, unit_id, aircraft_type='fighter'):
        super().__init__(position, unit_id, 'aircraft')
        self.aircraft_type = aircraft_type
        self.color = 'blue'
        self.altitude = position[2] if len(position) > 2 else 50
        
    def create_simplified_geometry(self, scale=1.0):
        """创建简化飞机模型"""
        # 机身 - 椭球体
        fuselage = pv.ParametricEllipsoid(5*scale, 1.5*scale, 1.5*scale)
        
        # 机翼
        wing_points = np.array([[-6, -8, 0], [-6, 8, 0], 
                               [2, 8, 0], [2, -8, 0]])
        wing_faces = np.array([4, 0, 1, 2, 3])
        wing = pv.PolyData(wing_points, wing_faces)
        wing = wing.triangulate().extrude([0, 0, 0.5*scale])
        
        # 垂直尾翼
        tail_points = np.array([[-5, 0, 0], [-7, 0, 2], 
                               [-3, 0, 2], [-5, 0, 4]])
        tail_faces = np.array([4, 0, 1, 2, 3])
        tail = pv.PolyData(tail_points, tail_faces)
        tail = tail.triangulate().extrude([0, 0.5*scale, 0])
        
        # 合并所有部件
        aircraft = fuselage.boolean_union(wing)
        aircraft = aircraft.boolean_union(tail)
        
        # 定位到正确位置和姿态
        aircraft.rotate_x(90, inplace=True)  # 水平飞行姿态
        aircraft.translate([0, 0, self.altitude], inplace=True)
        aircraft.translate(self.position, inplace=True)
        
        self.mesh = aircraft
        return self.mesh

class VehicleUnit(BattlefieldUnit):
    """地面车辆单位"""
    
    def __init__(self, position, unit_id, vehicle_type='apc'):
        super().__init__(position, unit_id, 'vehicle')
        self.vehicle_type = vehicle_type
        self.color = 'green'
        
    def create_simplified_geometry(self, scale=1.0):
        """创建简化车辆模型"""
        # 车体 - 长方体
        chassis = pv.Cube(center=[0, 0, 1.5*scale], 
                         x_length=6*scale, 
                         y_length=3*scale, 
                         z_length=2*scale)
        
        # 炮塔 - 圆柱体
        turret = pv.Cylinder(center=[1.5*scale, 0, 3.5*scale], 
                            direction=[0, 0, 1],
                            radius=1.5*scale, height=2*scale)
        
        # 炮管
        gun = pv.Cylinder(center=[3*scale, 0, 3.5*scale], 
                         direction=[1, 0, 0],
                         radius=0.3*scale, height=4*scale)
        
        # 合并几何体
        vehicle = chassis.boolean_union(turret)
        vehicle = vehicle.boolean_union(gun)
        vehicle.translate(self.position, inplace=True)
        
        self.mesh = vehicle
        return self.mesh

4.2 高保真版单位模型:精细3D建模

高级单位模型类

python 复制代码
class HighFidelityUnit(BattlefieldUnit):
    """高保真单位模型"""
    
    def __init__(self, position, unit_id, unit_type, model_file=None):
        super().__init__(position, unit_id, unit_type)
        self.model_file = model_file
        self.detailed_mesh = None
        self.texture_image = None
        
    def load_external_model(self, filepath):
        """加载外部3D模型文件"""
        try:
            # 支持STL, OBJ, PLY等格式
            mesh = pv.read(filepath)
            self.detailed_mesh = mesh
            return mesh
        except Exception as e:
            print(f"模型加载失败: {e}")
            return self.create_high_detail_geometry()
    
    def create_high_detail_radar(self, scale=1.0):
        """创建高细节雷达模型"""
        # 更精细的雷达塔结构
        tower = pv.Cylinder(center=[0, 0, 10], direction=[0, 0, 1],
                           radius=2*scale, height=20*scale)
        
        # 雷达天线罩
        radome = pv.Sphere(center=[8*scale, 0, 25*scale], radius=4*scale)
        
        # 支撑结构
        support = pv.Cone(center=[4*scale, 0, 20*scale], direction=[1, 0, 0],
                         height=8*scale, radius=0.5*scale)
        
        # 添加更多细节部件
        platform = pv.Cylinder(center=[0, 0, 20*scale], direction=[0, 0, 1],
                              radius=6*scale, height=1*scale)
        
        # 组合所有部件
        radar = tower.boolean_union(platform)
        radar = radar.boolean_union(radome)
        radar = radar.boolean_union(support)
        
        # 应用平滑处理
        radar = radar.smooth(n_iter=10)
        
        return radar
    
    def apply_unit_texture(self, mesh, texture_type):
        """应用单位纹理"""
        # 根据单位类型选择纹理
        textures = {
            'radar': 'gray_metallic',
            'aircraft': 'military_camo',
            'vehicle': 'green_camo'
        }
        
        # 这里可以集成实际纹理映射逻辑
        # 简化版中使用颜色代替
        color_map = {
            'gray_metallic': '#808080',
            'military_camo': '#556B2F', 
            'green_camo': '#006400'
        }
        
        return color_map.get(texture_type, '#FFFFFF')
    
    def add_animated_components(self, mesh):
        """添加动画组件(如雷达旋转)"""
        # 为雷达天线添加旋转属性
        if self.unit_type == 'radar':
            # 标记可旋转部件
            mesh.field_data['rotatable'] = [1]
            mesh.field_data['rotation_axis'] = [0, 0, 1]  # Z轴旋转
            mesh.field_data['rotation_speed'] = [10]  # 度/秒
        return mesh

5. 战场场景集成与管理

将地形和单位整合成完整的战场场景,并实现场景管理功能。

5.1 场景管理器设计

完整的场景管理器类,包含太阳位置计算、单位管理和场景更新功能

python 复制代码
import numpy as np
import pyvista as pv
from datetime import datetime
import math

class BattlefieldScene:
    """战场场景管理器"""
    
    def __init__(self, scene_name="Battlefield", use_high_fidelity=False):
        self.scene_name = scene_name
        self.use_high_fidelity = use_high_fidelity
        self.terrain = None
        self.units = {}  # 单位字典 {unit_id: unit_object}
        self.plotter = None
        self.unit_counter = 0
        
        # 场景参数
        self.scene_bounds = [-1000, 1000, -1000, 1000, -100, 500]
        self.time_of_day = 12.0  # 小时制,用于光照计算
        self.latitude = 40.0  # 纬度(度),用于太阳位置计算
        self.longitude = 116.0  # 经度(度)
        
        # 场景状态
        self.is_initialized = False
        self.current_time = 0.0  # 仿真时间(秒)
        self.time_scale = 1.0  # 时间缩放因子
        
        # 光照系统
        self.lights = []
        
    def _calculate_sun_altitude(self, time_hour, day_of_year=None):
        """
        计算太阳高度角(仰角)
        
        参数:
        time_hour: 小时(0-24)
        day_of_year: 一年中的第几天(1-365),None表示使用当前日期
        
        返回:
        太阳高度角(度),0-90
        """
        if day_of_year is None:
            # 使用当前日期
            day_of_year = datetime.now().timetuple().tm_yday
        
        # 1. 计算太阳赤纬
        declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365))
        
        # 2. 计算时角
        solar_noon = 12.0
        time_diff = time_hour - solar_noon
        hour_angle = 15 * time_diff  # 时角,15度/小时
        
        # 3. 转换为弧度
        lat_rad = math.radians(self.latitude)
        dec_rad = math.radians(declination)
        ha_rad = math.radians(hour_angle)
        
        # 4. 计算太阳高度角
        sin_altitude = (math.sin(lat_rad) * math.sin(dec_rad) + 
                       math.cos(lat_rad) * math.cos(dec_rad) * math.cos(ha_rad))
        
        # 确保在有效范围内
        sin_altitude = max(-1.0, min(1.0, sin_altitude))
        altitude = math.degrees(math.asin(sin_altitude))
        
        return max(0, altitude)  # 返回正值
    
    def _calculate_sun_azimuth(self, time_hour, day_of_year=None):
        """
        计算太阳方位角
        
        参数:
        time_hour: 小时(0-24)
        day_of_year: 一年中的第几天(1-365)
        
        返回:
        太阳方位角(度),0-360(正北为0,顺时针增加)
        """
        if day_of_year is None:
            day_of_year = datetime.now().timetuple().tm_yday
        
        # 计算太阳赤纬
        declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365))
        
        # 计算时角
        solar_noon = 12.0
        time_diff = time_hour - solar_noon
        hour_angle = 15 * time_diff
        
        # 转换为弧度
        lat_rad = math.radians(self.latitude)
        dec_rad = math.radians(declination)
        ha_rad = math.radians(hour_angle)
        
        # 计算太阳高度角
        altitude = math.radians(self._calculate_sun_altitude(time_hour, day_of_year))
        
        # 计算方位角
        if altitude <= 0.1:  # 太阳在地平线以下
            return 180  # 默认南方
        
        cos_azimuth = (math.sin(dec_rad) - math.sin(lat_rad) * math.sin(altitude)) / \
                     (math.cos(lat_rad) * math.cos(altitude))
        
        cos_azimuth = max(-1.0, min(1.0, cos_azimuth))
        
        azimuth = math.degrees(math.acos(cos_azimuth))
        
        # 调整方位角方向
        if hour_angle > 0:  # 下午
            azimuth = 360 - azimuth
        
        return azimuth
    
    def _setup_lighting(self):
        """设置场景光照系统"""
        if not self.plotter:
            return
        
        # 清除现有光源
        for light in self.lights:
            self.plotter.remove_light(light)
        self.lights = []
        
        if self.use_high_fidelity:
            # 高保真光照系统
            self._setup_hifi_lighting()
        else:
            # 简化光照系统
            self._setup_simple_lighting()
    
    def _setup_hifi_lighting(self):
        """设置高保真光照系统"""
        # 1. 主光源(太阳)
        sun_altitude = self._calculate_sun_altitude(self.time_of_day)
        sun_azimuth = self._calculate_sun_azimuth(self.time_of_day)
        
        # 计算太阳位置(球坐标转直角坐标)
        distance = 10000  # 光源距离
        sun_x = distance * math.cos(math.radians(sun_altitude)) * math.sin(math.radians(sun_azimuth))
        sun_y = distance * math.cos(math.radians(sun_altitude)) * math.cos(math.radians(sun_azimuth))
        sun_z = distance * math.sin(math.radians(sun_altitude))
        
        # 创建太阳光源
        sun_light = pv.Light(
            position=(sun_x, sun_y, sun_z),
            focal_point=(0, 0, 0),
            light_type='scene light',
            intensity=0.8,
            color='white'
        )
        self.plotter.add_light(sun_light)
        self.lights.append(sun_light)
        
        # 2. 环境光
        ambient_light = pv.Light(
            position=(0, 0, 1000),
            focal_point=(0, 0, 0),
            light_type='headlight',
            intensity=0.2,
            color=(0.9, 0.9, 1.0)  # 略带蓝色的环境光
        )
        self.plotter.add_light(ambient_light)
        self.lights.append(ambient_light)
        
        # 3. 补光
        if sun_altitude < 30:  # 太阳较低时增加补光
            fill_light = pv.Light(
                position=(-sun_x/2, -sun_y/2, 500),
                focal_point=(0, 0, 0),
                intensity=0.3,
                color='white'
            )
            self.plotter.add_light(fill_light)
            self.lights.append(fill_light)
    
    def _setup_simple_lighting(self):
        """设置简化光照系统"""
        # 单个主光源
        main_light = pv.Light(
            position=(1000, 1000, 1000),
            focal_point=(0, 0, 0),
            intensity=0.7,
            color='white'
        )
        self.plotter.add_light(main_light)
        self.lights.append(main_light)
        
        # 简化的环境光
        ambient_light = pv.Light(
            position=(0, 0, 500),
            focal_point=(0, 0, 0),
            light_type='headlight',
            intensity=0.3,
            color='white'
        )
        self.plotter.add_light(ambient_light)
        self.lights.append(ambient_light)
    
    def add_unit(self, unit_class, position, **kwargs):
        """添加作战单位到场景"""
        self.unit_counter += 1
        unit_id = f"{unit_class.__name__}_{self.unit_counter}"
        
        # 创建单位实例
        if self.use_high_fidelity and 'HighFidelity' in globals():
            # 使用高保真单位
            unit = unit_class(position=position, unit_id=unit_id, **kwargs)
            if hasattr(unit, 'load_external_model') and kwargs.get('model_file'):
                mesh = unit.load_external_model(kwargs['model_file'])
            else:
                mesh = unit.create_high_detail_geometry(scale=kwargs.get('scale', 1.0))
        else:
            # 使用简化单位
            unit = unit_class(position=position, unit_id=unit_id, **kwargs)
            mesh = unit.create_simplified_geometry(scale=kwargs.get('scale', 1.0))
        
        # 添加到场景
        if mesh and self.plotter:
            unit_actor = self.plotter.add_mesh(
                mesh,
                color=unit.color,
                name=unit_id,
                show_edges=False
            )
            unit.actor = unit_actor
        
        # 存储单位
        self.units[unit_id] = unit
        
        return unit_id
    
    def remove_unit(self, unit_id):
        """从场景中移除单位"""
        if unit_id in self.units:
            unit = self.units[unit_id]
            if hasattr(unit, 'actor') and unit.actor and self.plotter:
                self.plotter.remove_actor(unit.actor)
            del self.units[unit_id]
            return True
        return False
    
    def update_unit_position(self, unit_id, new_position, orientation=None):
        """更新单位位置和朝向"""
        if unit_id in self.units:
            unit = self.units[unit_id]
            
            # 移除旧的actor
            if hasattr(unit, 'actor') and unit.actor and self.plotter:
                self.plotter.remove_actor(unit.actor)
            
            # 更新单位位置
            unit.update_position(new_position)
            
            # 更新单位朝向
            if orientation is not None:
                unit.set_orientation(orientation)
            
            # 重新添加单位到场景
            if hasattr(unit, 'mesh') and unit.mesh and self.plotter:
                unit.actor = self.plotter.add_mesh(
                    unit.mesh,
                    color=unit.color,
                    name=unit_id,
                    show_edges=False
                )
            
            return True
        return False
    
    def create_unit_group(self, unit_class, positions, **kwargs):
        """创建单位编组"""
        group_ids = []
        for position in positions:
            unit_id = self.add_unit(unit_class, position, **kwargs)
            group_ids.append(unit_id)
        return group_ids
    
    def update_scene_time(self, time_hour):
        """更新场景时间"""
        self.time_of_day = time_hour % 24  # 确保在0-24范围内
        
        # 更新光照
        self._setup_lighting()
        
        # 更新背景(如果使用高保真版)
        if self.use_high_fidelity:
            self._update_sky_background()
    
    def _update_sky_background(self):
        """根据时间更新天空背景"""
        if not self.plotter:
            return
        
        # 根据时间计算天空颜色
        if 6 <= self.time_of_day < 18:  # 白天
            # 随时间变化的渐变
            if self.time_of_day < 12:  # 上午
                intensity = (self.time_of_day - 6) / 6
                top_color = self._interpolate_color('#87CEEB', '#FFFFFF', intensity)
            else:  # 下午
                intensity = (18 - self.time_of_day) / 6
                top_color = self._interpolate_color('#FFFFFF', '#FFA500', intensity)
            
            bottom_color = '#1e3c72'
            
        else:  # 夜晚
            # 夜晚的深蓝色
            if self.time_of_day < 6:  # 凌晨
                intensity = self.time_of_day / 6
                top_color = self._interpolate_color('#000033', '#1e3c72', intensity)
            else:  # 晚上
                intensity = (24 - self.time_of_day) / 6
                top_color = self._interpolate_color('#1e3c72', '#000033', intensity)
            
            bottom_color = '#000011'
        
        # 设置背景
        self.plotter.set_background('linear_gradient', 
                                  bottom=bottom_color, 
                                  top=top_color)
    
    def _interpolate_color(self, color1, color2, t):
        """颜色插值"""
        import matplotlib.colors as mcolors
        
        # 转换为RGB
        rgb1 = np.array(mcolors.to_rgb(color1))
        rgb2 = np.array(mcolors.to_rgb(color2))
        
        # 线性插值
        rgb = rgb1 + (rgb2 - rgb1) * t
        
        # 转换为十六进制
        return mcolors.to_hex(rgb)
    
    def get_scene_snapshot(self, filepath=None, resolution=None):
        """获取场景快照"""
        if not self.plotter:
            return None
        
        if resolution is None:
            resolution = (1920, 1080) if self.use_high_fidelity else (1280, 720)
        
        if filepath:
            # 保存到文件
            self.plotter.screenshot(filepath, window_size=resolution)
            return filepath
        else:
            # 返回图像数据
            return self.plotter.screenshot(window_size=resolution)
    
    def export_scene(self, filepath, format='vtk'):
        """导出场景数据"""
        if not self.terrain:
            return False
        
        try:
            if format.lower() == 'vtk':
                # 导出为VTK格式
                self.terrain.save(filepath)
                
                # 导出单位数据
                unit_data = []
                for unit_id, unit in self.units.items():
                    if hasattr(unit, 'mesh'):
                        unit_file = filepath.replace('.vtk', f'_{unit_id}.vtk')
                        unit.mesh.save(unit_file)
                        unit_data.append({
                            'id': unit_id,
                            'type': unit.unit_type,
                            'position': unit.position.tolist(),
                            'file': unit_file
                        })
                
                # 保存元数据
                import json
                meta_file = filepath.replace('.vtk', '_meta.json')
                metadata = {
                    'terrain_file': filepath,
                    'units': unit_data,
                    'time': self.time_of_day,
                    'bounds': self.scene_bounds
                }
                
                with open(meta_file, 'w') as f:
                    json.dump(metadata, f, indent=2)
                
                return True
                
        except Exception as e:
            print(f"导出场景失败: {e}")
            return False
    
    def import_scene(self, filepath):
        """导入场景数据"""
        try:
            import json
            import os
            
            # 加载元数据
            meta_file = filepath.replace('.vtk', '_meta.json')
            if not os.path.exists(meta_file):
                meta_file = filepath.replace('.vtk', '_meta.json')
            
            with open(meta_file, 'r') as f:
                metadata = json.load(f)
            
            # 加载地形
            self.terrain = pv.read(metadata['terrain_file'])
            
            # 重新创建绘图窗口
            if self.plotter:
                self.plotter.close()
            
            self.plotter = pv.Plotter(window_size=[1600, 900])
            
            # 添加地形
            self._add_terrain_to_scene()
            
            # 加载单位
            for unit_data in metadata.get('units', []):
                unit_file = unit_data.get('file')
                if unit_file and os.path.exists(unit_file):
                    unit_mesh = pv.read(unit_file)
                    
                    # 创建单位actor
                    unit_actor = self.plotter.add_mesh(
                        unit_mesh,
                        color='gray',  # 默认颜色
                        name=unit_data['id']
                    )
                    
                    # 创建单位对象
                    unit_type = unit_data.get('type', 'unknown')
                    if unit_type == 'radar':
                        unit_class = RadarUnit
                    elif unit_type == 'aircraft':
                        unit_class = AircraftUnit
                    elif unit_type == 'vehicle':
                        unit_class = VehicleUnit
                    else:
                        continue
                    
                    position = unit_data.get('position', [0, 0, 0])
                    unit = unit_class(position=position, unit_id=unit_data['id'])
                    unit.mesh = unit_mesh
                    unit.actor = unit_actor
                    
                    self.units[unit_data['id']] = unit
            
            # 恢复场景设置
            self.time_of_day = metadata.get('time', 12.0)
            self.scene_bounds = metadata.get('bounds', self.scene_bounds)
            
            # 设置光照
            self._setup_lighting()
            
            return True
            
        except Exception as e:
            print(f"导入场景失败: {e}")
            return False
    
    def run_simulation(self, duration=60, time_step=1.0):
        """运行场景仿真"""
        if not self.plotter or not self.is_initialized:
            print("场景未初始化")
            return
        
        print(f"开始仿真: 持续时间={duration}s, 时间步长={time_step}s")
        
        # 记录起始时间
        import time
        start_time = time.time()
        
        # 仿真循环
        for t in np.arange(0, duration, time_step):
            self.current_time = t
            
            # 更新场景时间
            scene_hour = (self.time_of_day + t/3600) % 24
            self.update_scene_time(scene_hour)
            
            # 更新单位状态
            self._update_units(time_step)
            
            # 更新显示
            if self.plotter and hasattr(self.plotter, 'app'):
                self.plotter.update()
                self.plotter.app.process_events()
            
            # 控制帧率
            elapsed = time.time() - start_time
            target_time = t / self.time_scale
            if elapsed < target_time:
                time.sleep(target_time - elapsed)
        
        print(f"仿真完成: 实际用时{time.time()-start_time:.2f}s")
    
    def _update_units(self, time_step):
        """更新所有单位状态"""
        for unit_id, unit in self.units.items():
            # 这里可以添加单位的具体更新逻辑
            if hasattr(unit, 'update_dynamics'):
                unit.update_dynamics(time_step)
            
            # 示例:简单的随机移动
            if unit.unit_type == 'aircraft' and np.random.random() < 0.1:
                # 随机移动飞机
                new_pos = unit.position.copy()
                new_pos[0] += np.random.uniform(-10, 10)
                new_pos[1] += np.random.uniform(-10, 10)
                new_pos[2] += np.random.uniform(-5, 5)
                
                self.update_unit_position(unit_id, new_pos)
    
    def show(self, interactive=True):
        """显示场景"""
        if not self.plotter:
            print("场景未初始化")
            return
        
        if interactive:
            self.plotter.show()
        else:
            # 非交互式显示
            self.plotter.render()
            return self.plotter.image
    
    def close(self):
        """关闭场景"""
        if self.plotter:
            self.plotter.close()
        self.is_initialized = False

5.2 场景配置管理器

用于管理和保存场景配置的辅助类

python 复制代码
import json
import yaml
import pickle
from dataclasses import dataclass, asdict
from typing import List, Dict, Any, Optional

@dataclass
class SceneConfig:
    """场景配置数据类"""
    name: str
    description: str
    terrain_type: str
    terrain_params: Dict[str, Any]
    units: List[Dict[str, Any]]
    time_of_day: float
    weather: str
    bounds: List[float]
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'SceneConfig':
        """从字典创建配置"""
        return cls(**data)
    
    def to_dict(self) -> Dict[str, Any]:
        """转换为字典"""
        return asdict(self)

class SceneConfigManager:
    """场景配置管理器"""
    
    def __init__(self, config_dir: str = "configs"):
        self.config_dir = config_dir
        self.configs: Dict[str, SceneConfig] = {}
        self.current_config: Optional[SceneConfig] = None
        
    def load_config(self, config_name: str, format: str = 'json') -> Optional[SceneConfig]:
        """加载场景配置"""
        filepath = f"{self.config_dir}/{config_name}.{format}"
        
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                if format == 'json':
                    data = json.load(f)
                elif format == 'yaml':
                    data = yaml.safe_load(f)
                elif format == 'pkl':
                    data = pickle.load(f)
                else:
                    print(f"不支持的格式: {format}")
                    return None
            
            config = SceneConfig.from_dict(data)
            self.configs[config_name] = config
            self.current_config = config
            
            return config
            
        except Exception as e:
            print(f"加载配置失败: {e}")
            return None
    
    def save_config(self, config_name: str, config: SceneConfig, format: str = 'json') -> bool:
        """保存场景配置"""
        import os
        
        # 确保配置目录存在
        os.makedirs(self.config_dir, exist_ok=True)
        
        filepath = f"{self.config_dir}/{config_name}.{format}"
        
        try:
            data = config.to_dict()
            
            with open(filepath, 'w', encoding='utf-8') as f:
                if format == 'json':
                    json.dump(data, f, indent=2, ensure_ascii=False)
                elif format == 'yaml':
                    yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
                elif format == 'pkl':
                    pickle.dump(data, f)
            
            self.configs[config_name] = config
            return True
            
        except Exception as e:
            print(f"保存配置失败: {e}")
            return False
    
    def create_battlefield_config(self, scene: BattlefieldScene) -> SceneConfig:
        """从场景创建配置"""
        units_data = []
        
        for unit_id, unit in scene.units.items():
            unit_data = {
                'id': unit_id,
                'type': unit.unit_type,
                'position': unit.position.tolist() if hasattr(unit.position, 'tolist') else list(unit.position),
                'color': unit.color if hasattr(unit, 'color') else 'gray'
            }
            units_data.append(unit_data)
        
        config = SceneConfig(
            name=scene.scene_name,
            description="PyVista战场场景配置",
            terrain_type="synthetic" if not scene.use_high_fidelity else "realistic",
            terrain_params={
                'size': 1000,
                'resolution': 200
            },
            units=units_data,
            time_of_day=scene.time_of_day,
            weather="clear",
            bounds=scene.scene_bounds
        )
        
        return config
    
    def apply_config_to_scene(self, config: SceneConfig, scene: BattlefieldScene) -> bool:
        """将配置应用到场景"""
        try:
            # 清除现有单位
            scene.units.clear()
            
            # 设置场景参数
            scene.scene_name = config.name
            scene.time_of_day = config.time_of_day
            
            # 重新初始化地形
            if not scene.is_initialized:
                scene.initialize_scene()
            
            # 添加单位
            for unit_data in config.units:
                unit_type = unit_data.get('type', 'unknown')
                position = unit_data.get('position', [0, 0, 0])
                
                if unit_type == 'radar':
                    unit_class = RadarUnit
                elif unit_type == 'aircraft':
                    unit_class = AircraftUnit
                elif unit_type == 'vehicle':
                    unit_class = VehicleUnit
                else:
                    continue
                
                scene.add_unit(unit_class, position, **unit_data)
            
            return True
            
        except Exception as e:
            print(f"应用配置失败: {e}")
            return False

5.3 完整的场景示例

使用场景管理器创建和运行完整战场场景

python 复制代码
def create_complete_battlefield_scene(use_high_fidelity=True):
    """创建完整的战场场景示例"""
    print("=" * 60)
    print("创建战场场景")
    print(f"模式: {'高保真' if use_high_fidelity else '简化'}")
    print("=" * 60)
    
    # 1. 创建场景管理器
    scene = BattlefieldScene(
        scene_name="红蓝对抗演习场景",
        use_high_fidelity=use_high_fidelity
    )
    
    # 2. 初始化场景
    if use_high_fidelity:
        scene.initialize_scene(terrain_size=2000, terrain_resolution=300)
    else:
        scene.initialize_scene(terrain_size=1000, terrain_resolution=150)
    
    # 3. 添加红方单位
    print("添加红方单位...")
    
    # 雷达站
    red_radar_id = scene.add_unit(
        RadarUnit, 
        position=[-300, 200, 50],
        radar_range=500,
        beam_width=30
    )
    
    # 飞机编队
    red_aircraft_positions = [
        [-200, 100, 100],
        [-250, 150, 110],
        [-300, 200, 120],
        [-350, 250, 130]
    ]
    
    red_aircraft_ids = scene.create_unit_group(
        AircraftUnit,
        positions=red_aircraft_positions,
        aircraft_type='fighter'
    )
    
    # 地面车辆
    red_vehicle_ids = []
    for i in range(3):
        pos = [-400 + i*50, 300, 0]
        vehicle_id = scene.add_unit(VehicleUnit, pos, vehicle_type='tank')
        red_vehicle_ids.append(vehicle_id)
    
    # 4. 添加蓝方单位
    print("添加蓝方单位...")
    
    # 蓝方雷达
    blue_radar_id = scene.add_unit(
        RadarUnit,
        position=[300, -200, 50],
        radar_range=600,
        beam_width=25
    )
    
    # 设置蓝方颜色
    if blue_radar_id in scene.units:
        scene.units[blue_radar_id].color = 'blue'
    
    # 蓝方飞机
    blue_aircraft_positions = [
        [200, -100, 150],
        [250, -150, 160],
        [300, -200, 170]
    ]
    
    blue_aircraft_ids = scene.create_unit_group(
        AircraftUnit,
        positions=blue_aircraft_positions,
        aircraft_type='bomber'
    )
    
    # 设置蓝方飞机颜色
    for aircraft_id in blue_aircraft_ids:
        if aircraft_id in scene.units:
            scene.units[aircraft_id].color = 'blue'
    
    # 5. 设置场景时间
    scene.update_scene_time(14.5)  # 下午2:30
    
    # 6. 显示场景统计
    print("\n场景统计:")
    print(f"地形分辨率: {scene.terrain.n_points} 个点")
    print(f"单位数量: {len(scene.units)} 个")
    print(f"红方单位: {len(red_aircraft_ids) + len(red_vehicle_ids) + 1} 个")
    print(f"蓝方单位: {len(blue_aircraft_ids) + 1} 个")
    print(f"场景时间: {scene.time_of_day:.1f}:00")
    
    return scene

def run_battlefield_demo():
    """运行战场场景演示"""
    
    # 创建简化版场景
    print("\n1. 创建简化版场景...")
    simple_scene = create_complete_battlefield_scene(use_high_fidelity=False)
    
    # 保存配置
    config_manager = SceneConfigManager()
    simple_config = config_manager.create_battlefield_config(simple_scene)
    config_manager.save_config("simple_battlefield", simple_config)
    
    # 显示简化版场景
    print("\n显示简化版场景...")
    simple_scene.show(interactive=False)
    
    # 保存快照
    simple_scene.get_scene_snapshot("simple_scene.png")
    print("简化版场景快照已保存: simple_scene.png")
    
    # 关闭简化版场景
    simple_scene.close()
    
    # 创建高保真版场景
    print("\n\n2. 创建高保真版场景...")
    hifi_scene = create_complete_battlefield_scene(use_high_fidelity=True)
    
    # 保存配置
    hifi_config = config_manager.create_battlefield_config(hifi_scene)
    config_manager.save_config("hifi_battlefield", hifi_config)
    
    # 显示高保真版场景
    print("\n显示高保真版场景...")
    
    # 运行短时间仿真
    hifi_scene.run_simulation(duration=30, time_step=0.5)
    
    # 保存快照
    hifi_scene.get_scene_snapshot("hifi_scene.png", resolution=(2560, 1440))
    print("高保真版场景快照已保存: hifi_scene.png")
    
    # 导出场景
    hifi_scene.export_scene("battlefield_scene.vtk")
    print("场景已导出: battlefield_scene.vtk")
    
    # 交互式显示
    print("\n开始交互式显示...")
    print("使用鼠标和键盘控制:")
    print("  - 左键拖拽: 旋转视角")
    print("  - 右键拖拽: 平移视角")
    print("  - 滚轮: 缩放")
    print("  - R键: 重置视角")
    print("  - Q键: 退出")
    
    hifi_scene.show(interactive=True)
    
    # 关闭场景
    hifi_scene.close()
    
    print("\n演示完成!")

if __name__ == "__main__":
    # 运行演示
    run_battlefield_demo()

5.4 性能优化建议

场景优化技巧,确保大规模战场场景的流畅运行

python 复制代码
class SceneOptimizer:
    """场景优化器"""
    
    @staticmethod
    def optimize_terrain(terrain, target_faces=10000):
        """优化地形网格"""
        if terrain.n_faces <= target_faces:
            return terrain
        
        # 计算简化比例
        reduction_ratio = 1.0 - (target_faces / terrain.n_faces)
        
        # 简化网格
        simplified = terrain.decimate(reduction_ratio)
        return simplified
    
    @staticmethod
    def create_lod_system(unit, lod_levels=3):
        """为作战单位创建LOD(细节层次)系统"""
        lod_meshes = []
        
        for level in range(lod_levels):
            if level == 0:  # 最高细节
                if hasattr(unit, 'create_high_detail_geometry'):
                    mesh = unit.create_high_detail_geometry(scale=1.0)
                else:
                    mesh = unit.create_simplified_geometry(scale=1.0)
            
            elif level == 1:  # 中等细节
                reduction = 0.5
                mesh = unit.mesh.decimate(reduction)
            
            else:  # 最低细节
                reduction = 0.2
                mesh = unit.mesh.decimate(reduction)
                # 使用更简单的材质
                if hasattr(mesh, 'texture'):
                    delattr(mesh, 'texture')
            
            lod_meshes.append(mesh)
        
        return lod_meshes
    
    @staticmethod
    def batch_render_units(units, plotter):
        """批量渲染相似单位以提高性能"""
        from collections import defaultdict
        
        # 按单位类型分组
        unit_groups = defaultdict(list)
        for unit in units:
            unit_groups[unit.unit_type].append(unit)
        
        # 批量创建和渲染
        for unit_type, group_units in unit_groups.items():
            if len(group_units) > 1:
                # 合并相似单位的网格
                combined_mesh = None
                for unit in group_units:
                    if combined_mesh is None:
                        combined_mesh = unit.mesh
                    else:
                        combined_mesh = combined_mesh.merge(unit.mesh)
                
                # 批量渲染
                if combined_mesh:
                    plotter.add_mesh(combined_mesh, color=group_units[0].color)
            else:
                # 单个渲染
                for unit in group_units:
                    plotter.add_mesh(unit.mesh, color=unit.color)
    
    @staticmethod
    def optimize_view_frustum(scene, plotter):
        """视锥体优化:只渲染可见区域"""
        # 获取当前视锥体
        camera = plotter.camera
        frustum = camera.view_frustum(plotter.renderer)
        
        # 检查哪些单位在视锥体内
        visible_units = []
        for unit_id, unit in scene.units.items():
            if hasattr(unit, 'mesh') and unit.mesh:
                bounds = unit.mesh.bounds
                # 简单的AABB测试
                if SceneOptimizer._aabb_in_frustum(bounds, frustum):
                    visible_units.append(unit)
        
        return visible_units
    
    @staticmethod
    def _aabb_in_frustum(aabb, frustum):
        """轴对齐包围盒与视锥体相交测试"""
        # 简化实现:检查包围盒的8个顶点是否在视锥体内
        x_min, x_max, y_min, y_max, z_min, z_max = aabb
        
        # 8个顶点
        vertices = [
            (x_min, y_min, z_min),
            (x_min, y_min, z_max),
            (x_min, y_max, z_min),
            (x_min, y_max, z_max),
            (x_max, y_min, z_min),
            (x_max, y_min, z_max),
            (x_max, y_max, z_min),
            (x_max, y_max, z_max)
        ]
        
        # 检查是否有至少一个顶点在视锥体内
        for vertex in vertices:
            # 这里需要实现具体的视锥体测试
            # 简化:假设所有顶点都在视锥体内
            pass
        
        return True  # 简化实现,总是返回True

5.5 总结

本章详细介绍了战场场景管理器的设计和实现,包括:

  1. 太阳位置计算:基于经纬度和时间计算准确的太阳高度角和方位角

  2. 光照系统:支持简化版和高保真版两种光照配置

  3. 单位管理:完整的单位添加、移除、更新功能

  4. 场景配置:支持场景的保存、加载和应用

  5. 仿真循环:实现基于时间的场景更新

  6. 性能优化:提供多种优化技术确保大规模场景的流畅运行

通过本章的学习,读者可以掌握使用PyVista构建完整3D战场场景的核心技术,为后续实现雷达探测、电子对抗等高级功能打下坚实基础。

相关推荐
二十雨辰2 小时前
[python]-基础语法
python
小白学大数据2 小时前
基于 Python 的知网文献批量采集与可视化分析
开发语言·爬虫·python·小程序
fai厅的秃头姐!2 小时前
01-python基础-day01python基础
python
霸道流氓气质2 小时前
Java 实现折线图整点数据补全与标准化处理示例代码讲解
java·开发语言·windows
深蓝电商API2 小时前
Scrapy 爬虫监控:结合 Prometheus+Grafana 实践
爬虫·python·scrapy
冬奇Lab2 小时前
【Kotlin系列10】协程原理与实战(上):结构化并发让异步编程不再是噩梦
android·开发语言·kotlin
薛不痒2 小时前
项目:矿物分类(训练模型)
开发语言·人工智能·python·学习·算法·机器学习·分类
姜太小白2 小时前
【前端】JavaScript字符串执行方法总结
开发语言·前端·javascript
被星1砸昏头2 小时前
C++与Node.js集成
开发语言·c++·算法