feat: initial commit for scene renderer addon

This commit is contained in:
2026-06-02 22:51:30 +02:00
commit 074a590194
5 changed files with 435 additions and 0 deletions

210
operators.py Normal file
View File

@@ -0,0 +1,210 @@
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'}