mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 07:07:58 +00:00
Initial commit for updated addon
This commit is contained in:
44
blender-addon/__init__.py
Normal file
44
blender-addon/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
|
||||
from . operators.exporter import *
|
||||
|
||||
bl_info = {
|
||||
"name": "Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"author": "Artem Vasiunik",
|
||||
"version": (0, 4, 4),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"description": "Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"warning": "",
|
||||
"wiki_url": "https://github.com/artem30801/blender-csv-animation/blob/master/README.md",
|
||||
"tracker_url": "https://github.com/artem30801/blender-csv-animation/issues",
|
||||
"category": "Import-Export"
|
||||
}
|
||||
|
||||
classes = (ExportCsv, )
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
self.layout.operator(
|
||||
ExportCsv.bl_idname,
|
||||
text="CSV Drone Swarm Animation Exporter (.csv)"
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
unregister_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -1,213 +0,0 @@
|
||||
import os
|
||||
import csv
|
||||
import math
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty
|
||||
|
||||
bl_info = {
|
||||
"name": "Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"author": "Artem Vasiunik",
|
||||
"version": (0, 4, 0),
|
||||
"blender": (2, 80, 0),
|
||||
#"api": 36079,
|
||||
"location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"description": "Export > CSV Drone Swarm Animation Exporter (.csv)",
|
||||
"warning": "",
|
||||
"wiki_url": "https://github.com/artem30801/blender-csv-animation/blob/master/README.md",
|
||||
"tracker_url": "https://github.com/artem30801/blender-csv-animation/issues",
|
||||
"category": "Import-Export"
|
||||
}
|
||||
|
||||
|
||||
class ExportCsv(Operator, ExportHelper):
|
||||
bl_idname = "export_swarm_anim.folder"
|
||||
bl_label = "Export Drone Swarm animation"
|
||||
filename_ext = ''
|
||||
use_filter_folder = True
|
||||
|
||||
use_namefilter: bpy.props.BoolProperty(
|
||||
name="Use name filter for objects",
|
||||
default=True,
|
||||
)
|
||||
|
||||
drones_name: bpy.props.StringProperty(
|
||||
name="Name identifier",
|
||||
description="Name identifier for all drone objects",
|
||||
default="copter"
|
||||
)
|
||||
|
||||
show_warnings: bpy.props.BoolProperty(
|
||||
name="Show detailed animation warnings",
|
||||
default=False,
|
||||
)
|
||||
|
||||
speed_warning_limit: bpy.props.FloatProperty(
|
||||
name="Speed limit",
|
||||
description="Limit of drone movement speed (m/s)",
|
||||
unit='VELOCITY',
|
||||
default=3,
|
||||
min=0,
|
||||
)
|
||||
drone_distance_limit: bpy.props.FloatProperty(
|
||||
name="Distance limit",
|
||||
description="Closest possible distance between drones (m)",
|
||||
unit='LENGTH',
|
||||
default=1.5,
|
||||
min=0,
|
||||
)
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="File path used for exporting CSV files",
|
||||
maxlen=1024,
|
||||
subtype='DIR_PATH',
|
||||
default=""
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
create_folder_if_does_not_exist(self.filepath)
|
||||
scene = context.scene
|
||||
objects = context.visible_objects
|
||||
|
||||
drone_objects = []
|
||||
if self.use_namefilter:
|
||||
for drone_obj in objects:
|
||||
if self.drones_name.lower() in drone_obj.name.lower():
|
||||
drone_objects.append(drone_obj)
|
||||
else:
|
||||
drone_objects = objects
|
||||
|
||||
frame_start = scene.frame_start
|
||||
frame_end = scene.frame_end
|
||||
|
||||
for drone_obj in drone_objects:
|
||||
with open(os.path.join(self.filepath, '{}.csv'.format(drone_obj.name.lower())), 'w') as csv_file:
|
||||
animation_file_writer = csv.writer(
|
||||
csv_file,
|
||||
delimiter=',',
|
||||
quotechar='|',
|
||||
quoting=csv.QUOTE_MINIMAL
|
||||
)
|
||||
speed_exeeded = False
|
||||
distance_exeeded = False
|
||||
|
||||
prev_x, prev_y, prev_z = 0, 0, 0
|
||||
|
||||
animation_file_writer.writerow([
|
||||
os.path.splitext(bpy.path.basename(bpy.data.filepath))[0]
|
||||
])
|
||||
|
||||
for frame_number in range(frame_start, frame_end + 1):
|
||||
scene.frame_set(frame_number)
|
||||
rgb = get_rgb_from_object(drone_obj)
|
||||
x, y, z = drone_obj.matrix_world.to_translation()
|
||||
rot_z = drone_obj.matrix_world.to_euler('XYZ')[2]
|
||||
|
||||
speed = calc_speed((x, y, z), (prev_x, prev_y, prev_z)) if frame_number != frame_start else 1
|
||||
prev_x, prev_y, prev_z = x, y, z
|
||||
|
||||
if speed > self.speed_warning_limit:
|
||||
speed_exeeded = True
|
||||
if self.show_warnings:
|
||||
self.report({'WARNING'},
|
||||
"Speed of drone '%s' is greater than %s m/s (%s m/s) on frame %s" %
|
||||
(drone_obj.name, round(self.speed_warning_limit, 5), round(speed, 5), frame_number))
|
||||
|
||||
for second_drone_obj in drone_objects:
|
||||
if second_drone_obj is not drone_obj:
|
||||
x2, y2, z2 = second_drone_obj.matrix_world.to_translation()
|
||||
distance = calc_distance((x, y, z), (x2, y2, z2))
|
||||
if distance < self.drone_distance_limit:
|
||||
distance_exeeded = True
|
||||
if self.show_warnings:
|
||||
self.report({'WARNING'},
|
||||
"Distance beteween drones '%s' and '%s' is less than %s m (%s m) on frame %s" %
|
||||
(drone_obj.name, second_drone_obj.name,
|
||||
round(self.drone_distance_limit, 5), round(distance, 5), frame_number))
|
||||
|
||||
animation_file_writer.writerow([
|
||||
str(frame_number),
|
||||
round(x, 5), round(y, 5), round(z, 5),
|
||||
round(rot_z, 5),
|
||||
*rgb,
|
||||
])
|
||||
|
||||
|
||||
|
||||
if speed_exeeded:
|
||||
self.report({'WARNING'}, "Drone '%s' speed limits exeeded" % drone_obj.name)
|
||||
if distance_exeeded:
|
||||
self.report({'WARNING'}, "Drone '%s' distance limits exeeded" % drone_obj.name)
|
||||
self.report({'WARNING'}, "Animation file exported for drone '%s'" % drone_obj.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def create_folder_if_does_not_exist(folder_path):
|
||||
if os.path.isdir(folder_path):
|
||||
return
|
||||
os.mkdir(folder_path)
|
||||
|
||||
|
||||
def get_rgb_from_object(obj):
|
||||
rgb = [0, 0, 0]
|
||||
try:
|
||||
if len(obj.material_slots) > 0:
|
||||
print('material slots true')
|
||||
for slot in obj.material_slots:
|
||||
if "led_color" in slot.name.lower():
|
||||
print('led color')
|
||||
if slot.material.use_nodes:
|
||||
for node in slot.material.node_tree.nodes:
|
||||
if node.type in ('EMISSION', 'BSDF_DIFFUSE'):
|
||||
alpha = node.inputs[0].default_value[3]
|
||||
for component in range(3):
|
||||
rgb[component] = int(node.inputs[0].default_value[component] * alpha * 255)
|
||||
else:
|
||||
print('no led color')
|
||||
for component in range(3):
|
||||
rgb[component] = int(slot.material.diffuse_color[component] * 255)
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
return rgb
|
||||
|
||||
|
||||
def calc_speed(start_point, end_point):
|
||||
time_delta = 0.1
|
||||
distance = calc_distance(start_point, end_point)
|
||||
return distance / time_delta
|
||||
|
||||
|
||||
def calc_distance(start_point, end_point):
|
||||
distance = math.sqrt(
|
||||
(start_point[0] - end_point[0]) ** 2 +
|
||||
(start_point[1] - end_point[1]) ** 2 +
|
||||
(start_point[2] - end_point[2]) ** 2
|
||||
)
|
||||
return distance
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
self.layout.operator(
|
||||
ExportCsv.bl_idname,
|
||||
text="CSV Drone Swarm Animation Exporter (.csv)"
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(ExportCsv)
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(ExportCsv)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
0
blender-addon/operators/drone_manipulation.py
Normal file
0
blender-addon/operators/drone_manipulation.py
Normal file
278
blender-addon/operators/exporter.py
Normal file
278
blender-addon/operators/exporter.py
Normal file
@@ -0,0 +1,278 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import csv
|
||||
import json
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty
|
||||
|
||||
|
||||
from . general_functions import *
|
||||
|
||||
|
||||
class ExportCsv(Operator, ExportHelper):
|
||||
bl_idname = "export_swarm_anim.folder"
|
||||
bl_label = "Export Drone Swarm animation"
|
||||
filename_ext = ''
|
||||
use_filter_folder = True
|
||||
'''
|
||||
|
||||
filter_obj: bpy.props.BoolProperty(
|
||||
name="Use name filter for objects",
|
||||
default=True,
|
||||
)
|
||||
'''
|
||||
filter_obj: bpy.props.EnumProperty(
|
||||
name="Filter objects:",
|
||||
description="",
|
||||
items=[('all', "No filter (all objects)", ""),
|
||||
('selected', "Only selected", ""),
|
||||
('name', "By object name", ""),
|
||||
('prop', "By object property", ""),
|
||||
],
|
||||
default="name"
|
||||
)
|
||||
|
||||
drones_name: bpy.props.StringProperty(
|
||||
name="Name identifier",
|
||||
description="Name identifier for all drone objects",
|
||||
default="drone"
|
||||
)
|
||||
|
||||
show_warnings: bpy.props.BoolProperty(
|
||||
name="Show detailed animation warnings",
|
||||
default=False,
|
||||
)
|
||||
|
||||
speed_warning_limit: bpy.props.FloatProperty(
|
||||
name="Speed limit",
|
||||
description="Limit of drone movement speed (m/s)",
|
||||
unit='VELOCITY',
|
||||
default=3,
|
||||
min=0,
|
||||
)
|
||||
|
||||
drone_distance_limit: bpy.props.FloatProperty(
|
||||
name="Distance limit",
|
||||
description="Closest possible distance between drones (m)",
|
||||
unit='LENGTH',
|
||||
default=1.5,
|
||||
min=0,
|
||||
)
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="File path used for exporting CSV files",
|
||||
maxlen=1024,
|
||||
subtype='DIR_PATH',
|
||||
default=""
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.label(text="Filtering properties")
|
||||
col.prop(self, "filter_obj")
|
||||
if self.filter_obj == "name":
|
||||
col.prop(self, "drones_name")
|
||||
col.separator()
|
||||
|
||||
col = layout.column()
|
||||
col.label(text="Limitation and warning properties")
|
||||
col.prop(self, "show_warnings")
|
||||
col.prop(self, "speed_warning_limit")
|
||||
col.prop(self, "drone_distance_limit")
|
||||
# TODO check button (operator)
|
||||
|
||||
def get_drone_objects(self, context):
|
||||
if self.filter_obj == "all":
|
||||
return context.visible_objects
|
||||
|
||||
if self.filter_obj == "selected":
|
||||
return context.selected_objects
|
||||
|
||||
if self.filter_obj == "name":
|
||||
objects = context.visible_objects
|
||||
return list(filter(lambda x: self.drones_name.lower() in x.name.lower(), objects))
|
||||
|
||||
if self.filter_obj == "prop":
|
||||
objects = context.visible_objects
|
||||
return list(filter(lambda x: x.get("is_drone", False), objects))
|
||||
|
||||
print("Invalid input")
|
||||
|
||||
def execute(self, context):
|
||||
create_missing_dir(self.filepath)
|
||||
|
||||
drone_objects = self.get_drone_objects(context)
|
||||
|
||||
frame_start = context.scene.frame_start
|
||||
frame_end = context.scene.frame_end
|
||||
|
||||
for drone_obj in drone_objects:
|
||||
|
||||
speed_exceeded = False
|
||||
distance_exceeded = False
|
||||
|
||||
context.scene.frame_set(frame_start)
|
||||
prev_point = get_position(drone_obj)
|
||||
|
||||
anim_frames = []
|
||||
|
||||
for frame_num in range(frame_start, frame_end + 1):
|
||||
context.scene.frame_set(frame_num)
|
||||
|
||||
rgb = get_rgb(drone_obj)
|
||||
point = drone_obj.matrix_world.to_translation()
|
||||
rot_z = drone_obj.matrix_world.to_euler('XYZ')[2]
|
||||
props = get_drone_properties(drone_obj)
|
||||
|
||||
speed = calc_speed(point, prev_point)
|
||||
speed_exceeded += self.check_speed(drone_obj, speed, frame_num)
|
||||
distance_exceeded += self.check_distances(drone_obj, drone_objects, frame_num)
|
||||
|
||||
row = (
|
||||
int(frame_num),
|
||||
round(point[0], 5), round(point[1], 5), round(point[2], 5),
|
||||
round(rot_z, 5),
|
||||
rgb[0], rgb[1], rgb[2],
|
||||
form_props(props),
|
||||
)
|
||||
anim_frames.append(row)
|
||||
|
||||
prev_point = point
|
||||
|
||||
if speed_exceeded:
|
||||
self.report({'WARNING'}, "Drone '{}' speed limits exceeded".format(drone_obj.name))
|
||||
if distance_exceeded:
|
||||
self.report({'WARNING'}, "Drone '{}' distance limits exceeded".format(drone_obj.name))
|
||||
|
||||
header = form_header({"name": drone_obj.name.lower(),
|
||||
"file": os.path.splitext(bpy.path.basename(bpy.data.filepath))[0],
|
||||
"fps": context.scene.render.fps,
|
||||
"version": get_addon_version(),
|
||||
})
|
||||
|
||||
self.write_csv(anim_frames, header, drone_obj.name.lower())
|
||||
|
||||
self.report({'WARNING'}, "Animation file exported for drone '{}'".format(drone_obj.name))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def check_speed(self, drone_obj, speed, frame="Not specified"): # TODO extract from class, add decorator
|
||||
if speed > self.speed_warning_limit:
|
||||
if self.show_warnings:
|
||||
self.report({'WARNING'},
|
||||
"Speed of drone '{}' is greater than limit of {.3f} m/s ({.3f} m/s) on frame {}".format(
|
||||
drone_obj.name,
|
||||
self.speed_warning_limit, speed,
|
||||
frame,
|
||||
))
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_distances(self, drone, drone_objects: list, frame=0):
|
||||
_drone_objects = drone_objects.copy()
|
||||
if drone in _drone_objects:
|
||||
_drone_objects.remove(drone)
|
||||
|
||||
close_drones = filter(lambda drone2:
|
||||
get_distance(drone, drone2) < self.drone_distance_limit,
|
||||
_drone_objects)
|
||||
|
||||
if close_drones:
|
||||
if self.show_warnings:
|
||||
for err_drone in close_drones:
|
||||
distance = calc_distance(get_position(drone), get_position(err_drone))
|
||||
self.report({'WARNING'},
|
||||
"Distance between drones '{}' and '{}'"
|
||||
" is less than {.3f} m ({.3f} m) on frame {.3f}".format(
|
||||
drone.name, err_drone.name,
|
||||
self.drone_distance_limit, distance,
|
||||
frame
|
||||
))
|
||||
return True
|
||||
return False
|
||||
|
||||
def write_csv(self, contents, header, name):
|
||||
with open(os.path.join(self.filepath, '{}.csv'.format(name)), 'w') as csv_file:
|
||||
anim_writer = csv.writer(
|
||||
csv_file,
|
||||
delimiter=',',
|
||||
quotechar='|',
|
||||
quoting=csv.QUOTE_MINIMAL
|
||||
)
|
||||
anim_writer.writerow([header])
|
||||
anim_writer.writerows(contents)
|
||||
|
||||
|
||||
def create_missing_dir(folder_path):
|
||||
if not os.path.isdir(folder_path):
|
||||
os.mkdir(folder_path)
|
||||
|
||||
|
||||
def form_header(d: dict):
|
||||
header = json.dumps(d)
|
||||
return header
|
||||
|
||||
|
||||
def form_props(d: dict):
|
||||
props = json.dumps(d)
|
||||
return props
|
||||
|
||||
|
||||
def get_rgb(drone):
|
||||
rgb = [0, 0, 0]
|
||||
try:
|
||||
slot = next(filter(lambda x: "led_color" in x.name.lower(),
|
||||
drone.material_slots))
|
||||
except StopIteration:
|
||||
print("No matching slots")
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
material = slot.material
|
||||
if material.use_nodes:
|
||||
print('Node led color')
|
||||
value = get_node_color(material)
|
||||
|
||||
else:
|
||||
print('Material led color')
|
||||
value = material.diffuse_color
|
||||
|
||||
alpha = value[3]
|
||||
rgb = [int(value[component] * alpha * 255) for component in range(3)]
|
||||
except AttributeError:
|
||||
print("Missing attributes")
|
||||
pass
|
||||
|
||||
finally:
|
||||
return rgb
|
||||
|
||||
|
||||
def get_node_color(material):
|
||||
try:
|
||||
node = next(filter(lambda x: x.type in ('EMISSION', 'BSDF_DIFFUSE', "Principled BSDF"),
|
||||
material.node_tree.nodes))
|
||||
except StopIteration:
|
||||
print("No matching nodes")
|
||||
raise AttributeError("No matching nodes")
|
||||
else:
|
||||
return node.inputs[0].default_value
|
||||
|
||||
|
||||
def get_addon_version():
|
||||
mod = sys.modules["blender-csv-animation"]
|
||||
return mod.bl_info.get('version', (-1, -1, -1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
48
blender-addon/operators/general_functions.py
Normal file
48
blender-addon/operators/general_functions.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import math
|
||||
|
||||
|
||||
def calc_distance(start_point, end_point):
|
||||
distance = math.sqrt(
|
||||
(start_point[0] - end_point[0]) ** 2 +
|
||||
(start_point[1] - end_point[1]) ** 2 +
|
||||
(start_point[2] - end_point[2]) ** 2
|
||||
)
|
||||
return distance
|
||||
|
||||
|
||||
def calc_speed(start_point, end_point, fps=10):
|
||||
time_delta = 1/fps
|
||||
distance = calc_distance(start_point, end_point)
|
||||
return distance / time_delta
|
||||
|
||||
|
||||
def get_position(drone):
|
||||
return drone.matrix_world.to_translation()
|
||||
|
||||
|
||||
def get_distance(drone1, drone2):
|
||||
point1 = get_position(drone1)
|
||||
point2 = get_position(drone2)
|
||||
|
||||
return calc_distance(point1, point2)
|
||||
|
||||
|
||||
def get_drone_properties(drone):
|
||||
return dict(filter(lambda x: x[0].lower().startswith("drone_"), drone.items()))
|
||||
|
||||
|
||||
def add_bool_property(obj, name, description="bool property"):
|
||||
rna_ui = obj.get('_RNA_UI')
|
||||
if rna_ui is None:
|
||||
rna_ui = obj['_RNA_UI'] = {}
|
||||
obj[name] = 0
|
||||
|
||||
rna_ui[name] = {"description": description,
|
||||
"default": False,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"soft_min": 0,
|
||||
"soft_max": 1}
|
||||
|
||||
# def insert_prop_keyframe(obj, prop_path: str, value):
|
||||
# obj.keyframe_insert(data_path='["prop"]')
|
||||
0
blender-addon/ui/panels.py
Normal file
0
blender-addon/ui/panels.py
Normal file
Reference in New Issue
Block a user