Compare commits
2 Commits
fix/materi
...
feat/exten
| Author | SHA1 | Date | |
|---|---|---|---|
| dc934fd3ac | |||
| 7f5a70da8a |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Scene Renderer",
|
||||
"author": "Marc Mintel",
|
||||
"version": (1, 0, 0),
|
||||
"version": (2, 0, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Scene Renderer",
|
||||
"description": "Render all scenes to //renders/ relative to the blend file",
|
||||
|
||||
217
operators.py
217
operators.py
@@ -3,15 +3,21 @@ import os
|
||||
from . import utils
|
||||
|
||||
|
||||
# -- Render-callback handlers -------------------------------------------------
|
||||
|
||||
@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
|
||||
|
||||
|
||||
# -- Cancel operator ----------------------------------------------------------
|
||||
|
||||
class SCENERENDERER_OT_cancel_batch(bpy.types.Operator):
|
||||
bl_idname = "scenerenderer.cancel_batch"
|
||||
bl_label = "Cancel Batch Render"
|
||||
@@ -19,10 +25,29 @@ class SCENERENDERER_OT_cancel_batch(bpy.types.Operator):
|
||||
|
||||
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.")
|
||||
self.report(
|
||||
{'INFO'},
|
||||
"Batch render cancellation requested. "
|
||||
"Will stop after current frame or on ESC.",
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# -- File-extension helper ----------------------------------------------------
|
||||
|
||||
# Why a map instead of relying on Blender's default: the extension must match
|
||||
# the actual format we configure (e.g. EXR → .exr, not .png).
|
||||
_FORMAT_TO_EXT = {
|
||||
'PNG': '.png',
|
||||
'JPEG': '.jpg',
|
||||
'TIFF': '.tif',
|
||||
'OPEN_EXR': '.exr',
|
||||
'OPEN_EXR_MULTILAYER': '.exr',
|
||||
}
|
||||
|
||||
|
||||
# -- Main render operator -----------------------------------------------------
|
||||
|
||||
class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
bl_idname = "scenerenderer.render_all"
|
||||
bl_label = "Render All Scenes"
|
||||
@@ -34,10 +59,12 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
_index = 0
|
||||
_total = 0
|
||||
_orig_lock = None
|
||||
|
||||
|
||||
_is_rendering = False
|
||||
_cancel_requested = False
|
||||
|
||||
# ---- modal loop ---------------------------------------------------------
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == 'ESC' or self._cancel_requested:
|
||||
self.report({'WARNING'}, "Render batch cancelled")
|
||||
@@ -67,6 +94,8 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
# ---- cleanup ------------------------------------------------------------
|
||||
|
||||
def finish(self, context):
|
||||
if self._timer:
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
@@ -81,6 +110,8 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
if on_render_cancel in bpy.app.handlers.render_cancel:
|
||||
bpy.app.handlers.render_cancel.remove(on_render_cancel)
|
||||
|
||||
# ---- per-scene render ---------------------------------------------------
|
||||
|
||||
def _render_scene(self, context, scene_name):
|
||||
settings = SCENERENDERER_OT_render_all._cached_settings
|
||||
scene = bpy.data.scenes.get(scene_name)
|
||||
@@ -103,60 +134,153 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
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
|
||||
self._apply_render_settings(scene, settings)
|
||||
self._setup_output_path(scene, settings)
|
||||
|
||||
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}")
|
||||
|
||||
# ---- apply all settings to scene ----------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _apply_render_settings(scene, settings):
|
||||
r = scene.render
|
||||
c = scene.cycles
|
||||
|
||||
# Engine & basics
|
||||
r.engine = 'CYCLES'
|
||||
r.film_transparent = settings["film_transparent"]
|
||||
|
||||
# Resolution override
|
||||
if settings["override_resolution"]:
|
||||
r.resolution_x = settings["resolution_x"]
|
||||
r.resolution_y = settings["resolution_y"]
|
||||
r.resolution_percentage = settings["resolution_percentage"]
|
||||
|
||||
# Test-mode shrinks resolution to 10%
|
||||
if settings["test_mode"]:
|
||||
r.resolution_percentage = 10
|
||||
|
||||
# Output format
|
||||
fmt = settings["output_format"]
|
||||
r.image_settings.file_format = fmt
|
||||
r.image_settings.color_management = 'FOLLOW_SCENE'
|
||||
|
||||
if fmt in ('PNG', 'TIFF', 'OPEN_EXR'):
|
||||
r.image_settings.color_depth = settings["color_depth"]
|
||||
if fmt in ('PNG',):
|
||||
r.image_settings.color_mode = 'RGBA'
|
||||
elif fmt in ('JPEG',):
|
||||
r.image_settings.color_mode = 'RGB'
|
||||
elif fmt in ('TIFF',):
|
||||
r.image_settings.color_mode = 'RGBA'
|
||||
elif fmt in ('OPEN_EXR', 'OPEN_EXR_MULTILAYER'):
|
||||
r.image_settings.color_mode = 'RGBA'
|
||||
# EXR supports 16 or 32 bit float
|
||||
depth = settings["color_depth"]
|
||||
r.image_settings.color_depth = '16' if depth == '8' else depth
|
||||
|
||||
# Sampling
|
||||
c.samples = 1 if settings["test_mode"] else settings["samples"]
|
||||
|
||||
if settings["min_samples"] > 0:
|
||||
c.adaptive_min_samples = settings["min_samples"]
|
||||
|
||||
c.use_adaptive_sampling = settings["use_noise_threshold"]
|
||||
if c.use_adaptive_sampling:
|
||||
c.adaptive_threshold = settings["noise_threshold"]
|
||||
|
||||
if settings["time_limit"] > 0.0:
|
||||
c.time_limit = settings["time_limit"]
|
||||
else:
|
||||
c.time_limit = 0.0
|
||||
|
||||
# Denoising
|
||||
c.use_denoising = settings["use_denoising"]
|
||||
if c.use_denoising:
|
||||
c.denoiser = settings["denoiser"]
|
||||
c.denoising_input_passes = settings["denoising_input_passes"]
|
||||
|
||||
# Light bounces
|
||||
if hasattr(c, 'max_bounces'):
|
||||
c.max_bounces = settings["max_bounces"]
|
||||
|
||||
if hasattr(c, 'diffuse_bounces'):
|
||||
c.diffuse_bounces = settings["diffuse_bounces"]
|
||||
if hasattr(c, 'glossy_bounces'):
|
||||
c.glossy_bounces = settings["glossy_bounces"]
|
||||
if hasattr(c, 'transmission_bounces'):
|
||||
c.transmission_bounces = settings["transmission_bounces"]
|
||||
if hasattr(c, 'transparent_max_bounces'):
|
||||
c.transparent_max_bounces = settings["transparent_max_bounces"]
|
||||
if hasattr(c, 'volume_bounces'):
|
||||
c.volume_bounces = settings["volume_bounces"]
|
||||
|
||||
# Clamping
|
||||
if settings["use_clamping"]:
|
||||
c.sample_clamp_direct = settings["clamp_direct"]
|
||||
c.sample_clamp_indirect = settings["clamp_indirect"]
|
||||
else:
|
||||
c.sample_clamp_direct = 0.0
|
||||
c.sample_clamp_indirect = 0.0
|
||||
|
||||
# Film filter
|
||||
c.filter_width = settings["film_filter_width"]
|
||||
|
||||
# Color management
|
||||
if settings["exposure"] != 0.0:
|
||||
scene.view_settings.exposure = settings["exposure"]
|
||||
scene.cycles.film_exposure = 2.0 ** settings["exposure"]
|
||||
|
||||
if settings["look"] != 'None':
|
||||
target_look = settings["look"]
|
||||
try:
|
||||
scene.view_settings.look = target_look
|
||||
except TypeError:
|
||||
# Fallback for Blender 4.0+ AgX Color Management
|
||||
# Fallback for Blender 4.0+ AgX
|
||||
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.")
|
||||
print(
|
||||
f" [WARNING] Look '{target_look}' / "
|
||||
f"'{agx_look}' not found, skipping."
|
||||
)
|
||||
|
||||
if settings["test_mode"]:
|
||||
r.resolution_percentage = 10
|
||||
# ---- build output path --------------------------------------------------
|
||||
|
||||
def _setup_output_path(self, scene, settings):
|
||||
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()
|
||||
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")
|
||||
fmt = settings["output_format"]
|
||||
ext = _FORMAT_TO_EXT.get(fmt, '.png')
|
||||
|
||||
if settings["skip_existing"] and os.path.exists(output_path + ext):
|
||||
print(f" [SKIP] Already exists: {safe_name}{ext}")
|
||||
SCENERENDERER_OT_render_all._is_rendering = False
|
||||
return
|
||||
|
||||
r.filepath = output_path
|
||||
scene.render.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}")
|
||||
# ---- confirmation dialog ------------------------------------------------
|
||||
|
||||
def invoke(self, context, event):
|
||||
scenes = utils.get_scenes()
|
||||
@@ -166,7 +290,10 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
scenes = utils.get_scenes()
|
||||
|
||||
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()
|
||||
col = box.column()
|
||||
for s in scenes[:8]:
|
||||
@@ -176,20 +303,50 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
layout.separator()
|
||||
layout.label(text="Click OK to start.", icon='INFO')
|
||||
|
||||
# ---- execute (entry point) ----------------------------------------------
|
||||
|
||||
def execute(self, context):
|
||||
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.
|
||||
|
||||
# Cache ALL settings at batch start so they survive scene-context
|
||||
# switches during the modal loop.
|
||||
SCENERENDERER_OT_render_all._cached_settings = {
|
||||
"test_mode": settings.test_mode,
|
||||
"skip_existing": settings.skip_existing,
|
||||
"use_cpu": settings.use_cpu,
|
||||
# Resolution
|
||||
"override_resolution": settings.override_resolution,
|
||||
"resolution_x": settings.resolution_x,
|
||||
"resolution_y": settings.resolution_y,
|
||||
"resolution_percentage": settings.resolution_percentage,
|
||||
# Sampling
|
||||
"samples": settings.samples,
|
||||
"min_samples": settings.min_samples,
|
||||
"use_noise_threshold": settings.use_noise_threshold,
|
||||
"noise_threshold": settings.noise_threshold,
|
||||
"time_limit": settings.time_limit,
|
||||
# Denoising
|
||||
"use_denoising": settings.use_denoising,
|
||||
"denoiser": settings.denoiser,
|
||||
"denoising_input_passes": settings.denoising_input_passes,
|
||||
# Bounces
|
||||
"max_bounces": settings.max_bounces,
|
||||
"diffuse_bounces": settings.diffuse_bounces,
|
||||
"glossy_bounces": settings.glossy_bounces,
|
||||
"transmission_bounces": settings.transmission_bounces,
|
||||
"transparent_max_bounces": settings.transparent_max_bounces,
|
||||
"volume_bounces": settings.volume_bounces,
|
||||
# Clamping
|
||||
"use_clamping": settings.use_clamping,
|
||||
"clamp_direct": settings.clamp_direct,
|
||||
"clamp_indirect": settings.clamp_indirect,
|
||||
# Film
|
||||
"film_transparent": settings.film_transparent,
|
||||
"film_filter_width": settings.film_filter_width,
|
||||
# Output
|
||||
"output_format": settings.output_format,
|
||||
"color_depth": settings.color_depth,
|
||||
# Color
|
||||
"exposure": settings.exposure,
|
||||
"look": settings.look,
|
||||
}
|
||||
@@ -241,12 +398,16 @@ class SCENERENDERER_OT_render_all(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f" Scene Renderer -- {self._total} scenes"
|
||||
f"{' (TEST MODE)' if settings.test_mode else ''}")
|
||||
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)
|
||||
self._timer = context.window_manager.event_timer_add(
|
||||
0.5, window=context.window
|
||||
)
|
||||
context.window.cursor_set('WAIT')
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
89
panel.py
89
panel.py
@@ -12,39 +12,106 @@ class SCENERENDERER_PT_panel(bpy.types.Panel):
|
||||
layout = self.layout
|
||||
settings = context.scene.scene_renderer
|
||||
|
||||
# --- General toggles ---
|
||||
row = layout.row(align=True)
|
||||
row.prop(settings, "test_mode", toggle=True)
|
||||
row.prop(settings, "skip_existing", toggle=True)
|
||||
|
||||
layout.prop(settings, "use_cpu")
|
||||
|
||||
# --- Resolution ---
|
||||
box = layout.box()
|
||||
box.label(text="Cycles Render", icon='SCENE_DATA')
|
||||
row = box.row()
|
||||
row.prop(settings, "override_resolution")
|
||||
if settings.override_resolution:
|
||||
col = box.column(align=True)
|
||||
col.prop(settings, "resolution_x")
|
||||
col.prop(settings, "resolution_y")
|
||||
col.prop(settings, "resolution_percentage", slider=True)
|
||||
|
||||
# --- Sampling ---
|
||||
box = layout.box()
|
||||
box.label(text="Sampling", icon='OUTLINER_OB_LIGHT')
|
||||
box.prop(settings, "samples")
|
||||
|
||||
box.prop(settings, "min_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.prop(settings, "time_limit")
|
||||
|
||||
# --- Denoising ---
|
||||
box = layout.box()
|
||||
box.label(text="Denoising", icon='MOD_SMOOTH')
|
||||
box.prop(settings, "use_denoising")
|
||||
if settings.use_denoising:
|
||||
box.prop(settings, "denoiser")
|
||||
box.prop(settings, "denoising_input_passes")
|
||||
|
||||
# --- Light Bounces ---
|
||||
box = layout.box()
|
||||
box.label(text="Light Bounces", icon='LIGHT_SUN')
|
||||
box.prop(settings, "max_bounces")
|
||||
col = box.column(align=True)
|
||||
col.prop(settings, "diffuse_bounces")
|
||||
col.prop(settings, "glossy_bounces")
|
||||
col.prop(settings, "transmission_bounces")
|
||||
col.prop(settings, "transparent_max_bounces")
|
||||
col.prop(settings, "volume_bounces")
|
||||
|
||||
# --- Clamping ---
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.prop(settings, "use_clamping")
|
||||
if settings.use_clamping:
|
||||
col = box.column(align=True)
|
||||
col.prop(settings, "clamp_direct")
|
||||
col.prop(settings, "clamp_indirect")
|
||||
|
||||
# --- Film ---
|
||||
box = layout.box()
|
||||
box.label(text="Film", icon='CAMERA_DATA')
|
||||
box.prop(settings, "film_transparent")
|
||||
box.prop(settings, "film_filter_width")
|
||||
|
||||
# --- Output ---
|
||||
box = layout.box()
|
||||
box.label(text="Output", icon='OUTPUT')
|
||||
box.prop(settings, "output_format")
|
||||
# Color depth only for formats that support it
|
||||
if settings.output_format in ('PNG', 'TIFF', 'OPEN_EXR'):
|
||||
box.prop(settings, "color_depth")
|
||||
|
||||
# --- Color Management ---
|
||||
box = layout.box()
|
||||
box.label(text="Color", icon='SHADING_RENDERED')
|
||||
box.prop(settings, "exposure", slider=True)
|
||||
box.prop(settings, "look")
|
||||
|
||||
# --- Render button ---
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
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')
|
||||
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',
|
||||
)
|
||||
168
properties.py
168
properties.py
@@ -2,6 +2,9 @@ import bpy
|
||||
|
||||
|
||||
class SceneRendererSettings(bpy.types.PropertyGroup):
|
||||
|
||||
# --- General ---
|
||||
|
||||
test_mode: bpy.props.BoolProperty(
|
||||
name="Test Mode",
|
||||
description="Render only the first scene at 10% resolution with 1 sample",
|
||||
@@ -17,11 +20,46 @@ class SceneRendererSettings(bpy.types.PropertyGroup):
|
||||
description="Force CPU rendering instead of GPU",
|
||||
default=False,
|
||||
)
|
||||
|
||||
# --- Resolution ---
|
||||
|
||||
override_resolution: bpy.props.BoolProperty(
|
||||
name="Override Resolution",
|
||||
description="Override the per-scene resolution with a global value",
|
||||
default=False,
|
||||
)
|
||||
resolution_x: bpy.props.IntProperty(
|
||||
name="Resolution X",
|
||||
description="Horizontal render resolution",
|
||||
default=1920, min=1, max=32768,
|
||||
)
|
||||
resolution_y: bpy.props.IntProperty(
|
||||
name="Resolution Y",
|
||||
description="Vertical render resolution",
|
||||
default=1080, min=1, max=32768,
|
||||
)
|
||||
resolution_percentage: bpy.props.IntProperty(
|
||||
name="Resolution %",
|
||||
description="Resolution scale percentage",
|
||||
default=100, min=1, max=100,
|
||||
subtype='PERCENTAGE',
|
||||
)
|
||||
|
||||
# --- Sampling ---
|
||||
|
||||
samples: bpy.props.IntProperty(
|
||||
name="Samples",
|
||||
description="Cycles render samples",
|
||||
name="Max Samples",
|
||||
description="Maximum number of Cycles render samples",
|
||||
default=4096, min=1, max=65536,
|
||||
)
|
||||
min_samples: bpy.props.IntProperty(
|
||||
name="Min Samples",
|
||||
description=(
|
||||
"Minimum number of samples before adaptive sampling can stop. "
|
||||
"Set to 0 to use Blender default"
|
||||
),
|
||||
default=0, min=0, max=65536,
|
||||
)
|
||||
use_noise_threshold: bpy.props.BoolProperty(
|
||||
name="Use Noise Threshold",
|
||||
description="Stop sampling when the noise is below this threshold",
|
||||
@@ -32,16 +70,140 @@ class SceneRendererSettings(bpy.types.PropertyGroup):
|
||||
description="Noise threshold for adaptive sampling",
|
||||
default=0.01, min=0.0001, max=1.0, precision=4,
|
||||
)
|
||||
time_limit: bpy.props.FloatProperty(
|
||||
name="Time Limit",
|
||||
description=(
|
||||
"Maximum render time per frame in seconds. "
|
||||
"0 = no limit"
|
||||
),
|
||||
default=0.0, min=0.0, max=86400.0,
|
||||
)
|
||||
|
||||
# --- Denoising ---
|
||||
|
||||
use_denoising: bpy.props.BoolProperty(
|
||||
name="Use Denoising",
|
||||
description="Enable denoising for final render",
|
||||
default=True,
|
||||
)
|
||||
denoiser: bpy.props.EnumProperty(
|
||||
name="Denoiser",
|
||||
description="Which denoiser to use",
|
||||
items=[
|
||||
('OPENIMAGEDENOISE', "OpenImageDenoise", "Intel Open Image Denoise (CPU)"),
|
||||
('OPTIX', "OptiX", "NVIDIA OptiX (GPU only)"),
|
||||
],
|
||||
default='OPENIMAGEDENOISE',
|
||||
)
|
||||
denoising_input_passes: bpy.props.EnumProperty(
|
||||
name="Denoise Passes",
|
||||
description="Passes used by the denoiser for better quality",
|
||||
items=[
|
||||
('RGB', "Color", "Use only color data"),
|
||||
('RGB_ALBEDO', "Color + Albedo", "Use color and albedo data"),
|
||||
('RGB_ALBEDO_NORMAL', "Color + Albedo + Normal",
|
||||
"Use color, albedo, and normal data (best quality)"),
|
||||
],
|
||||
default='RGB_ALBEDO_NORMAL',
|
||||
)
|
||||
|
||||
# --- Light Bounces ---
|
||||
|
||||
max_bounces: bpy.props.IntProperty(
|
||||
name="Max Bounces",
|
||||
name="Total",
|
||||
description="Total maximum number of light bounces",
|
||||
default=12, min=0, max=1024,
|
||||
)
|
||||
diffuse_bounces: bpy.props.IntProperty(
|
||||
name="Diffuse",
|
||||
description="Maximum number of diffuse bounces",
|
||||
default=4, min=0, max=1024,
|
||||
)
|
||||
glossy_bounces: bpy.props.IntProperty(
|
||||
name="Glossy",
|
||||
description="Maximum number of glossy bounces",
|
||||
default=4, min=0, max=1024,
|
||||
)
|
||||
transmission_bounces: bpy.props.IntProperty(
|
||||
name="Transmission",
|
||||
description="Maximum number of transmission bounces",
|
||||
default=12, min=0, max=1024,
|
||||
)
|
||||
transparent_max_bounces: bpy.props.IntProperty(
|
||||
name="Transparent",
|
||||
description="Maximum number of transparent bounces",
|
||||
default=8, min=0, max=1024,
|
||||
)
|
||||
volume_bounces: bpy.props.IntProperty(
|
||||
name="Volume",
|
||||
description="Maximum number of volume scattering bounces",
|
||||
default=0, min=0, max=1024,
|
||||
)
|
||||
|
||||
# --- Clamping ---
|
||||
|
||||
use_clamping: bpy.props.BoolProperty(
|
||||
name="Use Clamping",
|
||||
description="Clamp sample values to reduce fireflies",
|
||||
default=False,
|
||||
)
|
||||
clamp_direct: bpy.props.FloatProperty(
|
||||
name="Direct Light",
|
||||
description=(
|
||||
"Clamp value for direct light samples. "
|
||||
"0 = no clamping"
|
||||
),
|
||||
default=0.0, min=0.0, max=1e8,
|
||||
)
|
||||
clamp_indirect: bpy.props.FloatProperty(
|
||||
name="Indirect Light",
|
||||
description=(
|
||||
"Clamp value for indirect light samples. "
|
||||
"0 = no clamping"
|
||||
),
|
||||
default=10.0, min=0.0, max=1e8,
|
||||
)
|
||||
|
||||
# --- Film ---
|
||||
|
||||
film_transparent: bpy.props.BoolProperty(
|
||||
name="Transparent Background",
|
||||
description="Render the background as transparent",
|
||||
default=True,
|
||||
)
|
||||
film_filter_width: bpy.props.FloatProperty(
|
||||
name="Pixel Filter Width",
|
||||
description="Pixel filter width for anti-aliasing",
|
||||
default=1.5, min=0.01, max=10.0,
|
||||
)
|
||||
|
||||
# --- Output ---
|
||||
|
||||
output_format: bpy.props.EnumProperty(
|
||||
name="Format",
|
||||
description="Output file format",
|
||||
items=[
|
||||
('PNG', "PNG", "PNG image with alpha support"),
|
||||
('JPEG', "JPEG", "JPEG image (no alpha)"),
|
||||
('TIFF', "TIFF", "TIFF image"),
|
||||
('OPEN_EXR', "OpenEXR", "OpenEXR HDR image"),
|
||||
('OPEN_EXR_MULTILAYER', "OpenEXR Multilayer",
|
||||
"OpenEXR with all render passes"),
|
||||
],
|
||||
default='PNG',
|
||||
)
|
||||
color_depth: bpy.props.EnumProperty(
|
||||
name="Color Depth",
|
||||
description="Bit depth per channel",
|
||||
items=[
|
||||
('8', "8 bit", "8 bits per channel"),
|
||||
('16', "16 bit", "16 bits per channel"),
|
||||
],
|
||||
default='16',
|
||||
)
|
||||
|
||||
# --- Color Management ---
|
||||
|
||||
exposure: bpy.props.FloatProperty(
|
||||
name="Exposure",
|
||||
description="Exposure boost in stops",
|
||||
|
||||
Reference in New Issue
Block a user