Compare commits
No commits in common. "08954dd7d23cd5a25286c87f6944658f411d5ea0" and "2797e6146c030cf935eee9858fe24ef7ba170c93" have entirely different histories.
08954dd7d2
...
2797e6146c
|
@ -1,8 +0,0 @@
|
||||||
all: mesh_baking.zip
|
|
||||||
|
|
||||||
%.zip: *.py
|
|
||||||
zip -r $@ $^
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
${RM} *.zip
|
|
|
@ -1,33 +0,0 @@
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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()
|
|
|
@ -1,157 +0,0 @@
|
||||||
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
|
|
|
@ -1,70 +0,0 @@
|
||||||
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)
|
|
|
@ -1 +0,0 @@
|
||||||
*.zip
|
|
|
@ -1,8 +0,0 @@
|
||||||
all: retopo_tools_addon.zip
|
|
||||||
|
|
||||||
%.zip: *.py
|
|
||||||
zip -r $@ $^
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
${RM} *.zip
|
|
|
@ -1,81 +0,0 @@
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.types import Context
|
|
||||||
|
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name" : "RetopoTools",
|
|
||||||
"author" : "Name",
|
|
||||||
"description" : "Set up a standard retopology workflow for a selected object, if 2 objects are selected, then the non-active object is used as the target of mirror and shrinkwrap",
|
|
||||||
"blender" : (2, 80, 0),
|
|
||||||
"version" : (0, 0, 1),
|
|
||||||
"location" : "",
|
|
||||||
"warning" : "",
|
|
||||||
"category" : "Generic"
|
|
||||||
}
|
|
||||||
|
|
||||||
from .tool import RetopoProjectModeOperator, RetopoVertexModeOperator
|
|
||||||
|
|
||||||
|
|
||||||
def RetopoProjectModeOperator_menu_func(self, context: Context):
|
|
||||||
return self.layout.operator(RetopoProjectModeOperator.bl_idname)
|
|
||||||
|
|
||||||
def RetopoVertexModeOperator_menu_func(self, context: Context):
|
|
||||||
return self.layout.operator(RetopoVertexModeOperator.bl_idname)
|
|
||||||
|
|
||||||
from . import auto_load
|
|
||||||
|
|
||||||
auto_load.init()
|
|
||||||
|
|
||||||
|
|
||||||
addon_keymaps = []
|
|
||||||
|
|
||||||
def register_keymaps():
|
|
||||||
addon_kc = bpy.context.window_manager.keyconfigs.addon
|
|
||||||
if addon_kc is None:
|
|
||||||
return
|
|
||||||
for mode in ("Window",):
|
|
||||||
km = addon_kc.keymaps.new(name=mode)
|
|
||||||
kmis = tuple(
|
|
||||||
km.keymap_items.new(
|
|
||||||
Operator.bl_idname,
|
|
||||||
type="NONE",
|
|
||||||
value="ANY",
|
|
||||||
)
|
|
||||||
for Operator in (RetopoProjectModeOperator, RetopoVertexModeOperator)
|
|
||||||
)
|
|
||||||
addon_keymaps.append((km, kmis))
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_keymaps():
|
|
||||||
# Don't know if this is really neccessary but this is what they do in the docs tutorial...
|
|
||||||
for km, kmis in addon_keymaps:
|
|
||||||
for kmi in kmis:
|
|
||||||
km.keymap_items.remove(kmi)
|
|
||||||
bpy.context.window_manager.keyconfigs.addon.keymaps.remove(km)
|
|
||||||
addon_keymaps.clear()
|
|
||||||
|
|
||||||
def register():
|
|
||||||
auto_load.register()
|
|
||||||
bpy.types.VIEW3D_MT_object.append(RetopoProjectModeOperator_menu_func)
|
|
||||||
bpy.types.VIEW3D_MT_object.append(RetopoVertexModeOperator_menu_func)
|
|
||||||
register_keymaps()
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
auto_load.unregister()
|
|
||||||
bpy.types.VIEW3D_MT_object.remove(RetopoProjectModeOperator_menu_func)
|
|
||||||
bpy.types.VIEW3D_MT_object.remove(RetopoVertexModeOperator_menu_func)
|
|
||||||
unregister_keymaps()
|
|
|
@ -1,162 +0,0 @@
|
||||||
"""
|
|
||||||
File is autogenerated by the vscode blender extension.
|
|
||||||
Handles the tedium of registering blender classes by searching for them and automagically registering them
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,108 +0,0 @@
|
||||||
import bpy
|
|
||||||
from bpy.types import Context, Object, Mesh, Scene
|
|
||||||
|
|
||||||
def assert_mirror_modifier(obj: Object, target: Object = None):
|
|
||||||
modifier = next((modifier for modifier in obj.modifiers if modifier.type == "MIRROR"), None)
|
|
||||||
if modifier is None:
|
|
||||||
modifier = obj.modifiers.new("Mirror", "MIRROR")
|
|
||||||
modifier.use_clip = True
|
|
||||||
|
|
||||||
if target is not None and modifier.mirror_object is None:
|
|
||||||
modifier.mirror_object = target
|
|
||||||
|
|
||||||
|
|
||||||
def assert_shrinkwrap_modifier(obj: Object, target: Object = None):
|
|
||||||
modifier = next((modifier for modifier in obj.modifiers if modifier.type == "SHRINKWRAP"), None)
|
|
||||||
if modifier is None:
|
|
||||||
modifier = obj.modifiers.new("Shrinkwrap", "SHRINKWRAP")
|
|
||||||
|
|
||||||
modifier.show_viewport = False
|
|
||||||
modifier.wrap_method = "PROJECT"
|
|
||||||
modifier.wrap_mode = "ABOVE_SURFACE"
|
|
||||||
modifier.project_limit = 0.0
|
|
||||||
modifier.use_negative_direction = True
|
|
||||||
modifier.use_positive_direction = True
|
|
||||||
|
|
||||||
|
|
||||||
vgroup = next((group for group in obj.vertex_groups if "no_shrinkwrap" in group.name), None)
|
|
||||||
if vgroup is None:
|
|
||||||
vgroup = obj.vertex_groups.new(name="no_shrinkwrap")
|
|
||||||
|
|
||||||
|
|
||||||
modifier.vertex_group = vgroup.name
|
|
||||||
modifier.invert_vertex_group = True
|
|
||||||
|
|
||||||
if target is not None and modifier.target is None:
|
|
||||||
modifier.target = target
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def assert_retopo_modifiers(ctx: Context, target: Object = None):
|
|
||||||
active = ctx.active_object
|
|
||||||
assert_mirror_modifier(active, target)
|
|
||||||
assert_shrinkwrap_modifier(active, target)
|
|
||||||
|
|
||||||
def setup_project_snap_settings(scene: Scene):
|
|
||||||
scene.tool_settings.use_snap = True
|
|
||||||
scene.tool_settings.snap_elements = {"FACE"}
|
|
||||||
scene.tool_settings.use_snap_self = True
|
|
||||||
scene.tool_settings.use_snap_edit = True
|
|
||||||
scene.tool_settings.use_snap_nonedit = True
|
|
||||||
scene.tool_settings.use_snap_selectable = True
|
|
||||||
scene.tool_settings.use_snap_align_rotation = False
|
|
||||||
scene.tool_settings.use_snap_backface_culling = True
|
|
||||||
scene.tool_settings.use_snap_project = True
|
|
||||||
scene.tool_settings.use_snap_translate = True
|
|
||||||
scene.tool_settings.use_snap_rotate = True
|
|
||||||
scene.tool_settings.use_snap_scale = True
|
|
||||||
|
|
||||||
def setup_vertex_snap_settings(scene: Scene):
|
|
||||||
scene.tool_settings.use_snap = True
|
|
||||||
scene.tool_settings.snap_elements = {"VERTEX"}
|
|
||||||
scene.tool_settings.use_snap_self = True
|
|
||||||
scene.tool_settings.use_snap_edit = True
|
|
||||||
scene.tool_settings.use_snap_nonedit = False
|
|
||||||
scene.tool_settings.use_snap_selectable = True
|
|
||||||
scene.tool_settings.use_snap_align_rotation = False
|
|
||||||
scene.tool_settings.use_snap_backface_culling = True
|
|
||||||
scene.tool_settings.use_snap_translate = True
|
|
||||||
scene.tool_settings.use_snap_rotate = True
|
|
||||||
scene.tool_settings.use_snap_scale = True
|
|
||||||
|
|
||||||
def get_context_target(ctx: Context):
|
|
||||||
if len(ctx.selected_objects) != 2:
|
|
||||||
return None
|
|
||||||
return next(obj for obj in ctx.selected_objects if obj is not ctx.active_object)
|
|
||||||
|
|
||||||
class RetopoProjectModeOperator(bpy.types.Operator):
|
|
||||||
bl_idname = "object.retopoprojectmode"
|
|
||||||
bl_label = "RetopoProjectMode"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(Cls, context: Context):
|
|
||||||
return context.active_object is not None and type(context.active_object.data) is Mesh
|
|
||||||
|
|
||||||
def execute(self, context: Context):
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
|
||||||
target = get_context_target(context)
|
|
||||||
assert_retopo_modifiers(context, target)
|
|
||||||
setup_project_snap_settings(context.scene)
|
|
||||||
self.report({"INFO"}, "Project Mode")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class RetopoVertexModeOperator(bpy.types.Operator):
|
|
||||||
bl_idname = "object.retopovertexmode"
|
|
||||||
bl_label = "RetopoVertexMode"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
bpy.ops.object.mode_set(mode="EDIT")
|
|
||||||
target = get_context_target(context)
|
|
||||||
assert_retopo_modifiers(context, target)
|
|
||||||
setup_vertex_snap_settings(context.scene)
|
|
||||||
self.report({"INFO"}, "Vertex Mode")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,255 +0,0 @@
|
||||||
# See notes for source repo
|
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# 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 2
|
|
||||||
# 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
|
|
||||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# <pep8 compliant>
|
|
||||||
|
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name": "Vertex Animation",
|
|
||||||
"author": "Joshua Bogart",
|
|
||||||
"version": (1, 0),
|
|
||||||
"blender": (2, 83, 0),
|
|
||||||
"location": "View3D > Sidebar > Vertex Animation",
|
|
||||||
"description": "A tool for storing per frame vertex data for use in a vertex shader.",
|
|
||||||
"warning": "",
|
|
||||||
"doc_url": "",
|
|
||||||
"category": "tool",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def get_per_frame_mesh_data(context, data, objects):
|
|
||||||
"""Return a list of combined mesh data per frame"""
|
|
||||||
meshes = []
|
|
||||||
for i in frame_range(context.scene):
|
|
||||||
context.scene.frame_set(i)
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
|
||||||
bm = bmesh.new()
|
|
||||||
for ob in objects:
|
|
||||||
eval_object = ob.evaluated_get(depsgraph)
|
|
||||||
me = data.meshes.new_from_object(eval_object)
|
|
||||||
me.transform(ob.matrix_world)
|
|
||||||
bm.from_mesh(me)
|
|
||||||
data.meshes.remove(me)
|
|
||||||
me = data.meshes.new("mesh")
|
|
||||||
bm.to_mesh(me)
|
|
||||||
bm.free()
|
|
||||||
me.calc_normals()
|
|
||||||
meshes.append(me)
|
|
||||||
return meshes
|
|
||||||
|
|
||||||
|
|
||||||
def create_export_mesh_object(context, data, me):
|
|
||||||
"""Return a mesh object with correct UVs"""
|
|
||||||
while len(me.uv_layers) < 2:
|
|
||||||
me.uv_layers.new()
|
|
||||||
uv_layer = me.uv_layers[1]
|
|
||||||
uv_layer.name = "vertex_anim"
|
|
||||||
for loop in me.loops:
|
|
||||||
uv_layer.data[loop.index].uv = ((loop.vertex_index + 0.5)/len(me.vertices), 0.0)
|
|
||||||
ob = data.objects.new("export_mesh", me)
|
|
||||||
context.scene.collection.objects.link(ob)
|
|
||||||
return ob
|
|
||||||
|
|
||||||
|
|
||||||
def get_vertex_data(data, meshes):
|
|
||||||
"""Return lists of vertex offsets and normals from a list of mesh data"""
|
|
||||||
original = meshes[0].vertices
|
|
||||||
offsets = []
|
|
||||||
normals = []
|
|
||||||
for me in meshes: #for me in reversed(meshes):
|
|
||||||
for v in me.vertices:
|
|
||||||
offset = v.co - original[v.index].co
|
|
||||||
x, y, z = offset
|
|
||||||
offsets.extend((x, -y, z, 1))
|
|
||||||
x, y, z = v.normal
|
|
||||||
normals.extend(((x + 1) * 0.5, (-y + 1) * 0.5, (z + 1) * 0.5, 1))
|
|
||||||
#if not me.users:
|
|
||||||
#data.meshes.remove(me)
|
|
||||||
for me in meshes:
|
|
||||||
if not me.users:
|
|
||||||
data.meshes.remove(me)
|
|
||||||
return offsets, normals
|
|
||||||
|
|
||||||
|
|
||||||
def frame_range(scene):
|
|
||||||
"""Return a range object with with scene's frame start, end, and step"""
|
|
||||||
return range(scene.frame_start, scene.frame_end, scene.frame_step)
|
|
||||||
|
|
||||||
|
|
||||||
def bake_vertex_data(context, data, offsets, normals, size):
|
|
||||||
"""Stores vertex offsets and normals in separate image textures"""
|
|
||||||
width, height = size
|
|
||||||
|
|
||||||
blend_path = bpy.data.filepath
|
|
||||||
blend_path = os.path.dirname(bpy.path.abspath(blend_path))
|
|
||||||
subfolder_path = os.path.join(blend_path, "vaexport")
|
|
||||||
if not os.path.exists(subfolder_path):
|
|
||||||
os.makedirs(subfolder_path)
|
|
||||||
openexr_filepath = os.path.join(subfolder_path, "offsets.exr")
|
|
||||||
png_filepath = os.path.join(subfolder_path, "normals.png")
|
|
||||||
|
|
||||||
openexr_export_scene = bpy.data.scenes.new('openexr export scene')
|
|
||||||
openexr_export_scene.sequencer_colorspace_settings.name = 'Non-Color'
|
|
||||||
openexr_export_scene.render.image_settings.color_depth = '16'
|
|
||||||
openexr_export_scene.render.image_settings.color_mode = 'RGBA'
|
|
||||||
openexr_export_scene.render.image_settings.file_format = 'OPEN_EXR'
|
|
||||||
openexr_export_scene.render.image_settings.exr_codec = 'NONE'
|
|
||||||
|
|
||||||
if 'offsets' in bpy.data.images:
|
|
||||||
offset_tex = bpy.data.images['offsets']
|
|
||||||
bpy.data.images.remove(offset_tex)
|
|
||||||
|
|
||||||
offset_texture = data.images.new(
|
|
||||||
name="offsets",
|
|
||||||
width=width,
|
|
||||||
height=height,
|
|
||||||
alpha=True,
|
|
||||||
float_buffer=True
|
|
||||||
)
|
|
||||||
|
|
||||||
offset_texture.file_format = 'OPEN_EXR'
|
|
||||||
offset_texture.colorspace_settings.name = 'Non-Color'
|
|
||||||
offset_texture.pixels = offsets
|
|
||||||
offset_texture.save_render(openexr_filepath, scene=openexr_export_scene)
|
|
||||||
bpy.data.scenes.remove(openexr_export_scene)
|
|
||||||
|
|
||||||
png_export_scene = bpy.data.scenes.new('png export scene')
|
|
||||||
png_export_scene.render.image_settings.color_depth = '8'
|
|
||||||
png_export_scene.render.image_settings.color_mode = 'RGBA'
|
|
||||||
png_export_scene.render.image_settings.file_format = 'PNG'
|
|
||||||
png_export_scene.render.image_settings.compression = 15
|
|
||||||
|
|
||||||
if 'normals' in bpy.data.images:
|
|
||||||
normals_tex = bpy.data.images['normals']
|
|
||||||
bpy.data.images.remove(normals_tex)
|
|
||||||
|
|
||||||
normal_texture = data.images.new(
|
|
||||||
name="normals",
|
|
||||||
width=width,
|
|
||||||
height=height,
|
|
||||||
alpha=True
|
|
||||||
)
|
|
||||||
|
|
||||||
normal_texture.file_format = 'PNG'
|
|
||||||
normal_texture.pixels = normals
|
|
||||||
normal_texture.save_render(png_filepath, scene=png_export_scene)
|
|
||||||
bpy.data.scenes.remove(png_export_scene)
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_ProcessAnimMeshes(bpy.types.Operator):
|
|
||||||
"""Store combined per frame vertex offsets and normals for all
|
|
||||||
selected mesh objects into seperate image textures"""
|
|
||||||
bl_idname = "object.process_anim_meshes"
|
|
||||||
bl_label = "Process Anim Meshes"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_modifiers(self):
|
|
||||||
return [
|
|
||||||
'ARMATURE', 'CAST', 'CURVE', 'DISPLACE', 'HOOK',
|
|
||||||
'LAPLACIANDEFORM', 'LATTICE', 'MESH_DEFORM',
|
|
||||||
'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH',
|
|
||||||
'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH',
|
|
||||||
'SURFACE_DEFORM', 'WARP', 'WAVE', 'MESH_CACHE'
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
ob = context.active_object
|
|
||||||
return ob and ob.type == 'MESH' and ob.mode == 'OBJECT'
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
units = context.scene.unit_settings
|
|
||||||
data = bpy.data
|
|
||||||
objects = [ob for ob in context.selected_objects if ob.type == 'MESH']
|
|
||||||
vertex_count = sum([len(ob.data.vertices) for ob in objects])
|
|
||||||
frame_count = len(frame_range(context.scene))
|
|
||||||
for ob in objects:
|
|
||||||
for mod in ob.modifiers:
|
|
||||||
if mod.type not in self.allowed_modifiers:
|
|
||||||
self.report(
|
|
||||||
{'ERROR'},
|
|
||||||
f"Objects with {mod.type.title()} modifiers are not allowed!"
|
|
||||||
)
|
|
||||||
return {'CANCELLED'}
|
|
||||||
#if units.system != 'METRIC' or round(units.scale_length, 2) != 0.01:
|
|
||||||
# self.report(
|
|
||||||
# {'ERROR'},
|
|
||||||
# "Scene Unit must be Metric with a Unit Scale of 0.01!"
|
|
||||||
# )
|
|
||||||
# return {'CANCELLED'}
|
|
||||||
if vertex_count > 8192:
|
|
||||||
self.report(
|
|
||||||
{'ERROR'},
|
|
||||||
f"Vertex count of {vertex_count :,}, execedes limit of 8,192!"
|
|
||||||
)
|
|
||||||
return {'CANCELLED'}
|
|
||||||
if frame_count > 8192:
|
|
||||||
self.report(
|
|
||||||
{'ERROR'},
|
|
||||||
f"Frame count of {frame_count :,}, execedes limit of 8,192!"
|
|
||||||
)
|
|
||||||
return {'CANCELLED'}
|
|
||||||
meshes = get_per_frame_mesh_data(context, data, objects)
|
|
||||||
export_mesh_data = meshes[0].copy()
|
|
||||||
create_export_mesh_object(context, data, export_mesh_data)
|
|
||||||
offsets, normals = get_vertex_data(data, meshes)
|
|
||||||
texture_size = vertex_count, frame_count
|
|
||||||
bake_vertex_data(context, data, offsets, normals, texture_size)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class VIEW3D_PT_VertexAnimation(bpy.types.Panel):
|
|
||||||
"""Creates a Panel in 3D Viewport"""
|
|
||||||
bl_label = "Vertex Animation"
|
|
||||||
bl_idname = "VIEW3D_PT_vertex_animation"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = "Vertex Animation"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.use_property_split = True
|
|
||||||
layout.use_property_decorate = False
|
|
||||||
scene = context.scene
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.prop(scene, "frame_start", text="Frame Start")
|
|
||||||
col.prop(scene, "frame_end", text="End")
|
|
||||||
col.prop(scene, "frame_step", text="Step")
|
|
||||||
row = layout.row()
|
|
||||||
row.operator("object.process_anim_meshes")
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
bpy.utils.register_class(OBJECT_OT_ProcessAnimMeshes)
|
|
||||||
bpy.utils.register_class(VIEW3D_PT_VertexAnimation)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
bpy.utils.unregister_class(OBJECT_OT_ProcessAnimMeshes)
|
|
||||||
bpy.utils.unregister_class(VIEW3D_PT_VertexAnimation)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
register()
|
|
|
@ -1,55 +0,0 @@
|
||||||
- The alembic format serves as a good way to bake mesh data with lots of modifiers
|
|
||||||
- This script will convert mesh animations to shapekeys. Digusting, but it just might work
|
|
||||||
- https://gist.github.com/fire/495fb6a35168500df53c002695a1f5fe
|
|
||||||
Code reproduced below for archival purposes
|
|
||||||
|
|
||||||
- Enable the .mdd addon in blender to export as a mesh cache
|
|
||||||
- It might work as a direct export to .mdd, but I did alembic first, then rexported to mdd
|
|
||||||
- Create new object with same mesh and empty modifier stack and add a mesh cache modifier to load exported file.
|
|
||||||
|
|
||||||
|
|
||||||
- This method is better
|
|
||||||
- https://github.com/yanorax/Godot-VertexAnimation-Demo
|
|
||||||
- But it would need to be extended to support a mesh cache modifier. (Should be possible given following code
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
# MIT LICENSE
|
|
||||||
# Authored by iFire#6518 and alexfreyre#1663
|
|
||||||
# This code ONLY apply to a mesh and simulations with ONLY the same vertex number
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
#Converts a MeshCache or Cloth modifiers to ShapeKeys
|
|
||||||
frame = bpy.context.scene.frame_start
|
|
||||||
for frame in range(bpy.context.scene.frame_end + 1):
|
|
||||||
bpy.context.scene.frame_current = frame
|
|
||||||
|
|
||||||
#for alembic files converted to MDD and loaded as MeshCache
|
|
||||||
bpy.ops.object.modifier_apply_as_shapekey(keep_modifier=True, modifier="MeshCache")
|
|
||||||
|
|
||||||
#for cloth simulations inside blender using a Cloth modifier
|
|
||||||
#bpy.ops.object.modifier_apply_as_shapekey(keep_modifier=True, modifier="Cloth")
|
|
||||||
|
|
||||||
# loop through shapekeys and add as keyframe per frame
|
|
||||||
# https://blender.stackexchange.com/q/149045/87258
|
|
||||||
frame = bpy.context.scene.frame_start
|
|
||||||
for frame in range(bpy.context.scene.frame_end + 1):
|
|
||||||
bpy.context.scene.frame_current = frame
|
|
||||||
|
|
||||||
for shapekey in bpy.data.shape_keys:
|
|
||||||
for i, keyblock in enumerate(shapekey.key_blocks):
|
|
||||||
if keyblock.name != "Basis":
|
|
||||||
curr = i - 1
|
|
||||||
if curr != frame:
|
|
||||||
keyblock.value = 0
|
|
||||||
keyblock.keyframe_insert("value", frame=frame)
|
|
||||||
else:
|
|
||||||
keyblock.value = 1
|
|
||||||
keyblock.keyframe_insert("value", frame=frame)
|
|
||||||
|
|
||||||
# bpy.ops.object.modifier_remove(modifier="MeshCache")
|
|
||||||
# bpy.ops.object.modifier_remove(modifier="Cloth")
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue