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
)
2022-03-24 14:41:32 +01:00
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
2022-03-24 14:41:32 +01:00
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)
2022-03-24 14:41:32 +01:00
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 " )
2022-03-24 14:41:32 +01:00
"""
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 ( )
2022-03-24 14:41:32 +01:00
"""
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
2022-03-24 18:25:56 +01:00
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
2022-03-24 18:25:56 +01:00
# 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 "
2022-03-24 18:25:56 +01:00
## 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
2022-03-24 14:41:32 +01:00
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