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 import subprocess, threading # viewport draw import gpu, blf from gpu_extras.batch import batch_for_shader from .utils import open_file, open_folder, get_addon_prefs, detect_OS ## based on playblaster exclude = ( ### add lines here to exclude specific attribute 'bl_rna', 'identifier','name_property','rna_type','properties', 'compare', 'to_string',#basic ) def delete_file(filepath): fp = Path(filepath) if fp.exists() and fp.is_file(): try: print('removing', fp) fp.unlink(missing_ok=False) # os.remove(fp) return True except PermissionError: print(f'impossible to remove (permission error): {fp}') return False except FileNotFoundError: print(f'Impossible to remove (file not found error): {fp}') return False # render function def render_function(cmd, total_frame, scene) : debug = bpy.context.window_manager.pblast_debug # launch rendering if debug : print(cmd) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) frame_count = 0 while True : if not bpy.context.window_manager.pblast_is_rendering : # print('!!! Not rendering') if detect_OS() == 'Windows': subprocess.Popen(f"TASKKILL /F /PID {process.pid} /T") else: process.kill() break line = process.stdout.readline() if line != '' : #debug if debug : print(line) if b'Traceback' in line: print('/!\\ Traceback in line return') if b"Append frame " in line : frame_count += 1 try : # print('frame_count: ', frame_count, 'total_frame: ', total_frame) bpy.context.window_manager.pblast_completion = int(frame_count / total_frame * 100) except AttributeError : #debug if debug : print("AttributeError avoided") pass if b"Blender quit" in line : break else: break print('ENDED') # launch threading def threading_render(arguments) : render_thread = threading.Thread(target=render_function, args=arguments) render_thread.start() # callback for loading bar in 3D view def draw_callback_px(self, context): # get color and size of progress bar # prefs = get_addon_prefs() color_bar = [1, 1, 1] # prefs.progress_bar_color background = [0.2, 0.2, 0.2] # prefs.progress_bar_background_color bar_thickness = 10 # prefs.progress_bar_size # Progress Bar width = context.area.width # context.window_manager.pblast_completion complete = self.completion / 100# context.window_manager.pblast_completion / 100 size = int(width * complete) # rectangle background vertices_2 = ( (0, 0), (width, 0), (0, bar_thickness + 20), (width, bar_thickness + 20)) indices = ( (0, 1, 2), (2, 1, 3)) shader2 = gpu.shader.from_builtin('2D_UNIFORM_COLOR') batch2 = batch_for_shader(shader2, 'TRIS', {"pos": vertices_2}, indices=indices) shader2.bind() shader2.uniform_float("color", [*background, 1]) batch2.draw(shader2) # rectangle 1 vertices = ( (0, 0), (size, 0), (0, bar_thickness), (size, bar_thickness)) shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) shader.bind() shader.uniform_float("color", [*color_bar, 1]) batch.draw(shader) # Text # text = f'Playblast in Progress ({complete}%) - Shift + Esc to Cancel' text = f'Playblast in Progress - Shift + Esc to Cancel' blf.color(0, *color_bar, 1) blf.size(0, 12, 72) blf.position(0, 10, bar_thickness + 5, 0) blf.draw(0, text) class BGBLAST_OT_playblast_modal_check(bpy.types.Operator): '''Modal and external render from Samy Tichadou (Tonton)''' bl_idname = "render.playblast_modal_check" bl_label = "Playblast Modal Check" bl_options = {'INTERNAL'} _timer = None blend_pb : bpy.props.StringProperty() video_pb : bpy.props.StringProperty() @classmethod def poll(cls, context): return context.window_manager.pblast_is_rendering def modal(self, context, event): self.completion = context.window_manager.pblast_completion # redraw area try: context.area.tag_redraw() except AttributeError: pass # handle cancelling if event.type in {'ESC'} and event.shift: self.cancel(context) return {'CANCELLED'} if event.type == 'TIMER' : if self.completion == 100 : self.finish(context) return {'FINISHED'} return {'PASS_THROUGH'} # def invoke(self, context, event): def execute(self, context): context.window_manager.pblast_completion = 0 wm = context.window_manager args = (self, context) wm = context.window_manager self.debug = wm.pblast_debug self.completion = wm.pblast_completion self._timer = wm.event_timer_add(0.1, window = context.window) self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') wm.modal_handler_add(self) return {'RUNNING_MODAL'} def cancel(self, context): print('in CANCEL') bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') wm = context.window_manager wm.event_timer_remove(self._timer) # turn off is_rendering wm.pblast_is_rendering = False wm.pblast_completion = 0 # delete temp file delete_file(self.blend_pb) self.blend_pb = "" # delete temp video delete_file(self.video_pb) self.video_pb = "" self.report({'WARNING'}, "Render Canceled") def finish(self, context): print('in FINISH') bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') wm = context.window_manager wm.event_timer_remove(self._timer) # turn off is_rendering wm.pblast_is_rendering = False wm.pblast_completion = 0 # debug if self.debug : print("blend temp : " + self.blend_pb) print("video temp : " + self.video_pb) # delete temp file delete_file(self.blend_pb) # open video file and/or folder prefs = get_addon_prefs() if prefs.playblast_auto_play: # open_file(self.video_pb) bpy.ops.wm.path_open(filepath=self.video_pb) if prefs.playblast_auto_open_folder: # open_folder(dirname(self.video_pb)) bpy.ops.wm.path_open(filepath=dirname(self.video_pb)) wm.pblast_previous_render = self.video_pb self.report({'INFO'}, "Render Finished") """ 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 = {} 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) 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) 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 = {} 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) 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) def playblast(context, viewport = False, stamping = True): scn = bpy.context.scene res_factor = scn.gptoolprops.resolution_percentage rd = scn.render ff = rd.ffmpeg playblast_path = get_addon_prefs().playblast_path prefix = 'tempblast_' # delete old playblast and blend files folder = dirname(bpy.data.filepath) for f in os.listdir(folder): if f.startswith(prefix): delete_file(join(folder, f)) pblast_folder = join(folder, 'playblast') if exists(pblast_folder): for f in os.listdir(pblast_folder): if f.startswith(prefix): delete_file(join(pblast_folder, f)) tempblend = str( Path(folder) / f'{prefix}{basename(bpy.data.filepath)}' ) 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 backup_img_settings = rd.image_settings.file_format 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.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 incremental or just use fulldate (cannot create conflict and filter OK but long name) date_format = "%Y-%m-%d_%H-%M-%S" ## old blend = Path(bpy.data.filepath) # fp = blend.parent / "playblast" / f'{prefix}{blend.stem}_{strftime(date_format)}.mp4' ## with path variable fp = Path(bpy.path.abspath(playblast_path)).resolve() / f'{prefix}{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 # get total number of frames total_frame = context.scene.frame_end - context.scene.frame_start + 1 # bpy.ops.render.render_wrap(use_view=viewport) bpy.ops.wm.save_as_mainfile(filepath = tempblend, copy=True) cmd = f'"{bpy.app.binary_path}" -b "{tempblend}" -a' # cmd = '"' + bpy.app.binary_path + '"' + " -b " + '"' + new_blend_filepath + '"' + " -E " + render_engine + " -a" threading_render([cmd, total_frame, context.scene]) rd.image_settings.file_format = backup_img_settings#seems it tries to set some properties before the man imgs settings # lauch BG render modal (ovrerride ?) context.window_manager.pblast_completion = 0 context.window_manager.pblast_is_rendering = True bpy.ops.render.playblast_modal_check(blend_pb=tempblend,video_pb=fp) # print("Playblast Done :", fp)#Dbg return fp class BGBLAST_OT_playblast_anim(bpy.types.Operator): bl_idname = "render.thread_playblast" bl_label = "Playblast anim" bl_description = "Launch animation playblast in a thread (non-blocking)" bl_options = {"REGISTER"} def execute(self, context): if not bpy.data.is_saved: self.report({'ERROR'}, 'File is not saved, Playblast cancelled') return {"CANCELLED"} playblast(context, viewport = False, stamping = True) return {"FINISHED"} def register(): bpy.types.WindowManager.pblast_is_rendering = bpy.props.BoolProperty() bpy.types.WindowManager.pblast_completion = bpy.props.IntProperty(min = 0, max = 100) bpy.types.WindowManager.pblast_previous_render = bpy.props.StringProperty() bpy.types.WindowManager.pblast_debug = bpy.props.BoolProperty('INVOKE_DEFAULT', name="Debug", default=False) bpy.utils.register_class(BGBLAST_OT_playblast_anim) bpy.utils.register_class(BGBLAST_OT_playblast_modal_check) def unregister(): bpy.utils.unregister_class(BGBLAST_OT_playblast_modal_check) bpy.utils.unregister_class(BGBLAST_OT_playblast_anim) del bpy.types.WindowManager.pblast_is_rendering del bpy.types.WindowManager.pblast_completion del bpy.types.WindowManager.pblast_previous_render del bpy.types.WindowManager.pblast_debug