Compare commits

...

6 Commits

Author SHA1 Message Date
Spencer Killen 08954dd7d2
Add mesh baker 2022-12-25 12:10:23 -07:00
Spencer Killen 23faf4f2a8
Merge branch 'master' of ssh://github.com/sjkillen/blenderings 2022-12-22 13:36:13 -07:00
Spencer Killen 16e1fedf82
A retopotools 2022-12-22 13:35:06 -07:00
Spencer Killen 6190f5aac5
Create vertex_animation.py 2022-11-29 18:53:58 -07:00
Spencer Killen 42196519ae
Update vertex_animation_notes.md 2022-11-26 20:37:56 -07:00
Spencer Killen 23f2343941
Create vertex_animation_notes.md 2022-11-26 16:22:17 -07:00
11 changed files with 938 additions and 0 deletions

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

255
vertex_animation.py Normal file
View 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
View 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")
```