256 lines
8.8 KiB
Python
256 lines
8.8 KiB
Python
|
# 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()
|