Compare commits
	
		
			6 commits
		
	
	
		
			2797e6146c
			...
			08954dd7d2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 08954dd7d2 | |||
| 23faf4f2a8 | |||
| 16e1fedf82 | |||
| 
							 | 
						6190f5aac5 | ||
| 
							 | 
						42196519ae | ||
| 
							 | 
						23f2343941 | 
					 11 changed files with 938 additions and 0 deletions
				
			
		
							
								
								
									
										8
									
								
								mesh_baking/Makefile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								mesh_baking/Makefile
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					all: mesh_baking.zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%.zip: *.py
 | 
				
			||||||
 | 
						zip -r $@ $^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.PHONY: clean
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
						${RM} *.zip
 | 
				
			||||||
							
								
								
									
										33
									
								
								mesh_baking/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mesh_baking/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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()
 | 
				
			||||||
							
								
								
									
										157
									
								
								mesh_baking/auto_load.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								mesh_baking/auto_load.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
							
								
								
									
										70
									
								
								mesh_baking/tool.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								mesh_baking/tool.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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)
 | 
				
			||||||
							
								
								
									
										1
									
								
								retopo_tools/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								retopo_tools/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					*.zip
 | 
				
			||||||
							
								
								
									
										8
									
								
								retopo_tools/Makefile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								retopo_tools/Makefile
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					all: retopo_tools_addon.zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%.zip: *.py
 | 
				
			||||||
 | 
						zip -r $@ $^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.PHONY: clean
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
						${RM} *.zip
 | 
				
			||||||
							
								
								
									
										81
									
								
								retopo_tools/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								retopo_tools/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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()
 | 
				
			||||||
							
								
								
									
										162
									
								
								retopo_tools/auto_load.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								retopo_tools/auto_load.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
							
								
								
									
										108
									
								
								retopo_tools/tool.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								retopo_tools/tool.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										255
									
								
								vertex_animation.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								vertex_animation.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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()
 | 
				
			||||||
							
								
								
									
										55
									
								
								vertex_animation_notes.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								vertex_animation_notes.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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…
	
	Add table
		
		Reference in a new issue