424 lines
15 KiB
Python
424 lines
15 KiB
Python
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 |