diff --git a/utilities/aerender_worker.py b/utilities/aerender_worker.py index cc2bc37..614c610 100644 --- a/utilities/aerender_worker.py +++ b/utilities/aerender_worker.py @@ -19,8 +19,8 @@ def aerender_path(): class AERenderWorker(BaseRenderWorker): - @staticmethod - def version(): + @classmethod + def version(cls): version = None try: x = subprocess.Popen([aerender_path(), '-version'], stdout=subprocess.PIPE) diff --git a/utilities/blender_worker.py b/utilities/blender_worker.py index 1bad831..2b4016d 100644 --- a/utilities/blender_worker.py +++ b/utilities/blender_worker.py @@ -23,16 +23,18 @@ class BlenderRenderWorker(BaseRenderWorker): render_engine = 'blender' supported_extensions = ['.blend'] install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender'] + supported_export_formats = ['TGA', 'RAWTGA', 'JPEG', 'IRIS', 'IRIZ', 'AVIRAW', 'AVIJPEG', 'PNG', 'BMP', 'HDR', 'TIFF', + 'OPEN_EXR', 'OPEN_EXR_MULTILAYER', 'MPEG', 'CINEON', 'DPX', 'DDS', 'JP2'] - def __init__(self, input_path, output_path, args=None, render_all_frames=False, engine='BLENDER_EEVEE'): + def __init__(self, input_path, output_path, args=None): super(BlenderRenderWorker, self).__init__(input_path=input_path, output_path=output_path, ignore_extensions=False, args=args) - self.engine = engine # or 'CYCLES' - self.format = 'JPEG' + self.engine = self.args.get('engine', 'BLENDER_EEVEE').upper() + self.export_format = self.args.get('export_format', 'JPEG') self.frame = 0 - self.render_all_frames = render_all_frames + self.render_all_frames = self.args.get('render_all_frames', False) # Stats self.current_frame = -1 @@ -46,15 +48,15 @@ class BlenderRenderWorker(BaseRenderWorker): def _generate_subprocess(self): - if self.format not in SUPPORTED_FORMATS: - raise ValueError("Unsupported format for Blender: {}".format(self.format)) + if self.export_format not in self.supported_export_formats: + raise ValueError("Unsupported format for Blender: {}".format(self.export_format)) if self.render_all_frames: cmd = [self.renderer_path(), '-b', self.input, '-E', self.engine, '-o', self.output, - '-F', self.format, '-a'] + '-F', self.export_format, '-a'] else: cmd = [self.renderer_path(), '-b', self.input, '-E', self.engine, '-o', self.output, - '-F', self.format, '-f', str(self.frame)] + '-F', self.export_format, '-f', str(self.frame)] return cmd def _parse_stdout(self, line): @@ -73,7 +75,6 @@ class BlenderRenderWorker(BaseRenderWorker): samples = re.sub(r'[^\d/]', '', sample_string) self.frame_percent_complete = int(samples.split('/')[0]) / int(samples.split('/')[-1]) - # Calculate rough percent based on cycles # EEVEE # 10-Apr-22 22:42:06 - RENDERER: Fra:0 Mem:857.99M (Peak 928.55M) | Time:00:03.96 | Rendering 1 / 65 samples @@ -87,19 +88,22 @@ class BlenderRenderWorker(BaseRenderWorker): if int(stats['frame']) > self.current_frame: self.current_frame = int(stats['frame']) - logger.info( + logger.debug( 'Frame:{0} | Mem:{1} | Time:{2} | Remaining:{3}'.format(self.current_frame, self.memory_use, self.time_elapsed, self.time_remaining)) elif 'error' in line.lower(): logger.error(line) self.errors.append(line) elif 'Saved' in line or 'Saving' in line or 'quit' in line: - x = re.match(r'Time: (.*) \(Saving', line) - if x: - time_completed = x.groups()[0] - logger.info('Render completed in {}'.format(time_completed)) + match = re.match(r'Time: (.*) \(Saving', line) + if match: + time_completed = match.groups()[0] + if self.render_all_frames: + logger.debug(f'Frame {self.current_frame} completed in {time_completed}') + else: + logger.info(f'Render completed in {time_completed}') else: - logger.info(line) + logger.debug(line) else: pass # if len(line.strip()): @@ -113,23 +117,41 @@ class BlenderRenderWorker(BaseRenderWorker): (self.frame_percent_complete * (self.current_frame / self.total_frames)) +def run_python_expression_in_blend(path, python_expression): + if os.path.exists(path): + try: + return subprocess.run(['blender', '-b', path, '--python-expr', python_expression], capture_output=True) + except Exception as e: + logger.warning(f"Error running python expression in blender: {e}") + pass + else: + raise FileNotFoundError + + def pack_blender_files(path): # Credit to L0Lock for pack script - https://blender.stackexchange.com/a/243935 pack_script = "import bpy\nbpy.ops.file.pack_all()\nmyPath = bpy.data.filepath\nmyPath = str(myPath)\n" \ "bpy.ops.wm.save_as_mainfile(filepath=myPath[:-6]+'_packed'+myPath[-6:])" - if os.path.exists(path): - try: - results = subprocess.check_output(['blender', '-b', path, '--python-expr', pack_script]) - result_text = results.decode() - dir_name = os.path.dirname(path) + try: + results = run_python_expression_in_blend(path, pack_script) - p = re.compile('Info: Saved "(.*)"') - match = p.search(result_text) + result_text = results.stdout.decode() + dir_name = os.path.dirname(path) + + # report any missing textures + not_found = re.findall("(Unable to pack file, source path .*)\n", result_text) + for err in not_found: + logger.error(err) + + p = re.compile('Info: Saved "(.*)"') + match = p.search(result_text) + if match: new_path = os.path.join(dir_name, match.group(1)) + logger.info(f'Blender file packed successfully to {new_path}') return new_path - except Exception as e: - logger.error(f'Error packing .blend file: {e}') + except Exception as e: + logger.error(f'Error packing .blend file: {e}') return None diff --git a/utilities/ffmpeg_worker.py b/utilities/ffmpeg_worker.py index 17f3c21..7ee2588 100644 --- a/utilities/ffmpeg_worker.py +++ b/utilities/ffmpeg_worker.py @@ -7,10 +7,11 @@ from utilities.render_worker import * class FFMPEGRenderWorker(BaseRenderWorker): - def version(self): + @classmethod + def version(cls): version = None try: - ver_out = subprocess.check_output([self.renderer_path(), '-version']).decode('utf-8') + ver_out = subprocess.check_output([cls.renderer_path(), '-version']).decode('utf-8') match = re.match(".*version\s*(\S+)\s*Copyright", ver_out) version = match.groups()[0] except Exception as e: diff --git a/utilities/render_worker.py b/utilities/render_worker.py index d677695..5e4d848 100644 --- a/utilities/render_worker.py +++ b/utilities/render_worker.py @@ -33,10 +33,11 @@ class BaseRenderWorker(object): render_engine = None supported_extensions = [] install_paths = [] + supported_export_formats = [] - @staticmethod - def version(): - return 'Unknown' + @classmethod + def version(cls): + raise NotImplementedError("Unknown version") def __init__(self, input_path, output_path, args=None, ignore_extensions=True): @@ -78,12 +79,13 @@ class BaseRenderWorker(object): self.is_finished = False self.last_output = None - def renderer_path(self): + @classmethod + def renderer_path(cls): path = None try: - path = subprocess.check_output(['which', self.render_engine]).decode('utf-8').strip() + path = subprocess.check_output(['which', cls.render_engine]).decode('utf-8').strip() except Exception as e: - for p in self.install_paths: + for p in cls.install_paths: if os.path.exists(p): path = p # if not path: @@ -91,7 +93,7 @@ class BaseRenderWorker(object): return path def _generate_subprocess(self): - return [] + raise NotImplementedError("_generate_subprocess not implemented") def start(self): @@ -142,6 +144,7 @@ class BaseRenderWorker(object): f.write("{3} - Starting {0} {1} Render for {2}\n".format(self.renderer, self.version(), self.input, self.start_time.isoformat())) + f.write(f"Running command: {' '.join(subprocess_cmds)}\n") for c in io.TextIOWrapper(self.process.stdout, encoding="utf-8"): # or another encoding f.write(c) logger.debug(f"{self.renderer}Worker: {c.strip()}") @@ -198,7 +201,7 @@ class BaseRenderWorker(object): return 0 def _parse_stdout(self, line): - pass + raise NotImplementedError("_parse_stdout not implemented") def elapsed_time(self): elapsed = "" @@ -214,25 +217,30 @@ class RenderWorkerFactory: @staticmethod def create_worker(renderer, input_path, output_path, args=None): + worker_class = RenderWorkerFactory.class_for_name(renderer) + return worker_class(input_path=input_path, output_path=output_path, args=args) + + @staticmethod + def supported_renderers(): + return ['aerender', 'blender', 'ffmpeg'] + + @staticmethod + def class_for_name(name): from utilities.blender_worker import BlenderRenderWorker from utilities.aerender_worker import AERenderWorker from utilities.ffmpeg_worker import FFMPEGRenderWorker - if "blender" == renderer.lower(): - worker = BlenderRenderWorker(input_path, output_path, args=args) - elif "aerender" == renderer.lower() or "after effects" == renderer.lower(): - worker = AERenderWorker(input_path, output_path, args=args) - elif "ffmpeg" == renderer.lower(): - worker = FFMPEGRenderWorker(input_path, output_path, args=args) - else: - raise ValueError(f"Cannot find renderer for type '{renderer}'") + name = name.lower() - return worker + if "blender" == name: + return BlenderRenderWorker + elif "aerender" == name: + return AERenderWorker + elif "ffmpeg" == name: + return FFMPEGRenderWorker - @staticmethod - def supported_renderers(): - return ['aerender', 'blender', 'ffmpeg'] + raise LookupError(f'Cannot find class for name: {name}') def timecode_to_frames(timecode, frame_rate):