updated versions
This commit is contained in:
332
py_scripts/blender_animator.py
Normal file
332
py_scripts/blender_animator.py
Normal file
@@ -0,0 +1,332 @@
|
||||
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:
|
||||
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user