import bpy import os from . import utils @bpy.app.handlers.persistent def on_render_complete(scene, depsgraph=None): SCENERENDERER_OT_render_all._is_rendering = False @bpy.app.handlers.persistent def on_render_cancel(scene, depsgraph=None): SCENERENDERER_OT_render_all._is_rendering = False SCENERENDERER_OT_render_all._cancel_requested = True class SCENERENDERER_OT_render_all(bpy.types.Operator): bl_idname = "scenerenderer.render_all" bl_label = "Render All Scenes" bl_description = "Render all scenes to //renders/" bl_options = {'REGISTER'} _timer = None _scenes = [] _index = 0 _total = 0 _orig_lock = None _is_rendering = False _cancel_requested = False def modal(self, context, event): if event.type == 'ESC' or self._cancel_requested: self.report({'WARNING'}, "Render batch cancelled") self.finish(context) return {'CANCELLED'} if event.type == 'TIMER': if self._is_rendering: return {'PASS_THROUGH'} if self._index >= self._total: self.report({'INFO'}, f"All {self._total} scenes rendered") self.finish(context) return {'FINISHED'} scene_name = self._scenes[self._index] self._index += 1 try: SCENERENDERER_OT_render_all._is_rendering = True self._render_scene(context, scene_name) except Exception as e: SCENERENDERER_OT_render_all._is_rendering = False self.report({'ERROR'}, f"Failed {scene_name}: {e}") context.area.tag_redraw() return {'PASS_THROUGH'} def finish(self, context): if self._timer: context.window_manager.event_timer_remove(self._timer) self._timer = None context.window.cursor_set('DEFAULT') if self._orig_lock is not None: for sc in bpy.data.scenes: sc.render.use_lock_interface = self._orig_lock if on_render_complete in bpy.app.handlers.render_complete: bpy.app.handlers.render_complete.remove(on_render_complete) if on_render_cancel in bpy.app.handlers.render_cancel: bpy.app.handlers.render_cancel.remove(on_render_cancel) def _render_scene(self, context, scene_name): settings = context.scene.scene_renderer scene = bpy.data.scenes.get(scene_name) if not scene: print(f" [SKIP] Scene not found: {scene_name}") SCENERENDERER_OT_render_all._is_rendering = False return if scene.name == "_Shots": SCENERENDERER_OT_render_all._is_rendering = False return bpy.context.window.scene = scene cam = scene.objects.get("Profile") if cam: scene.camera = cam elif not scene.camera: print(f" [SKIP] No camera in {scene.name}") SCENERENDERER_OT_render_all._is_rendering = False return r = scene.render r.engine = 'CYCLES' r.image_settings.file_format = 'PNG' r.image_settings.color_mode = 'RGBA' r.film_transparent = True c = scene.cycles c.samples = 1 if settings.test_mode else settings.samples if settings.exposure != 0.0: scene.view_settings.exposure = settings.exposure scene.cycles.film_exposure = 2.0 ** settings.exposure if settings.look != 'None': scene.view_settings.look = settings.look if settings.test_mode: r.resolution_percentage = 10 render_dir = bpy.path.abspath("//renders") if settings.test_mode: render_dir = os.path.join(render_dir, "test") os.makedirs(render_dir, exist_ok=True) safe_name = scene.name.replace("/", "_").replace("\\", "_").replace(":", "_").strip() output_path = os.path.join(render_dir, safe_name) if settings.skip_existing and os.path.exists(output_path + ".png"): print(f" [SKIP] Already exists: {safe_name}.png") SCENERENDERER_OT_render_all._is_rendering = False return r.filepath = output_path print(f"\n [{self._index}/{self._total}] Rendering: {scene.name}") with context.temp_override(window=context.window, area=context.area, region=context.region): bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, scene=scene.name) print(f" [STARTED] {scene.name}") def invoke(self, context, event): scenes = utils.get_scenes() return context.window_manager.invoke_props_dialog(self, width=300) def draw(self, context): layout = self.layout scenes = utils.get_scenes() layout.label(text=f"Ready to render {len(scenes)} scenes:", icon='RENDER_STILL') box = layout.box() col = box.column(flow=True) for s in scenes[:8]: col.label(text=f" * {s}", icon='SCENE_DATA') if len(scenes) > 8: col.label(text=f" ... and {len(scenes) - 8} more") layout.separator() layout.label(text="Click OK to start.", icon='INFO') def execute(self, context): settings = context.scene.scene_renderer if on_render_complete not in bpy.app.handlers.render_complete: bpy.app.handlers.render_complete.append(on_render_complete) if on_render_cancel not in bpy.app.handlers.render_cancel: bpy.app.handlers.render_cancel.append(on_render_cancel) SCENERENDERER_OT_render_all._is_rendering = False SCENERENDERER_OT_render_all._cancel_requested = False silenced = utils.silence_puresky() if silenced: print(f" [FIX] Silenced {silenced} PureSky handlers.") fxd = utils.fix_lib_overrides() print(f"Fixed {fxd} lib-override parameters") img_fxd = utils.fix_missing_images() if img_fxd: print(f"Fixed {img_fxd} missing images with magenta dummies") if not settings.use_cpu: metal_ok = utils.init_metal_gpu() if metal_ok: print("Initialized Metal GPU") else: print("Metal GPU init failed, using whatever is available") self._orig_lock = bpy.data.scenes[0].render.use_lock_interface for sc in bpy.data.scenes: sc.render.use_lock_interface = True sc.cycles.device = 'CPU' if settings.use_cpu else 'GPU' bpy.context.view_layer.update() depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph.update() self._scenes = utils.get_scenes() if settings.test_mode: self._scenes = self._scenes[:1] self._total = len(self._scenes) self._index = 0 if self._total == 0: self.report({'WARNING'}, "No scenes to render!") return {'CANCELLED'} print(f"\n{'='*50}") print(f" Scene Renderer -- {self._total} scenes" f"{' (TEST MODE)' if settings.test_mode else ''}") print(f"{'='*50}") context.window_manager.modal_handler_add(self) self._timer = context.window_manager.event_timer_add(0.5, window=context.window) context.window.cursor_set('WAIT') return {'RUNNING_MODAL'}