572 lines
19 KiB
Python
572 lines
19 KiB
Python
import bpy
|
||
import math
|
||
import bmesh
|
||
import re
|
||
from mathutils.bvhtree import BVHTree
|
||
from mathutils import Vector, Euler, Matrix
|
||
from typing import List, Tuple
|
||
|
||
import time
|
||
|
||
print("\n\n\n\n\n\n\n\n", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), "Starting script execution")
|
||
|
||
class Airport:
|
||
name: str
|
||
longitude: float
|
||
latitude: float
|
||
x: float
|
||
y: float
|
||
z: float
|
||
object: bpy.types.Object | None
|
||
|
||
def __init__(self, name: str, lon_lat: str, bvh: BVHTree, origin: Vector, radius: float, shader: bpy.types.Material | None):
|
||
self.name = name
|
||
lat, lon = parse_coordinates(lon_lat)
|
||
print(f"Parsed coordinates: {lat}, {lon}")
|
||
self.latitude = lat
|
||
self.longitude = lon
|
||
# print(self.longitude, self.latitude)
|
||
direction = direction_from_lat_lon(self.latitude, self.longitude)
|
||
# print("direction:", direction)
|
||
distance = distance_to_surface_from_latlon(bvh, origin, self.latitude, self.longitude)
|
||
# print("distance:", distance)
|
||
if distance is None:
|
||
raise ValueError(f"No surface found at latitude {self.latitude} and longitude {self.longitude}")
|
||
pos_vector = vector_to_coordinates(direction, distance, origin)
|
||
# print("position vector:", pos_vector)
|
||
# pos_vector = vector_to_coordinates(direction, distance, origin)
|
||
|
||
self.x = pos_vector.x
|
||
self.y = pos_vector.y
|
||
self.z = pos_vector.z
|
||
try:
|
||
sphere = bpy.data.objects.get(name)
|
||
if not sphere is None:
|
||
# print(f"Sphere with name '{name}' already exists.")
|
||
if not pos_vector == sphere.location:
|
||
print("Sphere already exists, but location is different. Updating location.")
|
||
sphere.location.x = pos_vector.x
|
||
sphere.location.y = pos_vector.y
|
||
sphere.location.z = pos_vector.z
|
||
else:
|
||
print(f"Sphere with name '{name}' does not exist, creating a new one.")
|
||
sphere = create_sphere(pos_vector, radius=radius, name=name, shader=shader)
|
||
except:
|
||
print("Creating new sphere object.")
|
||
sphere = create_sphere(pos_vector, radius=radius, name=name, shader=shader)
|
||
self.object = sphere
|
||
|
||
def __str__(self):
|
||
return f"Airport(name={self.name}, longitude={self.longitude}, latitude={self.latitude}, x={self.x}, y={self.y}, z={self.z})"
|
||
|
||
def get_coordinates(self):
|
||
return Vector((self.x, self.y, self.z))
|
||
|
||
def get_location(self):
|
||
return Vector((self.phi, self.theta, self.height))
|
||
|
||
### World Building Functions
|
||
|
||
def parse_coordinates(coord_str: str) -> Tuple[float, float]:
|
||
"""
|
||
Parses a coordinate string in the format '15° 52′ 16″ S, 47° 55′ 7″ W'
|
||
and returns (latitude, longitude) in decimal degrees.
|
||
|
||
Args:
|
||
coord_str (str): Coordinate string.
|
||
|
||
Returns:
|
||
tuple: (latitude, longitude) in decimal degrees.
|
||
"""
|
||
# Regular expression to match DMS components and direction
|
||
pattern = r"(\d+)[°º]\s*(\d+)[′']\s*(\d+)[″\"]?\s*([NSEOW])"
|
||
matches = re.findall(pattern, coord_str)
|
||
|
||
if len(matches) != 2:
|
||
raise ValueError("Invalid coordinate format. Expecting two sets of DMS with direction.")
|
||
|
||
def dms_to_decimal(degree, minute, second, direction):
|
||
degree = int(degree)
|
||
minute = int(minute)
|
||
second = int(second)
|
||
decimal = degree + minute / 60 + second / 3600
|
||
dir = direction.upper()
|
||
|
||
# Normalize 'O' (German "Ost") to 'E' (East)
|
||
if dir == 'O':
|
||
dir = 'E'
|
||
|
||
if dir in ('S', 'W'):
|
||
return -decimal
|
||
return decimal
|
||
|
||
|
||
lat = dms_to_decimal(*matches[0])
|
||
lon = dms_to_decimal(*matches[1])
|
||
|
||
return lat, lon
|
||
|
||
def direction_from_lat_lon(latitude_deg: float, longitude_deg: float) -> Vector:
|
||
"""Convert latitude and longitude (in degrees) to a unit direction vector."""
|
||
lat = math.radians(latitude_deg)
|
||
lon = math.radians(longitude_deg)
|
||
|
||
x = math.cos(lat) * math.cos(lon)
|
||
y = math.cos(lat) * math.sin(lon)
|
||
z = math.sin(lat)
|
||
|
||
return Vector((x, y, z))
|
||
|
||
def distance_to_surface_from_latlon(
|
||
bvh: BVHTree,
|
||
origin: Vector,
|
||
latitude_deg: float,
|
||
longitude_deg: float,
|
||
max_dist=100.0
|
||
) -> float | None:
|
||
"""Cast a ray from origin in a direction defined by lat/lon and return the hit distance."""
|
||
direction = direction_from_lat_lon(latitude_deg, longitude_deg)
|
||
|
||
hit = bvh.ray_cast(origin, direction, max_dist)
|
||
|
||
if hit[0]: # hit[0] is hit location
|
||
hit_point = hit[0]
|
||
return (hit_point - origin).length
|
||
else:
|
||
return None # No hit found
|
||
|
||
def vector_to_coordinates(direction: Vector, length: float, origin: Vector = Vector((0, 0, 0))) -> Vector:
|
||
"""
|
||
Returns a 3D coordinate in space by extending a direction vector from an origin point.
|
||
|
||
Args:
|
||
direction (Vector): The direction vector (will be normalized).
|
||
length (float): Distance from the origin along the direction.
|
||
origin (Vector): Starting point (default is the origin (0, 0, 0)).
|
||
|
||
Returns:
|
||
Vector: The resulting 3D point.
|
||
"""
|
||
direction = direction.normalized()
|
||
return origin + direction * length
|
||
|
||
def create_sphere(location: Vector, radius: float, name: str, shader: bpy.types.Material | None) -> bpy.types.Object: # Create a red UV sphere at the specified location
|
||
"""
|
||
Creates a red UV sphere at the given location in the scene.
|
||
|
||
Args:
|
||
location (Vector): The location to place the sphere.
|
||
radius (float): Radius of the sphere.
|
||
name (str): Name of the sphere object and its mesh data.
|
||
"""
|
||
# Create the UV sphere
|
||
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, location=location)
|
||
sphere = bpy.context.active_object
|
||
sphere.name = name
|
||
sphere.data.name = f"{name}_Mesh"
|
||
|
||
# Create a red material if it doesn't exist
|
||
if shader == None:
|
||
mat_name = "RedMaterial"
|
||
if mat_name in bpy.data.materials:
|
||
shader = bpy.data.materials[mat_name]
|
||
else:
|
||
shader = bpy.data.materials.new(name=mat_name)
|
||
shader.diffuse_color = (1.0, 0.0, 0.0, 1.0) # RGBA red
|
||
shader.use_nodes = False
|
||
|
||
# Assign the material
|
||
if len(sphere.data.materials) == 0:
|
||
sphere.data.materials.append(shader)
|
||
else:
|
||
sphere.data.materials[0] = shader
|
||
return sphere
|
||
|
||
def create_sphere_from_coordinates(
|
||
lat_long_str: str,
|
||
name: str,
|
||
bvh: BVHTree,
|
||
origin: Vector,
|
||
radius: float,
|
||
shader: bpy.types.Material | None
|
||
) -> Tuple[Vector, bpy.types.Object]:
|
||
"""
|
||
Creates a sphere at the specified latitude and longitude on the surface of the sphere.
|
||
|
||
Args:
|
||
lat_long_str (str): Latitude and longitude in the format '15° 52′ 16″ S, 47° 55′ 7″ W'.
|
||
name (str): Name of the sphere object.
|
||
bvh (BVHTree): The BVH tree for ray casting.
|
||
origin (Vector): The origin point from which to calculate the sphere's position.
|
||
radius (float): Radius of the sphere.
|
||
shader (str | None): The shader to apply to the sphere, if any.
|
||
Returns:
|
||
bpy.types.Object: The created sphere object.
|
||
"""
|
||
lat_deg, lon_deg = parse_coordinates(lat_long_str)
|
||
print(lat_deg, lon_deg)
|
||
direction = direction_from_lat_lon(lat_deg, lon_deg)
|
||
print("direction:", direction)
|
||
distance = distance_to_surface_from_latlon(bvh, origin, lat_deg, lon_deg)
|
||
print("distance:", distance)
|
||
|
||
if distance is None:
|
||
raise ValueError(f"No surface found at latitude {lat_deg} and longitude {lon_deg}")
|
||
point = vector_to_coordinates(direction, distance, origin)
|
||
print("point:", point)
|
||
sphere = None
|
||
try:
|
||
sphere = bpy.data.objects.get(name)
|
||
if not sphere is None:
|
||
# print(f"Sphere with name '{name}' already exists.")
|
||
if not point == sphere.location:
|
||
print("Sphere already exists, but location is different. Updating location.")
|
||
sphere.location.x = point.x
|
||
sphere.location.y = point.y
|
||
sphere.location.z = point.z
|
||
else:
|
||
print(f"Sphere with name '{name}' does not exist, creating a new one.")
|
||
sphere = create_sphere(point, radius=radius, name=name, shader=shader)
|
||
except:
|
||
print("Creating new sphere object.")
|
||
sphere = create_sphere(point, radius=radius, name=name, shader=shader)
|
||
return point, sphere
|
||
|
||
def create_bvh_from_object(Object: bpy.types.Object) -> BVHTree:
|
||
"""Creates a new BVH from the main mesh.
|
||
Args:
|
||
Object (bpy.types.Object): The Object to create the BVH from.
|
||
Returns:
|
||
BVHTree: The created BVH object.
|
||
"""
|
||
main_mesh = Object.data
|
||
bm = bmesh.new()
|
||
bm.from_mesh(main_mesh)
|
||
bm.transform(Object.matrix_world)
|
||
bm.normal_update()
|
||
|
||
# Create the BVH tree from the bmesh
|
||
bvh = BVHTree.FromBMesh(bm)
|
||
|
||
# Free the bmesh to avoid memory leaks
|
||
bm.free()
|
||
return bvh
|
||
|
||
def reset_object(object: bpy.types.Object):
|
||
"""Resets the location and rotation of the main objects in the scene."""
|
||
if object is not None:
|
||
object.location = Vector((0, 0, 0))
|
||
object.rotation_euler = Euler((0, 0, 0), 'XYZ')
|
||
else:
|
||
print(f"Object {object} is None, cannot reset location and rotation.")
|
||
|
||
### Animation Functions
|
||
def align_vectors_via_xz_rotation(source: Vector, target: Vector) -> Tuple[float, float]:
|
||
"""
|
||
Aligns 'source' vector to 'target' vector using rotation around X and Z axes only.
|
||
|
||
Args:
|
||
source (Vector): The source direction vector (should be normalized).
|
||
target (Vector): The target direction vector (should be normalized).
|
||
|
||
Returns:
|
||
Tuple[float, float]: (rot_x, rot_z) in radians to align source to target.
|
||
"""
|
||
# Ensure both vectors are normalized
|
||
source = source.normalized()
|
||
target = target.normalized()
|
||
|
||
# Rotate source vector around X-axis to match target's Z component
|
||
# Compute angle between projected vectors in YZ plane
|
||
source_yz = Vector((source.y, source.z))
|
||
target_yz = Vector((target.y, target.z))
|
||
angle_x = source_yz.angle_signed(target_yz)
|
||
|
||
# Apply X rotation to source vector
|
||
temp_vec = source.copy()
|
||
temp_vec.rotate(Euler((angle_x, 0, 0), 'XYZ'))
|
||
|
||
# Rotate adjusted vector around Z-axis to match target in XY plane
|
||
temp_xy = Vector((temp_vec.x, temp_vec.y))
|
||
target_xy = Vector((target.x, target.y))
|
||
angle_z = temp_xy.angle_signed(target_xy)
|
||
|
||
return angle_x, angle_z
|
||
|
||
def apply_rotation_to_vector(
|
||
vector: Vector,
|
||
rot_x: float,
|
||
rot_z: float
|
||
) -> Vector:
|
||
"""
|
||
Applies rotation around X and Z axes to a vector.
|
||
|
||
Args:
|
||
vector (Vector): The original vector.
|
||
rot_x (float): Rotation angle around X axis in radians.
|
||
rot_z (float): Rotation angle around Z axis in radians.
|
||
|
||
Returns:
|
||
Vector: The rotated vector.
|
||
"""
|
||
vector = Vector(vector)
|
||
# Copy to avoid modifying original vector
|
||
rotated_vector = vector.copy()
|
||
|
||
# Apply rotations in XYZ order: first X, then Z
|
||
rotation = Euler((rot_x, 0.0, rot_z), 'XYZ')
|
||
rotated_vector.rotate(rotation)
|
||
|
||
return rotated_vector
|
||
|
||
def local_z_rotation_to_align(vec1: Vector, vec2: Vector, degrees=False) -> float:
|
||
"""
|
||
Calculates the rotation angle around the local Z axis of vec1 to align it with vec2.
|
||
|
||
Args:
|
||
vec1 (Vector): The source vector (must be normalized).
|
||
vec2 (Vector): The target vector (must be normalized).
|
||
degrees (bool): If True, return angle in degrees. Otherwise, radians.
|
||
|
||
Returns:
|
||
float: Angle to rotate vec1 around its local Z axis to align best with vec2.
|
||
"""
|
||
# Normalize input
|
||
v1 = vec1.normalized()
|
||
v2 = vec2.normalized()
|
||
|
||
# Project both vectors onto the XY plane (Z rotation only affects XY)
|
||
v1_xy = Vector((v1.x, v1.y)).normalized()
|
||
v2_xy = Vector((v2.x, v2.y)).normalized()
|
||
|
||
# Compute angle between projections
|
||
dot = max(-1.0, min(1.0, v1_xy.dot(v2_xy))) # Clamp for safety
|
||
angle = math.acos(dot)
|
||
|
||
# Determine rotation direction (sign) using 2D cross product
|
||
cross = v1_xy.x * v2_xy.y - v1_xy.y * v2_xy.x
|
||
if cross < 0:
|
||
angle = -angle
|
||
|
||
return math.degrees(angle) if degrees else angle
|
||
|
||
def get_point_of_triangle(v1: Vector, v2: Vector, vector1: Vector) -> Vector:
|
||
"""
|
||
Returns the point of intersection of the triangle formed by v1, v2, and vector with the XY plane.
|
||
|
||
Args:
|
||
v1 (Vector): First vertex of the triangle (with 90 degrees).
|
||
v2 (Vector): Second vertex of the triangle.
|
||
vector (Vector): Vector to 3rd vertex of the triangle.
|
||
|
||
Returns:
|
||
Vector: The point where the 3rd vertex would be.
|
||
"""
|
||
direction = vector1.normalized()
|
||
cos_alpha = v1.normalized().dot(direction)
|
||
cos_alpha = max(min(cos_alpha, 1.0), -1.0)
|
||
|
||
if abs(cos_alpha) < 1e-6:
|
||
raise ValueError("Angle too close to 90°, cannot compute third point reliably.")
|
||
|
||
v3_length = v1.length / cos_alpha
|
||
v3 = direction * v3_length
|
||
|
||
return v3
|
||
|
||
def clear_timeline(objects: List[bpy.types.Object]) -> None:
|
||
"""
|
||
Clears keyframes for 'rotation_euler' and 'location.z' from the given objects,
|
||
and resets those values to 0.0.
|
||
|
||
Args:
|
||
objects (List[bpy.types.Object]): List of objects to clear keyframes from.
|
||
"""
|
||
for obj in objects:
|
||
if obj.animation_data and obj.animation_data.action:
|
||
action = obj.animation_data.action
|
||
fcurves_to_remove = [
|
||
fc for fc in action.fcurves
|
||
if fc.data_path == "rotation_euler"
|
||
or (fc.data_path == "location" and fc.array_index == 2)
|
||
]
|
||
for fc in fcurves_to_remove:
|
||
action.fcurves.remove(fc)
|
||
|
||
# Reset rotation_euler (X, Y, Z) to 0
|
||
obj.rotation_euler = (0.0, 0.0, 0.0)
|
||
|
||
# Reset location.z to 0, keep x and y unchanged
|
||
loc = obj.location
|
||
obj.location = (loc.x, loc.y, 0.0)
|
||
|
||
if __name__ == "__main__":
|
||
|
||
### Initialization
|
||
|
||
main_sphere = bpy.data.objects["Sphere.002"]
|
||
|
||
origin: Vector = main_sphere.matrix_world.translation
|
||
|
||
bvh=create_bvh_from_object(main_sphere)
|
||
|
||
### Setting up world
|
||
|
||
airport_shader: bpy.types.Material | None = None # Placeholder for shader, can be set to a specific material if needed
|
||
|
||
# flughafen1_vec, flughafen1_obj = create_sphere_from_coordinates(
|
||
# lat_long_str="52° 21′ 44″ N, 13° 30′ 2″ O",
|
||
# name="BER1",
|
||
# bvh=bvh,
|
||
# origin=origin,
|
||
# radius=0.1,
|
||
# shader=airport_shader
|
||
# )
|
||
|
||
flughafen1 = Airport(
|
||
name="BER2",
|
||
lon_lat="52° 21′ 44″ N, 13° 30′ 2″ O",
|
||
bvh=bvh,
|
||
origin=origin,
|
||
radius=0.1,
|
||
shader=airport_shader
|
||
)
|
||
|
||
flughafen2 = Airport(
|
||
name="HEL",
|
||
lon_lat="60° 19′ 2″ N, 24° 57′ 48″ O",
|
||
bvh=bvh,
|
||
origin=origin,
|
||
radius=0.1,
|
||
shader=airport_shader
|
||
)
|
||
|
||
flughafen3 = Airport(
|
||
name="NRT",
|
||
lon_lat="35° 45′ 53″ N, 140° 23′ 11″ O",
|
||
bvh=bvh,
|
||
origin=origin,
|
||
radius=0.1,
|
||
shader=airport_shader
|
||
)
|
||
|
||
### Initialize Animation
|
||
|
||
airplane = bpy.data.objects["Airplane"]
|
||
# airplane_vector = Vector((0, 1, 0)) # Setting the airplane default vector (x, y, z)
|
||
# reset_object(airplane) # Resetting the airplane location and rotation
|
||
|
||
centerjoint = bpy.data.objects["Center_Joint"]
|
||
# centerjoint_vectors = Vector((0, 0, 1)) # Setting the center joint default vector (x, y, z)
|
||
# reset_object(centerjoint) # Resetting the center joint location and rotation
|
||
|
||
|
||
### Animation Logic
|
||
# Way between airport1 and airport2
|
||
for airport in [flughafen1, flughafen2, flughafen3]:
|
||
print("lat: ", airport.latitude, "lon: ", airport.longitude)
|
||
lat=[
|
||
math.radians(flughafen1.latitude - 90),
|
||
math.radians(flughafen2.latitude - 90),
|
||
math.radians(flughafen3.latitude - 90)
|
||
]
|
||
print("lat:", lat)
|
||
|
||
lon=[
|
||
math.radians(flughafen1.longitude-90),
|
||
math.radians(flughafen2.longitude-90),
|
||
math.radians(flughafen3.longitude-90)
|
||
]
|
||
print("lon:", lon)
|
||
|
||
airplane_height = [
|
||
flughafen1.get_coordinates().length + 0.1,
|
||
flughafen2.get_coordinates().length + 0.1,
|
||
flughafen3.get_coordinates().length + 0.1
|
||
]
|
||
print("airplane_height:", airplane_height)
|
||
|
||
travel_height = airplane_height[0] * 1.1
|
||
print("travel_height:", travel_height)
|
||
|
||
airplane_rotation = [
|
||
math.radians(-35),
|
||
math.radians(-42),
|
||
math.radians(-111),
|
||
math.radians(-102)
|
||
]
|
||
|
||
frame_count = 720
|
||
|
||
clear_timeline([airplane, centerjoint])
|
||
|
||
airplane.rotation_euler.x = math.radians(90)
|
||
|
||
### Start BER
|
||
bpy.context.scene.frame_set(0)
|
||
airplane.location.z = airplane_height[0]
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
airplane.rotation_euler.z = airplane_rotation[0]
|
||
airplane.keyframe_insert(data_path="rotation_euler", index=2)
|
||
|
||
centerjoint.rotation_euler.x = lat[0]
|
||
centerjoint.rotation_euler.z = lon[0]
|
||
centerjoint.keyframe_insert(data_path="rotation_euler", index=-1)
|
||
|
||
### flight height start
|
||
bpy.context.scene.frame_set(32)
|
||
airplane.location.z = travel_height
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
|
||
### End of high flight
|
||
bpy.context.scene.frame_set(49)
|
||
airplane.location.z = travel_height
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
|
||
### Landing HEL
|
||
bpy.context.scene.frame_set(81)
|
||
airplane.location.z = airplane_height[1]
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
airplane.rotation_euler.z = airplane_rotation[1]
|
||
airplane.keyframe_insert(data_path="rotation_euler", index=2)
|
||
|
||
centerjoint.rotation_euler.x = lat[1]
|
||
centerjoint.rotation_euler.z = lon[1]
|
||
centerjoint.keyframe_insert(data_path="rotation_euler", index=-1)
|
||
|
||
### Start HEL
|
||
bpy.context.scene.frame_set(127)
|
||
airplane.location.z = airplane_height[1]
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
airplane.rotation_euler.z = airplane_rotation[2]
|
||
airplane.keyframe_insert(data_path="rotation_euler", index=2)
|
||
|
||
centerjoint.rotation_euler.x = lat[1]
|
||
centerjoint.rotation_euler.z = lon[1]
|
||
centerjoint.keyframe_insert(data_path="rotation_euler", index=-1)
|
||
|
||
### flight height start
|
||
bpy.context.scene.frame_set(255)
|
||
airplane.location.z = travel_height
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
|
||
### End of high flight
|
||
bpy.context.scene.frame_set(592)
|
||
airplane.location.z = travel_height
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
|
||
### Landing NRT
|
||
bpy.context.scene.frame_set(720)
|
||
airplane.location.z = airplane_height[2]
|
||
airplane.keyframe_insert(data_path="location", index=2)
|
||
airplane.rotation_euler.z = airplane_rotation[3]
|
||
airplane.keyframe_insert(data_path="rotation_euler", index=2)
|
||
|
||
centerjoint.rotation_euler.x = lat[2]
|
||
centerjoint.rotation_euler.z = lon[2]
|
||
centerjoint.keyframe_insert(data_path="rotation_euler", index=-1)
|
||
|
||
### reset
|
||
bpy.context.scene.frame_set(0)
|
||
|
||
print("END") |