gp_toolbox/OP_playblast_bg.py

424 lines
15 KiB
Python
Raw Permalink Normal View History

2021-01-10 16:47:17 +01:00
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)
2021-01-10 16:47:17 +01:00
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
2021-01-10 16:47:17 +01:00
# 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)
2021-01-10 16:47:17 +01:00
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")
"""
2021-01-10 16:47:17 +01:00
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)
2021-01-10 16:47:17 +01:00
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
2021-01-10 16:47:17 +01:00
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)
2021-01-10 16:47:17 +01:00
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)
2021-01-10 16:47:17 +01:00
#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
2021-01-10 16:47:17 +01:00
# 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