import bpy import os from os import listdir, scandir from os.path import join, dirname, basename, exists, isfile, isdir, splitext import re, fnmatch, glob from pathlib import Path from time import strftime C = bpy.context D = bpy.data from .utils import open_file, open_folder, get_addon_prefs exclude = ( ### add lines here to exclude specific attribute 'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic ) """ rd_keep = [ "resolution_percentage", "resolution_x", "resolution_y", "filepath", "use_stamp", "stamp_font_size", ] im_keep = [ 'file_format', 'color_mode', 'quality', 'compression', ] ff_keep = [ 'codec', 'format', 'constant_rate_factor', 'ffmpeg_preset', 'gopsize', 'audio_codec', 'audio_bitrate', ] """ ''' def render_with_restore(): class RenderFileRestorer: rd = bpy.context.scene.render im = rd.image_settings ff = rd.ffmpeg # ffmpeg (ff) need to be before image_settings(im) in list # otherwise __exit__ may try to restore settings of image mode in video mode ! # ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block) zones = [rd, ff, im] val_dic = {} cam = bpy.context.scene.camera def __enter__(self): ## store attribute of data_path in self.zones list. for data_path in self.zones: self.val_dic[data_path] = {} for attr in dir(data_path):#iterate in attribute of given datapath if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr): self.val_dic[data_path][attr] = getattr(data_path, attr) if self.cam and self.cam.name == 'draw_cam': if self.cam.parent: bpy.context.scene.camera = self.cam.parent def __exit__(self, type, value, traceback): ## restore attribute from self.zones list for data_path, prop_dic in self.val_dic.items(): for attr, val in prop_dic.items(): try: setattr(data_path, attr, val) except Exception as e: print(f"/!\ Impossible to re-assign: {attr} = {val}") print(e) if self.cam: bpy.context.scene.camera = self.cam return RenderFileRestorer() ''' class render_with_restore: def __init__(self): rd = bpy.context.scene.render im = rd.image_settings ff = rd.ffmpeg # ffmpeg (ff) need to be before image_settings(im) in list # otherwise __exit__ may try to restore settings of image mode in video mode ! # ex : "RGBA" not found in ('BW', 'RGB') (will still not stop thx to try block) self.zones = [rd, ff, im] self.val_dic = {} self.cam = bpy.context.scene.camera def __enter__(self): ## store attribute of data_path in self.zones list. for data_path in self.zones: self.val_dic[data_path] = {} for attr in dir(data_path):#iterate in attribute of given datapath if attr not in exclude and not attr.startswith('__') and not callable(getattr(data_path, attr)) and not data_path.is_property_readonly(attr): self.val_dic[data_path][attr] = getattr(data_path, attr) if self.cam and self.cam.name == 'draw_cam': if self.cam.parent: bpy.context.scene.camera = self.cam.parent def __exit__(self, type, value, traceback): ## restore attribute from self.zones list for data_path, prop_dic in self.val_dic.items(): for attr, val in prop_dic.items(): try: setattr(data_path, attr, val) except Exception as e: print(f"/!\ Impossible to re-assign: {attr} = {val}") print(e) if self.cam: bpy.context.scene.camera = self.cam def playblast(viewport = False, stamping = True): scn = bpy.context.scene res_factor = scn.gptoolprops.resolution_percentage playblast_path = get_addon_prefs().playblast_path rd = scn.render ff = rd.ffmpeg with render_with_restore(): ### can add propeties for personalisation as toolsetting props rd.resolution_percentage = res_factor while ( rd.resolution_x * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage rd.resolution_x = rd.resolution_x + 1 while ( rd.resolution_y * res_factor / 100 ) % 2 != 0: # rd.resolution_percentage rd.resolution_y = rd.resolution_y + 1 rd.image_settings.file_format = 'FFMPEG' ff.format = 'MPEG4' ff.codec = 'H264' ff.constant_rate_factor = 'HIGH' # MEDIUM ff.ffmpeg_preset = 'REALTIME' ff.gopsize = 10 ff.audio_codec = 'AAC' ff.audio_bitrate = 128 rd.use_sequencer = False rd.stamp_background = (0.0, 0.0, 0.0, 0.75)# blacker notes BG (default 0.25) # rd.use_compositing # rd.filepath = join(dirname(bpy.data.filepath), basename(bpy.data.filepath)) # rd.frame_path(frame=0, preview=0, view="_sauce")## give absolute render filepath with some suffix # rd.is_movie_format# check if its movie mode ## set filepath # mode incermental or just use fulldate (cannot create conflict and filter OK but long name) blend = Path(bpy.data.filepath) date_format = "%Y-%m-%d_%H-%M-%S" ## old direct place # fp = join(blend.parent, "playblast", f'playblast_{blend.stem}_{strftime(date_format)}.mp4') fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'playblast_{blend.stem}_{strftime(date_format)}.mp4' fp = str(fp) #may need a properties for choosing location : bpy.types.Scene.qrd_savepath = bpy.props.StringProperty(subtype='DIR_PATH', description="Export location, if not specify, create a 'quick_render' directory aside blend location")#(change defaut name in user_prefernece) rd.filepath = fp rd.use_stamp = stamping# toolsetting.use_stamp# True for playblast #stamp options rd.stamp_font_size = int(rd.stamp_font_size * res_factor / 100) # rd.resolution_percentage # bpy.ops.render.render_wrap(use_view=viewport) ### render if viewport:## openGL bpy.ops.render.opengl(animation=True, view_context=True)# 'INVOKE_DEFAULT', else:## normal render bpy.ops.render.render(animation=True)# 'INVOKE_DEFAULT', # print("Playblast Done :", fp)#Dbg return fp class PBLAST_OT_playblast_anim(bpy.types.Operator): bl_idname = "render.playblast_anim" bl_label = "Playblast anim" bl_description = "Launch animation playblast, use resolution percentage (Lock blender during process)" bl_options = {"REGISTER"} use_view : bpy.props.BoolProperty(name='use_view', default=False) def execute(self, context): if not bpy.data.is_saved: self.report({'ERROR'}, 'File is not saved, Playblast cancelled') return {"CANCELLED"} fp = playblast(viewport = self.use_view, stamping = True) if fp: self.report({'INFO'}, f'File saved at: {fp}') addon_prefs = get_addon_prefs() if addon_prefs: if addon_prefs.playblast_auto_play: open_file(fp) if addon_prefs.playblast_auto_open_folder: open_folder(dirname(fp)) return {"FINISHED"} def register(): bpy.utils.register_class(PBLAST_OT_playblast_anim) def unregister(): bpy.utils.unregister_class(PBLAST_OT_playblast_anim) ''' ## Potential cancelling method for image sequence rendering. for cfra in range(start, end+1): print("Baking frame " + str(cfra)) # update scene to new frame and bake to template image scene.frame_set(cfra) ret = bpy.ops.object.bake_image() if 'CANCELLED' in ret: return {'CANCELLED'} ''' """ class PBLAST_OT_render_wrap(bpy.types.Operator): bl_idname = "render.render_wrap" bl_label = "Render wraped" bl_description = "render" bl_options = {"REGISTER"}## need hide use_view : bpy.props.BoolProperty(name='use_view', default=False) def execute(self, context): if self.use_view:## openGL ret = bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, view_context=True) else:## normal render ret = bpy.ops.render.render('INVOKE_DEFAULT', animation=True) return {"FINISHED"} """ """ if __name__ == "__main__": register() """