diff --git a/README.md b/README.md index 010ddd9..b1a9ac4 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Download/clone the repo, or just the file `export_diffusion.py`. In Blender go t * Start Frame - Which frame to start from, inclusive. Note: the first frame will always be exporteds as frame 0 * End Frame - Which frame to stop at, exclusive. * Translation Scale -- How much to multiply blender world units by for the translate x,y,z values. +* Added export raw data - This option removes the clutter for ease of use copy/paste into webui. Working on webui import and parseq import. # Usage Notes After exporting, open the text file. All exported cameras will have their own block of strings. The strings are designed as python code you can copy/paste as a block into the notebook animation section. diff --git a/export_diffusion.py b/export_diffusion.py index 1b64990..1bdbf40 100644 --- a/export_diffusion.py +++ b/export_diffusion.py @@ -1,245 +1,199 @@ bl_info = { - "name": "Export Camera Animation to Diffusion Notebook String", - "author": "Michael Walker (@mwalk10)", - "version": (1, 1, 4), - "blender": (3, 3, 0), - "location": "File > Export > Diffusion Notebook String", - "description": "Export camera animations formatted for use in Deforum diffusion collab notebook animations.", + "name": "Export Camera Animation to Deforum collab/webui", + "author": "Kewk", + "version": (1, 4, 0), + "blender": (4, 1, 0), + "location": "File > Export > Diffusion String", + "description": "Export camera animations formatted for use in Deforum", "warning": "", "wiki_url": "", "category": "Import-Export", } - import bpy +from bpy_extras.io_utils import ImportHelper, ExportHelper +from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty +from bpy.types import Operator from math import degrees -from mathutils import Vector -from bpy import context from math import isclose import json -def roundZero(num, magnitude_thresh = 0.00001): +def roundZero(num, magnitude_thresh=0.00001): if abs(num) > magnitude_thresh: return num else: return 0 - + def arr_to_keyframes(arr): keyframes = "" for i, val in enumerate(arr): val = roundZero(val) - #if we previously had a zero, then we can stop emitting zeroes until right before the next nonzero last_is_same = i > 0 and isclose(val, roundZero(arr[i-1])) next_is_same = (i+1) < len(arr) and isclose(val, roundZero(arr[i+1])) - omit = last_is_same and next_is_same - if not omit: keyframes += f"{i}:({val})," return keyframes - -def cameras_to_string(context, startFrame, endFrame, cameras, translation_scale = 50, output_camcode = True, output_json = False,): - # get the current selection + +def cameras_to_string(context, startFrame, endFrame, cameras, translation_scale=50, output_camcode=True, output_json=False, output_raw_frames=False): scene = context.scene currentFrame = scene.frame_current - if len(cameras) == 0: print("Nothing selected!") return "No Cameras selected for export" - export_string = "" - - # iterate through the selected objects for sel in cameras: - #init scene.frame_set(startFrame) - translation_x = [] translation_y = [] translation_z = [] rotation_3d_x = [] rotation_3d_y = [] rotation_3d_z = [] - oldMat = sel.matrix_world.copy() oldRot = oldMat.to_quaternion() - - #cycle trough all the animated frames for frame in range(startFrame+1, endFrame): - #Update animation frame to grab values from scene.frame_set(frame) - - newMat = sel.matrix_world.copy() #local to world matrix + newMat = sel.matrix_world.copy() newRot = newMat.to_quaternion() - worldToLocal = newMat.inverted() wlRot = worldToLocal.to_quaternion() - posDiff = newMat.to_translation() - oldMat.to_translation() posDiffLocal = wlRot @ posDiff - translation_x.append(translation_scale*posDiffLocal.x) translation_y.append(translation_scale*posDiffLocal.y) translation_z.append(-translation_scale*posDiffLocal.z) - rotDiff = oldRot.rotation_difference(newRot).to_euler("XYZ") - rotation_3d_x.append(degrees(rotDiff.x)) rotation_3d_y.append(degrees(-rotDiff.y)) rotation_3d_z.append(degrees(-rotDiff.z)) - oldMat = newMat oldRot = newRot - - #Done looping over frames, now to format for print - export_string += f"\nCamera Export: {sel.name}\n" - - - export_string += f'translation_x = "{arr_to_keyframes(translation_x)}" #@param {{type:"string"}}\n' - export_string += f'translation_y = "{arr_to_keyframes(translation_y)}" #@param {{type:"string"}}\n' - export_string += f'translation_z = "{arr_to_keyframes(translation_z)}" #@param {{type:"string"}}\n' - export_string += f'rotation_3d_x = "{arr_to_keyframes(rotation_3d_x)}" #@param {{type:"string"}}\n' - export_string += f'rotation_3d_y = "{arr_to_keyframes(rotation_3d_y)}" #@param {{type:"string"}}\n' - export_string += f'rotation_3d_z = "{arr_to_keyframes(rotation_3d_z)}" #@param {{type:"string"}}\n' - + if not output_raw_frames: + export_string += f"\nCamera Export: {sel.name}\n" + export_string += f'translation_x = "{arr_to_keyframes(translation_x)}" #@param {{type:"string"}}\n' + export_string += f'translation_y = "{arr_to_keyframes(translation_y)}" #@param {{type:"string"}}\n' + export_string += f'translation_z = "{arr_to_keyframes(translation_z)}" #@param {{type:"string"}}\n' + export_string += f'rotation_3d_x = "{arr_to_keyframes(rotation_3d_x)}" #@param {{type:"string"}}\n' + export_string += f'rotation_3d_y = "{arr_to_keyframes(rotation_3d_y)}" #@param {{type:"string"}}\n' + export_string += f'rotation_3d_z = "{arr_to_keyframes(rotation_3d_z)}" #@param {{type:"string"}}\n' if output_camcode: export_string += f'cam_code:\n(translation_x,translation_y,translation_z,rotation_3d_x,rotation_3d_y,rotation_3d_z) = ("{arr_to_keyframes(translation_x)}", "{arr_to_keyframes(translation_y)}", "{arr_to_keyframes(translation_z)}", "{arr_to_keyframes(rotation_3d_x)}", "{arr_to_keyframes(rotation_3d_y)}", "{arr_to_keyframes(rotation_3d_z)}")\n' - if output_json: jsondict = { - "translation_x" : translation_x, - "translation_y" : translation_y, - "translation_z" : translation_z, - "rotation_3d_x" : rotation_3d_x, - "rotation_3d_y" : rotation_3d_y, - "rotation_3d_z" : rotation_3d_z} + "translation_x": translation_x, + "translation_y": translation_y, + "translation_z": translation_z, + "rotation_3d_x": rotation_3d_x, + "rotation_3d_y": rotation_3d_y, + "rotation_3d_z": rotation_3d_z + } export_string += f"JSON:\n {json.dumps(jsondict)}\n" - + if output_raw_frames: + raw_frames = { + "translation_x": translation_x, + "translation_y": translation_y, + "translation_z": translation_z, + "rotation_3d_x": rotation_3d_x, + "rotation_3d_y": rotation_3d_y, + "rotation_3d_z": rotation_3d_z + } + for key, arr in raw_frames.items(): + raw_frame_str = "" + last_val = None + for i, val in enumerate(arr): + if last_val is None or not isclose(val, last_val): + raw_frame_str += f"{i}:({val})," + last_val = val + raw_frame_str = raw_frame_str.rstrip(",") + export_string += f"\nRaw frames for {key}:\n{raw_frame_str}\n" export_string += "\n" - - #Done saving all cameras, restore original animation frame scene.frame_set(currentFrame) return export_string -def write_camera_data(context, filepath, start, end, cams, scale, output_camcode, output_json): +def write_camera_data(context, filepath, start, end, cams, scale, output_camcode, output_json, output_raw_frames): print("running write_camera_data...") - outputString = cameras_to_string(context, start, end, cams, scale, output_camcode, output_json) + outputString = cameras_to_string(context, start, end, cams, scale, output_camcode, output_json, output_raw_frames) with open(filepath, 'w', encoding='utf-8') as f: f.write(f"Export frames {start} - {end}\n") - f.write(f"Export cameras {[c.name for c in cams]}\n") f.write(outputString) return {'FINISHED'} - -# ExportHelper is a helper class, defines filename and -# invoke() function which calls the file selector. -from bpy_extras.io_utils import ExportHelper -from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty -from bpy.types import Operator - - class ExportDiffusionString(Operator, ExportHelper): - """Export animation keyframes in the format of Deforum Diffusion camera animation keyframes""" - bl_idname = "export_scene.diffusion" # important since its how bpy.ops.import_test.some_data is constructed - bl_label = "Export Diffusion" + bl_idname = "export_scene.diffusion_string" + bl_label = "Export Diffusion String" - # ExportHelper mixin class uses this filename_ext = ".txt" filter_glob: StringProperty( default="*.txt", options={'HIDDEN'}, - maxlen=255, # Max internal buffer length, longer would be clamped. + maxlen=255, + ) + + start_frame: IntProperty( + name="Start Frame", + description="Starting frame for the export", + default=1, + min=1, + max=300000 ) + end_frame: IntProperty( + name="End Frame", + description="End frame for the export", + default=250, + min=1, + max=300000 + ) + + translation_scale: FloatProperty( + name="Position Scale", + description="Scales camera motion. Higher values make the camera move more", + default=1, + min=0, + max=1000 + ) + + output_camcode: BoolProperty( + name="Output Camcode", + description="Output a code block formatted for use as a camcode input", + default=True + ) + + output_json: BoolProperty( + name="Output JSON", + description="Output a JSON formatted dictionary", + default=False + ) -# animation_mode: EnumProperty( -# name="Animation Mode", -# description="2d or 3d. (only 3d supported currently)", -# items=( -# ('3D', "3d", "3d Camera Movement"), -# ), -# default='3D', -# ) - - output_json: BoolProperty(name="Output JSON", default=False) - output_cam_code: BoolProperty(name="Output cam_code", default=True) - - frame_start: IntProperty(name="Start", default=-1) - frame_end: IntProperty(name="End", default=-1)#bpy.context.scene.frame_end - - which_cams: EnumProperty( - name="Which Cams", - description="Which cameras to exprot", - items=( - ('ACTIVE', "Active", "Scene's active camera"), - ('SELECTED', "Selected", "Selected cameras only"), - ('ALL', "All", "All cameras in scene"), - ), - default='ACTIVE', + output_raw_frames: BoolProperty( + name="Output Raw Frames", + description="Output raw translation/rotation values for each frame for each axis", + default=False ) - - translation_scale: FloatProperty(default=50, name="Translation Scale", description = "Conversion factor between blender units and Diffusion units") - - def draw(self, context): - layout = self.layout - row = layout.row() - row.label(text="Which Cameras") - row = layout.row() - row.props_enum(self, "which_cams") - - row = layout.row() - row.label(text="Export Settings") - - row = layout.row() - row.label(text="Frames") - if self.frame_start == -1: - self.frame_start = bpy.context.scene.frame_start - if self.frame_end == -1: - self.frame_end = bpy.context.scene.frame_end - row.prop(self, "frame_start") - row.prop(self, "frame_end") - -# row = layout.row() -# row.prop(self, "animation_mode") - row = layout.row() - row.prop(self, "translation_scale") - - row = layout.row() - row.prop(self, "output_cam_code") - row = layout.row() - row.prop(self, "output_json") - + def execute(self, context): - export_cams = [] - if self.which_cams == "ACTIVE": - export_cams = [context.scene.camera] - elif self.which_cams == "SELECTED": - export_cams = [cam for cam in context.selected_objects if cam.type == 'CAMERA'] - elif self.which_cams == "ALL": - export_cams = [cam for cam in context.scene.objects if cam.type == 'CAMERA'] - return write_camera_data(context, self.filepath, self.frame_start, self.frame_end, export_cams, - self.translation_scale, self.output_cam_code, self.output_json) - - -# Only needed if you want to add into a dynamic menu -def menu_func_export(self, context): - self.layout.operator(ExportDiffusionString.bl_idname, text="Diffusion Notebook String") + start = self.start_frame + end = self.end_frame + cams = context.selected_objects + scale = self.translation_scale + camcode = self.output_camcode + json_output = self.output_json + raw_frames = self.output_raw_frames + write_camera_data(context, self.filepath, start, end, cams, scale, camcode, json_output, raw_frames) + return {'FINISHED'} +def menu_func_export(self, context): + self.layout.operator(ExportDiffusionString.bl_idname, text="Diffusion String (.txt)") -# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access). def register(): bpy.utils.register_class(ExportDiffusionString) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - def unregister(): bpy.utils.unregister_class(ExportDiffusionString) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) - if __name__ == "__main__": register() - - # test call - bpy.ops.export_scene.diffusion('INVOKE_DEFAULT')