From 6190f5aac5f14ea8e7827dd1e779339d4f6d0bf9 Mon Sep 17 00:00:00 2001 From: Spencer Killen <4724387+sjkillen@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:53:58 -0700 Subject: [PATCH] Create vertex_animation.py --- vertex_animation.py | 255 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 vertex_animation.py diff --git a/vertex_animation.py b/vertex_animation.py new file mode 100644 index 0000000..d088b74 --- /dev/null +++ b/vertex_animation.py @@ -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 ##### + +# + + +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()