A retopotools

This commit is contained in:
Spencer Killen 2022-12-22 13:35:06 -07:00
parent 2797e6146c
commit 16e1fedf82
Signed by: sjkillen
GPG Key ID: F307025B65C860BA
5 changed files with 360 additions and 0 deletions

1
retopo_tools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.zip

8
retopo_tools/Makefile Normal file
View 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
View 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
View 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
View 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'}