diff --git a/.gitignore b/.gitignore index 5ff46580..ccc6179f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -illustrator/ \ No newline at end of file +illustrator/ +__pycache__/ \ No newline at end of file diff --git a/scripts/blender/addons/macrovision.py b/scripts/blender/addons/macrovision.py new file mode 100644 index 00000000..303c3fb7 --- /dev/null +++ b/scripts/blender/addons/macrovision.py @@ -0,0 +1,26 @@ +import mv.ui +import mv.ops + +import bpy +import importlib + +bl_info = { + "name": "Macrovision", + "blender": (3, 0, 0), + "category": "Import-Export" +} + + +def register(): + importlib.reload(mv.ui) + importlib.reload(mv.ops) + cls_lists = [mv.ops.clses, mv.ui.clses] + for cls_list in cls_lists: + for cls in cls_list: + bpy.utils.register_class(cls) + +def unregister(): + cls_lists = [mv.ops.clses, mv.ui.clses][::-1] + for cls_list in cls_lists[::-1]: + for cls in cls_list: + bpy.utils.register_class(cls) \ No newline at end of file diff --git a/scripts/blender/addons/mv/__init__.py b/scripts/blender/addons/mv/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/blender/addons/mv/ops.py b/scripts/blender/addons/mv/ops.py new file mode 100644 index 00000000..59335675 --- /dev/null +++ b/scripts/blender/addons/mv/ops.py @@ -0,0 +1,245 @@ +import bpy +from mathutils import Vector, Euler, Color +import json +import pathlib +import os +from math import pi + +VIEW_DATA = { + "Front": [0, 1, 2, "Front"], + "Angled": [0.25, 1, 2, "Angled"], + "Corner": [0.5, 1, 2, "Corner"], + "Side": [1, 1, 2, "Side"], + "Back Angled": [1.5, 1, 2, "Back Angled"], + "Back": [2, 1, 2, "Back"], + "Top": [0, 0, 1, "Top"], + "Bottom": [0, 2, 1, "Bottom"], + "Bottom Flipped": [2, 2, 1, "Bottom Flipped"], +} + +def get_bounds(objects): + xl = [] + yl = [] + zl = [] + + for obj in objects: + if not obj.hide_render: + for bounds in obj.bound_box: + v = obj.matrix_world @ Vector(bounds) + xl += [v[0] for c in obj.bound_box] + yl += [v[1] for c in obj.bound_box] + zl += [v[2] for c in obj.bound_box] + + return ( + Vector([min(xl), min(yl), min(zl)]), + Vector([max(xl), max(yl), max(zl)]) + ) + +class MVConfigCollection(bpy.types.Operator): + bl_idname = "mv.config_collection" + bl_label = "Configure root collection" + + @classmethod + def poll(cls, context: bpy.types.Context): + return True + + def execute(self, context: bpy.types.Context): + coll = context.scene.collection.children[0] + coll.name = "Macrovision" + coll["MVName"] = "Name" + coll["MVViews"] = "Front" + coll["MVKind"] = "objects" + coll["MVViewLabels"] = "Front: Front" + + mats = [ + ("light", 0, 0, 1), + ("medium", 0, 1, 0), + ("dark", 1, 0, 0) + ] + + for name, red, green, blue in mats: + if name not in bpy.data.materials: + bpy.data.materials.new(name) + mat = bpy.data.materials[name] + + mat.use_nodes = True + mat.use_fake_user = True + + mat.node_tree.nodes.clear() + + rgb = mat.node_tree.nodes.new("ShaderNodeRGB") + out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial") + + input = out.inputs['Surface'] + output = rgb.outputs['Color'] + + mat.node_tree.links.new(input=input, output=output) + + output.default_value = (red, green, blue, 1.0) + + + if "cam" not in bpy.data.objects: + new_cam = bpy.data.cameras.new("cam") + new_obj = bpy.data.objects.new("cam", new_cam) + context.scene.collection.objects.link(new_obj) + + if "Lines" not in bpy.data.materials: + mat = bpy.data.materials.new("Lines") + bpy.data.materials.create_gpencil_data(mat) + + if "lineart" not in bpy.data.objects: + new_lineart = bpy.data.grease_pencils.new("lineart") + new_obj = bpy.data.objects.new("lineart", new_lineart) + + new_obj.show_in_front = True + + context.scene.collection.objects.link(new_obj) + + modifier = new_obj.grease_pencil_modifiers.new(name='Lineart', type='GP_LINEART') + material = bpy.data.materials["Lines"] + + new_lineart.materials.append(material) + + modifier.target_layer = "Lines" + modifier.target_material = material + + new_lineart.layers.new("Lines") + + # we have to clear the bake, for some reason + + bpy.ops.object.lineart_clear_all() + + return {"FINISHED"} + + + +class MVExport(bpy.types.Operator): + bl_idname = "mv.export" + bl_label = "Export objects" + + @classmethod + def poll(cls, context: bpy.types.Context): + return True + + def execute(self, context: bpy.types.Context): + path_info = pathlib.Path(bpy.data.filepath).parent.joinpath("macrovision-directory.txt") + config_path = pathlib.Path(open(path_info).read().strip()) + + json_path = config_path.joinpath("config.json") + config = json.load(open(json_path.resolve(), encoding="utf-8")) + + parent_workdir = config["work-directory"] + + c = bpy.data.objects["cam"] + context.scene.camera = c + + lineart = bpy.data.objects["lineart"] + lineart.grease_pencil_modifiers['Lineart'].source_type = 'COLLECTION' + + c.data.type = "ORTHO" + + bpy.data.scenes["Scene"].render.resolution_x = 2000 + bpy.data.scenes["Scene"].render.resolution_y = 2000 + bpy.data.scenes["Scene"].render.film_transparent = True + bpy.data.scenes["Scene"].view_settings.view_transform = "Raw" + + bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[1].default_value = 0 + + mv = bpy.data.collections["Macrovision"] + collections = mv.children + + all_data = {} + + all_data["name"] = mv["MVName"] + all_data["kind"] = mv["MVKind"] + all_data["forms"] = [] + + default_views = [] + + for view in mv["MVViews"].split(","): + default_views.append(VIEW_DATA[view.strip()]) + + if "MVViewLabels" in mv: + for pair in mv["MVViewLabels"].split(","): + key, val = pair.split(":") + VIEW_DATA[key.strip()][3] = val.strip() + + + workdir = pathlib.Path(parent_workdir).joinpath(all_data["name"]) + + os.makedirs(workdir, exist_ok=True) + + for coll in collections: + coll.hide_render = True + + for coll in collections: + coll.hide_render = False + + bpy.ops.object.select_all(action='DESELECT') + + for obj in coll.objects: + obj.select_set(True) + + data = {} + + data["name"] = coll.name + data["views"] = [] + + bound_min, bound_max = get_bounds(coll.objects) + dimensions = bound_max - bound_min + size = max(dimensions) + global_bbox_center = 0.5 * (bound_min + bound_max) + + view_list = [] + + lineart.grease_pencil_modifiers['Lineart'].source_collection = coll + + if "Views" in coll: + for view in coll["Views"].split(","): + view_list.append(VIEW_DATA[view]) + else: + view_list = default_views + + for angles in view_list: + c.location = global_bbox_center + c.rotation_euler = Euler([angles[1] * pi / 2, 0, angles[0] * pi / 2]) + print(list(bound_min) + list(bound_max)) + _, c.data.ortho_scale = c.camera_fit_coords(bpy.context.evaluated_depsgraph_get(), list(bound_min) + list(bound_max)) + c.location = Vector([c.location[0], c.location[1], c.location[2]]) + c.data.ortho_scale *= 1.2 + rot = c.rotation_euler.to_matrix() + rot.invert() + c.location += Vector([0, 0, size * 2]) @ rot + c.data.clip_start = size / 4 + c.data.clip_end = size * 8 + data["views"].append({ + "name": angles[3], + "height": dimensions[angles[2]] + }) + + if "Volume" in coll: + data["views"][-1]["volume"] = coll["Volume"] + if "Mass" in coll: + data["views"][-1]["mass"] = coll["Mass"] + + lineart.hide_render = False + filename = f"{coll.name}-{angles[3]}.png" + bpy.context.scene.render.filepath = workdir.joinpath(filename).resolve().__str__() + bpy.ops.render.render(write_still = True) + lineart.hide_render = True + filename = f"{coll.name}-{angles[3]}-noline.png" + bpy.context.scene.render.filepath = workdir.joinpath(filename).resolve().__str__() + bpy.ops.render.render(write_still = True) + + all_data["forms"].append(data) + coll.hide_render = True + + with open(workdir.joinpath("data.json"), "w") as file: + json.dump(all_data, file) + + return {"FINISHED"} + +clses = [ + MVExport, + MVConfigCollection +] \ No newline at end of file diff --git a/scripts/blender/addons/mv/ui.py b/scripts/blender/addons/mv/ui.py new file mode 100644 index 00000000..54f8a67c --- /dev/null +++ b/scripts/blender/addons/mv/ui.py @@ -0,0 +1,31 @@ +import mv.ops + +import bpy + +class MVPanel(bpy.types.Panel): + bl_idname="OBJECT_PT_MV_menu" + bl_label="Macrovision" + bl_space_type="VIEW_3D" + bl_region_type="UI" + bl_category = "Macrovision" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + + box = layout.box() + box.label(text="Setup") + + box.operator("mv.config_collection") + + box = layout.box() + box.label(text="Execute") + + box.operator("mv.export") + +clses = [ + MVPanel +]