import json import re from src.engines.core.base_engine import * from src.utilities.misc_helper import system_safe_path logger = logging.getLogger() class Blender(BaseRenderEngine): install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender'] binary_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender'} file_extensions = ['blend'] @staticmethod def downloader(): from src.engines.blender.blender_downloader import BlenderDownloader return BlenderDownloader @classmethod def worker_class(cls): from src.engines.blender.blender_worker import BlenderRenderWorker return BlenderRenderWorker def ui_options(self, project_info): from src.engines.blender.blender_ui import BlenderUI return BlenderUI.get_options(self) def version(self): version = None try: render_path = self.renderer_path() if render_path: ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT) version = ver_out.decode('utf-8').splitlines()[0].replace('Blender', '').strip() except Exception as e: logger.error(f'Failed to get Blender version: {e}') return version def get_output_formats(self): format_string = self.get_help().split('Format Options')[-1].split('Animation Playback Options')[0] formats = re.findall(r"'([A-Z_0-9]+)'", format_string) return formats def run_python_expression(self, project_path, python_expression, timeout=None): if os.path.exists(project_path): try: return subprocess.run([self.renderer_path(), '-b', project_path, '--python-expr', python_expression], capture_output=True, timeout=timeout) except Exception as e: logger.error(f"Error running python expression in blender: {e}") else: raise FileNotFoundError(f'Project file not found: {project_path}') def run_python_script(self, script_path, project_path=None, timeout=None): if project_path and not os.path.exists(project_path): raise FileNotFoundError(f'Project file not found: {project_path}') elif not os.path.exists(script_path): raise FileNotFoundError(f'Python script not found: {script_path}') try: command = [self.renderer_path(), '-b', '--python', script_path] if project_path: command.insert(2, project_path) return subprocess.run(command, capture_output=True, timeout=timeout) except Exception as e: logger.exception(f"Error running python script in blender: {e}") def get_project_info(self, project_path, timeout=10): scene_info = {} try: script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py') results = self.run_python_script(project_path=project_path, script_path=system_safe_path(script_path), timeout=timeout) result_text = results.stdout.decode() for line in result_text.splitlines(): if line.startswith('SCENE_DATA:'): raw_data = line.split('SCENE_DATA:')[-1] scene_info = json.loads(raw_data) break elif line.startswith('Error'): logger.error(f"get_scene_info error: {line.strip()}") except Exception as e: logger.error(f'Error getting file details for .blend file: {e}') return scene_info def pack_project_file(self, project_path, timeout=30): # Credit to L0Lock for pack script - https://blender.stackexchange.com/a/243935 try: logger.info(f"Starting to pack Blender file: {project_path}") script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'pack_project.py') results = self.run_python_script(project_path=project_path, script_path=system_safe_path(script_path), timeout=timeout) result_text = results.stdout.decode() dir_name = os.path.dirname(project_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('Saved to: (.*)\n') match = p.search(result_text) if match: new_path = os.path.join(dir_name, match.group(1).strip()) 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}') return None def get_arguments(self): # possibly deprecate help_text = subprocess.check_output([self.renderer_path(), '-h']).decode('utf-8') lines = help_text.splitlines() options = {} current_category = None current_option = None for line in lines: line = line.strip() # Check if line starts with - or --, indicating a new option if line.endswith('Options:'): current_category = line.split('Options')[0].strip() options[current_category] = {} elif line.startswith("-") or line.startswith("/"): parts = line.split(' or ') flag = parts[-1] # Choose the verbose option has_argument = '<' in flag and '>' in flag flag = flag.split(' <')[0] # Remove argument placeholder current_option = flag.strip("--") or flag options[current_category][current_option] = { 'flag': flag, 'description': '', 'takes_argument': has_argument } elif line == "" and current_option is not None: current_option = None elif current_option is not None: d = options[current_category][current_option]['description'] d = d + (' ' if d else '') + line options[current_category][current_option]['description'] = d return options def system_info(self): return {'render_devices': self.get_render_devices()} def get_render_devices(self): script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_system_info.py') results = self.run_python_script(script_path=script_path) output = results.stdout.decode() match = re.search(r"GPU DATA:(\[[\s\S]*\])", output) if match: gpu_data_json = match.group(1) gpus_info = json.loads(gpu_data_json) return gpus_info else: logger.error("GPU data not found in the output.") def supported_render_engines(self): engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT, capture_output=True).stdout.decode('utf-8').strip() render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()] return render_engines def perform_presubmission_tasks(self, project_path): packed_path = self.pack_project_file(project_path, timeout=30) return packed_path if __name__ == "__main__": x = Blender().get_render_devices() print(x)