1# OpenShot Video Editor is a program that creates, modifies, and edits video files.
2#   Copyright (C) 2009  Jonathan Thomas
3#
4# This file is part of OpenShot Video Editor (http://launchpad.net/openshot/).
5#
6# OpenShot Video Editor is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# OpenShot Video Editor is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with OpenShot Video Editor.  If not, see <http://www.gnu.org/licenses/>.
18
19
20# Import Blender's python API.  This only works when the script is being
21# run from the context of Blender.  Blender contains it's own version of Python
22# with this library pre-installed.
23import math
24import bpy
25import json
26
27
28def load_font(font_path):
29    """ Load a new TTF font into Blender, and return the font object """
30    # get the original list of fonts (before we add a new one)
31    original_fonts = bpy.data.fonts.keys()
32
33    # load new font
34    bpy.ops.font.open(filepath=font_path)
35
36    # get the new list of fonts (after we added a new one)
37    for font_name in bpy.data.fonts.keys():
38        if font_name not in original_fonts:
39            return bpy.data.fonts[font_name]
40
41    # no new font was added
42    return None
43
44# Debug Info:
45# ./blender -b test.blend -P demo.py
46# -b = background mode
47# -P = run a Python script within the context of the project file
48
49
50# Init all of the variables needed by this script.  Because Blender executes
51# this script, OpenShot will inject a dictionary of the required parameters
52# before this script is executed.
53params = {
54    'extrude': 0.1,
55    'bevel_depth': 0.02,
56    'spacemode': 'CENTER',
57    'text_size': 1.5,
58    'width': 1.0,
59    'fontname': 'Bfont',
60
61    'color': [0.8, 0.8, 0.8],
62    'alpha': 1.0,
63
64    'output_path': '/tmp/',
65    'fps': 24,
66    'quality': 90,
67    'file_format': 'PNG',
68    'color_mode': 'RGBA',
69    'horizon_color': [0.57, 0.57, 0.57],
70    'resolution_x': 1920,
71    'resolution_y': 1080,
72    'resolution_percentage': 100,
73    'start_frame': 1,
74    'end_frame': 250,
75    'length_multiplier': 1,
76
77    'depart_title': 'Paris',
78    'depart_lat_deg': 48,
79    'depart_lat_min': 51,
80    'depart_lat_sec': 24,
81    'depart_lat_dir': "N",
82    'depart_lon_deg': 2,
83    'depart_lon_min': 21,
84    'depart_lon_sec': 7,
85    'depart_lon_dir': "E",
86
87    'arrive_title': 'New York',
88    'arrive_lat_deg': 40,
89    'arrive_lat_min': 42,
90    'arrive_lat_sec': 51,
91    'arrive_lat_dir': "N",
92    'arrive_lon_deg': 74,
93    'arrive_lon_min': 0,
94    'arrive_lon_sec': 23,
95    'arrive_lon_dir': "E",
96}
97
98
99# INJECT_PARAMS_HERE
100
101# The remainder of this script will modify the current Blender .blend project
102# file, and adjust the settings.  The .blend file is specified in the XML file
103# that defines this template in OpenShot.
104# ----------------------------------------------------------------------------
105
106# Process parameters supplied as JSON serialization
107try:
108    injected_params = json.loads(params_json)
109    params.update(injected_params)
110except NameError:
111    pass
112
113
114depart = {
115    "lat_deg": params["depart_lat_deg"],
116    "lat_min": params["depart_lat_min"],
117    "lat_sec": params["depart_lat_sec"],
118    "lat_dir": params["depart_lat_dir"],
119
120    "lon_deg": params["depart_lon_deg"],
121    "lon_min": params["depart_lon_min"],
122    "lon_sec": params["depart_lon_sec"],
123    "lon_dir": params["depart_lon_dir"],
124}
125
126arrive = {
127    "lat_deg": params["arrive_lat_deg"],
128    "lat_min": params["arrive_lat_min"],
129    "lat_sec": params["arrive_lat_sec"],
130    "lat_dir": params["arrive_lat_dir"],
131
132    "lon_deg": params["arrive_lon_deg"],
133    "lon_min": params["arrive_lon_min"],
134    "lon_sec": params["arrive_lon_sec"],
135    "lon_dir": params["arrive_lon_dir"],
136}
137
138
139def get_latitude(direction, degrees, minutes, seconds):
140    lat = degrees + minutes / 60.0 + seconds / 3600.0
141    if direction == "N":
142        # North of the equator
143        return -lat
144    else:
145        # South of the equator
146        return lat
147
148
149def get_longitude(direction, degrees, minutes, seconds):
150    lon = degrees + minutes / 60.0 + seconds / 3600.0
151    if direction == "E":
152        # East of prime meridian
153        return lon
154    else:
155        # West of prime meridian
156        return -lon
157
158
159def correct_longitude(depart_longitude, arrive_longitude):
160    if -180 < (arrive_longitude - depart_longitude) < 180:
161        return depart_longitude
162    else:
163        if depart_longitude < 0:
164            return depart_longitude + 360
165        else:
166            return depart_longitude - 360
167
168
169class Point:
170    def __init__(self, x, y, z):
171        self.x = x
172        self.y = y
173        self.z = z
174
175
176class Coord(Point):
177    def __init__(self, latitude, longitude, radius):
178        self.lat = latitude
179        self.lon = longitude
180        self.radius = radius
181        p_x = (
182            radius
183            * math.cos(math.radians(latitude))
184            * math.sin(math.radians(longitude))
185            )
186        p_y = (
187            radius
188            * math.cos(math.radians(latitude))
189            * math.cos(math.radians(longitude))
190        )
191        p_z = (radius * math.sin(math.radians(latitude)))
192        super().__init__(p_x, p_y, p_z)
193
194
195# Calculate latitude / longitude for depart and arrive points
196sphere_radius = 10.0
197point_a = Coord(
198    get_latitude(
199        depart["lat_dir"], depart["lat_deg"],
200        depart["lat_min"], depart["lat_sec"]),
201    get_longitude(
202        depart["lon_dir"], depart["lon_deg"],
203        depart["lon_min"], depart["lon_sec"]),
204    sphere_radius
205)
206point_b = Coord(
207    get_latitude(
208        arrive["lat_dir"], arrive["lat_deg"],
209        arrive["lat_min"], arrive["lat_sec"]),
210    get_longitude(
211        arrive["lon_dir"], arrive["lon_deg"],
212        arrive["lon_min"], arrive["lon_sec"]),
213    sphere_radius
214)
215
216# Correct longitude if necessary
217orig_point_a_lon = point_a.lon
218point_a.lon = correct_longitude(orig_point_a_lon, point_b.lon)
219
220# Get angle between A & B points
221ab_angle_radians = math.acos(
222    (point_a.x * point_b.x + point_a.y * point_b.y + point_a.z * point_b.z)
223    / (sphere_radius * sphere_radius)
224)
225ab_angle_degrees = ab_angle_radians * 180 / math.pi
226
227# calculate points C & D
228point_c = Coord(
229    point_a.lat + 0.25 * (point_b.lat - point_a.lat),
230    point_a.lon + 0.25 * (point_b.lon - point_a.lon),
231    sphere_radius
232)
233point_d = Coord(
234    point_a.lat + 0.75 * (point_b.lat - point_a.lat),
235    point_a.lon + 0.75 * (point_b.lon - point_a.lon),
236    sphere_radius
237)
238
239# radius of CD line segment
240location_CD = (sphere_radius + 1.0) / math.cos(ab_angle_radians / 4.0)
241
242print("EmptyPointA Transform Rotation: Y= %f Z= %f" % (point_a.lat, point_a.lon))
243print("EmptyPointB Transform Rotation: Y= %f Z= %f" % (point_b.lat, point_b.lon))
244print("EmptyPointC Transform Rotation: Y= %f Z= %f" % (point_c.lat, point_c.lon))
245print("EmptyPointD Transform Rotation: Y= %f Z= %f" % (point_d.lat, point_d.lon))
246print("EmptyPointC.001 Transform Location: X= %f" % location_CD)
247print("EmptyPointD.001 Transform Location: X= %f" % location_CD)
248print("EmptyCam Frame 20 ->Transform Rotation: Y= %f Z= %f" % (point_a.lat, point_a.lon))
249print("EmptyCam Frame 80 ->Transform Rotation: Y= %f Z= %f" % (point_b.lat, point_b.lon))
250
251# Set Blender properties
252for pname, point in [
253        ("EmptyPointA", point_a),
254        ("EmptyPointB", point_b),
255        ("EmptyPointC", point_c),
256        ("EmptyPointD", point_d),
257        ]:
258    bpy.data.objects[pname].rotation_euler = (
259        0.0, math.radians(point.lat), math.radians(point.lon)
260    )
261bpy.data.objects["EmptyPointC.001"].location.x = location_CD
262bpy.data.objects["EmptyPointD.001"].location.x = location_CD
263
264
265def update_curve(curve, start, end):
266    coords = [
267        (20.0, start),
268        (80.0, end),
269        ]
270    for i, coord in enumerate(coords):
271        p = curve.keyframe_points[i]
272        p.co = coord
273        p.handle_left.y = coord[1]
274        p.handle_right.y = coord[1]
275
276
277# set Y rotation on the camera
278action = bpy.data.actions["EmptyCamAction"]
279update_curve(
280    action.fcurves[1], math.radians(point_a.lat), math.radians(point_b.lat))
281update_curve(
282    action.fcurves[2], math.radians(point_a.lon), math.radians(point_b.lon))
283
284# set world texture (i.e. the globe texture)
285if params["map_texture"]:
286    bpy.data.textures["Texture.002"].image.filepath = params["map_texture"]
287
288# Get font object
289font = None
290if params["fontname"] != "Bfont":
291    # Add font so it's available to Blender
292    font = load_font(params["fontname"])
293else:
294    # Get default font
295    font = bpy.data.fonts["Bfont"]
296
297# Modify Text for Departure
298text_object = bpy.data.curves["Text"]
299text_object.body = params["depart_title"]
300
301# Modify Text for Arrival
302text_object = bpy.data.curves["Text.001"]
303text_object.body = params["arrive_title"]
304
305# Set common text parameters
306for ob in [bpy.data.curves["Text"], bpy.data.curves["Text.001"]]:
307    ob.extrude = params["extrude"]
308    ob.bevel_depth = params["bevel_depth"]
309    ob.size = params["text_size"]
310    ob.space_character = params["width"]
311    ob.font = font
312
313# Modify the Materials for Text, Lines, and Pins
314for material in [
315        "Material.001",
316        "Material.002",
317        "Material.003",
318        "Material.004",
319        "Material.005",
320        ]:
321    ob = bpy.data.materials[material]
322    ob.diffuse_color = params["diffuse_color"]
323    ob.specular_color = params["specular_color"]
324    ob.specular_intensity = params["specular_intensity"]
325
326# Set the render options.  It is important that these are set
327# to the same values as the current OpenShot project.  These
328# params are automatically set by OpenShot
329render = bpy.context.scene.render
330render.filepath = params["output_path"]
331render.fps = params["fps"]
332if "fps_base" in params:
333    render.fps_base = params["fps_base"]
334render.image_settings.file_format = params["file_format"]
335render.image_settings.color_mode = params["color_mode"]
336render.resolution_x = params["resolution_x"]
337render.resolution_y = params["resolution_y"]
338render.resolution_percentage = params["resolution_percentage"]
339
340# Animation Speed (use Blender's time remapping to slow or speed up animation)
341length_multiplier = int(params["length_multiplier"])  # time remapping multiplier
342new_length = int(params["end_frame"]) * length_multiplier  # new length (in frames)
343render.frame_map_old = 1
344render.frame_map_new = length_multiplier
345
346# Set render length/position
347bpy.context.scene.frame_start = params["start_frame"]
348bpy.context.scene.frame_end = new_length
349