From a9e25b39e1bc4cc61516db0d3e319007c08ab3db Mon Sep 17 00:00:00 2001 From: duckz Date: Sat, 6 May 2023 18:44:04 +0800 Subject: [PATCH] Added gifski feature --- __init__.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++- main/exporter.py | 60 ++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index c3251d8..8ffe7e5 100644 --- a/__init__.py +++ b/__init__.py @@ -175,6 +175,14 @@ class BMNFTData: failed_dna: Any = None failed_dna_index: Any = None + gifski_path: str = "" + gifski_quality: int = 90 + gifski_fps: int = 24 + gifski_loop: int = 0 + gifski_width: int = "" + gifski_height: int = "" + gifski_delete_frames: bool = True + def __post_init__(self): self.custom_fields = {} @@ -238,7 +246,15 @@ def get_bmnft_data(): order_num_offset=bpy.context.scene.input_tool.order_num_offset, log_path=bpy.path.abspath(bpy.context.scene.input_tool.log_path), - enable_dry_run=bpy.context.scene.input_tool.enable_dry_run + enable_dry_run=bpy.context.scene.input_tool.enable_dry_run, + + gifski_path=bpy.context.scene.input_tool.gifski_path, + gifski_quality=bpy.context.scene.input_tool.gifski_quality, + gifski_fps=bpy.context.scene.input_tool.gifski_fps, + gifski_loop=bpy.context.scene.input_tool.gifski_loop, + gifski_width=bpy.context.scene.input_tool.gifski_width, + gifski_height=bpy.context.scene.input_tool.gifski_height, + gifski_delete_frames=bpy.context.scene.input_tool.gifski_delete_frames, ) return data @@ -344,6 +360,14 @@ def run_as_headless(): settings.enable_materials = pairs[23][1] == 'True' settings.materials_file = pairs[24][1] + settings.gifski_path = pairs[25][1] + settings.gifski_quality = pairs[26][1] + settings.gifski_fps= pairs[27][1] + settings.gifski_loop= pairs[28][1] + settings.gifski_width= pairs[29][1] + settings.gifski_height= pairs[30][1] + settings.gifski_delete_frames = pairs[31][1] == 'True' + if args.save_path: settings.save_path = args.save_path @@ -444,10 +468,57 @@ class BMNFTS_PGT_Input_Properties(bpy.types.PropertyGroup): ('FFMPEG', '.mkv (FFMPEG)', 'Export NFT as FFMPEG'), ('MP4', '.mp4', 'Export NFT as .mp4'), ('PNG', '.png', 'Export NFT as PNG'), - ('TIFF', '.tiff', 'Export NFT as TIFF') + ('TIFF', '.tiff', 'Export NFT as TIFF'), + ('GIF', '.gif (GifSki)', 'Export NFT as GIF') ] ) + gifski_path: bpy.props.StringProperty( + name="Gifski Path", + description="Define path to the executable for gifski.", + subtype="FILE_PATH", + ) + + gifski_quality: bpy.props.IntProperty( + name="Quality", + description="Lower quality may give smaller file [default: 90]", + default=90, + max=100, + min=1, + ) + + gifski_fps: bpy.props.IntProperty( + name="FPS", + description="Frame rate of animation. [default: 24]", + default=24, + max=100, + min=1, + ) + + gifski_width: bpy.props.IntProperty( + name="Width", + description="Maximum width. by default anims are limited to 800x600", + min=1, + ) + + gifski_height: bpy.props.IntProperty( + name="Height", + description="Maximum height. by default anims are limited to 800x600", + min=1, + ) + + gifski_loop: bpy.props.IntProperty( + name="Loop Count", + description="Loop x number of times; 0 = loop forever; -1 no loop", + default=0, + min=-1 + ) + + gifski_delete_frames: bpy.props.BoolProperty( + description="Delete the PNG frames folder after GIF is complete", + default=True + ) + model_bool: bpy.props.BoolProperty( name="3D Model" ) @@ -711,6 +782,14 @@ class ResumeFailedBatch(bpy.types.Operator): failed_dna_index=_failed_dna_index, custom_fields=render_settings["custom_fields"], + + gifski_path=render_settings["gifski_path"], + gifski_quality=render_settings["gifski_quality"], + gifski_fps=render_settings["gifski_fps"], + gifski_loop=render_settings["gifski_loop"], + gifski_width=render_settings["gifski_width"], + gifski_height=render_settings["gifski_height"], + gifski_delete_frames=render_settings["gifski_delete_frames"], ) exporter.render_and_save_nfts(input) @@ -807,6 +886,15 @@ class ExportSettings(bpy.types.Operator): "#Enable Materials\n" f"enable_materials={str(settings.enable_materials)}\n" f"materials_file={settings.materials_file}\n" + "\n" + "#GifSki Settings\n" + f"gifski_path={settings.gifski_path}\n" + f"gifski_quality={settings.gifski_quality}\n" + f"gifski_fps={settings.gifski_fps}\n" + f"gifski_loop={settings.gifski_loop}\n" + f"gifski_width={settings.gifski_width}\n" + f"gifski_height={settings.gifski_height}\n" + f"gifski_delete_frames={str(settings.gifski_delete_frames)}\n" ) print(output, file=config) @@ -920,6 +1008,32 @@ class BMNFTS_PT_GenerateNFTs(bpy.types.Panel): if bpy.context.scene.input_tool.animation_bool: row.prop(input_tool_scene, "animation_enum") + if bpy.context.scene.input_tool.animation_enum == 'GIF': + row = layout.row() + + row = layout.row() + row.prop(input_tool_scene, "gifski_path", text="GifSki Executable Path") + + row = layout.row() + row.prop(input_tool_scene, "gifski_quality", text="Quality") + + row = layout.row() + row.prop(input_tool_scene, "gifski_fps", text="FPS") + + row = layout.row() + row.prop(input_tool_scene, "gifski_loop", text="Loop") + + row = layout.row() + row.prop(input_tool_scene, "gifski_width", text="Width") + + row = layout.row() + row.prop(input_tool_scene, "gifski_height", text="Height") + + row = layout.row() + row.prop(input_tool_scene, "gifski_delete_frames", text="Cleanup on Completion?") + row = layout.row() + + row = layout.row() row.prop(input_tool_scene, "model_bool") if bpy.context.scene.input_tool.model_bool: diff --git a/main/exporter.py b/main/exporter.py index f2b098e..41b02e2 100644 --- a/main/exporter.py +++ b/main/exporter.py @@ -12,6 +12,8 @@ import logging import datetime import platform import traceback +import subprocess +import shutil from .helpers import TextColors, Loader from .metadata_templates import create_cardano_metadata, createSolanaMetaData, create_erc721_meta_data @@ -139,6 +141,48 @@ def get_batch_data(batch_to_generate, batch_json_save_path): return nfts_in_batch, hierarchy, batch_dna_list +# Convert PNG's into GIF using Gifski +def pngs_2_gifs(context, abspath, frames_folder): + """Convert the PNGs to Animated GIF""" + + o_file = ''.join([abspath]) + gifski = "gifski" + if not context.gifski_path.strip() == "": + gifski = bpy.path.abspath(context.gifski_path.strip()) + + command = [gifski] + + if context.gifski_quality: + command.append("--quality") + command.append(str(context.gifski_quality)) + + if context.gifski_fps: + command.append("--fps") + command.append(str(context.gifski_fps)) + + if context.gifski_loop: + command.append("--repeat") + command.append(str(context.gifski_loop)) + + if context.gifski_width: + command.append("-W") + command.append(str(context.gifski_width)) + + if context.gifski_height: + command.append("-H") + command.append(str(context.gifski_height)) + + command.append("-o") + command.append(o_file) + + # Need to figure out why subprocess hates *.png calls and remove this manual file injection + for file in os.listdir(frames_folder): + if file.endswith(".png"): + command.append(os.path.join(frames_folder, file)) + + subprocess.call(command) + + def render_and_save_nfts(input): """ Renders the NFT DNA in a Batch#.json, where # is renderBatch in config.py. Turns off the viewport camera and @@ -399,6 +443,22 @@ def render_and_save_nfts(input): bpy.context.scene.render.image_settings.file_format = input.animation_file_format bpy.ops.render.render(animation=True) + elif input.animation_file_format == 'GIF': + if not os.path.exists(animation_path): + os.makedirs(animation_path) + + bpy.context.scene.render.filepath = os.path.join(animation_path, name) + bpy.context.scene.render.image_settings.file_format = 'PNG' + bpy.ops.render.render(animation=True) + + i_file = os.path.join(animation_folder, name + '.gif') + + # Not sure where to store / add the generated image path to? + pngs_2_gifs(input, i_file, animation_path) + + if input.gifski_delete_frames: + shutil.rmtree(animation_path) + else: bpy.context.scene.render.filepath = animation_path bpy.context.scene.render.image_settings.file_format = input.animation_file_format