334 lines
12 KiB
Python
334 lines
12 KiB
Python
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)
|