import json import numpy as np import os from datetime import datetime import math # Blender dependencies with fallback handling try: import bpy import bmesh from mathutils import Vector, Euler BLENDER_AVAILABLE = True except ImportError: BLENDER_AVAILABLE = False print("Warning: Blender (bpy) not available. This module requires Blender to be installed with Python API access.") class BlenderGPSAnimator: """ Advanced GPS track animation using Blender for high-quality 3D rendering """ def __init__(self, output_folder): self.output_folder = output_folder if BLENDER_AVAILABLE: self.setup_blender_scene() else: # Don't raise error here, let the caller handle the check pass def check_dependencies(self): """Check if Blender dependencies are available""" if not BLENDER_AVAILABLE: raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.") return True def setup_blender_scene(self): """Setup Blender scene for GPS animation""" # Clear existing mesh objects bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # Add camera bpy.ops.object.camera_add(location=(0, 0, 10)) self.camera = bpy.context.object # Add sun light bpy.ops.object.light_add(type='SUN', location=(0, 0, 20)) light = bpy.context.object light.data.energy = 5 # Setup world environment world = bpy.context.scene.world world.use_nodes = True env_texture = world.node_tree.nodes.new('ShaderNodeTexEnvironment') world.node_tree.links.new(env_texture.outputs[0], world.node_tree.nodes['Background'].inputs[0]) # Set render settings scene = bpy.context.scene scene.render.engine = 'CYCLES' scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 scene.render.fps = 30 scene.cycles.samples = 64 def load_gps_data(self, positions_file): """Load GPS data from JSON file""" with open(positions_file, 'r') as f: positions = json.load(f) # Convert to numpy array for easier processing coords = [] times = [] speeds = [] for pos in positions: coords.append([pos['longitude'], pos['latitude'], pos.get('altitude', 0)]) times.append(pos['fixTime']) speeds.append(pos.get('speed', 0) * 1.852) # Convert to km/h return np.array(coords), times, speeds def create_terrain_mesh(self, coords): """Create a simple terrain mesh based on GPS bounds""" # Calculate bounds min_lon, min_lat = coords[:, :2].min(axis=0) max_lon, max_lat = coords[:, :2].max(axis=0) # Expand bounds slightly padding = 0.001 min_lon -= padding min_lat -= padding max_lon += padding max_lat += padding # Create terrain mesh bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, 0)) terrain = bpy.context.object terrain.name = "Terrain" # Scale terrain to match GPS bounds lon_range = max_lon - min_lon lat_range = max_lat - min_lat scale_factor = max(lon_range, lat_range) * 100000 # Convert to reasonable scale terrain.scale = (scale_factor, scale_factor, 1) # Apply material mat = bpy.data.materials.new(name="TerrainMaterial") mat.use_nodes = True mat.node_tree.nodes.clear() # Add principled BSDF bsdf = mat.node_tree.nodes.new(type='ShaderNodeBsdfPrincipled') bsdf.inputs['Base Color'].default_value = (0.2, 0.5, 0.2, 1.0) # Green bsdf.inputs['Roughness'].default_value = 0.8 material_output = mat.node_tree.nodes.new(type='ShaderNodeOutputMaterial') mat.node_tree.links.new(bsdf.outputs['BSDF'], material_output.inputs['Surface']) terrain.data.materials.append(mat) return terrain def create_gps_track_mesh(self, coords): """Create a 3D mesh for the GPS track""" # Normalize coordinates to Blender scale coords_normalized = self.normalize_coordinates(coords) # Create curve from GPS points curve_data = bpy.data.curves.new('GPSTrack', type='CURVE') curve_data.dimensions = '3D' curve_data.bevel_depth = 0.02 curve_data.bevel_resolution = 4 # Create spline spline = curve_data.splines.new('BEZIER') spline.bezier_points.add(len(coords_normalized) - 1) for i, coord in enumerate(coords_normalized): point = spline.bezier_points[i] point.co = coord point.handle_left_type = 'AUTO' point.handle_right_type = 'AUTO' # Create object from curve track_obj = bpy.data.objects.new('GPSTrack', curve_data) bpy.context.collection.objects.link(track_obj) # Apply material mat = bpy.data.materials.new(name="TrackMaterial") mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # Red bsdf.inputs['Emission'].default_value = (1.0, 0.2, 0.2, 1.0) bsdf.inputs['Emission Strength'].default_value = 2.0 track_obj.data.materials.append(mat) return track_obj def create_vehicle_model(self): """Create a simple vehicle model""" # Create a simple car shape using cubes bpy.ops.mesh.primitive_cube_add(size=0.1, location=(0, 0, 0.05)) vehicle = bpy.context.object vehicle.name = "Vehicle" vehicle.scale = (2, 1, 0.5) # Apply material mat = bpy.data.materials.new(name="VehicleMaterial") mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs['Base Color'].default_value = (0.0, 0.0, 1.0, 1.0) # Blue bsdf.inputs['Metallic'].default_value = 0.5 bsdf.inputs['Roughness'].default_value = 0.2 vehicle.data.materials.append(mat) return vehicle def normalize_coordinates(self, coords): """Normalize GPS coordinates to Blender scale""" # Center coordinates center = coords.mean(axis=0) coords_centered = coords - center # Scale to reasonable size for Blender scale_factor = 100 coords_scaled = coords_centered * scale_factor # Convert to Vector objects return [Vector((x, y, z)) for x, y, z in coords_scaled] def animate_vehicle(self, vehicle, coords, times, speeds): """Create animation keyframes for vehicle movement""" coords_normalized = self.normalize_coordinates(coords) scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = len(coords_normalized) * 2 # 2 frames per GPS point for i, (coord, speed) in enumerate(zip(coords_normalized, speeds)): frame = i * 2 + 1 # Set location vehicle.location = coord vehicle.keyframe_insert(data_path="location", frame=frame) # Calculate rotation based on direction if i < len(coords_normalized) - 1: next_coord = coords_normalized[i + 1] direction = next_coord - coord if direction.length > 0: direction.normalize() # Calculate rotation angle angle = math.atan2(direction.y, direction.x) vehicle.rotation_euler = Euler((0, 0, angle), 'XYZ') vehicle.keyframe_insert(data_path="rotation_euler", frame=frame) # Set interpolation mode if vehicle.animation_data: for fcurve in vehicle.animation_data.action.fcurves: for keyframe in fcurve.keyframe_points: keyframe.interpolation = 'BEZIER' def animate_camera(self, coords): """Create smooth camera animation following the vehicle""" coords_normalized = self.normalize_coordinates(coords) # Create camera path for i, coord in enumerate(coords_normalized): frame = i * 2 + 1 # Position camera above and behind the vehicle offset = Vector((0, -2, 3)) cam_location = coord + offset self.camera.location = cam_location self.camera.keyframe_insert(data_path="location", frame=frame) # Look at the vehicle direction = coord - cam_location if direction.length > 0: rot_quat = direction.to_track_quat('-Z', 'Y') self.camera.rotation_euler = rot_quat.to_euler() self.camera.keyframe_insert(data_path="rotation_euler", frame=frame) def add_particles_effects(self, vehicle): """Add particle effects for enhanced visuals""" # Add dust particles bpy.context.view_layer.objects.active = vehicle bpy.ops.object.modifier_add(type='PARTICLE_SYSTEM') particles = vehicle.modifiers["ParticleSystem"].particle_system particles.settings.count = 100 particles.settings.lifetime = 30 particles.settings.emit_from = 'FACE' particles.settings.physics_type = 'NEWTON' particles.settings.effector_weights.gravity = 0.1 # Set material for particles particles.settings.material = 1 def render_animation(self, output_path, progress_callback=None): """Render the animation to video""" scene = bpy.context.scene # Set output settings scene.render.filepath = output_path scene.render.image_settings.file_format = 'FFMPEG' scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = 'H264' # Render animation total_frames = scene.frame_end - scene.frame_start + 1 for frame in range(scene.frame_start, scene.frame_end + 1): scene.frame_set(frame) # Render frame frame_path = f"{output_path}_{frame:04d}.png" scene.render.filepath = frame_path bpy.ops.render.render(write_still=True) # Update progress if progress_callback: progress = ((frame - scene.frame_start) / total_frames) * 100 progress_callback(progress, f"Rendering frame {frame}/{scene.frame_end}") def create_gps_animation(self, positions_file, output_path, progress_callback=None): """Main method to create GPS animation in Blender""" try: # Load GPS data coords, times, speeds = self.load_gps_data(positions_file) # Create scene elements terrain = self.create_terrain_mesh(coords) track = self.create_gps_track_mesh(coords) vehicle = self.create_vehicle_model() # Create animations self.animate_vehicle(vehicle, coords, times, speeds) self.animate_camera(coords) # Add effects self.add_particles_effects(vehicle) # Render animation self.render_animation(output_path, progress_callback) return True except Exception as e: print(f"Error creating Blender animation: {e}") if progress_callback: progress_callback(-1, f"Error: {e}") return False def generate_blender_animation(positions_file, output_folder, progress_callback=None): """ Convenience function to generate Blender animation """ animator = BlenderGPSAnimator(output_folder) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = os.path.join(output_folder, f"blender_animation_{timestamp}") success = animator.create_gps_animation(positions_file, output_path, progress_callback) return f"{output_path}.mp4" if success else None # Note: This script should be run from within Blender's Python environment # or with Blender as a Python module (bpy)