Initial commit for updated addon

This commit is contained in:
Artem30801
2019-07-28 12:25:46 +03:00
parent cae0a1bd46
commit 1316e27c8f
6 changed files with 370 additions and 213 deletions

44
blender-addon/__init__.py Normal file
View 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()

View File

@@ -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()

View 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))

View 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"]')

View File