Compare commits
6 Commits
2797e6146c
...
08954dd7d2
Author | SHA1 | Date |
---|---|---|
Spencer Killen | 08954dd7d2 | |
Spencer Killen | 23faf4f2a8 | |
Spencer Killen | 16e1fedf82 | |
Spencer Killen | 6190f5aac5 | |
Spencer Killen | 42196519ae | |
Spencer Killen | 23f2343941 |
|
@ -0,0 +1,8 @@
|
||||||
|
all: mesh_baking.zip
|
||||||
|
|
||||||
|
%.zip: *.py
|
||||||
|
zip -r $@ $^
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
${RM} *.zip
|
|
@ -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 <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()
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
*.zip
|
|
@ -0,0 +1,8 @@
|
||||||
|
all: retopo_tools_addon.zip
|
||||||
|
|
||||||
|
%.zip: *.py
|
||||||
|
zip -r $@ $^
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
${RM} *.zip
|
|
@ -0,0 +1,81 @@
|
||||||
|
# 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()
|
|
@ -0,0 +1,162 @@
|
||||||
|
"""
|
||||||
|
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
|
|
@ -0,0 +1,108 @@
|
||||||
|
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'}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
# 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()
|
|
@ -0,0 +1,55 @@
|
||||||
|
- 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