diff --git a/mesh_baking/Makefile b/mesh_baking/Makefile new file mode 100644 index 0000000..0ece6bd --- /dev/null +++ b/mesh_baking/Makefile @@ -0,0 +1,8 @@ +all: mesh_baking.zip + +%.zip: *.py + zip -r $@ $^ + +.PHONY: clean +clean: + ${RM} *.zip diff --git a/mesh_baking/__init__.py b/mesh_baking/__init__.py new file mode 100644 index 0000000..9069663 --- /dev/null +++ b/mesh_baking/__init__.py @@ -0,0 +1,33 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +bl_info = { + "name" : "Mesh Baking", + "author" : "S", + "description" : "", + "blender" : (2, 80, 0), + "version" : (0, 0, 1), + "location" : "", + "warning" : "", + "category" : "Generic" +} + +from . import auto_load + +auto_load.init() + +def register(): + auto_load.register() + +def unregister(): + auto_load.unregister() diff --git a/mesh_baking/auto_load.py b/mesh_baking/auto_load.py new file mode 100644 index 0000000..72254c0 --- /dev/null +++ b/mesh_baking/auto_load.py @@ -0,0 +1,157 @@ +import os +import bpy +import sys +import typing +import inspect +import pkgutil +import importlib +from pathlib import Path + +__all__ = ( + "init", + "register", + "unregister", +) + +blender_version = bpy.app.version + +modules = None +ordered_classes = None + +def init(): + global modules + global ordered_classes + + modules = get_all_submodules(Path(__file__).parent) + ordered_classes = get_ordered_classes_to_register(modules) + +def register(): + for cls in ordered_classes: + bpy.utils.register_class(cls) + + for module in modules: + if module.__name__ == __name__: + continue + if hasattr(module, "register"): + module.register() + +def unregister(): + for cls in reversed(ordered_classes): + bpy.utils.unregister_class(cls) + + for module in modules: + if module.__name__ == __name__: + continue + if hasattr(module, "unregister"): + module.unregister() + + +# Import modules +################################################# + +def get_all_submodules(directory): + return list(iter_submodules(directory, directory.name)) + +def iter_submodules(path, package_name): + for name in sorted(iter_submodule_names(path)): + yield importlib.import_module("." + name, package_name) + +def iter_submodule_names(path, root=""): + for _, module_name, is_package in pkgutil.iter_modules([str(path)]): + if is_package: + sub_path = path / module_name + sub_root = root + module_name + "." + yield from iter_submodule_names(sub_path, sub_root) + else: + yield root + module_name + + +# Find classes to register +################################################# + +def get_ordered_classes_to_register(modules): + return toposort(get_register_deps_dict(modules)) + +def get_register_deps_dict(modules): + my_classes = set(iter_my_classes(modules)) + my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")} + + deps_dict = {} + for cls in my_classes: + deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) + return deps_dict + +def iter_my_register_deps(cls, my_classes, my_classes_by_idname): + yield from iter_my_deps_from_annotations(cls, my_classes) + yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) + +def iter_my_deps_from_annotations(cls, my_classes): + for value in typing.get_type_hints(cls, {}, {}).values(): + dependency = get_dependency_from_annotation(value) + if dependency is not None: + if dependency in my_classes: + yield dependency + +def get_dependency_from_annotation(value): + if blender_version >= (2, 93): + if isinstance(value, bpy.props._PropertyDeferred): + return value.keywords.get("type") + else: + if isinstance(value, tuple) and len(value) == 2: + if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): + return value[1]["type"] + return None + +def iter_my_deps_from_parent_id(cls, my_classes_by_idname): + if bpy.types.Panel in cls.__bases__: + parent_idname = getattr(cls, "bl_parent_id", None) + if parent_idname is not None: + parent_cls = my_classes_by_idname.get(parent_idname) + if parent_cls is not None: + yield parent_cls + +def iter_my_classes(modules): + base_types = get_register_base_types() + for cls in get_classes_in_modules(modules): + if any(base in base_types for base in cls.__bases__): + if not getattr(cls, "is_registered", False): + yield cls + +def get_classes_in_modules(modules): + classes = set() + for module in modules: + for cls in iter_classes_in_module(module): + classes.add(cls) + return classes + +def iter_classes_in_module(module): + for value in module.__dict__.values(): + if inspect.isclass(value): + yield value + +def get_register_base_types(): + return set(getattr(bpy.types, name) for name in [ + "Panel", "Operator", "PropertyGroup", + "AddonPreferences", "Header", "Menu", + "Node", "NodeSocket", "NodeTree", + "UIList", "RenderEngine", + "Gizmo", "GizmoGroup", + ]) + + +# Find order to register to solve dependencies +################################################# + +def toposort(deps_dict): + sorted_list = [] + sorted_values = set() + while len(deps_dict) > 0: + unsorted = [] + for value, deps in deps_dict.items(): + if len(deps) == 0: + sorted_list.append(value) + sorted_values.add(value) + else: + unsorted.append(value) + deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} + return sorted_list diff --git a/mesh_baking/tool.py b/mesh_baking/tool.py new file mode 100644 index 0000000..2507221 --- /dev/null +++ b/mesh_baking/tool.py @@ -0,0 +1,70 @@ +import bpy +from bpy.types import Context, Object, Scene, Collection + +def get_first_collection_sharing_scene(scene: Scene, obj: Object): + def collection_in_scene(collection: Collection, tree: Collection): + if collection == tree: + return True + return any(collection_in_scene(collection, child) for child in tree.children) + return next(candidate for candidate in obj.users_collection if collection_in_scene(candidate, scene.collection)) + +def is_pc2_enabled(): + return hasattr(bpy.ops, "export_shape") and hasattr(bpy.ops.export_shape, "pc2") + +def export_pc2_filepath(obj: Object, absolute=False): + path = f"//{obj.name}.pc2" + return bpy.path.abspath(path) if absolute else path + +def export_pc2(scene: Scene, obj: Object): + path = export_pc2_filepath(obj, absolute=True) + frame_start = scene.frame_start + scene.frame_set(frame_start) + frame_end = scene.frame_end + bpy.context.view_layer.objects.active = obj + bpy.context.view_layer.update() + bpy.ops.export_shape.pc2(filepath=path, check_existing=False, rot_x90=False, world_space=False, apply_modifiers=True, range_start=frame_start, range_end=frame_end, sampling='1') + bpy.context.view_layer.update() + frame_end = scene.frame_end + scene.frame_set(frame_start) + bpy.context.view_layer.update() + for modifier in obj.modifiers: + bpy.ops.object.modifier_apply(modifier=modifier.name) + bpy.context.view_layer.update() + +def create_proxy(scene: Scene, obj: Object): + copy = obj.copy() + copy.name = f"{obj.name}Bake" + copy.data = copy.data.copy() + copy.constraints.clear() + c = get_first_collection_sharing_scene(scene, obj) + c.objects.link(copy) + export_pc2(scene, copy) + mod = copy.modifiers.new("Mesh Bake", "MESH_CACHE") + mod.cache_format = "PC2" + mod.filepath = export_pc2_filepath(obj) + +class BakeMeshOperator(bpy.types.Operator): + bl_idname = "object.bakemesh" + bl_label = "Bake Mesh to PC2" + + @classmethod + def poll(Cls, context: Context): + return context.active_object is not None + + def execute(self, context: Context): + if not is_pc2_enabled(): + self.report({"ERROR"}, "PC2 export addon must be enabled") + return {'FINISHED'} + scene = context.scene + obj = context.active_object + create_proxy(scene, obj) + return {'FINISHED'} + +def BakeMeshOperator_menu_func(self, context: Context): + return self.layout.operator(BakeMeshOperator.bl_idname) + +def register(): + bpy.types.VIEW3D_MT_object.append(BakeMeshOperator_menu_func) + +def unregister(): + bpy.types.VIEW3D_MT_object.remove(BakeMeshOperator_menu_func)