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