Compare commits
5 Commits
d1a5655464
...
fix/materi
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f4af51e1 | |||
| 024f6f9ad4 | |||
| d40e3f0a0c | |||
| 49cd08494b | |||
| 634700e4db |
@@ -15,6 +15,7 @@ from . import properties, operators, panel
|
|||||||
classes = (
|
classes = (
|
||||||
properties.SceneRendererSettings,
|
properties.SceneRendererSettings,
|
||||||
operators.SCENERENDERER_OT_render_all,
|
operators.SCENERENDERER_OT_render_all,
|
||||||
|
operators.SCENERENDERER_OT_cancel_batch,
|
||||||
panel.SCENERENDERER_PT_panel,
|
panel.SCENERENDERER_PT_panel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
BIN
__pycache__/operators.cpython-311.pyc
Normal file
BIN
__pycache__/operators.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/panel.cpython-311.pyc
Normal file
BIN
__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/properties.cpython-311.pyc
Normal file
BIN
__pycache__/properties.cpython-311.pyc
Normal file
Binary file not shown.
64
operators.py
64
operators.py
@@ -12,6 +12,16 @@ def on_render_cancel(scene, depsgraph=None):
|
|||||||
SCENERENDERER_OT_render_all._is_rendering = False
|
SCENERENDERER_OT_render_all._is_rendering = False
|
||||||
SCENERENDERER_OT_render_all._cancel_requested = True
|
SCENERENDERER_OT_render_all._cancel_requested = True
|
||||||
|
|
||||||
|
class SCENERENDERER_OT_cancel_batch(bpy.types.Operator):
|
||||||
|
bl_idname = "scenerenderer.cancel_batch"
|
||||||
|
bl_label = "Cancel Batch Render"
|
||||||
|
bl_description = "Abort the current batch rendering process"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
SCENERENDERER_OT_render_all._cancel_requested = True
|
||||||
|
self.report({'INFO'}, "Batch render cancellation requested. Will stop after current frame or on ESC.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||||
bl_idname = "scenerenderer.render_all"
|
bl_idname = "scenerenderer.render_all"
|
||||||
@@ -72,7 +82,7 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
|||||||
bpy.app.handlers.render_cancel.remove(on_render_cancel)
|
bpy.app.handlers.render_cancel.remove(on_render_cancel)
|
||||||
|
|
||||||
def _render_scene(self, context, scene_name):
|
def _render_scene(self, context, scene_name):
|
||||||
settings = context.scene.scene_renderer
|
settings = SCENERENDERER_OT_render_all._cached_settings
|
||||||
scene = bpy.data.scenes.get(scene_name)
|
scene = bpy.data.scenes.get(scene_name)
|
||||||
if not scene:
|
if not scene:
|
||||||
print(f" [SKIP] Scene not found: {scene_name}")
|
print(f" [SKIP] Scene not found: {scene_name}")
|
||||||
@@ -100,26 +110,43 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
|||||||
r.film_transparent = True
|
r.film_transparent = True
|
||||||
|
|
||||||
c = scene.cycles
|
c = scene.cycles
|
||||||
c.samples = 1 if settings.test_mode else settings.samples
|
c.samples = 1 if settings["test_mode"] else settings["samples"]
|
||||||
|
c.use_adaptive_sampling = settings["use_noise_threshold"]
|
||||||
|
if c.use_adaptive_sampling:
|
||||||
|
c.adaptive_threshold = settings["noise_threshold"]
|
||||||
|
c.use_denoising = settings["use_denoising"]
|
||||||
|
if hasattr(c, 'max_bounces'):
|
||||||
|
c.max_bounces = settings["max_bounces"]
|
||||||
|
|
||||||
if settings.exposure != 0.0:
|
if settings["exposure"] != 0.0:
|
||||||
scene.view_settings.exposure = settings.exposure
|
scene.view_settings.exposure = settings["exposure"]
|
||||||
scene.cycles.film_exposure = 2.0 ** settings.exposure
|
scene.cycles.film_exposure = 2.0 ** settings["exposure"]
|
||||||
if settings.look != 'None':
|
if settings["look"] != 'None':
|
||||||
scene.view_settings.look = settings.look
|
target_look = settings["look"]
|
||||||
|
try:
|
||||||
|
scene.view_settings.look = target_look
|
||||||
|
except TypeError:
|
||||||
|
# Fallback for Blender 4.0+ AgX Color Management
|
||||||
|
agx_look = f"AgX - {target_look}"
|
||||||
|
if target_look == "Medium Contrast":
|
||||||
|
agx_look = "AgX - Base Contrast"
|
||||||
|
try:
|
||||||
|
scene.view_settings.look = agx_look
|
||||||
|
except TypeError:
|
||||||
|
print(f" [WARNING] Look '{target_look}' or '{agx_look}' not found, skipping look assignment.")
|
||||||
|
|
||||||
if settings.test_mode:
|
if settings["test_mode"]:
|
||||||
r.resolution_percentage = 10
|
r.resolution_percentage = 10
|
||||||
|
|
||||||
render_dir = bpy.path.abspath("//renders")
|
render_dir = bpy.path.abspath("//renders")
|
||||||
if settings.test_mode:
|
if settings["test_mode"]:
|
||||||
render_dir = os.path.join(render_dir, "test")
|
render_dir = os.path.join(render_dir, "test")
|
||||||
os.makedirs(render_dir, exist_ok=True)
|
os.makedirs(render_dir, exist_ok=True)
|
||||||
|
|
||||||
safe_name = scene.name.replace("/", "_").replace("\\", "_").replace(":", "_").strip()
|
safe_name = scene.name.replace("/", "_").replace("\\", "_").replace(":", "_").strip()
|
||||||
output_path = os.path.join(render_dir, safe_name)
|
output_path = os.path.join(render_dir, safe_name)
|
||||||
|
|
||||||
if settings.skip_existing and os.path.exists(output_path + ".png"):
|
if settings["skip_existing"] and os.path.exists(output_path + ".png"):
|
||||||
print(f" [SKIP] Already exists: {safe_name}.png")
|
print(f" [SKIP] Already exists: {safe_name}.png")
|
||||||
SCENERENDERER_OT_render_all._is_rendering = False
|
SCENERENDERER_OT_render_all._is_rendering = False
|
||||||
return
|
return
|
||||||
@@ -141,7 +168,7 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
|||||||
|
|
||||||
layout.label(text=f"Ready to render {len(scenes)} scenes:", icon='RENDER_STILL')
|
layout.label(text=f"Ready to render {len(scenes)} scenes:", icon='RENDER_STILL')
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
col = box.column(flow=True)
|
col = box.column()
|
||||||
for s in scenes[:8]:
|
for s in scenes[:8]:
|
||||||
col.label(text=f" * {s}", icon='SCENE_DATA')
|
col.label(text=f" * {s}", icon='SCENE_DATA')
|
||||||
if len(scenes) > 8:
|
if len(scenes) > 8:
|
||||||
@@ -152,6 +179,21 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
settings = context.scene.scene_renderer
|
settings = context.scene.scene_renderer
|
||||||
|
|
||||||
|
# Cache the settings exactly as they were when the user pressed "Render All Scenes".
|
||||||
|
# This prevents the settings from reverting to defaults when Blender switches the active scene context.
|
||||||
|
SCENERENDERER_OT_render_all._cached_settings = {
|
||||||
|
"test_mode": settings.test_mode,
|
||||||
|
"skip_existing": settings.skip_existing,
|
||||||
|
"use_cpu": settings.use_cpu,
|
||||||
|
"samples": settings.samples,
|
||||||
|
"use_noise_threshold": settings.use_noise_threshold,
|
||||||
|
"noise_threshold": settings.noise_threshold,
|
||||||
|
"use_denoising": settings.use_denoising,
|
||||||
|
"max_bounces": settings.max_bounces,
|
||||||
|
"exposure": settings.exposure,
|
||||||
|
"look": settings.look,
|
||||||
|
}
|
||||||
|
|
||||||
if on_render_complete not in bpy.app.handlers.render_complete:
|
if on_render_complete not in bpy.app.handlers.render_complete:
|
||||||
bpy.app.handlers.render_complete.append(on_render_complete)
|
bpy.app.handlers.render_complete.append(on_render_complete)
|
||||||
if on_render_cancel not in bpy.app.handlers.render_cancel:
|
if on_render_cancel not in bpy.app.handlers.render_cancel:
|
||||||
|
|||||||
22
panel.py
22
panel.py
@@ -17,7 +17,18 @@ class SCENERENDERER_PT_panel(bpy.types.Panel):
|
|||||||
row.prop(settings, "skip_existing", toggle=True)
|
row.prop(settings, "skip_existing", toggle=True)
|
||||||
|
|
||||||
layout.prop(settings, "use_cpu")
|
layout.prop(settings, "use_cpu")
|
||||||
layout.prop(settings, "samples")
|
box = layout.box()
|
||||||
|
box.label(text="Cycles Render", icon='SCENE_DATA')
|
||||||
|
box.prop(settings, "samples")
|
||||||
|
|
||||||
|
row = box.row()
|
||||||
|
row.prop(settings, "use_noise_threshold", text="Noise Threshold")
|
||||||
|
sub = row.row()
|
||||||
|
sub.active = settings.use_noise_threshold
|
||||||
|
sub.prop(settings, "noise_threshold", text="")
|
||||||
|
|
||||||
|
box.prop(settings, "use_denoising")
|
||||||
|
box.prop(settings, "max_bounces")
|
||||||
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Color", icon='SHADING_RENDERED')
|
box.label(text="Color", icon='SHADING_RENDERED')
|
||||||
@@ -27,4 +38,13 @@ class SCENERENDERER_PT_panel(bpy.types.Panel):
|
|||||||
layout.separator()
|
layout.separator()
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.scale_y = 2.0
|
row.scale_y = 2.0
|
||||||
|
|
||||||
|
# Check if rendering is running by accessing the timer on the operator
|
||||||
|
from .operators import SCENERENDERER_OT_render_all
|
||||||
|
is_running = getattr(SCENERENDERER_OT_render_all, "_timer", None) is not None
|
||||||
|
|
||||||
|
if is_running:
|
||||||
|
row.operator("scenerenderer.cancel_batch", text="Cancel Batch Render", icon='CANCEL')
|
||||||
|
layout.label(text="Press ESC to force stop the active frame.", icon='INFO')
|
||||||
|
else:
|
||||||
row.operator("scenerenderer.render_all", text="Render All Scenes", icon='RENDER_STILL')
|
row.operator("scenerenderer.render_all", text="Render All Scenes", icon='RENDER_STILL')
|
||||||
@@ -22,6 +22,26 @@ class SceneRendererSettings(bpy.types.PropertyGroup):
|
|||||||
description="Cycles render samples",
|
description="Cycles render samples",
|
||||||
default=4096, min=1, max=65536,
|
default=4096, min=1, max=65536,
|
||||||
)
|
)
|
||||||
|
use_noise_threshold: bpy.props.BoolProperty(
|
||||||
|
name="Use Noise Threshold",
|
||||||
|
description="Stop sampling when the noise is below this threshold",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
noise_threshold: bpy.props.FloatProperty(
|
||||||
|
name="Noise Threshold",
|
||||||
|
description="Noise threshold for adaptive sampling",
|
||||||
|
default=0.01, min=0.0001, max=1.0, precision=4,
|
||||||
|
)
|
||||||
|
use_denoising: bpy.props.BoolProperty(
|
||||||
|
name="Use Denoising",
|
||||||
|
description="Enable denoising for final render",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
max_bounces: bpy.props.IntProperty(
|
||||||
|
name="Max Bounces",
|
||||||
|
description="Total maximum number of light bounces",
|
||||||
|
default=12, min=0, max=1024,
|
||||||
|
)
|
||||||
exposure: bpy.props.FloatProperty(
|
exposure: bpy.props.FloatProperty(
|
||||||
name="Exposure",
|
name="Exposure",
|
||||||
description="Exposure boost in stops",
|
description="Exposure boost in stops",
|
||||||
|
|||||||
90
utils.py
90
utils.py
@@ -49,94 +49,10 @@ def _restore_fragile_sockets(mod, snapshot):
|
|||||||
|
|
||||||
|
|
||||||
def fix_lib_overrides():
|
def fix_lib_overrides():
|
||||||
fixed = 0
|
# Disabled as forcing "True" or "Index" onto generic modifier properties
|
||||||
for obj in bpy.data.objects:
|
# destroys the library overrides and clears all material assignments.
|
||||||
if not obj.modifiers:
|
return 0
|
||||||
continue
|
|
||||||
for mod in obj.modifiers:
|
|
||||||
if mod.type != 'NODES' or not mod.node_group:
|
|
||||||
continue
|
|
||||||
if not hasattr(mod.node_group, 'interface'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
ng = mod.node_group
|
|
||||||
iface = getattr(ng, 'interface', None)
|
|
||||||
if not iface or not hasattr(iface, 'items_tree'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Snapshot fragile sockets BEFORE any writes
|
|
||||||
snapshot = _snapshot_fragile_sockets(obj, mod, iface)
|
|
||||||
|
|
||||||
name_to_sid = {}
|
|
||||||
for item in iface.items_tree:
|
|
||||||
if item.item_type == 'SOCKET' and item.in_out == 'INPUT':
|
|
||||||
name_to_sid[item.name] = (item.identifier, item.socket_type)
|
|
||||||
|
|
||||||
for item in iface.items_tree:
|
|
||||||
if item.item_type != 'SOCKET' or item.in_out != 'INPUT':
|
|
||||||
continue
|
|
||||||
sid = item.identifier
|
|
||||||
if sid not in mod:
|
|
||||||
continue
|
|
||||||
stored = mod[sid]
|
|
||||||
st = item.socket_type
|
|
||||||
tt = type(stored).__name__
|
|
||||||
if st == 'NodeSocketBool' and tt == 'int':
|
|
||||||
mod[sid] = bool(stored)
|
|
||||||
fixed += 1
|
|
||||||
elif st == 'NodeSocketMenu' and tt == 'int':
|
|
||||||
mod[sid] = int(stored)
|
|
||||||
fixed += 1
|
|
||||||
|
|
||||||
ng_name = ng.name
|
|
||||||
if ng_name == "Add Shader v1.0":
|
|
||||||
if "Filter by ID" in name_to_sid:
|
|
||||||
sid = name_to_sid["Filter by ID"][0]
|
|
||||||
if mod.get(sid) != True:
|
|
||||||
mod[sid] = True
|
|
||||||
fixed += 1
|
|
||||||
if "ID" in name_to_sid and name_to_sid["ID"][1] == "NodeSocketString":
|
|
||||||
sid = name_to_sid["ID"][0]
|
|
||||||
if mod.get(sid) != "Index":
|
|
||||||
mod[sid] = "Index"
|
|
||||||
fixed += 1
|
|
||||||
elif ng_name in ["Add Shaders by Index v1.0", "Make Material", "Make Labels"]:
|
|
||||||
if "ID" in name_to_sid and name_to_sid["ID"][1] == "NodeSocketString":
|
|
||||||
sid = name_to_sid["ID"][0]
|
|
||||||
if mod.get(sid) != "Index":
|
|
||||||
mod[sid] = "Index"
|
|
||||||
fixed += 1
|
|
||||||
if ng_name == "Make Labels" and "Shrinkwrap" in name_to_sid:
|
|
||||||
sid = name_to_sid["Shrinkwrap"][0]
|
|
||||||
if mod.get(sid) != True:
|
|
||||||
mod[sid] = True
|
|
||||||
fixed += 1
|
|
||||||
elif ng_name == "Make attached":
|
|
||||||
if "Reset Children" in name_to_sid:
|
|
||||||
sid = name_to_sid["Reset Children"][0]
|
|
||||||
if mod.get(sid) != True:
|
|
||||||
mod[sid] = True
|
|
||||||
fixed += 1
|
|
||||||
if "Separate Children" in name_to_sid:
|
|
||||||
sid = name_to_sid["Separate Children"][0]
|
|
||||||
if mod.get(sid) != True:
|
|
||||||
mod[sid] = True
|
|
||||||
fixed += 1
|
|
||||||
elif ng_name == "Proxy v1.0":
|
|
||||||
if "Reset Children" in name_to_sid:
|
|
||||||
sid = name_to_sid["Reset Children"][0]
|
|
||||||
if mod.get(sid) != True:
|
|
||||||
mod[sid] = True
|
|
||||||
fixed += 1
|
|
||||||
|
|
||||||
# Restore any fragile sockets that got wiped
|
|
||||||
restored = _restore_fragile_sockets(mod, snapshot)
|
|
||||||
if restored:
|
|
||||||
print(f" [RESTORE] {obj.name}/{mod.name}: "
|
|
||||||
f"restored {restored} Material/Object sockets")
|
|
||||||
fixed += restored
|
|
||||||
|
|
||||||
return fixed
|
|
||||||
|
|
||||||
|
|
||||||
def fix_missing_images():
|
def fix_missing_images():
|
||||||
|
|||||||
Reference in New Issue
Block a user