3550 lines
196 KiB
Python
3550 lines
196 KiB
Python
import bpy # type: ignore
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import glob # Add missing import
|
|
from bpy.props import (StringProperty, BoolProperty, IntProperty, EnumProperty, PointerProperty, FloatProperty) # type: ignore
|
|
from bpy.types import (Panel, Operator, PropertyGroup, AddonPreferences) # type: ignore
|
|
import time
|
|
|
|
from .rainys_repo_bootstrap import ensure_rainys_extensions_repo
|
|
|
|
# Pre-defined items lists for EnumProperties
|
|
RESOLUTION_MODE_ITEMS = [
|
|
('SCENE', "Use Scene Resolution", "Use the scene's render resolution"),
|
|
('PRESET', "Preset Resolution", "Use a preset resolution"),
|
|
('CUSTOM', "Custom Resolution", "Use a custom resolution")
|
|
]
|
|
|
|
RESOLUTION_PRESET_ITEMS = [
|
|
('x1920y1080', "1920 x 1080 (16:9) HD1080p", ""),
|
|
('x1280y720', "1280 x 720 (16:9) HD720p", ""),
|
|
('x854y480', "854 x 480 (16:9) 480P", ""),
|
|
('x640y360', "640 x 360 (16:9) 360P", ""),
|
|
('x1920y1440', "1920 x 1440 (4:3)", ""),
|
|
('x1600y1200', "1600 x 1200 (4:3)", ""),
|
|
('x1280y960', "1280 x 960 (4:3)", ""),
|
|
('x1024y768', "1024 x 768 (4:3)", ""),
|
|
('x800y600', "800 x 600 (4:3)", ""),
|
|
('x640y480', "640 x 480 (4:3)", ""),
|
|
('x1024y1024', "1024 x 1024 (1:1)", ""),
|
|
('x512y512', "512 x 512 (1:1)", "")
|
|
]
|
|
|
|
FILE_FORMAT_ITEMS = [
|
|
('VIDEO', "Video File", "Save as video file")
|
|
]
|
|
|
|
VIDEO_FORMAT_ITEMS = [
|
|
('MPEG4', "MP4", "Standard container format with wide compatibility"),
|
|
('QUICKTIME', "QuickTime (MOV)", "Professional container format"),
|
|
('AVI', "AVI", "Classic container format"),
|
|
('MKV', "Matroska (MKV)", "Open source container with wide codec support")
|
|
]
|
|
|
|
VIDEO_CODEC_ITEMS = [
|
|
('H264', "H.264", "Standard H.264 codec with good quality and compression (recommended)"),
|
|
('H265', "H.265", "H.265 codec with better compression than H.264"),
|
|
('AV1', "AV1", "Modern AV1 codec with excellent compression"),
|
|
('MPEG4', "MPEG-4", "MPEG-4 codec for broad compatibility"),
|
|
('FFV1', "FFV1", "Lossless codec for archival purposes"),
|
|
('NONE', "None", "No video codec")
|
|
]
|
|
|
|
VIDEO_QUALITY_ITEMS = [
|
|
('LOWEST', "Lowest", "Lowest quality"),
|
|
('VERYLOW', "Very Low", "Very low quality"),
|
|
('LOW', "Low", "Low quality"),
|
|
('MEDIUM', "Medium", "Medium quality"),
|
|
('HIGH', "High", "High quality"),
|
|
('PERC_LOSSLESS', "Perceptually Lossless", "Perceptually lossless quality"),
|
|
('LOSSLESS', "Lossless", "Lossless quality"),
|
|
]
|
|
|
|
AUDIO_CODEC_ITEMS = [
|
|
('AAC', "AAC", "AAC codec"),
|
|
('AC3', "AC3", "AC3 codec"),
|
|
('MP3', "MP3", "MP3 codec"),
|
|
('NONE', "None", "No audio codec")
|
|
]
|
|
|
|
DISPLAY_MODE_ITEMS = [
|
|
('WIREFRAME', "Wireframe", "Display the wireframe"),
|
|
('SOLID', "Solid", "Display solid shading"),
|
|
('MATERIAL', "Material", "Display material preview"),
|
|
('RENDERED', "Rendered", "Display rendered preview")
|
|
]
|
|
|
|
# Helper function to get file extension based on video format
|
|
def get_file_extension(video_format):
|
|
if video_format == 'MPEG4':
|
|
return ".mp4"
|
|
elif video_format == 'QUICKTIME':
|
|
return ".mov"
|
|
elif video_format == 'AVI':
|
|
return ".avi"
|
|
elif video_format == 'MKV':
|
|
return ".mkv"
|
|
else:
|
|
return ".mp4" # Default to mp4 if unknown
|
|
|
|
# Helper function to convert quality enum to FFmpeg CRF value
|
|
def get_ffmpeg_quality(quality_enum):
|
|
quality_map = {
|
|
'LOWEST': 'HIGH', # Lowest quality = High CRF value
|
|
'VERYLOW': 'HIGH',
|
|
'LOW': 'MEDIUM',
|
|
'MEDIUM': 'MEDIUM',
|
|
'HIGH': 'LOW', # High quality = Low CRF value
|
|
'PERC_LOSSLESS': 'PERC_LOSSLESS',
|
|
'LOSSLESS': 'LOSSLESS',
|
|
}
|
|
return quality_map.get(quality_enum, 'MEDIUM')
|
|
|
|
# Helper function to safely set video file format (Blender 5.0 compatibility)
|
|
def set_video_file_format(scene):
|
|
"""Set file format for video output, handling Blender 5.0 API changes."""
|
|
# Check Blender version - 5.0+ may have different API
|
|
blender_version = bpy.app.version
|
|
is_blender_5 = blender_version[0] >= 5
|
|
|
|
if is_blender_5:
|
|
# In Blender 5.0+, FFMPEG is not in image_settings.file_format enum
|
|
# Try alternative video formats that might be available
|
|
# Order matters - try most compatible formats first
|
|
video_formats = ['AVI_JPEG', 'AVI_RAW', 'H264', 'THEORA', 'XVID', 'FFMPEG']
|
|
for fmt in video_formats:
|
|
try:
|
|
scene.render.image_settings.file_format = fmt
|
|
print(f"Set video format to: {fmt} (Blender 5.0 compatibility mode)")
|
|
return True
|
|
except (TypeError, ValueError, AttributeError) as e:
|
|
# Continue trying other formats
|
|
continue
|
|
|
|
# If no video format works, check if we can still use ffmpeg settings
|
|
# In some Blender versions, ffmpeg might work even without setting file_format
|
|
if hasattr(scene.render, 'ffmpeg'):
|
|
print("Warning: Could not set video file_format, but ffmpeg settings are available.")
|
|
print("Attempting to proceed with ffmpeg configuration...")
|
|
# Set to a valid image format as fallback
|
|
try:
|
|
scene.render.image_settings.file_format = 'PNG'
|
|
# Note: This might not work for direct video output
|
|
# User may need to render as image sequence and encode separately
|
|
return False
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
else:
|
|
# For Blender < 5.0, use FFMPEG as before
|
|
try:
|
|
scene.render.image_settings.file_format = 'FFMPEG'
|
|
return True
|
|
except (TypeError, ValueError) as e:
|
|
print(f"Warning: Could not set FFMPEG format: {e}")
|
|
return False
|
|
|
|
# Function to get all cameras in the scene for the dropdown
|
|
def get_cameras(self, context) -> list[tuple[str, str, str]]:
|
|
cameras = []
|
|
for obj in context.scene.objects:
|
|
if obj.type == 'CAMERA':
|
|
cameras.append((obj.name, obj.name, f"Use camera: {obj.name}"))
|
|
|
|
if not cameras:
|
|
cameras.append(("NONE", "No Cameras", "No cameras in scene"))
|
|
|
|
return cameras
|
|
|
|
# Main Properties class
|
|
class BPLProperties(PropertyGroup):
|
|
output_path: StringProperty( # type: ignore
|
|
name="Output Path",
|
|
description="Path to save the playblast",
|
|
default="//blast/",
|
|
subtype='DIR_PATH'
|
|
)
|
|
|
|
file_name: StringProperty( # type: ignore
|
|
name="File Name",
|
|
description="Base name for the playblast files",
|
|
default="blast_"
|
|
)
|
|
|
|
last_playblast_file: StringProperty( # type: ignore
|
|
name="Last Playblast File",
|
|
description="Path to the last created playblast file",
|
|
default=""
|
|
)
|
|
|
|
camera_object: EnumProperty( # type: ignore
|
|
name="Camera",
|
|
description="Camera to use for playblast",
|
|
items=get_cameras
|
|
)
|
|
|
|
use_active_camera: BoolProperty( # type: ignore
|
|
name="Use Active Camera",
|
|
description="Use the scene's active camera",
|
|
default=True
|
|
)
|
|
|
|
resolution_mode: EnumProperty( # type: ignore
|
|
name="Resolution Mode",
|
|
description="How to determine the resolution",
|
|
items=RESOLUTION_MODE_ITEMS,
|
|
default='SCENE'
|
|
)
|
|
|
|
resolution_preset: EnumProperty( # type: ignore
|
|
name="Resolution Preset",
|
|
description="Common resolution presets",
|
|
items=RESOLUTION_PRESET_ITEMS,
|
|
default='x1920y1080'
|
|
)
|
|
|
|
resolution_x: IntProperty( # type: ignore
|
|
name="Resolution X",
|
|
description="Width of the playblast",
|
|
default=1920,
|
|
min=4
|
|
)
|
|
|
|
resolution_y: IntProperty( # type: ignore
|
|
name="Resolution Y",
|
|
description="Height of the playblast",
|
|
default=1080,
|
|
min=4
|
|
)
|
|
|
|
resolution_percentage: IntProperty( # type: ignore
|
|
name="Resolution %",
|
|
description="Percentage of the resolution",
|
|
default=100,
|
|
min=1,
|
|
max=100,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
|
|
use_scene_frame_range: BoolProperty( # type: ignore
|
|
name="Use Scene Frame Range",
|
|
description="Use the scene's frame range for the playblast",
|
|
default=True
|
|
)
|
|
|
|
start_frame: IntProperty( # type: ignore
|
|
name="Start Frame",
|
|
description="First frame to playblast",
|
|
default=1
|
|
)
|
|
|
|
end_frame: IntProperty( # type: ignore
|
|
name="End Frame",
|
|
description="Last frame to playblast",
|
|
default=250
|
|
)
|
|
|
|
file_format: EnumProperty( # type: ignore
|
|
name="File Format",
|
|
description="Format to save the playblast",
|
|
items=FILE_FORMAT_ITEMS,
|
|
default='VIDEO'
|
|
)
|
|
|
|
video_format: EnumProperty( # type: ignore
|
|
name="Video Format",
|
|
description="Format for video file",
|
|
items=VIDEO_FORMAT_ITEMS,
|
|
default='MPEG4'
|
|
)
|
|
|
|
video_codec: EnumProperty( # type: ignore
|
|
name="Video Codec",
|
|
description="Codec for video file",
|
|
items=VIDEO_CODEC_ITEMS,
|
|
default='H264'
|
|
)
|
|
|
|
video_quality: EnumProperty( # type: ignore
|
|
name="Quality",
|
|
description="Quality of the video",
|
|
items=VIDEO_QUALITY_ITEMS,
|
|
default='MEDIUM'
|
|
)
|
|
|
|
include_audio: BoolProperty( # type: ignore
|
|
name="Include Audio",
|
|
description="Include audio in the playblast",
|
|
default=False
|
|
)
|
|
|
|
audio_codec: EnumProperty( # type: ignore
|
|
name="Audio Codec",
|
|
description="Codec for audio",
|
|
items=AUDIO_CODEC_ITEMS,
|
|
default='AAC'
|
|
)
|
|
|
|
audio_bitrate: IntProperty( # type: ignore
|
|
name="Audio Bitrate",
|
|
description="Bitrate for audio (kb/s)",
|
|
default=192,
|
|
min=32,
|
|
max=384
|
|
)
|
|
|
|
display_mode: EnumProperty( # type: ignore
|
|
name="Display Mode",
|
|
description="How to display the viewport",
|
|
items=DISPLAY_MODE_ITEMS,
|
|
default='SOLID'
|
|
)
|
|
|
|
auto_disable_overlays: BoolProperty( # type: ignore
|
|
name="Auto Disable Overlays",
|
|
description="Automatically disable viewport overlays during playblast",
|
|
default=True
|
|
)
|
|
|
|
enable_depth_of_field: BoolProperty( # type: ignore
|
|
name="Enable Depth of Field",
|
|
description="Enable camera depth of field effect in playblast",
|
|
default=True
|
|
)
|
|
|
|
show_metadata: BoolProperty( # type: ignore
|
|
name="Show Metadata",
|
|
description="Show metadata in the playblast",
|
|
default=True
|
|
)
|
|
|
|
metadata_resolution: BoolProperty( # type: ignore
|
|
name="Resolution",
|
|
description="Show resolution in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_frame: BoolProperty( # type: ignore
|
|
name="Frame",
|
|
description="Show frame number in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_scene: BoolProperty( # type: ignore
|
|
name="Scene",
|
|
description="Show scene name in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_camera: BoolProperty( # type: ignore
|
|
name="Camera",
|
|
description="Show camera name in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_lens: BoolProperty( # type: ignore
|
|
name="Lens",
|
|
description="Show camera lens in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_date: BoolProperty( # type: ignore
|
|
name="Date",
|
|
description="Show date in metadata",
|
|
default=True
|
|
)
|
|
|
|
metadata_note: StringProperty( # type: ignore
|
|
name="Note",
|
|
description="Custom note to include in metadata",
|
|
default=""
|
|
)
|
|
|
|
use_custom_ffmpeg_args: BoolProperty( # type: ignore
|
|
name="Use Custom FFmpeg Args",
|
|
description="Enable custom FFmpeg command line arguments for advanced users",
|
|
default=False
|
|
)
|
|
|
|
custom_ffmpeg_args: StringProperty( # type: ignore
|
|
name="Custom FFmpeg Args",
|
|
description="Custom FFmpeg command line arguments (for advanced users)",
|
|
default="-c:v h264_nvenc -preset fast -crf 0"
|
|
)
|
|
|
|
is_rendering: BoolProperty( # type: ignore
|
|
name="Is Rendering",
|
|
default=False
|
|
)
|
|
|
|
render_progress: FloatProperty( # type: ignore
|
|
name="Render Progress",
|
|
default=0.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
subtype='PERCENTAGE'
|
|
)
|
|
|
|
status_message: StringProperty( # type: ignore
|
|
name="Status Message",
|
|
default=""
|
|
)
|
|
|
|
# Add the property to store original settings at scene level
|
|
original_settings: StringProperty( # type: ignore
|
|
name="Original Settings",
|
|
description="JSON string holding original render settings",
|
|
default=""
|
|
)
|
|
|
|
# Add property to store extended settings like light states
|
|
original_settings_extended: StringProperty( # type: ignore
|
|
name="Extended Original Settings",
|
|
description="String holding additional original settings like light states",
|
|
default=""
|
|
)
|
|
|
|
# Main Operator
|
|
class BPL_OT_create_playblast(Operator):
|
|
bl_idname = "bpl.create_playblast"
|
|
bl_label = "Create Playblast"
|
|
bl_description = "Create a playblast of the current scene"
|
|
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
|
|
|
|
_timer = None
|
|
_area = None
|
|
_space = None
|
|
_region_3d = None
|
|
_original_settings = None
|
|
_original_shading = None
|
|
_original_overlays = None
|
|
_original_view_perspective = None
|
|
_original_use_local_camera = None
|
|
_phase = 'SETUP' # SETUP, RENDER, ENCODE, COMPLETE
|
|
_last_reported_frame = 0
|
|
_frame_start = 0
|
|
_frame_end = 0
|
|
_current_frame = 0
|
|
_original_render_engine = None
|
|
_original_cycles_viewport = None
|
|
_use_actual_render = False
|
|
_original_cycles_render = None
|
|
_max_frame_seen = 0
|
|
_has_triggered_complete = False
|
|
_needs_video_encode = False # Flag for Blender 5.0 PNG fallback
|
|
|
|
def modal(self, context, event):
|
|
if event.type == 'ESC':
|
|
context.window_manager.event_timer_remove(self._timer)
|
|
bpy.ops.render.render('INVOKE_DEFAULT', animation=False) # This cancels the render
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
|
|
if event.type == 'TIMER':
|
|
props = context.scene.basedplayblast
|
|
|
|
if self._phase == 'SETUP':
|
|
props.render_progress = 0.0
|
|
props.status_message = "Setting up playblast..."
|
|
props.is_rendering = True
|
|
self._phase = 'RENDER'
|
|
|
|
# CRITICAL: Final viewport validation and refresh before render
|
|
if self._space and self._region_3d:
|
|
# Ensure camera view is active
|
|
if self._region_3d.view_perspective != 'CAMERA':
|
|
self._region_3d.view_perspective = 'CAMERA'
|
|
print("Force-set camera view before render")
|
|
|
|
# Final viewport refresh
|
|
self._area.tag_redraw()
|
|
context.view_layer.update()
|
|
|
|
# Add a brief delay to ensure viewport is ready
|
|
import time
|
|
time.sleep(0.1)
|
|
|
|
# Start the render - choose between actual render or OpenGL based on engine
|
|
if getattr(self, '_use_actual_render', False):
|
|
# Use actual Cycles rendering for RENDERED mode
|
|
print(f"Starting Cycles animation render with:")
|
|
print(f" - Engine: {context.scene.render.engine}")
|
|
print(f" - Samples: {getattr(context.scene.cycles, 'samples', 'unknown')}")
|
|
print(f" - Scene camera: {context.scene.camera.name if context.scene.camera else 'None'}")
|
|
print(f" - Output format: {context.scene.render.image_settings.file_format}")
|
|
print(f" - Output path: {context.scene.render.filepath}")
|
|
|
|
# Use simpler render call without context override to avoid errors
|
|
bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
|
|
else:
|
|
# Use OpenGL viewport rendering for other engines
|
|
# Check Blender version for compatibility
|
|
blender_version = bpy.app.version
|
|
is_blender_5 = blender_version[0] >= 5
|
|
|
|
print(f"Starting OpenGL render with:")
|
|
print(f" - Area: {self._area.type if self._area else 'None'}")
|
|
print(f" - Shading: {self._space.shading.type if self._space else 'None'}")
|
|
print(f" - View perspective: {self._region_3d.view_perspective if self._region_3d else 'None'}")
|
|
print(f" - Scene camera: {context.scene.camera.name if context.scene.camera else 'None'}")
|
|
|
|
try:
|
|
# In Blender 5.0+, use simpler context override or no override
|
|
if is_blender_5 and self._area:
|
|
# Try to get a valid region
|
|
regions = [r for r in self._area.regions if r.type == 'WINDOW']
|
|
if regions:
|
|
override = context.copy()
|
|
override["area"] = self._area
|
|
override["region"] = regions[0]
|
|
|
|
# In Blender 5.0, view_context parameter might not be needed or might cause issues
|
|
# Try without it first, then with it if needed
|
|
try:
|
|
with context.temp_override(**override):
|
|
bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, sequencer=False, write_still=False)
|
|
except TypeError:
|
|
# If that fails, try with view_context=False
|
|
with context.temp_override(**override):
|
|
bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, sequencer=False, write_still=False, view_context=False)
|
|
else:
|
|
# No valid region, try without override
|
|
bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, sequencer=False, write_still=False)
|
|
elif self._area:
|
|
# Blender < 5.0, use original approach
|
|
override = context.copy()
|
|
override["area"] = self._area
|
|
override["region"] = [r for r in self._area.regions if r.type == 'WINDOW'][0]
|
|
|
|
with context.temp_override(**override):
|
|
bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, sequencer=False, write_still=False, view_context=True)
|
|
else:
|
|
# No area available, use simple call
|
|
bpy.ops.render.opengl('INVOKE_DEFAULT', animation=True, sequencer=False, write_still=False)
|
|
except Exception as e:
|
|
print(f"Error during OpenGL render: {e}")
|
|
self.report({'ERROR'}, f"Render failed: {str(e)}")
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
|
|
# Force redraw of UI
|
|
for area in context.screen.areas:
|
|
if area.type == 'PROPERTIES':
|
|
area.tag_redraw()
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
elif self._phase == 'RENDER':
|
|
# Get current frame and calculate progress
|
|
current_frame = context.scene.frame_current
|
|
|
|
# Check if frame has changed since last time
|
|
if current_frame != self._last_reported_frame:
|
|
self._last_reported_frame = current_frame
|
|
if current_frame > self._max_frame_seen:
|
|
self._max_frame_seen = current_frame
|
|
total_frames = self._frame_end - self._frame_start + 1
|
|
|
|
# Calculate progress based on current frame
|
|
if current_frame >= self._frame_start:
|
|
completed_frames = max(0, self._max_frame_seen - self._frame_start + 1)
|
|
frame_progress = min(completed_frames, total_frames)
|
|
progress = min((frame_progress / total_frames) * 100, 100)
|
|
|
|
# Update properties
|
|
props.render_progress = progress
|
|
props.status_message = f"Rendering frame {current_frame}/{self._frame_end} ({int(progress)}%)"
|
|
print(f"Progress update: frame {current_frame}, progress {int(progress)}%")
|
|
|
|
# Force UI redraw
|
|
for area in context.screen.areas:
|
|
if area.type == 'PROPERTIES':
|
|
area.tag_redraw()
|
|
|
|
# Force all 3D viewports to update
|
|
for area in context.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
area.tag_redraw()
|
|
|
|
# Check if rendering is complete based on frame count or file existence
|
|
expected_frames = self._frame_end - self._frame_start + 1
|
|
frame_range_done = self._max_frame_seen >= self._frame_end
|
|
file_output_done = False
|
|
|
|
output_path = bpy.path.abspath(context.scene.render.filepath)
|
|
if getattr(self, '_use_actual_render', False):
|
|
frame_output_dir = os.path.join(bpy.path.abspath(context.scene.basedplayblast.output_path), "frames")
|
|
if os.path.exists(frame_output_dir):
|
|
frame_files = glob.glob(os.path.join(frame_output_dir, "*.png"))
|
|
file_output_done = len(frame_files) >= expected_frames
|
|
elif getattr(self, '_needs_video_encode', False):
|
|
# Check for last PNG file or completed frame count
|
|
expected_png = f"{output_path}{expected_frames:04d}.png"
|
|
if os.path.exists(expected_png):
|
|
file_output_done = True
|
|
else:
|
|
png_matches = glob.glob(f"{output_path}*.png")
|
|
file_output_done = len(png_matches) >= expected_frames
|
|
else:
|
|
file_ext = get_file_extension(context.scene.basedplayblast.video_format)
|
|
file_output_done = os.path.exists(output_path + file_ext)
|
|
|
|
if not self._has_triggered_complete and (frame_range_done or file_output_done):
|
|
print(f"Detected playblast completion - frames complete: {frame_range_done}, files complete: {file_output_done}")
|
|
self._has_triggered_complete = True
|
|
self._phase = 'COMPLETE'
|
|
props.render_progress = 100.0
|
|
props.status_message = "Finalizing output..."
|
|
|
|
# Force UI redraw
|
|
for area in context.screen.areas:
|
|
if area.type == 'PROPERTIES':
|
|
area.tag_redraw()
|
|
|
|
elif self._phase == 'COMPLETE':
|
|
props.render_progress = 0.0
|
|
props.status_message = ""
|
|
props.is_rendering = False
|
|
context.window_manager.event_timer_remove(self._timer)
|
|
self.finish(context)
|
|
|
|
# Force UI redraw
|
|
for area in context.screen.areas:
|
|
if area.type == 'PROPERTIES':
|
|
area.tag_redraw()
|
|
|
|
return {'FINISHED'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
def invoke(self, context, event):
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# DEBUG: Check engine at very start
|
|
print(f"DEBUG: Engine at very start of invoke: {scene.render.engine}")
|
|
|
|
# Initialize phase
|
|
self._phase = 'SETUP'
|
|
self._last_reported_frame = 0
|
|
|
|
# Store frame range
|
|
self._frame_start = scene.frame_start if props.use_scene_frame_range else props.start_frame
|
|
self._frame_end = scene.frame_end if props.use_scene_frame_range else props.end_frame
|
|
self._current_frame = scene.frame_current
|
|
self._max_frame_seen = self._frame_start - 1
|
|
self._has_triggered_complete = False
|
|
|
|
# Temporarily override Blender's frame range if using manual range
|
|
original_frame_start = scene.frame_start
|
|
original_frame_end = scene.frame_end
|
|
if not props.use_scene_frame_range:
|
|
scene.frame_start = props.start_frame
|
|
scene.frame_end = props.end_frame
|
|
print(f"Using manual frame range: {props.start_frame} - {props.end_frame}")
|
|
|
|
# Store basic original settings for this operator's cleanup
|
|
self._original_settings = {
|
|
'filepath': scene.render.filepath,
|
|
'resolution_x': scene.render.resolution_x,
|
|
'resolution_y': scene.render.resolution_y,
|
|
'resolution_percentage': scene.render.resolution_percentage,
|
|
'use_file_extension': scene.render.use_file_extension,
|
|
'use_overwrite': scene.render.use_overwrite,
|
|
'use_placeholder': scene.render.use_placeholder,
|
|
'camera': scene.camera,
|
|
'frame_start': original_frame_start, # Store original frame start
|
|
'frame_end': original_frame_end, # Store original frame end
|
|
'image_settings': {
|
|
'file_format': scene.render.image_settings.file_format,
|
|
'color_mode': scene.render.image_settings.color_mode
|
|
},
|
|
'display_mode': context.preferences.view.render_display_type,
|
|
# Store metadata settings
|
|
'use_stamp': scene.render.use_stamp,
|
|
'use_stamp_date': scene.render.use_stamp_date,
|
|
'use_stamp_time': scene.render.use_stamp_time,
|
|
'use_stamp_frame': scene.render.use_stamp_frame,
|
|
'use_stamp_camera': scene.render.use_stamp_camera,
|
|
'use_stamp_lens': scene.render.use_stamp_lens,
|
|
'use_stamp_scene': scene.render.use_stamp_scene,
|
|
'use_stamp_note': scene.render.use_stamp_note,
|
|
'stamp_note_text': scene.render.stamp_note_text
|
|
}
|
|
|
|
# Set render display type to NONE to hide render window
|
|
context.preferences.view.render_display_type = 'NONE'
|
|
|
|
# Find a 3D view
|
|
for a in context.screen.areas:
|
|
if a.type == 'VIEW_3D':
|
|
self._area = a
|
|
self._space = a.spaces.active
|
|
for region in a.regions:
|
|
if region.type == 'WINDOW':
|
|
region_3d = region.data
|
|
if region_3d:
|
|
self._region_3d = region_3d
|
|
self._original_view_perspective = region_3d.view_perspective
|
|
if hasattr(region_3d, 'use_local_camera'):
|
|
self._original_use_local_camera = region_3d.use_local_camera
|
|
break
|
|
break
|
|
|
|
if not self._area or not self._space:
|
|
self.report({'ERROR'}, "No 3D viewport found")
|
|
return {'CANCELLED'}
|
|
|
|
# Store viewport settings
|
|
self._original_shading = self._space.shading.type
|
|
self._original_overlays = self._space.overlay.show_overlays
|
|
|
|
# CRITICAL: Store comprehensive original settings NOW, before ANY changes in try block
|
|
if not props.original_settings:
|
|
import json
|
|
|
|
def safe_getattr(obj, attr, default=None):
|
|
try:
|
|
return getattr(obj, attr, default)
|
|
except:
|
|
return default
|
|
|
|
def make_json_serializable(obj):
|
|
if isinstance(obj, dict):
|
|
return {key: make_json_serializable(value) for key, value in obj.items()}
|
|
elif isinstance(obj, (list, tuple)):
|
|
return [make_json_serializable(item) for item in obj]
|
|
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
return obj
|
|
else:
|
|
try:
|
|
json.dumps(obj)
|
|
return obj
|
|
except:
|
|
return str(obj)
|
|
|
|
# Store ALL original settings comprehensively - EXACT copy from apply_blast_settings
|
|
original_settings = {
|
|
# SCENE.RENDER - Complete render settings
|
|
'render_engine': scene.render.engine,
|
|
'filepath': scene.render.filepath,
|
|
'resolution_x': scene.render.resolution_x,
|
|
'resolution_y': scene.render.resolution_y,
|
|
'resolution_percentage': scene.render.resolution_percentage,
|
|
'pixel_aspect_x': scene.render.pixel_aspect_x,
|
|
'pixel_aspect_y': scene.render.pixel_aspect_y,
|
|
'use_file_extension': scene.render.use_file_extension,
|
|
'use_overwrite': scene.render.use_overwrite,
|
|
'use_placeholder': scene.render.use_placeholder,
|
|
'frame_start': scene.frame_start,
|
|
'frame_end': scene.frame_end,
|
|
'frame_step': scene.frame_step,
|
|
'frame_current': scene.frame_current,
|
|
|
|
# Film settings
|
|
'film_transparent': scene.render.film_transparent,
|
|
'filter_size': scene.render.filter_size,
|
|
|
|
# Performance settings
|
|
'use_persistent_data': scene.render.use_persistent_data,
|
|
'use_simplify': scene.render.use_simplify,
|
|
'simplify_subdivision': scene.render.simplify_subdivision,
|
|
'simplify_child_particles': scene.render.simplify_child_particles,
|
|
'simplify_volumes': scene.render.simplify_volumes,
|
|
'simplify_subdivision_render': safe_getattr(scene.render, 'simplify_subdivision_render', 6),
|
|
'simplify_child_particles_render': safe_getattr(scene.render, 'simplify_child_particles_render', 1.0),
|
|
'simplify_volumes_render': safe_getattr(scene.render, 'simplify_volumes_render', 1.0),
|
|
|
|
# Motion blur
|
|
'use_motion_blur': scene.render.use_motion_blur,
|
|
'motion_blur_shutter': scene.render.motion_blur_shutter,
|
|
'motion_blur_shutter_curve': str(safe_getattr(scene.render, 'motion_blur_shutter_curve', 'AUTO')),
|
|
'rolling_shutter_type': safe_getattr(scene.render, 'rolling_shutter_type', 'NONE'),
|
|
'rolling_shutter_duration': safe_getattr(scene.render, 'rolling_shutter_duration', 0.1),
|
|
|
|
# Threading
|
|
'threads_mode': scene.render.threads_mode,
|
|
'threads': scene.render.threads,
|
|
|
|
# Memory and caching
|
|
'tile_x': safe_getattr(scene.render, 'tile_x', 64),
|
|
'tile_y': safe_getattr(scene.render, 'tile_y', 64),
|
|
'use_save_buffers': safe_getattr(scene.render, 'use_save_buffers', False),
|
|
|
|
# Preview and display
|
|
'display_mode': context.preferences.view.render_display_type,
|
|
'preview_pixel_size': safe_getattr(scene.render, 'preview_pixel_size', 'AUTO'),
|
|
|
|
# SCENE.RENDER.IMAGE_SETTINGS - Complete image settings
|
|
'image_settings': {
|
|
'file_format': scene.render.image_settings.file_format,
|
|
'color_mode': scene.render.image_settings.color_mode,
|
|
'color_depth': scene.render.image_settings.color_depth,
|
|
'compression': scene.render.image_settings.compression,
|
|
'quality': scene.render.image_settings.quality,
|
|
'use_preview': scene.render.image_settings.use_preview,
|
|
'exr_codec': safe_getattr(scene.render.image_settings, 'exr_codec', 'ZIP'),
|
|
'use_zbuffer': safe_getattr(scene.render.image_settings, 'use_zbuffer', False),
|
|
'jpeg2k_codec': safe_getattr(scene.render.image_settings, 'jpeg2k_codec', 'JP2'),
|
|
'tiff_codec': safe_getattr(scene.render.image_settings, 'tiff_codec', 'DEFLATE'),
|
|
},
|
|
|
|
# SCENE.RENDER.FFMPEG - Complete FFmpeg settings
|
|
'ffmpeg': {
|
|
'format': scene.render.ffmpeg.format,
|
|
'codec': scene.render.ffmpeg.codec,
|
|
'video_bitrate': scene.render.ffmpeg.video_bitrate,
|
|
'minrate': scene.render.ffmpeg.minrate,
|
|
'maxrate': scene.render.ffmpeg.maxrate,
|
|
'buffersize': scene.render.ffmpeg.buffersize,
|
|
'muxrate': scene.render.ffmpeg.muxrate,
|
|
'packetsize': scene.render.ffmpeg.packetsize,
|
|
'constant_rate_factor': scene.render.ffmpeg.constant_rate_factor,
|
|
'gopsize': scene.render.ffmpeg.gopsize,
|
|
'use_max_b_frames': safe_getattr(scene.render.ffmpeg, 'use_max_b_frames', False),
|
|
'max_b_frames': safe_getattr(scene.render.ffmpeg, 'max_b_frames', 2),
|
|
'use_autosplit': safe_getattr(scene.render.ffmpeg, 'use_autosplit', False),
|
|
'autosplit_size': safe_getattr(scene.render.ffmpeg, 'autosplit_size', 2048),
|
|
'audio_codec': scene.render.ffmpeg.audio_codec,
|
|
'audio_bitrate': scene.render.ffmpeg.audio_bitrate,
|
|
'audio_channels': scene.render.ffmpeg.audio_channels,
|
|
'audio_mixrate': scene.render.ffmpeg.audio_mixrate,
|
|
'audio_volume': scene.render.ffmpeg.audio_volume,
|
|
},
|
|
|
|
# Scene/world settings
|
|
'world': scene.world.name if scene.world else "",
|
|
'use_nodes': scene.use_nodes,
|
|
|
|
# Compositing settings
|
|
'use_compositing': scene.render.use_compositing,
|
|
'use_sequencer': scene.render.use_sequencer,
|
|
|
|
# Border and crop settings
|
|
'use_border': scene.render.use_border,
|
|
'border_min_x': scene.render.border_min_x,
|
|
'border_max_x': scene.render.border_max_x,
|
|
'border_min_y': scene.render.border_min_y,
|
|
'border_max_y': scene.render.border_max_y,
|
|
'use_crop_to_border': scene.render.use_crop_to_border,
|
|
|
|
# Metadata settings - comprehensive
|
|
'use_stamp': scene.render.use_stamp,
|
|
'use_stamp_date': scene.render.use_stamp_date,
|
|
'use_stamp_time': scene.render.use_stamp_time,
|
|
'use_stamp_frame': scene.render.use_stamp_frame,
|
|
'use_stamp_camera': scene.render.use_stamp_camera,
|
|
'use_stamp_lens': scene.render.use_stamp_lens,
|
|
'use_stamp_scene': scene.render.use_stamp_scene,
|
|
'use_stamp_note': scene.render.use_stamp_note,
|
|
'stamp_note_text': scene.render.stamp_note_text,
|
|
'use_stamp_marker': scene.render.use_stamp_marker,
|
|
'use_stamp_filename': scene.render.use_stamp_filename,
|
|
'use_stamp_render_time': scene.render.use_stamp_render_time,
|
|
'use_stamp_memory': scene.render.use_stamp_memory,
|
|
'use_stamp_hostname': scene.render.use_stamp_hostname,
|
|
'stamp_font_size': scene.render.stamp_font_size,
|
|
'stamp_foreground': [float(x) for x in scene.render.stamp_foreground] if hasattr(scene.render.stamp_foreground, '__iter__') else [1.0, 1.0, 1.0, 1.0],
|
|
'stamp_background': [float(x) for x in scene.render.stamp_background] if hasattr(scene.render.stamp_background, '__iter__') else [0.0, 0.0, 0.0, 0.8],
|
|
|
|
# Hair settings
|
|
'hair_type': safe_getattr(scene.render, 'hair_type', 'PATH'),
|
|
'hair_subdiv': safe_getattr(scene.render, 'hair_subdiv', 3),
|
|
|
|
# SCENE.CYCLES - Complete Cycles settings
|
|
'cycles': {
|
|
'device': safe_getattr(scene.cycles, 'device', 'CPU'),
|
|
'feature_set': safe_getattr(scene.cycles, 'feature_set', 'SUPPORTED'),
|
|
'shading_system': safe_getattr(scene.cycles, 'shading_system', 'SVM'),
|
|
'samples': safe_getattr(scene.cycles, 'samples', 128),
|
|
'preview_samples': safe_getattr(scene.cycles, 'preview_samples', 32),
|
|
'aa_samples': safe_getattr(scene.cycles, 'aa_samples', 4),
|
|
'preview_aa_samples': safe_getattr(scene.cycles, 'preview_aa_samples', 4),
|
|
'use_denoising': safe_getattr(scene.cycles, 'use_denoising', True),
|
|
'denoiser': safe_getattr(scene.cycles, 'denoiser', 'OPENIMAGEDENOISE'),
|
|
'denoising_input_passes': safe_getattr(scene.cycles, 'denoising_input_passes', 'RGB_ALBEDO_NORMAL'),
|
|
'use_denoising_input_passes': safe_getattr(scene.cycles, 'use_denoising_input_passes', True),
|
|
'denoising_prefilter': safe_getattr(scene.cycles, 'denoising_prefilter', 'ACCURATE'),
|
|
'use_adaptive_sampling': safe_getattr(scene.cycles, 'use_adaptive_sampling', True),
|
|
'adaptive_threshold': safe_getattr(scene.cycles, 'adaptive_threshold', 0.01),
|
|
'adaptive_min_samples': safe_getattr(scene.cycles, 'adaptive_min_samples', 0),
|
|
'time_limit': safe_getattr(scene.cycles, 'time_limit', 0.0),
|
|
'use_preview_adaptive_sampling': safe_getattr(scene.cycles, 'use_preview_adaptive_sampling', False),
|
|
'preview_adaptive_threshold': safe_getattr(scene.cycles, 'preview_adaptive_threshold', 0.1),
|
|
'preview_adaptive_min_samples': safe_getattr(scene.cycles, 'preview_adaptive_min_samples', 0),
|
|
'seed': safe_getattr(scene.cycles, 'seed', 0),
|
|
'use_animated_seed': safe_getattr(scene.cycles, 'use_animated_seed', False),
|
|
'sample_clamp_direct': safe_getattr(scene.cycles, 'sample_clamp_direct', 0.0),
|
|
'sample_clamp_indirect': safe_getattr(scene.cycles, 'sample_clamp_indirect', 0.0),
|
|
'light_sampling_threshold': safe_getattr(scene.cycles, 'light_sampling_threshold', 0.01),
|
|
'sample_all_lights_direct': safe_getattr(scene.cycles, 'sample_all_lights_direct', True),
|
|
'sample_all_lights_indirect': safe_getattr(scene.cycles, 'sample_all_lights_indirect', True),
|
|
'max_bounces': safe_getattr(scene.cycles, 'max_bounces', 12),
|
|
'diffuse_bounces': safe_getattr(scene.cycles, 'diffuse_bounces', 4),
|
|
'glossy_bounces': safe_getattr(scene.cycles, 'glossy_bounces', 4),
|
|
'transmission_bounces': safe_getattr(scene.cycles, 'transmission_bounces', 12),
|
|
'volume_bounces': safe_getattr(scene.cycles, 'volume_bounces', 0),
|
|
'transparent_max_bounces': safe_getattr(scene.cycles, 'transparent_max_bounces', 8),
|
|
'caustics_reflective': safe_getattr(scene.cycles, 'caustics_reflective', True),
|
|
'caustics_refractive': safe_getattr(scene.cycles, 'caustics_refractive', True),
|
|
'filter_type': safe_getattr(scene.cycles, 'filter_type', 'GAUSSIAN'),
|
|
'filter_width': safe_getattr(scene.cycles, 'filter_width', 1.5),
|
|
'pixel_filter_width': safe_getattr(scene.cycles, 'pixel_filter_width', 1.5),
|
|
'use_persistent_data': safe_getattr(scene.cycles, 'use_persistent_data', False),
|
|
'debug_use_spatial_splits': safe_getattr(scene.cycles, 'debug_use_spatial_splits', False),
|
|
'debug_use_hair_bvh': safe_getattr(scene.cycles, 'debug_use_hair_bvh', True),
|
|
'debug_bvh_type': safe_getattr(scene.cycles, 'debug_bvh_type', 'DYNAMIC_BVH'),
|
|
'debug_use_compact_bvh': safe_getattr(scene.cycles, 'debug_use_compact_bvh', True),
|
|
'tile_size': safe_getattr(scene.cycles, 'tile_size', 256),
|
|
'use_auto_tile': safe_getattr(scene.cycles, 'use_auto_tile', False),
|
|
'progressive': safe_getattr(scene.cycles, 'progressive', 'PATH'),
|
|
'use_square_samples': safe_getattr(scene.cycles, 'use_square_samples', False),
|
|
'blur_glossy': safe_getattr(scene.cycles, 'blur_glossy', 0.0),
|
|
'use_transparent_shadows': safe_getattr(scene.cycles, 'use_transparent_shadows', True),
|
|
'volume_step_rate': safe_getattr(scene.cycles, 'volume_step_rate', 1.0),
|
|
'volume_preview_step_rate': safe_getattr(scene.cycles, 'volume_preview_step_rate', 1.0),
|
|
'volume_max_steps': safe_getattr(scene.cycles, 'volume_max_steps', 1024),
|
|
},
|
|
}
|
|
|
|
try:
|
|
safe_settings = make_json_serializable(original_settings)
|
|
props.original_settings = json.dumps(safe_settings)
|
|
print(f"Stored comprehensive Cycles settings: samples={original_settings['cycles']['samples']}, engine={original_settings['render_engine']}")
|
|
print(f"DEBUG: JSON engine name stored: {safe_settings['render_engine']}")
|
|
print(f"DEBUG: Current scene engine: {scene.render.engine}")
|
|
except Exception as e:
|
|
print(f"Error storing settings: {e}")
|
|
props.original_settings = ""
|
|
|
|
try:
|
|
# Set resolution based on mode
|
|
if props.resolution_mode == 'PRESET':
|
|
preset = props.resolution_preset
|
|
x_str = preset.split('y')[0].replace('x', '')
|
|
y_str = preset.split('y')[1]
|
|
scene.render.resolution_x = int(x_str)
|
|
scene.render.resolution_y = int(y_str)
|
|
elif props.resolution_mode == 'CUSTOM':
|
|
scene.render.resolution_x = props.resolution_x
|
|
scene.render.resolution_y = props.resolution_y
|
|
|
|
scene.render.resolution_percentage = props.resolution_percentage
|
|
|
|
# Create output directory
|
|
output_dir = bpy.path.abspath(props.output_path)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Set file format first (Blender 5.0 compatible)
|
|
video_format_set = set_video_file_format(scene)
|
|
if not video_format_set and hasattr(scene.render, 'ffmpeg'):
|
|
# Still try to configure ffmpeg even if file_format couldn't be set
|
|
# This might work in some Blender 5.0 configurations
|
|
self.report({'WARNING'}, "Could not set video file_format. Attempting to proceed with ffmpeg settings...")
|
|
elif not video_format_set:
|
|
self.report({'ERROR'}, "Video rendering not supported in this Blender version.")
|
|
return {'CANCELLED'}
|
|
|
|
# Configure ffmpeg settings (these should still work even if file_format is different)
|
|
if hasattr(scene.render, 'ffmpeg'):
|
|
scene.render.ffmpeg.format = props.video_format
|
|
scene.render.ffmpeg.codec = props.video_codec
|
|
scene.render.ffmpeg.constant_rate_factor = get_ffmpeg_quality(props.video_quality)
|
|
|
|
# Audio settings
|
|
if props.include_audio:
|
|
scene.render.ffmpeg.audio_codec = props.audio_codec
|
|
scene.render.ffmpeg.audio_bitrate = props.audio_bitrate
|
|
else:
|
|
scene.render.ffmpeg.audio_codec = 'NONE'
|
|
else:
|
|
self.report({'ERROR'}, "FFMPEG settings not available in this Blender version.")
|
|
return {'CANCELLED'}
|
|
|
|
# Set output path - handle PNG vs video format differently
|
|
file_name = props.file_name
|
|
if '.' in file_name:
|
|
file_name = os.path.splitext(file_name)[0]
|
|
|
|
# Add frame range to filename
|
|
file_name = file_name.rstrip('_')
|
|
frame_range_str = f"_{self._frame_start}-{self._frame_end}"
|
|
file_name += frame_range_str
|
|
|
|
# Check if we're using PNG format (Blender 5.0 fallback)
|
|
is_png_format = scene.render.image_settings.file_format == 'PNG'
|
|
|
|
if is_png_format:
|
|
# For PNG format, use proper frame numbering pattern (no video extension)
|
|
# Blender will append frame numbers automatically
|
|
scene.render.filepath = os.path.join(output_dir, file_name + "_")
|
|
scene.render.use_file_extension = True # This enables frame numbering
|
|
scene.render.use_overwrite = True
|
|
scene.render.use_placeholder = False
|
|
# Store flag that we need to encode after render
|
|
self._needs_video_encode = True
|
|
else:
|
|
# For FFMPEG video, set path with proper video extension and NO frame numbers
|
|
video_ext = get_file_extension(props.video_format)
|
|
scene.render.filepath = os.path.join(output_dir, file_name + video_ext)
|
|
scene.render.use_file_extension = True
|
|
scene.render.use_overwrite = True
|
|
scene.render.use_placeholder = False
|
|
self._needs_video_encode = False
|
|
|
|
# Confirm FFMPEG format for debugging
|
|
print(f"File format set to: {scene.render.image_settings.file_format}")
|
|
print(f"FFMPEG format: {scene.render.ffmpeg.format}, codec: {scene.render.ffmpeg.codec}")
|
|
print(f"Video output path: {scene.render.filepath}")
|
|
print(f"File extension enabled: {scene.render.use_file_extension}")
|
|
print(f"Overwrite enabled: {scene.render.use_overwrite}")
|
|
print(f"Placeholder disabled: {scene.render.use_placeholder}")
|
|
|
|
# Set camera if specified
|
|
if not props.use_active_camera and props.camera_object != "NONE":
|
|
camera_obj = context.scene.objects.get(props.camera_object)
|
|
if camera_obj and camera_obj.type == 'CAMERA':
|
|
scene.camera = camera_obj
|
|
print(f"Using selected camera: {camera_obj.name}")
|
|
else:
|
|
self.report({'ERROR'}, f"Selected camera '{props.camera_object}' not found or not a camera")
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
else:
|
|
# Validate scene camera exists
|
|
if not scene.camera:
|
|
self.report({'ERROR'}, "No active camera in scene. Please add a camera or select one in the properties.")
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
print(f"Using scene camera: {scene.camera.name}")
|
|
|
|
# Set frame range if using manual range
|
|
if not props.use_scene_frame_range:
|
|
scene.frame_start = props.start_frame
|
|
scene.frame_end = props.end_frame
|
|
|
|
# Setup metadata
|
|
if props.show_metadata:
|
|
scene.render.use_stamp = True
|
|
scene.render.use_stamp_date = props.metadata_date
|
|
scene.render.use_stamp_time = props.metadata_date # Usually linked with date
|
|
scene.render.use_stamp_frame = props.metadata_frame
|
|
scene.render.use_stamp_camera = props.metadata_camera
|
|
scene.render.use_stamp_lens = props.metadata_lens
|
|
scene.render.use_stamp_scene = props.metadata_scene
|
|
|
|
# Set note if provided
|
|
if props.metadata_note:
|
|
scene.render.use_stamp_note = True
|
|
|
|
# Build the note text
|
|
note = props.metadata_note
|
|
|
|
# Add resolution info if enabled
|
|
if props.metadata_resolution:
|
|
res_x = scene.render.resolution_x * (scene.render.resolution_percentage / 100.0)
|
|
res_y = scene.render.resolution_y * (scene.render.resolution_percentage / 100.0)
|
|
note += f"\nResolution: {int(res_x)} x {int(res_y)}"
|
|
|
|
scene.render.stamp_note_text = note
|
|
else:
|
|
scene.render.use_stamp = False
|
|
|
|
# Set viewport display mode
|
|
if self._space:
|
|
# CRITICAL: Ensure we have a valid camera first
|
|
if not scene.camera:
|
|
self.report({'ERROR'}, "No active camera found. Please set an active camera for the scene.")
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
|
|
# Set shading type according to display_mode
|
|
if self._space.shading.type != props.display_mode:
|
|
self._space.shading.type = props.display_mode
|
|
print(f"Set viewport shading to: {props.display_mode}")
|
|
|
|
# Set overlay visibility
|
|
if props.auto_disable_overlays:
|
|
self._space.overlay.show_overlays = False
|
|
|
|
# Switch to camera view if needed
|
|
if self._region_3d:
|
|
self._region_3d.view_perspective = 'CAMERA'
|
|
if hasattr(self._region_3d, 'use_local_camera'):
|
|
self._region_3d.use_local_camera = False
|
|
print(f"Set viewport to camera view")
|
|
|
|
# CRITICAL: Force viewport refresh and update
|
|
self._area.tag_redraw()
|
|
context.view_layer.update()
|
|
|
|
# Additional viewport settings based on display mode
|
|
if props.display_mode == 'SOLID':
|
|
# Ensure proper solid shading settings
|
|
self._space.shading.color_type = 'MATERIAL'
|
|
self._space.shading.light = 'STUDIO'
|
|
elif props.display_mode == 'MATERIAL':
|
|
# Ensure material preview settings
|
|
self._space.shading.color_type = 'MATERIAL'
|
|
self._space.shading.light = 'STUDIO'
|
|
elif props.display_mode == 'RENDERED':
|
|
# CRITICAL: For Cycles, use actual rendering instead of viewport rendering
|
|
current_engine = scene.render.engine
|
|
if current_engine == 'CYCLES':
|
|
print(f"WARNING: Cycles RENDERED mode detected - switching to actual render mode for stability")
|
|
|
|
# Mark that we're using actual rendering instead of viewport rendering
|
|
self._use_actual_render = True
|
|
self._original_render_engine = None # Don't change engine
|
|
self._original_cycles_viewport = None
|
|
|
|
# CRITICAL: For Cycles, render individual frames and convert to video afterwards
|
|
# This avoids FFMPEG issues with Cycles animation rendering
|
|
scene.render.image_settings.file_format = 'PNG'
|
|
scene.render.image_settings.color_mode = 'RGBA'
|
|
scene.render.image_settings.compression = 15 # Minimal compression for speed
|
|
|
|
# Set frame-based output path for individual frames
|
|
frame_output_dir = os.path.join(output_dir, "frames")
|
|
os.makedirs(frame_output_dir, exist_ok=True)
|
|
scene.render.filepath = os.path.join(frame_output_dir, file_name + "_")
|
|
|
|
print(f"WARNING: Using frame-based rendering for Cycles stability")
|
|
print(f"Frame output: {scene.render.filepath}")
|
|
print(f"Will convert to video after rendering completes")
|
|
|
|
# Apply ultra-fast Cycles settings for playblast
|
|
cycles = scene.cycles
|
|
|
|
# Store original render settings to restore later
|
|
if not hasattr(self, '_original_cycles_render'):
|
|
self._original_cycles_render = {
|
|
'samples': getattr(cycles, 'samples', 128),
|
|
'use_denoising': getattr(cycles, 'use_denoising', True),
|
|
'max_bounces': getattr(cycles, 'max_bounces', 12),
|
|
'diffuse_bounces': getattr(cycles, 'diffuse_bounces', 4),
|
|
'glossy_bounces': getattr(cycles, 'glossy_bounces', 4),
|
|
'transmission_bounces': getattr(cycles, 'transmission_bounces', 12),
|
|
'volume_bounces': getattr(cycles, 'volume_bounces', 0),
|
|
'use_adaptive_sampling': getattr(cycles, 'use_adaptive_sampling', True),
|
|
'adaptive_threshold': getattr(cycles, 'adaptive_threshold', 0.01),
|
|
}
|
|
|
|
# Apply ultra-fast settings for playblast
|
|
if hasattr(cycles, 'samples'):
|
|
cycles.samples = 8 # Very low for speed
|
|
if hasattr(cycles, 'use_denoising'):
|
|
cycles.use_denoising = False # Disable for speed
|
|
if hasattr(cycles, 'max_bounces'):
|
|
cycles.max_bounces = 2 # Minimal bounces
|
|
if hasattr(cycles, 'diffuse_bounces'):
|
|
cycles.diffuse_bounces = 1
|
|
if hasattr(cycles, 'glossy_bounces'):
|
|
cycles.glossy_bounces = 1
|
|
if hasattr(cycles, 'transmission_bounces'):
|
|
cycles.transmission_bounces = 1
|
|
if hasattr(cycles, 'volume_bounces'):
|
|
cycles.volume_bounces = 0
|
|
if hasattr(cycles, 'use_adaptive_sampling'):
|
|
cycles.use_adaptive_sampling = True
|
|
if hasattr(cycles, 'adaptive_threshold'):
|
|
cycles.adaptive_threshold = 0.5 # High threshold for fast convergence
|
|
|
|
print(f"Applied ultra-fast Cycles settings: {cycles.samples} samples, no denoising, {cycles.max_bounces} max bounces")
|
|
else:
|
|
# For non-Cycles engines, use viewport rendering as normal
|
|
self._use_actual_render = False
|
|
self._original_render_engine = None
|
|
self._original_cycles_viewport = None
|
|
|
|
print(f"Viewport setup complete for {props.display_mode} mode")
|
|
|
|
# Create override context
|
|
override = context.copy()
|
|
override["area"] = self._area
|
|
override["region"] = [r for r in self._area.regions if r.type == 'WINDOW'][0]
|
|
|
|
# Start progress bar
|
|
context.window_manager.progress_begin(0, 1.0)
|
|
|
|
# Add timer for modal - update every 0.1 seconds for more frequent updates
|
|
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error creating playblast: {str(e)}")
|
|
self.cleanup(context)
|
|
return {'CANCELLED'}
|
|
|
|
def finish(self, context):
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# Check if we need to convert frames to video
|
|
# This happens when: 1) Using Cycles rendering, or 2) PNG format was used (Blender 5.0 fallback)
|
|
if getattr(self, '_use_actual_render', False) or getattr(self, '_needs_video_encode', False):
|
|
self.convert_frames_to_video(context)
|
|
|
|
# Find and open the output file
|
|
file_ext = get_file_extension(props.video_format)
|
|
output_dir = bpy.path.abspath(props.output_path)
|
|
all_files = glob.glob(os.path.join(output_dir, "*" + file_ext))
|
|
if all_files:
|
|
latest_file = max(all_files, key=os.path.getmtime)
|
|
props.last_playblast_file = latest_file
|
|
|
|
try:
|
|
if sys.platform == 'win32':
|
|
os.startfile(latest_file)
|
|
elif sys.platform == 'darwin':
|
|
subprocess.call(('open', latest_file))
|
|
else:
|
|
subprocess.call(('xdg-open', latest_file))
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Failed to open playblast: {str(e)}")
|
|
|
|
self.cleanup(context)
|
|
|
|
def convert_frames_to_video(self, context):
|
|
"""Convert individual PNG frames to video using FFmpeg"""
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
try:
|
|
output_dir = bpy.path.abspath(props.output_path)
|
|
|
|
# Get file name without extension
|
|
file_name = props.file_name
|
|
if '.' in file_name:
|
|
file_name = os.path.splitext(file_name)[0]
|
|
|
|
# Add frame range to filename to match the rendered frames
|
|
file_name = file_name.rstrip('_')
|
|
frame_range_str = f"_{self._frame_start}-{self._frame_end}"
|
|
file_name += frame_range_str
|
|
|
|
# Define video output path
|
|
video_ext = get_file_extension(props.video_format)
|
|
video_output = os.path.join(output_dir, file_name + video_ext)
|
|
|
|
# Frame pattern for FFmpeg - check both possible locations and patterns
|
|
# Pattern 1: Files in output_dir with format "filename_0001.png"
|
|
frame_pattern1 = os.path.join(output_dir, file_name + "_%04d.png")
|
|
# Pattern 2: Files in frames subdirectory
|
|
frame_output_dir = os.path.join(output_dir, "frames")
|
|
frame_pattern2 = os.path.join(frame_output_dir, file_name + "_%04d.png")
|
|
# Pattern 3: Files with .mp4 in name (Blender 5.0 issue - wrong extension in path)
|
|
frame_pattern3 = os.path.join(output_dir, file_name + ".mp4%04d.png")
|
|
|
|
# Try to find which pattern matches actual files
|
|
import glob
|
|
test_patterns = [
|
|
(frame_pattern1, output_dir),
|
|
(frame_pattern2, frame_output_dir),
|
|
(frame_pattern3, output_dir)
|
|
]
|
|
|
|
frame_pattern = None
|
|
frame_dir = None
|
|
for pattern, dir_path in test_patterns:
|
|
# Test if files matching this pattern exist
|
|
test_files = glob.glob(pattern.replace("%04d", "????"))
|
|
if test_files:
|
|
frame_pattern = pattern
|
|
frame_dir = dir_path
|
|
print(f"Found frame files matching pattern: {pattern}")
|
|
break
|
|
|
|
if not frame_pattern:
|
|
# Fallback: search for any PNG files with the base filename (handle various patterns)
|
|
# Try pattern with .mp4 in name first (Blender 5.0 issue)
|
|
all_pngs = glob.glob(os.path.join(output_dir, file_name + ".mp4*.png"))
|
|
if not all_pngs:
|
|
# Try standard pattern
|
|
all_pngs = glob.glob(os.path.join(output_dir, file_name + "_*.png"))
|
|
if not all_pngs:
|
|
# Try any PNG files starting with filename
|
|
all_pngs = glob.glob(os.path.join(output_dir, file_name + "*.png"))
|
|
|
|
if all_pngs:
|
|
# Sort files to find the pattern
|
|
all_pngs.sort()
|
|
# Try to determine the pattern from the first file
|
|
first_file = os.path.basename(all_pngs[0])
|
|
if ".mp4" in first_file:
|
|
# Handle .mp4####.png pattern
|
|
frame_pattern = os.path.join(output_dir, file_name + ".mp4%04d.png")
|
|
elif "_" in first_file:
|
|
# Handle _####.png pattern
|
|
frame_pattern = os.path.join(output_dir, file_name + "_%04d.png")
|
|
else:
|
|
# Generic pattern
|
|
frame_pattern = os.path.join(output_dir, file_name + "%04d.png")
|
|
frame_dir = output_dir
|
|
print(f"Using detected pattern from files: {frame_pattern}")
|
|
else:
|
|
self.report({'ERROR'}, f"No PNG frame files found to convert. Searched in: {output_dir}")
|
|
return
|
|
|
|
# Build FFmpeg command using configured settings
|
|
framerate = scene.render.fps / scene.render.fps_base
|
|
|
|
# Get codec and quality settings from props
|
|
codec_map = {
|
|
'H264': 'libx264',
|
|
'H265': 'libx265',
|
|
'AV1': 'libaom-av1',
|
|
'MPEG4': 'mpeg4',
|
|
'FFV1': 'ffv1'
|
|
}
|
|
video_codec = codec_map.get(props.video_codec, 'libx264')
|
|
|
|
# Get CRF value from quality
|
|
crf_map = {
|
|
'LOWEST': '28',
|
|
'VERYLOW': '26',
|
|
'LOW': '23',
|
|
'MEDIUM': '20',
|
|
'HIGH': '18',
|
|
'PERC_LOSSLESS': '15',
|
|
'LOSSLESS': '0'
|
|
}
|
|
crf_value = crf_map.get(props.video_quality, '20')
|
|
|
|
ffmpeg_cmd = [
|
|
"ffmpeg", "-y", # Overwrite output file
|
|
"-framerate", str(framerate),
|
|
"-i", frame_pattern,
|
|
"-c:v", video_codec,
|
|
"-pix_fmt", "yuv420p",
|
|
"-crf", crf_value,
|
|
]
|
|
|
|
# Add audio if enabled
|
|
if props.include_audio and props.audio_codec != 'NONE':
|
|
audio_codec_map = {
|
|
'AAC': 'aac',
|
|
'AC3': 'ac3',
|
|
'MP3': 'mp3'
|
|
}
|
|
audio_codec = audio_codec_map.get(props.audio_codec, 'aac')
|
|
ffmpeg_cmd.extend([
|
|
"-c:a", audio_codec,
|
|
"-b:a", f"{props.audio_bitrate}k"
|
|
])
|
|
|
|
# Add custom ffmpeg args if provided
|
|
if props.use_custom_ffmpeg_args and props.custom_ffmpeg_args:
|
|
import shlex
|
|
custom_args = shlex.split(props.custom_ffmpeg_args)
|
|
ffmpeg_cmd.extend(custom_args)
|
|
|
|
ffmpeg_cmd.append(video_output)
|
|
|
|
print(f"Converting frames to video...")
|
|
print(f"Command: {' '.join(ffmpeg_cmd)}")
|
|
|
|
# Run FFmpeg
|
|
result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
print(f"Video conversion successful: {video_output}")
|
|
|
|
# Clean up frame files from the directory where they were found
|
|
import glob
|
|
if frame_dir:
|
|
frame_files = glob.glob(os.path.join(frame_dir, file_name + "*.png"))
|
|
for frame_file in frame_files:
|
|
try:
|
|
os.remove(frame_file)
|
|
print(f"Removed frame file: {frame_file}")
|
|
except Exception as e:
|
|
print(f"Could not remove frame file {frame_file}: {e}")
|
|
|
|
# Remove frame directory if it exists and is empty
|
|
frame_output_dir = os.path.join(output_dir, "frames")
|
|
if os.path.exists(frame_output_dir):
|
|
try:
|
|
if not os.listdir(frame_output_dir):
|
|
os.rmdir(frame_output_dir)
|
|
except:
|
|
pass
|
|
|
|
else:
|
|
print(f"FFmpeg error: {result.stderr}")
|
|
self.report({'ERROR'}, f"Video conversion failed: {result.stderr}")
|
|
|
|
except Exception as e:
|
|
print(f"Error converting frames to video: {str(e)}")
|
|
self.report({'ERROR'}, f"Video conversion error: {str(e)}")
|
|
|
|
def cleanup(self, context):
|
|
# Reset progress properties
|
|
props = context.scene.basedplayblast
|
|
props.is_rendering = False
|
|
props.render_progress = 0.0
|
|
props.status_message = ""
|
|
self._max_frame_seen = 0
|
|
self._has_triggered_complete = False
|
|
|
|
# End progress bar if it's still running
|
|
context.window_manager.progress_end()
|
|
|
|
# Remove timer if it exists
|
|
if self._timer is not None:
|
|
context.window_manager.event_timer_remove(self._timer)
|
|
|
|
# Restore viewport settings
|
|
if self._space:
|
|
self._space.shading.type = self._original_shading
|
|
self._space.overlay.show_overlays = self._original_overlays
|
|
|
|
# Restore view settings
|
|
if self._region_3d:
|
|
if self._original_view_perspective:
|
|
self._region_3d.view_perspective = self._original_view_perspective
|
|
if self._original_use_local_camera is not None:
|
|
self._region_3d.use_local_camera = self._original_use_local_camera
|
|
|
|
# PRIMARY RESTORATION: Use self._original_settings first (most reliable)
|
|
if self._original_settings:
|
|
scene = context.scene
|
|
print("Restoring render settings from self._original_settings...")
|
|
|
|
# Restore render settings - CRITICAL: These must be restored to original values
|
|
scene.render.filepath = self._original_settings['filepath']
|
|
scene.render.resolution_x = self._original_settings['resolution_x']
|
|
scene.render.resolution_y = self._original_settings['resolution_y']
|
|
scene.render.resolution_percentage = self._original_settings['resolution_percentage']
|
|
scene.render.use_file_extension = self._original_settings['use_file_extension']
|
|
scene.render.use_overwrite = self._original_settings['use_overwrite']
|
|
scene.render.use_placeholder = self._original_settings['use_placeholder']
|
|
scene.camera = self._original_settings['camera']
|
|
scene.render.image_settings.file_format = self._original_settings['image_settings']['file_format']
|
|
scene.render.image_settings.color_mode = self._original_settings['image_settings']['color_mode']
|
|
context.preferences.view.render_display_type = self._original_settings['display_mode']
|
|
|
|
# CRITICAL: Restore frame range to original values - THIS FIXES THE MAIN BUG
|
|
scene.frame_start = self._original_settings['frame_start']
|
|
scene.frame_end = self._original_settings['frame_end']
|
|
|
|
# Restore metadata settings
|
|
scene.render.use_stamp = self._original_settings['use_stamp']
|
|
scene.render.use_stamp_date = self._original_settings['use_stamp_date']
|
|
scene.render.use_stamp_time = self._original_settings['use_stamp_time']
|
|
scene.render.use_stamp_frame = self._original_settings['use_stamp_frame']
|
|
scene.render.use_stamp_camera = self._original_settings['use_stamp_camera']
|
|
scene.render.use_stamp_lens = self._original_settings['use_stamp_lens']
|
|
scene.render.use_stamp_scene = self._original_settings['use_stamp_scene']
|
|
scene.render.use_stamp_note = self._original_settings['use_stamp_note']
|
|
scene.render.stamp_note_text = self._original_settings['stamp_note_text']
|
|
|
|
print(f"Restored resolution: {scene.render.resolution_x}x{scene.render.resolution_y}")
|
|
print(f"Restored frame range: {scene.frame_start}-{scene.frame_end}")
|
|
|
|
# Restore original render engine if it was changed
|
|
if self._original_render_engine is not None:
|
|
context.scene.render.engine = self._original_render_engine
|
|
print(f"Restored original render engine: {self._original_render_engine}")
|
|
|
|
# Restore original Cycles viewport settings if they were changed
|
|
if self._original_cycles_viewport is not None:
|
|
cycles = context.scene.cycles
|
|
for attr, value in self._original_cycles_viewport.items():
|
|
if hasattr(cycles, attr):
|
|
setattr(cycles, attr, value)
|
|
print(f"Restored original Cycles viewport settings")
|
|
|
|
# Restore original Cycles render settings if they were changed
|
|
if self._original_cycles_render is not None:
|
|
cycles = context.scene.cycles
|
|
scene = context.scene
|
|
for attr, value in self._original_cycles_render.items():
|
|
if attr == 'file_format':
|
|
scene.render.image_settings.file_format = value
|
|
elif hasattr(cycles, attr):
|
|
setattr(cycles, attr, value)
|
|
print(f"Restored original Cycles render settings")
|
|
|
|
# SECONDARY RESTORATION: Only use JSON backup if primary restoration didn't work
|
|
# This prevents conflicts and ensures we don't overwrite the correct restoration
|
|
if not self._original_settings and props.original_settings:
|
|
try:
|
|
print("Primary restoration not available, using JSON backup...")
|
|
import json
|
|
original = json.loads(props.original_settings)
|
|
scene = context.scene
|
|
|
|
def safe_restore(obj, attr, value):
|
|
try:
|
|
if hasattr(obj, attr):
|
|
setattr(obj, attr, value)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Could not restore {attr}: {e}")
|
|
return False
|
|
|
|
# Restore render engine first
|
|
if 'render_engine' in original:
|
|
scene.render.engine = original['render_engine']
|
|
print(f"Restored render engine to: {original['render_engine']}")
|
|
|
|
# Restore critical render settings from JSON backup
|
|
scene.render.filepath = original.get('filepath', scene.render.filepath)
|
|
scene.render.resolution_x = original.get('resolution_x', scene.render.resolution_x)
|
|
scene.render.resolution_y = original.get('resolution_y', scene.render.resolution_y)
|
|
scene.render.resolution_percentage = original.get('resolution_percentage', scene.render.resolution_percentage)
|
|
safe_restore(scene.render, 'pixel_aspect_x', original.get('pixel_aspect_x', 1.0))
|
|
safe_restore(scene.render, 'pixel_aspect_y', original.get('pixel_aspect_y', 1.0))
|
|
scene.render.use_file_extension = original.get('use_file_extension', scene.render.use_file_extension)
|
|
scene.render.use_overwrite = original.get('use_overwrite', scene.render.use_overwrite)
|
|
scene.render.use_placeholder = original.get('use_placeholder', scene.render.use_placeholder)
|
|
|
|
# CRITICAL: Restore frame range from JSON backup
|
|
scene.frame_start = original.get('frame_start', scene.frame_start)
|
|
scene.frame_end = original.get('frame_end', scene.frame_end)
|
|
scene.frame_step = original.get('frame_step', scene.frame_step)
|
|
scene.frame_current = original.get('frame_current', 1)
|
|
|
|
print(f"JSON backup restored resolution: {scene.render.resolution_x}x{scene.render.resolution_y}")
|
|
print(f"JSON backup restored frame range: {scene.frame_start}-{scene.frame_end}")
|
|
|
|
# Film settings
|
|
scene.render.film_transparent = original.get('film_transparent', scene.render.film_transparent)
|
|
scene.render.filter_size = original.get('filter_size', scene.render.filter_size)
|
|
|
|
# Performance settings
|
|
scene.render.use_persistent_data = original.get('use_persistent_data', scene.render.use_persistent_data)
|
|
scene.render.use_simplify = original.get('use_simplify', scene.render.use_simplify)
|
|
scene.render.simplify_subdivision = original.get('simplify_subdivision', scene.render.simplify_subdivision)
|
|
scene.render.simplify_child_particles = original.get('simplify_child_particles', scene.render.simplify_child_particles)
|
|
scene.render.simplify_volumes = original.get('simplify_volumes', scene.render.simplify_volumes)
|
|
|
|
# Motion blur
|
|
scene.render.use_motion_blur = original.get('use_motion_blur', scene.render.use_motion_blur)
|
|
scene.render.motion_blur_shutter = original.get('motion_blur_shutter', scene.render.motion_blur_shutter)
|
|
|
|
# Threading
|
|
scene.render.threads_mode = original.get('threads_mode', scene.render.threads_mode)
|
|
scene.render.threads = original.get('threads', scene.render.threads)
|
|
|
|
# Preview and display
|
|
context.preferences.view.render_display_type = original.get('display_mode', context.preferences.view.render_display_type)
|
|
|
|
# SCENE.RENDER.IMAGE_SETTINGS - Restore image settings
|
|
if 'image_settings' in original:
|
|
img_settings = original['image_settings']
|
|
scene.render.image_settings.file_format = img_settings.get('file_format', scene.render.image_settings.file_format)
|
|
scene.render.image_settings.color_mode = img_settings.get('color_mode', scene.render.image_settings.color_mode)
|
|
scene.render.image_settings.color_depth = img_settings.get('color_depth', scene.render.image_settings.color_depth)
|
|
scene.render.image_settings.compression = img_settings.get('compression', scene.render.image_settings.compression)
|
|
scene.render.image_settings.quality = img_settings.get('quality', scene.render.image_settings.quality)
|
|
scene.render.image_settings.use_preview = img_settings.get('use_preview', scene.render.image_settings.use_preview)
|
|
|
|
# Scene/world settings
|
|
scene.use_nodes = original.get('use_nodes', scene.use_nodes)
|
|
|
|
# Compositing settings
|
|
scene.render.use_compositing = original.get('use_compositing', scene.render.use_compositing)
|
|
scene.render.use_sequencer = original.get('use_sequencer', scene.render.use_sequencer)
|
|
|
|
# Border and crop settings
|
|
scene.render.use_border = original.get('use_border', scene.render.use_border)
|
|
scene.render.border_min_x = original.get('border_min_x', scene.render.border_min_x)
|
|
scene.render.border_max_x = original.get('border_max_x', scene.render.border_max_x)
|
|
scene.render.border_min_y = original.get('border_min_y', scene.render.border_min_y)
|
|
scene.render.border_max_y = original.get('border_max_y', scene.render.border_max_y)
|
|
scene.render.use_crop_to_border = original.get('use_crop_to_border', scene.render.use_crop_to_border)
|
|
|
|
# Metadata settings - comprehensive
|
|
scene.render.use_stamp = original.get('use_stamp', scene.render.use_stamp)
|
|
scene.render.use_stamp_date = original.get('use_stamp_date', scene.render.use_stamp_date)
|
|
scene.render.use_stamp_time = original.get('use_stamp_time', scene.render.use_stamp_time)
|
|
scene.render.use_stamp_frame = original.get('use_stamp_frame', scene.render.use_stamp_frame)
|
|
scene.render.use_stamp_camera = original.get('use_stamp_camera', scene.render.use_stamp_camera)
|
|
scene.render.use_stamp_lens = original.get('use_stamp_lens', scene.render.use_stamp_lens)
|
|
scene.render.use_stamp_scene = original.get('use_stamp_scene', scene.render.use_stamp_scene)
|
|
scene.render.use_stamp_note = original.get('use_stamp_note', scene.render.use_stamp_note)
|
|
scene.render.stamp_note_text = original.get('stamp_note_text', scene.render.stamp_note_text)
|
|
scene.render.use_stamp_marker = original.get('use_stamp_marker', scene.render.use_stamp_marker)
|
|
scene.render.use_stamp_filename = original.get('use_stamp_filename', scene.render.use_stamp_filename)
|
|
scene.render.use_stamp_render_time = original.get('use_stamp_render_time', scene.render.use_stamp_render_time)
|
|
scene.render.use_stamp_memory = original.get('use_stamp_memory', scene.render.use_stamp_memory)
|
|
scene.render.use_stamp_hostname = original.get('use_stamp_hostname', scene.render.use_stamp_hostname)
|
|
scene.render.stamp_font_size = original.get('stamp_font_size', scene.render.stamp_font_size)
|
|
if 'stamp_foreground' in original:
|
|
scene.render.stamp_foreground = original['stamp_foreground']
|
|
if 'stamp_background' in original:
|
|
scene.render.stamp_background = original['stamp_background']
|
|
|
|
# SCENE.RENDER.FFMPEG - Restore FFmpeg settings
|
|
if 'ffmpeg' in original:
|
|
ffmpeg = original['ffmpeg']
|
|
scene.render.ffmpeg.format = ffmpeg.get('format', scene.render.ffmpeg.format)
|
|
scene.render.ffmpeg.codec = ffmpeg.get('codec', scene.render.ffmpeg.codec)
|
|
scene.render.ffmpeg.video_bitrate = ffmpeg.get('video_bitrate', scene.render.ffmpeg.video_bitrate)
|
|
scene.render.ffmpeg.minrate = ffmpeg.get('minrate', scene.render.ffmpeg.minrate)
|
|
scene.render.ffmpeg.maxrate = ffmpeg.get('maxrate', scene.render.ffmpeg.maxrate)
|
|
scene.render.ffmpeg.buffersize = ffmpeg.get('buffersize', scene.render.ffmpeg.buffersize)
|
|
scene.render.ffmpeg.muxrate = ffmpeg.get('muxrate', scene.render.ffmpeg.muxrate)
|
|
scene.render.ffmpeg.packetsize = ffmpeg.get('packetsize', scene.render.ffmpeg.packetsize)
|
|
scene.render.ffmpeg.constant_rate_factor = ffmpeg.get('constant_rate_factor', scene.render.ffmpeg.constant_rate_factor)
|
|
scene.render.ffmpeg.gopsize = ffmpeg.get('gopsize', scene.render.ffmpeg.gopsize)
|
|
scene.render.ffmpeg.audio_codec = ffmpeg.get('audio_codec', scene.render.ffmpeg.audio_codec)
|
|
scene.render.ffmpeg.audio_bitrate = ffmpeg.get('audio_bitrate', scene.render.ffmpeg.audio_bitrate)
|
|
scene.render.ffmpeg.audio_channels = ffmpeg.get('audio_channels', scene.render.ffmpeg.audio_channels)
|
|
scene.render.ffmpeg.audio_mixrate = ffmpeg.get('audio_mixrate', scene.render.ffmpeg.audio_mixrate)
|
|
scene.render.ffmpeg.audio_volume = ffmpeg.get('audio_volume', scene.render.ffmpeg.audio_volume)
|
|
|
|
# Restore world if it exists
|
|
if 'world' in original and original['world']:
|
|
if original['world'] in bpy.data.worlds:
|
|
scene.world = bpy.data.worlds[original['world']]
|
|
elif 'world' in original and not original['world']:
|
|
scene.world = None
|
|
|
|
# SCENE.CYCLES - Always restore Cycles settings if available
|
|
if 'cycles' in original and original['cycles']:
|
|
cycles_settings = original['cycles']
|
|
cycles = scene.cycles
|
|
print(f"Restoring ALL Cycles settings - samples: {cycles_settings.get('samples', 'unknown')}")
|
|
|
|
# Restore ALL Cycles settings comprehensively
|
|
cycles.device = cycles_settings.get('device', cycles.device)
|
|
safe_restore(cycles, 'feature_set', cycles_settings.get('feature_set', 'SUPPORTED'))
|
|
safe_restore(cycles, 'shading_system', cycles_settings.get('shading_system', 'SVM'))
|
|
cycles.samples = cycles_settings.get('samples', cycles.samples)
|
|
cycles.preview_samples = cycles_settings.get('preview_samples', cycles.preview_samples)
|
|
safe_restore(cycles, 'aa_samples', cycles_settings.get('aa_samples', 4))
|
|
safe_restore(cycles, 'preview_aa_samples', cycles_settings.get('preview_aa_samples', 4))
|
|
cycles.use_denoising = cycles_settings.get('use_denoising', cycles.use_denoising)
|
|
safe_restore(cycles, 'denoiser', cycles_settings.get('denoiser', 'OPENIMAGEDENOISE'))
|
|
safe_restore(cycles, 'denoising_input_passes', cycles_settings.get('denoising_input_passes', 'RGB_ALBEDO_NORMAL'))
|
|
safe_restore(cycles, 'use_denoising_input_passes', cycles_settings.get('use_denoising_input_passes', True))
|
|
safe_restore(cycles, 'denoising_prefilter', cycles_settings.get('denoising_prefilter', 'ACCURATE'))
|
|
cycles.use_adaptive_sampling = cycles_settings.get('use_adaptive_sampling', cycles.use_adaptive_sampling)
|
|
cycles.adaptive_threshold = cycles_settings.get('adaptive_threshold', cycles.adaptive_threshold)
|
|
cycles.adaptive_min_samples = cycles_settings.get('adaptive_min_samples', cycles.adaptive_min_samples)
|
|
safe_restore(cycles, 'time_limit', cycles_settings.get('time_limit', 0.0))
|
|
safe_restore(cycles, 'use_preview_adaptive_sampling', cycles_settings.get('use_preview_adaptive_sampling', False))
|
|
safe_restore(cycles, 'preview_adaptive_threshold', cycles_settings.get('preview_adaptive_threshold', 0.1))
|
|
safe_restore(cycles, 'preview_adaptive_min_samples', cycles_settings.get('preview_adaptive_min_samples', 0))
|
|
safe_restore(cycles, 'seed', cycles_settings.get('seed', 0))
|
|
safe_restore(cycles, 'use_animated_seed', cycles_settings.get('use_animated_seed', False))
|
|
safe_restore(cycles, 'sample_clamp_direct', cycles_settings.get('sample_clamp_direct', 0.0))
|
|
safe_restore(cycles, 'sample_clamp_indirect', cycles_settings.get('sample_clamp_indirect', 0.0))
|
|
cycles.light_sampling_threshold = cycles_settings.get('light_sampling_threshold', cycles.light_sampling_threshold)
|
|
safe_restore(cycles, 'sample_all_lights_direct', cycles_settings.get('sample_all_lights_direct', True))
|
|
safe_restore(cycles, 'sample_all_lights_indirect', cycles_settings.get('sample_all_lights_indirect', True))
|
|
cycles.max_bounces = cycles_settings.get('max_bounces', cycles.max_bounces)
|
|
cycles.diffuse_bounces = cycles_settings.get('diffuse_bounces', cycles.diffuse_bounces)
|
|
cycles.glossy_bounces = cycles_settings.get('glossy_bounces', cycles.glossy_bounces)
|
|
cycles.transmission_bounces = cycles_settings.get('transmission_bounces', cycles.transmission_bounces)
|
|
cycles.volume_bounces = cycles_settings.get('volume_bounces', cycles.volume_bounces)
|
|
safe_restore(cycles, 'transparent_max_bounces', cycles_settings.get('transparent_max_bounces', 8))
|
|
cycles.caustics_reflective = cycles_settings.get('caustics_reflective', cycles.caustics_reflective)
|
|
cycles.caustics_refractive = cycles_settings.get('caustics_refractive', cycles.caustics_refractive)
|
|
safe_restore(cycles, 'filter_type', cycles_settings.get('filter_type', 'GAUSSIAN'))
|
|
safe_restore(cycles, 'filter_width', cycles_settings.get('filter_width', 1.5))
|
|
cycles.pixel_filter_width = cycles_settings.get('pixel_filter_width', cycles.pixel_filter_width)
|
|
cycles.use_persistent_data = cycles_settings.get('use_persistent_data', cycles.use_persistent_data)
|
|
safe_restore(cycles, 'debug_use_spatial_splits', cycles_settings.get('debug_use_spatial_splits', False))
|
|
safe_restore(cycles, 'debug_use_hair_bvh', cycles_settings.get('debug_use_hair_bvh', True))
|
|
safe_restore(cycles, 'debug_bvh_type', cycles_settings.get('debug_bvh_type', 'DYNAMIC_BVH'))
|
|
safe_restore(cycles, 'debug_use_compact_bvh', cycles_settings.get('debug_use_compact_bvh', True))
|
|
safe_restore(cycles, 'tile_size', cycles_settings.get('tile_size', 256))
|
|
safe_restore(cycles, 'use_auto_tile', cycles_settings.get('use_auto_tile', False))
|
|
safe_restore(cycles, 'progressive', cycles_settings.get('progressive', 'PATH'))
|
|
safe_restore(cycles, 'use_square_samples', cycles_settings.get('use_square_samples', False))
|
|
safe_restore(cycles, 'blur_glossy', cycles_settings.get('blur_glossy', 0.0))
|
|
safe_restore(cycles, 'use_transparent_shadows', cycles_settings.get('use_transparent_shadows', True))
|
|
safe_restore(cycles, 'volume_step_rate', cycles_settings.get('volume_step_rate', 1.0))
|
|
safe_restore(cycles, 'volume_preview_step_rate', cycles_settings.get('volume_preview_step_rate', 1.0))
|
|
safe_restore(cycles, 'volume_max_steps', cycles_settings.get('volume_max_steps', 1024))
|
|
|
|
print(f"ALL Cycles settings restoration completed")
|
|
|
|
# Clear the stored settings
|
|
props.original_settings = ""
|
|
print("Comprehensive settings restoration completed")
|
|
|
|
except Exception as e:
|
|
print(f"Error restoring comprehensive settings: {e}")
|
|
|
|
# Force a redraw to ensure viewport updates
|
|
for area in context.screen.areas:
|
|
area.tag_redraw()
|
|
|
|
# View Playblast Operator
|
|
class BPL_OT_view_playblast(Operator):
|
|
bl_idname = "bpl.view_playblast"
|
|
bl_label = "View Playblast"
|
|
bl_description = "Play back rendered Playblast"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# Check if we have a playblast file
|
|
if not props.last_playblast_file or not os.path.exists(props.last_playblast_file):
|
|
self.report({'ERROR'}, "No playblast file found")
|
|
return {'CANCELLED'}
|
|
|
|
# Get the file path
|
|
filepath = props.last_playblast_file
|
|
|
|
# Report which file we're playing
|
|
self.report({'INFO'}, f"Opening playblast externally: {os.path.basename(filepath)}")
|
|
|
|
# Open the file with the default system application
|
|
try:
|
|
if sys.platform == 'win32':
|
|
os.startfile(filepath)
|
|
elif sys.platform == 'darwin': # macOS
|
|
subprocess.call(('open', filepath))
|
|
else: # Linux and other Unix-like
|
|
subprocess.call(('xdg-open', filepath))
|
|
return {'FINISHED'}
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Failed to open playblast: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
# View Latest Playblast Operator
|
|
class BPL_OT_view_latest_playblast(Operator):
|
|
bl_idname = "bpl.view_latest_playblast"
|
|
bl_label = "View Latest"
|
|
bl_description = "Play back the most recent playblast"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# Try to find the latest file in temp directory first
|
|
temp_dir = os.path.join(tempfile.gettempdir(), "basedplayblast")
|
|
latest_filepath = None
|
|
|
|
# Check all possible video formats
|
|
for format_name in ['MPEG4', 'QUICKTIME', 'AVI', 'MKV']:
|
|
file_ext = get_file_extension(format_name)
|
|
latest_filename = os.path.join(temp_dir, f"blast_latest{file_ext}")
|
|
|
|
if os.path.exists(latest_filename):
|
|
latest_filepath = latest_filename
|
|
break
|
|
|
|
# If no latest file found, try the last playblast file
|
|
if not latest_filepath and props.last_playblast_file and os.path.exists(props.last_playblast_file):
|
|
latest_filepath = props.last_playblast_file
|
|
|
|
if not latest_filepath:
|
|
self.report({'ERROR'}, "No recent playblast found")
|
|
return {'CANCELLED'}
|
|
|
|
# Report which file we're playing
|
|
self.report({'INFO'}, f"Opening playblast externally: {os.path.basename(latest_filepath)}")
|
|
|
|
# Open the file with the default system application
|
|
try:
|
|
if sys.platform == 'win32':
|
|
os.startfile(latest_filepath)
|
|
elif sys.platform == 'darwin': # macOS
|
|
subprocess.call(('open', latest_filepath))
|
|
else: # Linux and other Unix-like
|
|
subprocess.call(('xdg-open', latest_filepath))
|
|
return {'FINISHED'}
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Failed to open playblast: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
# Operator to sync output path with Blender's render output path
|
|
class BPL_OT_sync_output_path(Operator):
|
|
bl_idname = "bpl.sync_output_path"
|
|
bl_label = "Sync Output Path"
|
|
bl_description = "Use Blender's render output path"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
# Get Blender's render output path
|
|
blender_output_path = bpy.path.abspath(scene.render.filepath)
|
|
|
|
# If it's a file path, extract the directory
|
|
if os.path.isfile(blender_output_path) or '.' in os.path.basename(blender_output_path):
|
|
blender_output_path = os.path.dirname(blender_output_path)
|
|
|
|
# Ensure it ends with a separator
|
|
if not blender_output_path.endswith(os.sep):
|
|
blender_output_path += os.sep
|
|
|
|
# Set the BasedPlayblast output path
|
|
scene.basedplayblast.output_path = blender_output_path
|
|
|
|
# Clear the last playblast file paths since we're changing the output path
|
|
scene.basedplayblast.last_playblast_file = ""
|
|
|
|
self.report({'INFO'}, f"Output path synced with Blender's render output path")
|
|
return {'FINISHED'}
|
|
|
|
# Operator to sync file name with Blender's output file name
|
|
class BPL_OT_sync_file_name(Operator):
|
|
bl_idname = "bpl.sync_file_name"
|
|
bl_label = "Sync File Name"
|
|
bl_description = "Use Blender's output file name"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
# Get Blender's render output path
|
|
blender_output_path = bpy.path.abspath(scene.render.filepath)
|
|
|
|
# Extract the file name without extension
|
|
file_name = os.path.basename(blender_output_path)
|
|
|
|
# Remove frame number pattern if present
|
|
if '#' in file_name:
|
|
file_name = file_name.split('#')[0]
|
|
|
|
# Remove extension if present
|
|
file_name = os.path.splitext(file_name)[0]
|
|
|
|
# If file_name is empty, use a default
|
|
if not file_name:
|
|
file_name = "blast_"
|
|
else:
|
|
# Add the blast_ prefix if it's not already there
|
|
if not file_name.startswith("blast_"):
|
|
file_name = "blast_" + file_name
|
|
|
|
# Set the BasedPlayblast file name
|
|
scene.basedplayblast.file_name = file_name
|
|
|
|
# Clear the last playblast file paths since we're changing the filename
|
|
scene.basedplayblast.last_playblast_file = ""
|
|
|
|
self.report({'INFO'}, f"File name synced with Blender's output file name")
|
|
return {'FINISHED'}
|
|
|
|
# New operator to apply user defaults
|
|
class BPL_OT_apply_user_defaults(Operator):
|
|
bl_idname = "bpl.apply_user_defaults"
|
|
bl_label = "Apply User Defaults"
|
|
bl_description = "Apply the user's default settings from Add-on Preferences to the current scene"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
prefs = context.preferences.addons[__name__].preferences
|
|
props = context.scene.basedplayblast
|
|
|
|
props.video_quality = prefs.default_video_quality
|
|
props.use_custom_ffmpeg_args = prefs.default_use_custom_ffmpeg_args
|
|
props.custom_ffmpeg_args = prefs.default_ffmpeg_args
|
|
|
|
self.report({'INFO'}, "User defaults applied to scene.")
|
|
return {'FINISHED'}
|
|
|
|
# New operator to apply blast render settings
|
|
class BPL_OT_apply_blast_settings(Operator):
|
|
bl_idname = "bpl.apply_blast_settings"
|
|
bl_label = "Apply Blast Render Settings"
|
|
bl_description = "Apply Playblast render settings to the scene without rendering"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
# We need os, sys and json in this scope where they're used
|
|
import os
|
|
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# First, save ALL original settings - always save fresh settings each time
|
|
# Clear any previously saved settings to ensure we get current state
|
|
props.original_settings = ""
|
|
props.original_settings_extended = ""
|
|
|
|
# TEMPORARY TEST: Set a minimal test setting to verify restore works
|
|
import json
|
|
test_settings = {
|
|
'render_engine': scene.render.engine,
|
|
'cycles': {
|
|
'samples': getattr(scene.cycles, 'samples', 128),
|
|
'use_denoising': getattr(scene.cycles, 'use_denoising', True)
|
|
}
|
|
}
|
|
props.original_settings = json.dumps(test_settings)
|
|
print(f"TEMP TEST: Set minimal test settings - engine: {test_settings['render_engine']}, cycles samples: {test_settings['cycles']['samples']}")
|
|
|
|
import json
|
|
|
|
# COMPREHENSIVE SETTINGS STORAGE - Save EVERYTHING
|
|
print(f"Saving comprehensive render settings for engine: {scene.render.engine}")
|
|
print(f"DEBUG: Starting comprehensive settings save process")
|
|
|
|
def safe_getattr(obj, attr, default=None):
|
|
"""Safely get attribute with fallback"""
|
|
try:
|
|
return getattr(obj, attr, default)
|
|
except:
|
|
return default
|
|
|
|
def make_json_serializable(obj):
|
|
"""Convert object to JSON-serializable format"""
|
|
if isinstance(obj, dict):
|
|
# Handle dictionaries - recursively process values
|
|
return {key: make_json_serializable(value) for key, value in obj.items()}
|
|
elif isinstance(obj, (list, tuple)):
|
|
# Handle lists and tuples
|
|
return [make_json_serializable(item) for item in obj]
|
|
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
# Already JSON serializable
|
|
return obj
|
|
else:
|
|
# Convert everything else to string
|
|
try:
|
|
json.dumps(obj) # Test if it's serializable
|
|
return obj
|
|
except:
|
|
return str(obj)
|
|
|
|
original_settings = {
|
|
# SCENE.RENDER - Complete render settings
|
|
'render_engine': scene.render.engine,
|
|
'filepath': scene.render.filepath,
|
|
'resolution_x': scene.render.resolution_x,
|
|
'resolution_y': scene.render.resolution_y,
|
|
'resolution_percentage': scene.render.resolution_percentage,
|
|
'pixel_aspect_x': scene.render.pixel_aspect_x,
|
|
'pixel_aspect_y': scene.render.pixel_aspect_y,
|
|
'use_file_extension': scene.render.use_file_extension,
|
|
'use_overwrite': scene.render.use_overwrite,
|
|
'use_placeholder': scene.render.use_placeholder,
|
|
'frame_start': scene.frame_start,
|
|
'frame_end': scene.frame_end,
|
|
'frame_step': scene.frame_step,
|
|
'frame_current': scene.frame_current,
|
|
|
|
# Film settings
|
|
'film_transparent': scene.render.film_transparent,
|
|
'filter_size': scene.render.filter_size,
|
|
|
|
# Performance settings
|
|
'use_persistent_data': scene.render.use_persistent_data,
|
|
'use_simplify': scene.render.use_simplify,
|
|
'simplify_subdivision': scene.render.simplify_subdivision,
|
|
'simplify_child_particles': scene.render.simplify_child_particles,
|
|
'simplify_volumes': scene.render.simplify_volumes,
|
|
'simplify_subdivision_render': safe_getattr(scene.render, 'simplify_subdivision_render', 6),
|
|
'simplify_child_particles_render': safe_getattr(scene.render, 'simplify_child_particles_render', 1.0),
|
|
'simplify_volumes_render': safe_getattr(scene.render, 'simplify_volumes_render', 1.0),
|
|
|
|
# Motion blur
|
|
'use_motion_blur': scene.render.use_motion_blur,
|
|
'motion_blur_shutter': scene.render.motion_blur_shutter,
|
|
'motion_blur_shutter_curve': str(safe_getattr(scene.render, 'motion_blur_shutter_curve', 'AUTO')),
|
|
'rolling_shutter_type': safe_getattr(scene.render, 'rolling_shutter_type', 'NONE'),
|
|
'rolling_shutter_duration': safe_getattr(scene.render, 'rolling_shutter_duration', 0.1),
|
|
|
|
# Threading
|
|
'threads_mode': scene.render.threads_mode,
|
|
'threads': scene.render.threads,
|
|
|
|
# Memory and caching
|
|
'tile_x': safe_getattr(scene.render, 'tile_x', 64),
|
|
'tile_y': safe_getattr(scene.render, 'tile_y', 64),
|
|
'use_save_buffers': safe_getattr(scene.render, 'use_save_buffers', False),
|
|
|
|
# Preview and display
|
|
'display_mode': context.preferences.view.render_display_type,
|
|
'preview_pixel_size': safe_getattr(scene.render, 'preview_pixel_size', 'AUTO'),
|
|
|
|
# SCENE.RENDER.IMAGE_SETTINGS - Complete image settings
|
|
'image_settings': {
|
|
'file_format': scene.render.image_settings.file_format,
|
|
'color_mode': scene.render.image_settings.color_mode,
|
|
'color_depth': scene.render.image_settings.color_depth,
|
|
'compression': scene.render.image_settings.compression,
|
|
'quality': scene.render.image_settings.quality,
|
|
'use_preview': scene.render.image_settings.use_preview,
|
|
'exr_codec': safe_getattr(scene.render.image_settings, 'exr_codec', 'ZIP'),
|
|
'use_zbuffer': safe_getattr(scene.render.image_settings, 'use_zbuffer', False),
|
|
'jpeg2k_codec': safe_getattr(scene.render.image_settings, 'jpeg2k_codec', 'JP2'),
|
|
'tiff_codec': safe_getattr(scene.render.image_settings, 'tiff_codec', 'DEFLATE'),
|
|
},
|
|
|
|
# SCENE.RENDER.FFMPEG - Complete FFmpeg settings
|
|
'ffmpeg': {
|
|
'format': scene.render.ffmpeg.format,
|
|
'codec': scene.render.ffmpeg.codec,
|
|
'video_bitrate': scene.render.ffmpeg.video_bitrate,
|
|
'minrate': scene.render.ffmpeg.minrate,
|
|
'maxrate': scene.render.ffmpeg.maxrate,
|
|
'buffersize': scene.render.ffmpeg.buffersize,
|
|
'muxrate': scene.render.ffmpeg.muxrate,
|
|
'packetsize': scene.render.ffmpeg.packetsize,
|
|
'constant_rate_factor': scene.render.ffmpeg.constant_rate_factor,
|
|
'gopsize': scene.render.ffmpeg.gopsize,
|
|
'use_max_b_frames': safe_getattr(scene.render.ffmpeg, 'use_max_b_frames', False),
|
|
'max_b_frames': safe_getattr(scene.render.ffmpeg, 'max_b_frames', 2),
|
|
'use_autosplit': safe_getattr(scene.render.ffmpeg, 'use_autosplit', False),
|
|
'autosplit_size': safe_getattr(scene.render.ffmpeg, 'autosplit_size', 2048),
|
|
'audio_codec': scene.render.ffmpeg.audio_codec,
|
|
'audio_bitrate': scene.render.ffmpeg.audio_bitrate,
|
|
'audio_channels': scene.render.ffmpeg.audio_channels,
|
|
'audio_mixrate': scene.render.ffmpeg.audio_mixrate,
|
|
'audio_volume': scene.render.ffmpeg.audio_volume,
|
|
},
|
|
|
|
# Scene/world settings
|
|
'world': scene.world.name if scene.world else "",
|
|
'use_nodes': scene.use_nodes,
|
|
|
|
# Compositing settings
|
|
'use_compositing': scene.render.use_compositing,
|
|
'use_sequencer': scene.render.use_sequencer,
|
|
|
|
# Border and crop settings
|
|
'use_border': scene.render.use_border,
|
|
'border_min_x': scene.render.border_min_x,
|
|
'border_max_x': scene.render.border_max_x,
|
|
'border_min_y': scene.render.border_min_y,
|
|
'border_max_y': scene.render.border_max_y,
|
|
'use_crop_to_border': scene.render.use_crop_to_border,
|
|
|
|
# Metadata settings - comprehensive
|
|
'use_stamp': scene.render.use_stamp,
|
|
'use_stamp_date': scene.render.use_stamp_date,
|
|
'use_stamp_time': scene.render.use_stamp_time,
|
|
'use_stamp_frame': scene.render.use_stamp_frame,
|
|
'use_stamp_camera': scene.render.use_stamp_camera,
|
|
'use_stamp_lens': scene.render.use_stamp_lens,
|
|
'use_stamp_scene': scene.render.use_stamp_scene,
|
|
'use_stamp_note': scene.render.use_stamp_note,
|
|
'stamp_note_text': scene.render.stamp_note_text,
|
|
'use_stamp_marker': scene.render.use_stamp_marker,
|
|
'use_stamp_filename': scene.render.use_stamp_filename,
|
|
'use_stamp_render_time': scene.render.use_stamp_render_time,
|
|
'use_stamp_memory': scene.render.use_stamp_memory,
|
|
'use_stamp_hostname': scene.render.use_stamp_hostname,
|
|
'stamp_font_size': scene.render.stamp_font_size,
|
|
'stamp_foreground': [float(x) for x in scene.render.stamp_foreground] if hasattr(scene.render.stamp_foreground, '__iter__') else [1.0, 1.0, 1.0, 1.0],
|
|
'stamp_background': [float(x) for x in scene.render.stamp_background] if hasattr(scene.render.stamp_background, '__iter__') else [0.0, 0.0, 0.0, 0.8],
|
|
|
|
# Hair settings
|
|
'hair_type': safe_getattr(scene.render, 'hair_type', 'PATH'),
|
|
'hair_subdiv': safe_getattr(scene.render, 'hair_subdiv', 3),
|
|
}
|
|
|
|
# SCENE.CYCLES - Always save Cycles settings regardless of current engine
|
|
print(f"DEBUG: About to start Cycles saving section")
|
|
try:
|
|
cycles = scene.cycles
|
|
print(f"Attempting to save Cycles settings...")
|
|
original_settings['cycles'] = {
|
|
'device': safe_getattr(cycles, 'device', 'CPU'),
|
|
'feature_set': safe_getattr(cycles, 'feature_set', 'SUPPORTED'),
|
|
'shading_system': safe_getattr(cycles, 'shading_system', 'SVM'),
|
|
'samples': safe_getattr(cycles, 'samples', 128),
|
|
'preview_samples': safe_getattr(cycles, 'preview_samples', 32),
|
|
'aa_samples': safe_getattr(cycles, 'aa_samples', 4),
|
|
'preview_aa_samples': safe_getattr(cycles, 'preview_aa_samples', 4),
|
|
'use_denoising': safe_getattr(cycles, 'use_denoising', True),
|
|
'denoiser': safe_getattr(cycles, 'denoiser', 'OPENIMAGEDENOISE'),
|
|
'denoising_input_passes': safe_getattr(cycles, 'denoising_input_passes', 'RGB_ALBEDO_NORMAL'),
|
|
'use_denoising_input_passes': safe_getattr(cycles, 'use_denoising_input_passes', True),
|
|
'denoising_prefilter': safe_getattr(cycles, 'denoising_prefilter', 'ACCURATE'),
|
|
'use_adaptive_sampling': safe_getattr(cycles, 'use_adaptive_sampling', True),
|
|
'adaptive_threshold': safe_getattr(cycles, 'adaptive_threshold', 0.01),
|
|
'adaptive_min_samples': safe_getattr(cycles, 'adaptive_min_samples', 0),
|
|
'time_limit': safe_getattr(cycles, 'time_limit', 0.0),
|
|
'use_preview_adaptive_sampling': safe_getattr(cycles, 'use_preview_adaptive_sampling', False),
|
|
'preview_adaptive_threshold': safe_getattr(cycles, 'preview_adaptive_threshold', 0.1),
|
|
'preview_adaptive_min_samples': safe_getattr(cycles, 'preview_adaptive_min_samples', 0),
|
|
'seed': safe_getattr(cycles, 'seed', 0),
|
|
'use_animated_seed': safe_getattr(cycles, 'use_animated_seed', False),
|
|
'sample_clamp_direct': safe_getattr(cycles, 'sample_clamp_direct', 0.0),
|
|
'sample_clamp_indirect': safe_getattr(cycles, 'sample_clamp_indirect', 0.0),
|
|
'light_sampling_threshold': safe_getattr(cycles, 'light_sampling_threshold', 0.01),
|
|
'sample_all_lights_direct': safe_getattr(cycles, 'sample_all_lights_direct', True),
|
|
'sample_all_lights_indirect': safe_getattr(cycles, 'sample_all_lights_indirect', True),
|
|
'max_bounces': safe_getattr(cycles, 'max_bounces', 12),
|
|
'diffuse_bounces': safe_getattr(cycles, 'diffuse_bounces', 4),
|
|
'glossy_bounces': safe_getattr(cycles, 'glossy_bounces', 4),
|
|
'transmission_bounces': safe_getattr(cycles, 'transmission_bounces', 12),
|
|
'volume_bounces': safe_getattr(cycles, 'volume_bounces', 0),
|
|
'transparent_max_bounces': safe_getattr(cycles, 'transparent_max_bounces', 8),
|
|
'caustics_reflective': safe_getattr(cycles, 'caustics_reflective', True),
|
|
'caustics_refractive': safe_getattr(cycles, 'caustics_refractive', True),
|
|
'filter_type': safe_getattr(cycles, 'filter_type', 'GAUSSIAN'),
|
|
'filter_width': safe_getattr(cycles, 'filter_width', 1.5),
|
|
'pixel_filter_width': safe_getattr(cycles, 'pixel_filter_width', 1.5),
|
|
'use_persistent_data': safe_getattr(cycles, 'use_persistent_data', False),
|
|
'debug_use_spatial_splits': safe_getattr(cycles, 'debug_use_spatial_splits', False),
|
|
'debug_use_hair_bvh': safe_getattr(cycles, 'debug_use_hair_bvh', True),
|
|
'debug_bvh_type': safe_getattr(cycles, 'debug_bvh_type', 'DYNAMIC_BVH'),
|
|
'debug_use_compact_bvh': safe_getattr(cycles, 'debug_use_compact_bvh', True),
|
|
'tile_size': safe_getattr(cycles, 'tile_size', 256),
|
|
'use_auto_tile': safe_getattr(cycles, 'use_auto_tile', False),
|
|
'progressive': safe_getattr(cycles, 'progressive', 'PATH'),
|
|
'use_square_samples': safe_getattr(cycles, 'use_square_samples', False),
|
|
'blur_glossy': safe_getattr(cycles, 'blur_glossy', 0.0),
|
|
'use_transparent_shadows': safe_getattr(cycles, 'use_transparent_shadows', True),
|
|
'volume_step_rate': safe_getattr(cycles, 'volume_step_rate', 1.0),
|
|
'volume_preview_step_rate': safe_getattr(cycles, 'volume_preview_step_rate', 1.0),
|
|
'volume_max_steps': safe_getattr(cycles, 'volume_max_steps', 1024),
|
|
}
|
|
print(f"Successfully saved Cycles settings with {len(original_settings['cycles'])} keys")
|
|
except Exception as e:
|
|
print(f"Could not save Cycles settings: {e}")
|
|
original_settings['cycles'] = {}
|
|
print(f"Set empty Cycles settings due to error")
|
|
|
|
# SCENE.EEVEE - Always save EEVEE settings regardless of current engine
|
|
try:
|
|
eevee_attr = 'eevee' if hasattr(scene, 'eevee') else 'eevee_next'
|
|
eevee = getattr(scene, eevee_attr) if hasattr(scene, eevee_attr) else None
|
|
if eevee:
|
|
original_settings['eevee'] = {
|
|
'taa_render_samples': safe_getattr(eevee, 'taa_render_samples', 64),
|
|
'taa_samples': safe_getattr(eevee, 'taa_samples', 16),
|
|
'use_bloom': safe_getattr(eevee, 'use_bloom', False),
|
|
'bloom_threshold': safe_getattr(eevee, 'bloom_threshold', 0.8),
|
|
'bloom_knee': safe_getattr(eevee, 'bloom_knee', 0.5),
|
|
'bloom_radius': safe_getattr(eevee, 'bloom_radius', 6.5),
|
|
'bloom_intensity': safe_getattr(eevee, 'bloom_intensity', 0.05),
|
|
'use_ssr': safe_getattr(eevee, 'use_ssr', False),
|
|
'use_ssr_refraction': safe_getattr(eevee, 'use_ssr_refraction', False),
|
|
'ssr_max_roughness': safe_getattr(eevee, 'ssr_max_roughness', 0.5),
|
|
'ssr_thickness': safe_getattr(eevee, 'ssr_thickness', 0.2),
|
|
'ssr_border_fade': safe_getattr(eevee, 'ssr_border_fade', 0.075),
|
|
'ssr_firefly_fac': safe_getattr(eevee, 'ssr_firefly_fac', 10.0),
|
|
'use_motion_blur': safe_getattr(eevee, 'use_motion_blur', False),
|
|
'motion_blur_samples': safe_getattr(eevee, 'motion_blur_samples', 8),
|
|
'motion_blur_shutter': safe_getattr(eevee, 'motion_blur_shutter', 0.5),
|
|
'use_volumetric_lights': safe_getattr(eevee, 'use_volumetric_lights', False),
|
|
'volumetric_start': safe_getattr(eevee, 'volumetric_start', 0.1),
|
|
'volumetric_end': safe_getattr(eevee, 'volumetric_end', 100.0),
|
|
'volumetric_tile_size': safe_getattr(eevee, 'volumetric_tile_size', '8'),
|
|
'volumetric_samples': safe_getattr(eevee, 'volumetric_samples', 64),
|
|
'volumetric_sample_distribution': safe_getattr(eevee, 'volumetric_sample_distribution', 0.8),
|
|
'use_volumetric_shadows': safe_getattr(eevee, 'use_volumetric_shadows', False),
|
|
'volumetric_shadow_samples': safe_getattr(eevee, 'volumetric_shadow_samples', 16),
|
|
'gi_diffuse_bounces': safe_getattr(eevee, 'gi_diffuse_bounces', 3),
|
|
'gi_cubemap_resolution': safe_getattr(eevee, 'gi_cubemap_resolution', '512'),
|
|
'gi_visibility_resolution': safe_getattr(eevee, 'gi_visibility_resolution', '16'),
|
|
'gi_irradiance_smoothing': safe_getattr(eevee, 'gi_irradiance_smoothing', 0.1),
|
|
'gi_glossy_clamp': safe_getattr(eevee, 'gi_glossy_clamp', 0.0),
|
|
'gi_filter_quality': safe_getattr(eevee, 'gi_filter_quality', 1.0),
|
|
'use_persistent_data': safe_getattr(eevee, 'use_persistent_data', False),
|
|
'shadow_cube_size': safe_getattr(eevee, 'shadow_cube_size', '512'),
|
|
'shadow_cascade_size': safe_getattr(eevee, 'shadow_cascade_size', '1024'),
|
|
'use_shadow_high_bitdepth': safe_getattr(eevee, 'use_shadow_high_bitdepth', False),
|
|
'use_soft_shadows': safe_getattr(eevee, 'use_soft_shadows', True),
|
|
'use_shadows': safe_getattr(eevee, 'use_shadows', True),
|
|
'light_threshold': safe_getattr(eevee, 'light_threshold', 0.01),
|
|
'use_gtao': safe_getattr(eevee, 'use_gtao', False),
|
|
'gtao_distance': safe_getattr(eevee, 'gtao_distance', 0.2),
|
|
'gtao_factor': safe_getattr(eevee, 'gtao_factor', 1.0),
|
|
'gtao_quality': safe_getattr(eevee, 'gtao_quality', 0.25),
|
|
'use_overscan': safe_getattr(eevee, 'use_overscan', False),
|
|
'overscan_size': safe_getattr(eevee, 'overscan_size', 3.0),
|
|
'shadow_ray_count': safe_getattr(eevee, 'shadow_ray_count', 1),
|
|
'shadow_step_count': safe_getattr(eevee, 'shadow_step_count', 6),
|
|
'fast_gi_method': safe_getattr(eevee, 'fast_gi_method', 'GLOBAL_ILLUMINATION'),
|
|
'fast_gi_ray_count': safe_getattr(eevee, 'fast_gi_ray_count', 4),
|
|
'fast_gi_step_count': safe_getattr(eevee, 'fast_gi_step_count', 4),
|
|
'fast_gi_quality': safe_getattr(eevee, 'fast_gi_quality', 0.25),
|
|
'fast_gi_distance': safe_getattr(eevee, 'fast_gi_distance', 10.0),
|
|
}
|
|
print("Saved EEVEE settings")
|
|
else:
|
|
original_settings['eevee'] = {}
|
|
except Exception as e:
|
|
print(f"Could not save EEVEE settings: {e}")
|
|
original_settings['eevee'] = {}
|
|
|
|
# SCENE.DISPLAY (WORKBENCH) settings
|
|
try:
|
|
original_settings['workbench'] = {
|
|
'shading_type': scene.display.shading.type,
|
|
'light': scene.display.shading.light,
|
|
'color_type': scene.display.shading.color_type,
|
|
'single_color': list(safe_getattr(scene.display.shading, 'single_color', (0.8, 0.8, 0.8))),
|
|
'background_type': safe_getattr(scene.display.shading, 'background_type', 'THEME'),
|
|
'background_color': list(safe_getattr(scene.display.shading, 'background_color', (0.05, 0.05, 0.05))),
|
|
'cavity_ridge_factor': safe_getattr(scene.display.shading, 'cavity_ridge_factor', 1.0),
|
|
'cavity_valley_factor': safe_getattr(scene.display.shading, 'cavity_valley_factor', 1.0),
|
|
'curvature_ridge_factor': safe_getattr(scene.display.shading, 'curvature_ridge_factor', 1.0),
|
|
'curvature_valley_factor': safe_getattr(scene.display.shading, 'curvature_valley_factor', 1.0),
|
|
'render_aa': safe_getattr(scene.display, 'render_aa', 'FXAA'),
|
|
'show_cavity': safe_getattr(scene.display.shading, 'show_cavity', False),
|
|
'show_object_outline': safe_getattr(scene.display.shading, 'show_object_outline', False),
|
|
'show_specular_highlight': safe_getattr(scene.display.shading, 'show_specular_highlight', True),
|
|
'use_dof': safe_getattr(scene.display.shading, 'use_dof', False),
|
|
'show_xray': safe_getattr(scene.display.shading, 'show_xray', False),
|
|
'xray_alpha': safe_getattr(scene.display.shading, 'xray_alpha', 0.5),
|
|
'show_shadows': safe_getattr(scene.display.shading, 'show_shadows', False),
|
|
'shadow_intensity': safe_getattr(scene.display.shading, 'shadow_intensity', 0.5),
|
|
'studio_light': safe_getattr(scene.display.shading, 'studio_light', 'DEFAULT'),
|
|
'studiolight_rotate_z': safe_getattr(scene.display.shading, 'studiolight_rotate_z', 0.0),
|
|
'studiolight_intensity': safe_getattr(scene.display.shading, 'studiolight_intensity', 1.0),
|
|
'studiolight_background_alpha': safe_getattr(scene.display.shading, 'studiolight_background_alpha', 0.0),
|
|
'studiolight_background_blur': safe_getattr(scene.display.shading, 'studiolight_background_blur', 0.0),
|
|
}
|
|
print("Saved Workbench settings")
|
|
except Exception as e:
|
|
print(f"Could not save Workbench settings: {e}")
|
|
original_settings['workbench'] = {}
|
|
|
|
# Try to save the settings with detailed error reporting
|
|
try:
|
|
# Make sure all objects are JSON serializable
|
|
safe_settings = make_json_serializable(original_settings)
|
|
props.original_settings = json.dumps(safe_settings)
|
|
print(f"Comprehensive settings saved to JSON ({len(props.original_settings)} characters)")
|
|
print(f"Saved settings include: {list(original_settings.keys())}")
|
|
print(f"Cycles settings saved: {'cycles' in original_settings and bool(original_settings['cycles'])}")
|
|
if 'cycles' in original_settings:
|
|
print(f"Cycles settings keys: {list(original_settings['cycles'].keys())}")
|
|
print(f"EEVEE settings saved: {'eevee' in original_settings and bool(original_settings['eevee'])}")
|
|
except Exception as json_error:
|
|
print(f"ERROR: Failed to save settings to JSON: {str(json_error)}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
# Don't clear the test settings - keep them so restore works
|
|
if not props.original_settings:
|
|
print(f"FALLBACK: Using minimal test settings since comprehensive save failed")
|
|
else:
|
|
print(f"KEEPING existing settings since JSON save failed")
|
|
|
|
try:
|
|
# Apply render engine and settings based on display mode
|
|
if props.display_mode == 'RENDERED':
|
|
# For rendered preview, we'll optimize the render settings
|
|
# while keeping the scene's chosen render engine
|
|
try:
|
|
# Store current render engine to report later
|
|
current_engine = scene.render.engine
|
|
print(f"Using existing render engine: {current_engine}")
|
|
|
|
# Apply engine-specific optimizations
|
|
if current_engine == 'BLENDER_EEVEE' or current_engine == 'BLENDER_EEVEE_NEXT':
|
|
# Apply EEVEE-specific optimizations for faster rendering
|
|
eevee_attr = 'eevee' if hasattr(scene, 'eevee') else 'eevee_next'
|
|
eevee = getattr(scene, eevee_attr) if hasattr(scene, eevee_attr) else None
|
|
|
|
if eevee:
|
|
# Set minimal acceptable quality
|
|
if hasattr(eevee, 'taa_render_samples'):
|
|
eevee.taa_render_samples = 4 # Balance between quality and speed for final render
|
|
print(f"Set render samples to 4 for RENDERED mode")
|
|
|
|
# Minimal shadow settings - but keep shadows for realism
|
|
if hasattr(eevee, 'shadow_cube_size'):
|
|
eevee.shadow_cube_size = '512' # Medium shadow resolution
|
|
if hasattr(eevee, 'use_soft_shadows'):
|
|
eevee.use_soft_shadows = True # Keep soft shadows for realism
|
|
|
|
# Disable expensive effects
|
|
if hasattr(eevee, 'use_bloom'):
|
|
eevee.use_bloom = False
|
|
if hasattr(eevee, 'use_ssr'):
|
|
eevee.use_ssr = False
|
|
if hasattr(eevee, 'use_motion_blur'):
|
|
eevee.use_motion_blur = False
|
|
if hasattr(eevee, 'use_volumetric_lights'):
|
|
eevee.use_volumetric_lights = False
|
|
|
|
# Use moderate global illumination
|
|
if hasattr(eevee, 'gi_diffuse_bounces'):
|
|
eevee.gi_diffuse_bounces = 1 # Just one bounce for indirect lighting
|
|
|
|
# Set minimal ray and step settings for maximum performance
|
|
if hasattr(eevee, 'gi_irradiance_smoothing'):
|
|
eevee.gi_irradiance_smoothing = 0.1 # Minimal smoothing
|
|
if hasattr(eevee, 'gi_glossy_clamp'):
|
|
eevee.gi_glossy_clamp = 0.0 # No clamping
|
|
|
|
# Set raytracing settings to minimum (1 ray, 2 steps)
|
|
if hasattr(eevee, 'ssr_max_roughness'):
|
|
eevee.ssr_max_roughness = 0.5 # Limit SSR roughness
|
|
if hasattr(eevee, 'ssr_thickness'):
|
|
eevee.ssr_thickness = 0.2 # Thin SSR thickness
|
|
if hasattr(eevee, 'ssr_border_fade'):
|
|
eevee.ssr_border_fade = 0.075 # Minimal border fade
|
|
if hasattr(eevee, 'ssr_firefly_fac'):
|
|
eevee.ssr_firefly_fac = 10.0 # Standard firefly suppression
|
|
|
|
# Set shadow raytracing to minimal (1 ray, 2 steps)
|
|
if hasattr(eevee, 'shadow_ray_count'):
|
|
eevee.shadow_ray_count = 1 # 1 ray for shadows
|
|
if hasattr(eevee, 'shadow_step_count'):
|
|
eevee.shadow_step_count = 2 # 2 steps for shadows
|
|
|
|
# Set fast GI to minimal settings (1 ray, 2 steps)
|
|
if hasattr(eevee, 'fast_gi_method'):
|
|
eevee.fast_gi_method = 'GLOBAL_ILLUMINATION' # Use valid method
|
|
if hasattr(eevee, 'fast_gi_ray_count'):
|
|
eevee.fast_gi_ray_count = 1 # 1 ray for fast GI
|
|
if hasattr(eevee, 'fast_gi_step_count'):
|
|
eevee.fast_gi_step_count = 2 # 2 steps for fast GI
|
|
if hasattr(eevee, 'fast_gi_quality'):
|
|
eevee.fast_gi_quality = 0.25 # Low quality for speed
|
|
if hasattr(eevee, 'fast_gi_distance'):
|
|
eevee.fast_gi_distance = 1.0 # Short distance
|
|
|
|
# Enable persistent data if available for faster animation rendering
|
|
if hasattr(eevee, 'use_persistent_data'):
|
|
eevee.use_persistent_data = True
|
|
print(f"Enabled persistent data for faster EEVEE animation rendering")
|
|
|
|
print(f"Set EEVEE raytracing to 1 ray, 2 steps for maximum performance")
|
|
print(f"Applied optimized EEVEE settings for RENDERED mode")
|
|
elif current_engine == 'CYCLES':
|
|
# Apply Cycles-specific optimizations
|
|
cycles = scene.cycles
|
|
|
|
# Use extremely low samples for preview
|
|
if hasattr(cycles, 'samples'):
|
|
cycles.samples = 8 # Absolute minimum for playblast
|
|
print(f"Set Cycles samples to 8 for maximum speed")
|
|
|
|
# Disable denoising entirely for faster rendering
|
|
if hasattr(cycles, 'use_denoising'):
|
|
cycles.use_denoising = False
|
|
print(f"Disabled Cycles denoising for maximum speed")
|
|
|
|
# Use fastest render settings
|
|
if hasattr(cycles, 'max_bounces'):
|
|
cycles.max_bounces = 2 # Almost no light bounces
|
|
if hasattr(cycles, 'diffuse_bounces'):
|
|
cycles.diffuse_bounces = 1 # Minimal diffuse
|
|
if hasattr(cycles, 'glossy_bounces'):
|
|
cycles.glossy_bounces = 1 # Minimal reflections
|
|
if hasattr(cycles, 'transmission_bounces'):
|
|
cycles.transmission_bounces = 1 # Minimal glass/transparency
|
|
if hasattr(cycles, 'volume_bounces'):
|
|
cycles.volume_bounces = 0 # No volume scattering
|
|
if hasattr(cycles, 'caustics_reflective'):
|
|
cycles.caustics_reflective = False # Disable reflective caustics
|
|
if hasattr(cycles, 'caustics_refractive'):
|
|
cycles.caustics_refractive = False # Disable refractive caustics
|
|
|
|
# Set pixel filter width to 0.01 for faster rendering
|
|
if hasattr(cycles, 'pixel_filter_width'):
|
|
cycles.pixel_filter_width = 0.01
|
|
|
|
# Use lowest quality shadow and AO settings
|
|
if hasattr(cycles, 'ao_bounces'):
|
|
cycles.ao_bounces = 1
|
|
if hasattr(cycles, 'ao_bounces_render'):
|
|
cycles.ao_bounces_render = 1
|
|
|
|
# Use adaptive sampling with very low thresholds
|
|
if hasattr(cycles, 'use_adaptive_sampling'):
|
|
cycles.use_adaptive_sampling = True
|
|
if hasattr(cycles, 'adaptive_threshold'):
|
|
cycles.adaptive_threshold = 0.8 # Even higher threshold = faster convergence
|
|
if hasattr(cycles, 'adaptive_min_samples'):
|
|
cycles.adaptive_min_samples = 0 # Allow adaptive sampling to stop early
|
|
|
|
# Use fastest integrator settings
|
|
if hasattr(cycles, 'light_sampling_threshold'):
|
|
cycles.light_sampling_threshold = 1.0 # Maximum threshold for fastest convergence
|
|
|
|
# Disable expensive sampling features
|
|
if hasattr(cycles, 'sample_clamp_direct'):
|
|
cycles.sample_clamp_direct = 0.0 # No clamping for speed
|
|
if hasattr(cycles, 'sample_clamp_indirect'):
|
|
cycles.sample_clamp_indirect = 0.0 # No clamping for speed
|
|
if hasattr(cycles, 'blur_glossy'):
|
|
cycles.blur_glossy = 0.0 # Disable glossy blur
|
|
if hasattr(cycles, 'sample_all_lights_direct'):
|
|
cycles.sample_all_lights_direct = False # Don't sample all lights
|
|
if hasattr(cycles, 'sample_all_lights_indirect'):
|
|
cycles.sample_all_lights_indirect = False # Don't sample all lights
|
|
|
|
# Use fastest filter and preview settings
|
|
if hasattr(cycles, 'filter_type'):
|
|
cycles.filter_type = 'BOX' # Fastest filter type
|
|
if hasattr(cycles, 'preview_samples'):
|
|
cycles.preview_samples = 1 # Minimum viewport samples
|
|
if hasattr(cycles, 'aa_samples'):
|
|
cycles.aa_samples = 1 # Minimum anti-aliasing samples
|
|
|
|
# Disable expensive transparency features
|
|
if hasattr(cycles, 'use_transparent_shadows'):
|
|
cycles.use_transparent_shadows = False
|
|
if hasattr(cycles, 'transparent_max_bounces'):
|
|
cycles.transparent_max_bounces = 0 # No transparent bounces
|
|
|
|
# Disable tiling for Cycles (OptiX) to avoid slowdowns
|
|
if hasattr(cycles, 'use_auto_tile'):
|
|
cycles.use_auto_tile = False
|
|
# Intentionally do not override tile_size; keep user/default setting
|
|
|
|
# Use fastest integrator path
|
|
if hasattr(cycles, 'progressive'):
|
|
cycles.progressive = 'PATH' # Use path tracing (usually fastest)
|
|
|
|
# CRITICAL: Enable persistent data for much faster animation rendering
|
|
if hasattr(cycles, 'use_persistent_data'):
|
|
cycles.use_persistent_data = True
|
|
print(f"Enabled persistent data for faster animation rendering")
|
|
|
|
# Use faster GPU rendering if available
|
|
if hasattr(cycles, 'device'):
|
|
# Try to use GPU if available
|
|
try:
|
|
cycles.device = 'GPU'
|
|
except:
|
|
# If setting GPU fails, stick with current device
|
|
pass
|
|
|
|
# Additional GPU optimizations
|
|
if hasattr(cycles, 'feature_set'):
|
|
cycles.feature_set = 'SUPPORTED' # Use only supported GPU features
|
|
if hasattr(cycles, 'use_cpu_device'):
|
|
cycles.use_cpu_device = False # Force GPU only if available
|
|
|
|
print(f"Applied optimized Cycles settings for RENDERED mode")
|
|
|
|
# General optimizations regardless of render engine
|
|
# Ensure Cycles persistent data is always enabled for animation performance
|
|
try:
|
|
cy = getattr(scene, 'cycles', None)
|
|
if cy and hasattr(cy, 'use_persistent_data'):
|
|
cy.use_persistent_data = True
|
|
except Exception:
|
|
pass
|
|
|
|
# Enable simplify settings for render
|
|
if hasattr(scene.render, 'use_simplify'):
|
|
scene.render.use_simplify = True
|
|
|
|
# Set moderate simplification for final render
|
|
if hasattr(scene.render, 'simplify_subdivision'):
|
|
scene.render.simplify_subdivision = 1
|
|
if hasattr(scene.render, 'simplify_child_particles'):
|
|
scene.render.simplify_child_particles = 0.5
|
|
if hasattr(scene.render, 'simplify_volumes'):
|
|
scene.render.simplify_volumes = 0.5
|
|
|
|
# Disable compositor for faster rendering
|
|
scene.use_nodes = False
|
|
|
|
# Reduce texture size limit for faster material evaluation
|
|
if hasattr(scene.render, 'texture_limit'):
|
|
scene.render.texture_limit = '2048' # Reduced but still decent quality
|
|
|
|
# Disable motion blur
|
|
if hasattr(scene.render, 'use_motion_blur'):
|
|
scene.render.use_motion_blur = False
|
|
|
|
# Keep all lights and world settings for RENDERED mode
|
|
# This is the key difference from MATERIAL mode - we want to use
|
|
# the actual scene lighting and world settings
|
|
|
|
print(f"RENDERED preview mode configured with optimized settings")
|
|
|
|
except Exception as e:
|
|
self.report({'WARNING'}, f"Note: Couldn't set all RENDERED mode settings: {str(e)}")
|
|
elif props.display_mode == 'MATERIAL':
|
|
# For material preview, use EEVEE
|
|
scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
|
|
|
# Material preview uses an HDRI environment for lighting
|
|
try:
|
|
# Completely remove scene world - critical for studio lights
|
|
scene.world = None
|
|
|
|
# CRITICAL FIX: Store and temporarily disable all scene lights
|
|
original_light_states = {}
|
|
for obj in scene.objects:
|
|
if obj.type == 'LIGHT':
|
|
# Store original visibility and hide status
|
|
original_light_states[obj.name] = {
|
|
'hide_viewport': obj.hide_viewport,
|
|
'hide_render': obj.hide_render,
|
|
'visible_camera': obj.visible_camera,
|
|
'visible_diffuse': obj.visible_diffuse,
|
|
'visible_glossy': obj.visible_glossy,
|
|
'visible_transmission': obj.visible_transmission,
|
|
'visible_volume_scatter': obj.visible_volume_scatter
|
|
}
|
|
|
|
# Disable the light completely for rendering
|
|
obj.hide_render = True
|
|
obj.hide_viewport = True
|
|
obj.visible_camera = False
|
|
obj.visible_diffuse = False
|
|
obj.visible_glossy = False
|
|
obj.visible_transmission = False
|
|
obj.visible_volume_scatter = False
|
|
|
|
print(f"Temporarily disabled light: {obj.name}")
|
|
|
|
# Get path to Blender installation and construct studio lights path
|
|
# Make sure modules are available for this section
|
|
import os
|
|
import sys
|
|
|
|
# Get the Blender executable path
|
|
blender_exe = bpy.app.binary_path
|
|
blender_dir = os.path.dirname(blender_exe)
|
|
|
|
# Construct path to studio lights directory
|
|
# Note: This may vary based on Blender installation but should work for most setups
|
|
studio_lights_dir = os.path.join(blender_dir, "datafiles", "studiolights", "world")
|
|
|
|
# Additional paths for different Blender installations (specifically for Blender 4.4)
|
|
possible_paths = [
|
|
# Standard path
|
|
studio_lights_dir,
|
|
# Blender 4.4 specific path structure with extra version directory
|
|
os.path.join(blender_dir, "4.4", "datafiles", "studiolights", "world"),
|
|
# Other possible locations
|
|
os.path.join(blender_dir, "..", "datafiles", "studiolights", "world"),
|
|
os.path.join(blender_dir, "..", "..", "datafiles", "studiolights", "world"),
|
|
os.path.join(blender_dir, "..", "4.4", "datafiles", "studiolights", "world"),
|
|
os.path.join(os.path.dirname(os.path.dirname(blender_exe)), "4.4", "datafiles", "studiolights", "world"),
|
|
# Version-specific paths for various Blender installations
|
|
os.path.join(os.path.dirname(blender_dir), "4.4", "datafiles", "studiolights", "world")
|
|
]
|
|
|
|
# Get Blender's version and construct a version-specific path
|
|
blender_version = bpy.app.version
|
|
version_str = f"{blender_version[0]}.{blender_version[1]}"
|
|
possible_paths.append(os.path.join(blender_dir, version_str, "datafiles", "studiolights", "world"))
|
|
possible_paths.append(os.path.join(os.path.dirname(blender_dir), version_str, "datafiles", "studiolights", "world"))
|
|
|
|
# Specific path for this user's installation
|
|
possible_paths.append(r"C:\Program Files\Blender Foundation\Blender 4.4\4.4\datafiles\studiolights\world")
|
|
|
|
# Try to find the studio lights directory
|
|
studio_lights_dir = None
|
|
for path in possible_paths:
|
|
if os.path.exists(path):
|
|
print(f"Found studio lights directory: {path}")
|
|
studio_lights_dir = path
|
|
break
|
|
|
|
if not studio_lights_dir:
|
|
print("Could not find studio lights directory, using fallback")
|
|
studio_lights_dir = possible_paths[0] # Use the first path as fallback
|
|
|
|
# Find the specific HDRI to use - these are common in Blender 4.4
|
|
common_hdri_files = [
|
|
"forest.exr", # Forest environment - good general lighting (preferred)
|
|
"studio.exr", # Clean studio environment
|
|
"city.exr", # Urban environment
|
|
"courtyard.exr", # Outdoor courtyard
|
|
"night.exr", # Night environment
|
|
"sunrise.exr", # Sunrise lighting
|
|
"sunset.exr", # Sunset lighting
|
|
]
|
|
|
|
# Try to find an existing HDRI file
|
|
hdri_path = None
|
|
for hdri_filename in common_hdri_files:
|
|
path = os.path.join(studio_lights_dir, hdri_filename)
|
|
if os.path.exists(path):
|
|
hdri_path = path
|
|
print(f"Found HDRI file: {hdri_path}")
|
|
break
|
|
|
|
# If no common HDRI was found, try any .exr file
|
|
if not hdri_path and os.path.exists(studio_lights_dir):
|
|
try:
|
|
exr_files = [f for f in os.listdir(studio_lights_dir) if f.endswith('.exr')]
|
|
if exr_files:
|
|
hdri_filename = exr_files[0]
|
|
hdri_path = os.path.join(studio_lights_dir, hdri_filename)
|
|
print(f"Using alternative HDRI: {hdri_path}")
|
|
except Exception as e:
|
|
print(f"Error listing studio lights directory: {str(e)}")
|
|
|
|
# Hardcoded paths as last resort
|
|
if not hdri_path or not os.path.exists(hdri_path):
|
|
direct_paths = [
|
|
r"C:\Program Files\Blender Foundation\Blender 4.4\4.4\datafiles\studiolights\world\forest.exr",
|
|
r"C:\Program Files\Blender Foundation\Blender 4.4\4.4\datafiles\studiolights\world\studio.exr",
|
|
# Try both common locations
|
|
os.path.join(studio_lights_dir, "forest.exr"),
|
|
os.path.join(os.path.dirname(studio_lights_dir), "world", "forest.exr")
|
|
]
|
|
for path in direct_paths:
|
|
if os.path.exists(path):
|
|
hdri_path = path
|
|
print(f"Using hardcoded HDRI path: {hdri_path}")
|
|
break
|
|
|
|
if hdri_path and os.path.exists(hdri_path):
|
|
print(f"Using HDRI path: {hdri_path}")
|
|
else:
|
|
print("WARNING: Could not find any suitable HDRI file!")
|
|
|
|
# Create a new world to use for rendering
|
|
new_world = None
|
|
# First, check if we already have a world with this name
|
|
world_name = f"BasedPlayblast_StudioHDRI"
|
|
if world_name in bpy.data.worlds:
|
|
new_world = bpy.data.worlds[world_name]
|
|
else:
|
|
# Create a new world
|
|
new_world = bpy.data.worlds.new(world_name)
|
|
|
|
# Setup world to use the HDRI
|
|
new_world.use_nodes = True
|
|
nodes = new_world.node_tree.nodes
|
|
|
|
# Clear existing nodes
|
|
for node in nodes:
|
|
nodes.remove(node)
|
|
|
|
# Create background and output nodes
|
|
background = nodes.new(type='ShaderNodeBackground')
|
|
output = nodes.new(type='ShaderNodeOutputWorld')
|
|
|
|
# Set background strength for proper lighting intensity
|
|
if hasattr(background.inputs[1], 'default_value'):
|
|
background.inputs[1].default_value = 1.0 # Strength of 1.0 is standard for material preview
|
|
|
|
# Set a default color for the background (light gray to provide some lighting)
|
|
if hasattr(background.inputs[0], 'default_value'):
|
|
background.inputs[0].default_value = (0.8, 0.8, 0.8, 1.0)
|
|
|
|
# Position nodes
|
|
background.location = (0, 0)
|
|
output.location = (300, 0)
|
|
|
|
# Link nodes for basic background
|
|
links = new_world.node_tree.links
|
|
links.new(background.outputs["Background"], output.inputs["Surface"])
|
|
|
|
# Only add the texture node if we have a valid HDRI
|
|
if hdri_path and os.path.exists(hdri_path):
|
|
# Create texture node
|
|
tex_node = nodes.new(type='ShaderNodeTexEnvironment')
|
|
tex_node.location = (-300, 0)
|
|
|
|
# Load the HDRI file
|
|
try:
|
|
# Try to load the image with performance optimizations
|
|
image = bpy.data.images.load(hdri_path, check_existing=True)
|
|
tex_node.image = image
|
|
|
|
# Optimize the image for rendering performance
|
|
if hasattr(image, 'colorspace_settings'):
|
|
# Use a proper linear colorspace from the available options
|
|
# "Linear" alone isn't valid in Blender 4.4
|
|
try:
|
|
image.colorspace_settings.name = 'Linear Rec.709' # Most common linear space
|
|
except:
|
|
# If that fails, try a different linear option
|
|
try:
|
|
image.colorspace_settings.name = 'Linear ACES'
|
|
except:
|
|
# Just use the default - don't change it
|
|
pass
|
|
|
|
# Link the texture to background
|
|
links.new(tex_node.outputs["Color"], background.inputs["Color"])
|
|
print(f"Successfully loaded HDRI: {hdri_path}")
|
|
except Exception as e:
|
|
print(f"Error loading HDRI: {str(e)}")
|
|
print("Using default background color instead")
|
|
else:
|
|
print("No valid HDRI path found - using default background color")
|
|
|
|
# Set the world for rendering
|
|
scene.world = new_world
|
|
|
|
# Set the appropriate attribute for EEVEE settings
|
|
eevee_attr = 'eevee' if hasattr(scene, 'eevee') else 'eevee_next'
|
|
eevee = getattr(scene, eevee_attr) if hasattr(scene, eevee_attr) else None
|
|
|
|
if eevee:
|
|
# For material preview, we need to use the environment rather than studio lights
|
|
if hasattr(eevee, 'use_scene_lights'):
|
|
eevee.use_scene_lights = False
|
|
print(f"Disabled scene lights for EEVEE render")
|
|
if hasattr(eevee, 'use_scene_world'):
|
|
# THIS IS IMPORTANT - we're using our own world node setup, not studio light
|
|
eevee.use_scene_world = True
|
|
print(f"Enabled scene world for EEVEE render")
|
|
|
|
# CRITICAL: Always disable shadows and raytracing for material preview
|
|
if hasattr(eevee, 'use_shadows'):
|
|
eevee.use_shadows = False
|
|
print(f"Disabled shadows for material preview")
|
|
if hasattr(eevee, 'use_soft_shadows'):
|
|
eevee.use_soft_shadows = False
|
|
print(f"Disabled soft shadows for material preview")
|
|
if hasattr(eevee, 'use_raytrace'):
|
|
eevee.use_raytrace = False
|
|
print(f"Disabled raytracing for material preview")
|
|
if hasattr(eevee, 'use_ssr'):
|
|
eevee.use_ssr = False
|
|
print(f"Disabled screen space reflections for material preview")
|
|
if hasattr(eevee, 'use_ssr_refraction'):
|
|
eevee.use_ssr_refraction = False
|
|
print(f"Disabled screen space refractions for material preview")
|
|
|
|
# Use minimum possible samples for fastest rendering
|
|
if hasattr(eevee, 'taa_render_samples'):
|
|
eevee.taa_render_samples = 4
|
|
print(f"Set render samples to 4")
|
|
|
|
# Disable features not used in material preview
|
|
if hasattr(eevee, 'use_bloom'):
|
|
eevee.use_bloom = False
|
|
if hasattr(eevee, 'use_ssr'):
|
|
eevee.use_ssr = False
|
|
if hasattr(eevee, 'use_gtao'):
|
|
eevee.use_gtao = False
|
|
if hasattr(eevee, 'use_volumetric_lights'):
|
|
eevee.use_volumetric_lights = False
|
|
|
|
# Disable global illumination
|
|
if hasattr(eevee, 'gi_diffuse_bounces'):
|
|
eevee.gi_diffuse_bounces = 0
|
|
|
|
# Set additional minimum quality settings
|
|
if hasattr(eevee, 'shadow_cube_size'):
|
|
eevee.shadow_cube_size = '64' # Minimum shadow resolution
|
|
if hasattr(eevee, 'shadow_cascade_size'):
|
|
eevee.shadow_cascade_size = '64' # Minimum shadow resolution
|
|
if hasattr(eevee, 'use_soft_shadows'):
|
|
eevee.use_soft_shadows = False # Disable soft shadows
|
|
if hasattr(eevee, 'sss_samples'):
|
|
eevee.sss_samples = 1 # Minimum subsurface scattering samples
|
|
if hasattr(eevee, 'volumetric_samples'):
|
|
eevee.volumetric_samples = 1 # Minimum volumetric samples
|
|
|
|
# Additional performance optimizations
|
|
# Disable motion blur
|
|
if hasattr(eevee, 'use_motion_blur'):
|
|
eevee.use_motion_blur = False
|
|
|
|
# Disable ambient occlusion (AO)
|
|
if hasattr(eevee, 'use_gtao'):
|
|
eevee.use_gtao = False
|
|
|
|
# Disable screen space reflections entirely
|
|
if hasattr(eevee, 'use_ssr'):
|
|
eevee.use_ssr = False
|
|
|
|
# Reduce texture size limit for faster material evaluation
|
|
if hasattr(scene.render, 'texture_limit'):
|
|
scene.render.texture_limit = '1024'
|
|
|
|
# Enable simplify settings for render
|
|
if hasattr(scene.render, 'use_simplify'):
|
|
scene.render.use_simplify = True
|
|
|
|
# CRITICAL: Always set maximum simplification for material preview
|
|
if hasattr(scene.render, 'simplify_subdivision'):
|
|
scene.render.simplify_subdivision = 0
|
|
print(f"Set maximum subdivision simplification (0) for material preview")
|
|
if hasattr(scene.render, 'simplify_child_particles'):
|
|
scene.render.simplify_child_particles = 0
|
|
print(f"Set maximum particle simplification (0) for material preview")
|
|
if hasattr(scene.render, 'simplify_volumes'):
|
|
scene.render.simplify_volumes = 0
|
|
print(f"Set maximum volume simplification (0) for material preview")
|
|
if hasattr(scene.render, 'simplify_shadows'):
|
|
scene.render.simplify_shadows = 0
|
|
print(f"Set maximum shadow simplification (0) for material preview")
|
|
if hasattr(scene.render, 'simplify_culling'):
|
|
scene.render.simplify_culling = True
|
|
print(f"Enabled culling simplification for material preview")
|
|
|
|
# Optimize compositor settings
|
|
scene.use_nodes = False # Disable compositor nodes
|
|
|
|
# Use smaller tile size for faster updating
|
|
if hasattr(eevee, 'tile_size'):
|
|
eevee.tile_size = '8' # Use 8x8 tiles for faster rendering
|
|
|
|
# Disable film transparency if not needed
|
|
if hasattr(scene.render, 'film_transparent'):
|
|
scene.render.film_transparent = False
|
|
|
|
# Ensure background is colored by the environment
|
|
background = new_world.node_tree.nodes.get('Background')
|
|
if background and hasattr(background.inputs[0], 'default_value'):
|
|
# Make sure the background node uses the HDRI color
|
|
pass # Already properly set up in node setup
|
|
|
|
print(f"All EEVEE settings set to minimum quality for fastest rendering")
|
|
|
|
# Save original settings to restore later
|
|
props.original_settings_extended = str(original_light_states)
|
|
else:
|
|
self.report({'WARNING'}, f"Could not find EEVEE settings - material preview may not render correctly")
|
|
|
|
except Exception as e:
|
|
self.report({'WARNING'}, f"Note: Couldn't set all EEVEE settings: {str(e)}")
|
|
else:
|
|
# For SOLID or WIREFRAME, use Workbench
|
|
scene.render.engine = 'BLENDER_WORKBENCH'
|
|
|
|
# Configure workbench settings for optimal performance
|
|
scene.display.shading.light = 'STUDIO'
|
|
scene.display.shading.color_type = 'MATERIAL'
|
|
if props.display_mode == 'WIREFRAME':
|
|
scene.display.shading.type = 'WIREFRAME'
|
|
else:
|
|
scene.display.shading.type = 'SOLID'
|
|
|
|
# Disable anti-aliasing for maximum speed in workbench
|
|
# Viewport anti-aliasing
|
|
if hasattr(scene.display, 'render_aa'):
|
|
scene.display.render_aa = 'OFF'
|
|
# Render anti-aliasing (render passes)
|
|
if hasattr(scene.display.shading, 'render_pass'):
|
|
scene.display.shading.render_pass = 'COMBINED'
|
|
# Disable any other performance-impacting settings
|
|
if hasattr(scene.display.shading, 'show_cavity'):
|
|
scene.display.shading.show_cavity = False
|
|
# The show_shadow attribute doesn't exist in Blender 4.4
|
|
# if hasattr(scene.display.shading, 'show_shadow'):
|
|
# scene.display.shading.show_shadow = False
|
|
if hasattr(scene.display.shading, 'show_object_outline'):
|
|
scene.display.shading.show_object_outline = False
|
|
if hasattr(scene.display.shading, 'show_specular_highlight'):
|
|
scene.display.shading.show_specular_highlight = False
|
|
|
|
# Handle depth of field in Workbench
|
|
if hasattr(scene.display.shading, 'use_dof'):
|
|
scene.display.shading.use_dof = props.enable_depth_of_field
|
|
if props.enable_depth_of_field:
|
|
print(f"Enabled Workbench depth of field")
|
|
else:
|
|
print(f"Disabled Workbench depth of field")
|
|
|
|
print(f"Workbench anti-aliasing disabled for maximum performance")
|
|
|
|
# Set resolution based on mode
|
|
if props.resolution_mode == 'PRESET':
|
|
preset = props.resolution_preset
|
|
x_str = preset.split('y')[0].replace('x', '')
|
|
y_str = preset.split('y')[1]
|
|
scene.render.resolution_x = int(x_str)
|
|
scene.render.resolution_y = int(y_str)
|
|
elif props.resolution_mode == 'CUSTOM':
|
|
scene.render.resolution_x = props.resolution_x
|
|
scene.render.resolution_y = props.resolution_y
|
|
|
|
scene.render.resolution_percentage = props.resolution_percentage
|
|
|
|
# Create output directory
|
|
output_dir = bpy.path.abspath(props.output_path)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Set output path
|
|
file_name = props.file_name
|
|
if '.' in file_name:
|
|
file_name = os.path.splitext(file_name)[0]
|
|
scene.render.filepath = os.path.join(output_dir, file_name)
|
|
|
|
# Set file format (Blender 5.0 compatible)
|
|
video_format_set = set_video_file_format(scene)
|
|
if not video_format_set and hasattr(scene.render, 'ffmpeg'):
|
|
# Still try to configure ffmpeg even if file_format couldn't be set
|
|
# This might work in some Blender 5.0 configurations
|
|
self.report({'WARNING'}, "Could not set video file_format. Attempting to proceed with ffmpeg settings...")
|
|
elif not video_format_set:
|
|
self.report({'ERROR'}, "Video rendering not supported in this Blender version.")
|
|
return {'CANCELLED'}
|
|
|
|
# Configure ffmpeg settings (these should still work even if file_format is different)
|
|
if hasattr(scene.render, 'ffmpeg'):
|
|
scene.render.ffmpeg.format = props.video_format
|
|
scene.render.ffmpeg.codec = props.video_codec
|
|
scene.render.ffmpeg.constant_rate_factor = get_ffmpeg_quality(props.video_quality)
|
|
|
|
# Audio settings
|
|
if props.include_audio:
|
|
scene.render.ffmpeg.audio_codec = props.audio_codec
|
|
scene.render.ffmpeg.audio_bitrate = props.audio_bitrate
|
|
else:
|
|
scene.render.ffmpeg.audio_codec = 'NONE'
|
|
else:
|
|
self.report({'ERROR'}, "FFMPEG settings not available in this Blender version.")
|
|
return {'CANCELLED'}
|
|
|
|
# Set frame range if using manual range
|
|
if not props.use_scene_frame_range:
|
|
scene.frame_start = props.start_frame
|
|
scene.frame_end = props.end_frame
|
|
|
|
# Setup metadata
|
|
if props.show_metadata:
|
|
scene.render.use_stamp = True
|
|
scene.render.use_stamp_date = props.metadata_date
|
|
scene.render.use_stamp_time = props.metadata_date # Usually linked with date
|
|
scene.render.use_stamp_frame = props.metadata_frame
|
|
scene.render.use_stamp_camera = props.metadata_camera
|
|
scene.render.use_stamp_lens = props.metadata_lens
|
|
scene.render.use_stamp_scene = props.metadata_scene
|
|
|
|
# Set note if provided
|
|
if props.metadata_note:
|
|
scene.render.use_stamp_note = True
|
|
|
|
# Build the note text
|
|
note = props.metadata_note
|
|
|
|
# Add resolution info if enabled
|
|
if props.metadata_resolution:
|
|
res_x = scene.render.resolution_x * (scene.render.resolution_percentage / 100.0)
|
|
res_y = scene.render.resolution_y * (scene.render.resolution_percentage / 100.0)
|
|
note += f"\nResolution: {int(res_x)} x {int(res_y)}"
|
|
|
|
scene.render.stamp_note_text = note
|
|
else:
|
|
scene.render.use_stamp = False
|
|
|
|
self.report({'INFO'}, f"Blast settings applied, render engine set to {scene.render.engine}")
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error saving original settings: {str(e)}")
|
|
print(f"DETAILED ERROR in saving settings: {str(e)}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
# Continue with applying settings even if saving fails
|
|
print(f"Continuing with applying blast settings despite saving error...")
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error applying settings: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
# New operator to restore original render settings
|
|
class BPL_OT_restore_original_settings(Operator):
|
|
bl_idname = "bpl.restore_original_settings"
|
|
bl_label = "Restore Original Render Settings"
|
|
bl_description = "Restore the original render settings before the blast settings were applied"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# Check if we have original settings saved
|
|
if not props.original_settings:
|
|
self.report({'ERROR'}, "No original settings saved to restore")
|
|
return {'CANCELLED'}
|
|
|
|
try:
|
|
import json
|
|
import ast # For evaluating the saved light states
|
|
original = json.loads(props.original_settings)
|
|
|
|
print(f"Restoring comprehensive settings for engine: {original.get('render_engine', 'unknown')}")
|
|
|
|
def safe_restore(obj, attr, value):
|
|
"""Safely restore attribute"""
|
|
try:
|
|
if hasattr(obj, attr):
|
|
setattr(obj, attr, value)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Could not restore {attr}: {e}")
|
|
return False
|
|
|
|
# SCENE.RENDER - Restore all basic render settings
|
|
scene.render.filepath = original['filepath']
|
|
scene.render.resolution_x = original['resolution_x']
|
|
scene.render.resolution_y = original['resolution_y']
|
|
scene.render.resolution_percentage = original['resolution_percentage']
|
|
safe_restore(scene.render, 'pixel_aspect_x', original.get('pixel_aspect_x', 1.0))
|
|
safe_restore(scene.render, 'pixel_aspect_y', original.get('pixel_aspect_y', 1.0))
|
|
scene.render.use_file_extension = original['use_file_extension']
|
|
scene.render.use_overwrite = original['use_overwrite']
|
|
scene.render.use_placeholder = original['use_placeholder']
|
|
scene.frame_start = original['frame_start']
|
|
scene.frame_end = original['frame_end']
|
|
scene.frame_step = original['frame_step']
|
|
scene.frame_current = original.get('frame_current', 1)
|
|
|
|
# Film settings
|
|
scene.render.film_transparent = original['film_transparent']
|
|
scene.render.filter_size = original['filter_size']
|
|
|
|
# Performance settings
|
|
scene.render.use_persistent_data = original['use_persistent_data']
|
|
scene.render.use_simplify = original['use_simplify']
|
|
scene.render.simplify_subdivision = original['simplify_subdivision']
|
|
scene.render.simplify_child_particles = original['simplify_child_particles']
|
|
scene.render.simplify_volumes = original['simplify_volumes']
|
|
safe_restore(scene.render, 'simplify_subdivision_render', original.get('simplify_subdivision_render', 6))
|
|
safe_restore(scene.render, 'simplify_child_particles_render', original.get('simplify_child_particles_render', 1.0))
|
|
safe_restore(scene.render, 'simplify_volumes_render', original.get('simplify_volumes_render', 1.0))
|
|
|
|
# Motion blur
|
|
scene.render.use_motion_blur = original['use_motion_blur']
|
|
scene.render.motion_blur_shutter = original['motion_blur_shutter']
|
|
safe_restore(scene.render, 'motion_blur_shutter_curve', original.get('motion_blur_shutter_curve', 'AUTO'))
|
|
safe_restore(scene.render, 'rolling_shutter_type', original.get('rolling_shutter_type', 'NONE'))
|
|
safe_restore(scene.render, 'rolling_shutter_duration', original.get('rolling_shutter_duration', 0.1))
|
|
|
|
# Threading
|
|
scene.render.threads_mode = original['threads_mode']
|
|
scene.render.threads = original['threads']
|
|
|
|
# Memory and caching
|
|
safe_restore(scene.render, 'tile_x', original.get('tile_x', 64))
|
|
safe_restore(scene.render, 'tile_y', original.get('tile_y', 64))
|
|
safe_restore(scene.render, 'use_save_buffers', original.get('use_save_buffers', False))
|
|
|
|
# Preview and display
|
|
context.preferences.view.render_display_type = original['display_mode']
|
|
safe_restore(scene.render, 'preview_pixel_size', original.get('preview_pixel_size', 'AUTO'))
|
|
|
|
# SCENE.RENDER.IMAGE_SETTINGS - Restore image settings
|
|
if 'image_settings' in original:
|
|
img_settings = original['image_settings']
|
|
scene.render.image_settings.file_format = img_settings['file_format']
|
|
scene.render.image_settings.color_mode = img_settings['color_mode']
|
|
scene.render.image_settings.color_depth = img_settings['color_depth']
|
|
scene.render.image_settings.compression = img_settings['compression']
|
|
scene.render.image_settings.quality = img_settings['quality']
|
|
scene.render.image_settings.use_preview = img_settings['use_preview']
|
|
safe_restore(scene.render.image_settings, 'exr_codec', img_settings.get('exr_codec', 'ZIP'))
|
|
safe_restore(scene.render.image_settings, 'use_zbuffer', img_settings.get('use_zbuffer', False))
|
|
safe_restore(scene.render.image_settings, 'jpeg2k_codec', img_settings.get('jpeg2k_codec', 'JP2'))
|
|
safe_restore(scene.render.image_settings, 'tiff_codec', img_settings.get('tiff_codec', 'DEFLATE'))
|
|
|
|
# Scene/world settings
|
|
scene.use_nodes = original['use_nodes']
|
|
|
|
# Compositing settings
|
|
scene.render.use_compositing = original['use_compositing']
|
|
scene.render.use_sequencer = original['use_sequencer']
|
|
|
|
# Border and crop settings
|
|
scene.render.use_border = original['use_border']
|
|
scene.render.border_min_x = original['border_min_x']
|
|
scene.render.border_max_x = original['border_max_x']
|
|
scene.render.border_min_y = original['border_min_y']
|
|
scene.render.border_max_y = original['border_max_y']
|
|
scene.render.use_crop_to_border = original['use_crop_to_border']
|
|
|
|
# Metadata settings - comprehensive
|
|
scene.render.use_stamp = original['use_stamp']
|
|
scene.render.use_stamp_date = original['use_stamp_date']
|
|
scene.render.use_stamp_time = original['use_stamp_time']
|
|
scene.render.use_stamp_frame = original['use_stamp_frame']
|
|
scene.render.use_stamp_camera = original['use_stamp_camera']
|
|
scene.render.use_stamp_lens = original['use_stamp_lens']
|
|
scene.render.use_stamp_scene = original['use_stamp_scene']
|
|
scene.render.use_stamp_note = original['use_stamp_note']
|
|
scene.render.stamp_note_text = original['stamp_note_text']
|
|
scene.render.use_stamp_marker = original['use_stamp_marker']
|
|
scene.render.use_stamp_filename = original['use_stamp_filename']
|
|
scene.render.use_stamp_render_time = original['use_stamp_render_time']
|
|
scene.render.use_stamp_memory = original['use_stamp_memory']
|
|
scene.render.use_stamp_hostname = original['use_stamp_hostname']
|
|
scene.render.stamp_font_size = original['stamp_font_size']
|
|
scene.render.stamp_foreground = original['stamp_foreground']
|
|
scene.render.stamp_background = original['stamp_background']
|
|
|
|
# Hair settings
|
|
safe_restore(scene.render, 'hair_type', original.get('hair_type', 'PATH'))
|
|
safe_restore(scene.render, 'hair_subdiv', original.get('hair_subdiv', 3))
|
|
|
|
# SCENE.RENDER.FFMPEG - Restore FFmpeg settings
|
|
if 'ffmpeg' in original:
|
|
ffmpeg = original['ffmpeg']
|
|
scene.render.ffmpeg.format = ffmpeg['format']
|
|
scene.render.ffmpeg.codec = ffmpeg['codec']
|
|
scene.render.ffmpeg.video_bitrate = ffmpeg['video_bitrate']
|
|
scene.render.ffmpeg.minrate = ffmpeg['minrate']
|
|
scene.render.ffmpeg.maxrate = ffmpeg['maxrate']
|
|
scene.render.ffmpeg.buffersize = ffmpeg['buffersize']
|
|
scene.render.ffmpeg.muxrate = ffmpeg['muxrate']
|
|
scene.render.ffmpeg.packetsize = ffmpeg['packetsize']
|
|
scene.render.ffmpeg.constant_rate_factor = ffmpeg['constant_rate_factor']
|
|
scene.render.ffmpeg.gopsize = ffmpeg['gopsize']
|
|
safe_restore(scene.render.ffmpeg, 'use_max_b_frames', ffmpeg.get('use_max_b_frames', False))
|
|
safe_restore(scene.render.ffmpeg, 'max_b_frames', ffmpeg.get('max_b_frames', 2))
|
|
safe_restore(scene.render.ffmpeg, 'use_autosplit', ffmpeg.get('use_autosplit', False))
|
|
safe_restore(scene.render.ffmpeg, 'autosplit_size', ffmpeg.get('autosplit_size', 2048))
|
|
scene.render.ffmpeg.audio_codec = ffmpeg['audio_codec']
|
|
scene.render.ffmpeg.audio_bitrate = ffmpeg['audio_bitrate']
|
|
scene.render.ffmpeg.audio_channels = ffmpeg['audio_channels']
|
|
scene.render.ffmpeg.audio_mixrate = ffmpeg['audio_mixrate']
|
|
scene.render.ffmpeg.audio_volume = ffmpeg['audio_volume']
|
|
|
|
# Restore render engine first
|
|
if 'render_engine' in original:
|
|
scene.render.engine = original['render_engine']
|
|
print(f"Restored render engine to: {original['render_engine']}")
|
|
|
|
# SCENE.CYCLES - Always restore Cycles settings if available
|
|
print(f"Checking for Cycles settings in saved data...")
|
|
print(f"'cycles' in original: {'cycles' in original}")
|
|
if 'cycles' in original:
|
|
print(f"original['cycles'] exists: {bool(original['cycles'])}")
|
|
print(f"original['cycles'] keys: {list(original['cycles'].keys()) if original['cycles'] else 'empty'}")
|
|
else:
|
|
print(f"ERROR: 'cycles' key not found in original settings! Keys available: {list(original.keys())}")
|
|
|
|
if 'cycles' in original and original['cycles']:
|
|
cycles_settings = original['cycles']
|
|
cycles = scene.cycles
|
|
print(f"Restoring ALL Cycles settings - samples: {cycles_settings.get('samples', 'unknown')}")
|
|
|
|
# Restore ALL Cycles settings comprehensively
|
|
cycles.device = cycles_settings['device']
|
|
safe_restore(cycles, 'feature_set', cycles_settings.get('feature_set', 'SUPPORTED'))
|
|
safe_restore(cycles, 'shading_system', cycles_settings.get('shading_system', 'SVM'))
|
|
cycles.samples = cycles_settings['samples']
|
|
cycles.preview_samples = cycles_settings['preview_samples']
|
|
safe_restore(cycles, 'aa_samples', cycles_settings.get('aa_samples', 4))
|
|
safe_restore(cycles, 'preview_aa_samples', cycles_settings.get('preview_aa_samples', 4))
|
|
cycles.use_denoising = cycles_settings['use_denoising']
|
|
safe_restore(cycles, 'denoiser', cycles_settings.get('denoiser', 'OPENIMAGEDENOISE'))
|
|
safe_restore(cycles, 'denoising_input_passes', cycles_settings.get('denoising_input_passes', 'RGB_ALBEDO_NORMAL'))
|
|
safe_restore(cycles, 'use_denoising_input_passes', cycles_settings.get('use_denoising_input_passes', True))
|
|
safe_restore(cycles, 'denoising_prefilter', cycles_settings.get('denoising_prefilter', 'ACCURATE'))
|
|
cycles.use_adaptive_sampling = cycles_settings['use_adaptive_sampling']
|
|
cycles.adaptive_threshold = cycles_settings['adaptive_threshold']
|
|
cycles.adaptive_min_samples = cycles_settings['adaptive_min_samples']
|
|
safe_restore(cycles, 'time_limit', cycles_settings.get('time_limit', 0.0))
|
|
safe_restore(cycles, 'use_preview_adaptive_sampling', cycles_settings.get('use_preview_adaptive_sampling', False))
|
|
safe_restore(cycles, 'preview_adaptive_threshold', cycles_settings.get('preview_adaptive_threshold', 0.1))
|
|
safe_restore(cycles, 'preview_adaptive_min_samples', cycles_settings.get('preview_adaptive_min_samples', 0))
|
|
safe_restore(cycles, 'seed', cycles_settings.get('seed', 0))
|
|
safe_restore(cycles, 'use_animated_seed', cycles_settings.get('use_animated_seed', False))
|
|
safe_restore(cycles, 'sample_clamp_direct', cycles_settings.get('sample_clamp_direct', 0.0))
|
|
safe_restore(cycles, 'sample_clamp_indirect', cycles_settings.get('sample_clamp_indirect', 0.0))
|
|
cycles.light_sampling_threshold = cycles_settings['light_sampling_threshold']
|
|
safe_restore(cycles, 'sample_all_lights_direct', cycles_settings.get('sample_all_lights_direct', True))
|
|
safe_restore(cycles, 'sample_all_lights_indirect', cycles_settings.get('sample_all_lights_indirect', True))
|
|
cycles.max_bounces = cycles_settings['max_bounces']
|
|
cycles.diffuse_bounces = cycles_settings['diffuse_bounces']
|
|
cycles.glossy_bounces = cycles_settings['glossy_bounces']
|
|
cycles.transmission_bounces = cycles_settings['transmission_bounces']
|
|
cycles.volume_bounces = cycles_settings['volume_bounces']
|
|
safe_restore(cycles, 'transparent_max_bounces', cycles_settings.get('transparent_max_bounces', 8))
|
|
cycles.caustics_reflective = cycles_settings['caustics_reflective']
|
|
cycles.caustics_refractive = cycles_settings['caustics_refractive']
|
|
safe_restore(cycles, 'filter_type', cycles_settings.get('filter_type', 'GAUSSIAN'))
|
|
safe_restore(cycles, 'filter_width', cycles_settings.get('filter_width', 1.5))
|
|
cycles.pixel_filter_width = cycles_settings['pixel_filter_width']
|
|
cycles.use_persistent_data = cycles_settings['use_persistent_data']
|
|
safe_restore(cycles, 'debug_use_spatial_splits', cycles_settings.get('debug_use_spatial_splits', False))
|
|
safe_restore(cycles, 'debug_use_hair_bvh', cycles_settings.get('debug_use_hair_bvh', True))
|
|
safe_restore(cycles, 'debug_bvh_type', cycles_settings.get('debug_bvh_type', 'DYNAMIC_BVH'))
|
|
safe_restore(cycles, 'debug_use_compact_bvh', cycles_settings.get('debug_use_compact_bvh', True))
|
|
safe_restore(cycles, 'tile_size', cycles_settings.get('tile_size', 256))
|
|
safe_restore(cycles, 'use_auto_tile', cycles_settings.get('use_auto_tile', False))
|
|
safe_restore(cycles, 'progressive', cycles_settings.get('progressive', 'PATH'))
|
|
safe_restore(cycles, 'use_square_samples', cycles_settings.get('use_square_samples', False))
|
|
safe_restore(cycles, 'blur_glossy', cycles_settings.get('blur_glossy', 0.0))
|
|
safe_restore(cycles, 'use_transparent_shadows', cycles_settings.get('use_transparent_shadows', True))
|
|
safe_restore(cycles, 'volume_step_rate', cycles_settings.get('volume_step_rate', 1.0))
|
|
safe_restore(cycles, 'volume_preview_step_rate', cycles_settings.get('volume_preview_step_rate', 1.0))
|
|
safe_restore(cycles, 'volume_max_steps', cycles_settings.get('volume_max_steps', 1024))
|
|
|
|
print(f"ALL Cycles settings restoration completed")
|
|
|
|
# SCENE.EEVEE - Always restore EEVEE settings if available
|
|
if 'eevee' in original and original['eevee']:
|
|
eevee_settings = original['eevee']
|
|
eevee_attr = 'eevee' if hasattr(scene, 'eevee') else 'eevee_next'
|
|
eevee = getattr(scene, eevee_attr) if hasattr(scene, eevee_attr) else None
|
|
if eevee:
|
|
print(f"Restoring ALL EEVEE settings - samples: {eevee_settings.get('taa_render_samples', 'unknown')}")
|
|
|
|
# Restore ALL EEVEE settings comprehensively
|
|
safe_restore(eevee, 'taa_render_samples', eevee_settings.get('taa_render_samples', 64))
|
|
safe_restore(eevee, 'taa_samples', eevee_settings.get('taa_samples', 16))
|
|
safe_restore(eevee, 'use_bloom', eevee_settings.get('use_bloom', False))
|
|
safe_restore(eevee, 'bloom_threshold', eevee_settings.get('bloom_threshold', 0.8))
|
|
safe_restore(eevee, 'bloom_knee', eevee_settings.get('bloom_knee', 0.5))
|
|
safe_restore(eevee, 'bloom_radius', eevee_settings.get('bloom_radius', 6.5))
|
|
safe_restore(eevee, 'bloom_intensity', eevee_settings.get('bloom_intensity', 0.05))
|
|
safe_restore(eevee, 'use_ssr', eevee_settings.get('use_ssr', False))
|
|
safe_restore(eevee, 'use_ssr_refraction', eevee_settings.get('use_ssr_refraction', False))
|
|
safe_restore(eevee, 'ssr_max_roughness', eevee_settings.get('ssr_max_roughness', 0.5))
|
|
safe_restore(eevee, 'ssr_thickness', eevee_settings.get('ssr_thickness', 0.2))
|
|
safe_restore(eevee, 'ssr_border_fade', eevee_settings.get('ssr_border_fade', 0.075))
|
|
safe_restore(eevee, 'ssr_firefly_fac', eevee_settings.get('ssr_firefly_fac', 10.0))
|
|
safe_restore(eevee, 'use_motion_blur', eevee_settings.get('use_motion_blur', False))
|
|
safe_restore(eevee, 'motion_blur_samples', eevee_settings.get('motion_blur_samples', 8))
|
|
safe_restore(eevee, 'motion_blur_shutter', eevee_settings.get('motion_blur_shutter', 0.5))
|
|
safe_restore(eevee, 'use_volumetric_lights', eevee_settings.get('use_volumetric_lights', False))
|
|
safe_restore(eevee, 'volumetric_start', eevee_settings.get('volumetric_start', 0.1))
|
|
safe_restore(eevee, 'volumetric_end', eevee_settings.get('volumetric_end', 100.0))
|
|
safe_restore(eevee, 'volumetric_tile_size', eevee_settings.get('volumetric_tile_size', '8'))
|
|
safe_restore(eevee, 'volumetric_samples', eevee_settings.get('volumetric_samples', 64))
|
|
safe_restore(eevee, 'volumetric_sample_distribution', eevee_settings.get('volumetric_sample_distribution', 0.8))
|
|
safe_restore(eevee, 'use_volumetric_shadows', eevee_settings.get('use_volumetric_shadows', False))
|
|
safe_restore(eevee, 'volumetric_shadow_samples', eevee_settings.get('volumetric_shadow_samples', 16))
|
|
safe_restore(eevee, 'gi_diffuse_bounces', eevee_settings.get('gi_diffuse_bounces', 3))
|
|
safe_restore(eevee, 'gi_cubemap_resolution', eevee_settings.get('gi_cubemap_resolution', '512'))
|
|
safe_restore(eevee, 'gi_visibility_resolution', eevee_settings.get('gi_visibility_resolution', '16'))
|
|
safe_restore(eevee, 'gi_irradiance_smoothing', eevee_settings.get('gi_irradiance_smoothing', 0.1))
|
|
safe_restore(eevee, 'gi_glossy_clamp', eevee_settings.get('gi_glossy_clamp', 0.0))
|
|
safe_restore(eevee, 'gi_filter_quality', eevee_settings.get('gi_filter_quality', 1.0))
|
|
safe_restore(eevee, 'use_persistent_data', eevee_settings.get('use_persistent_data', False))
|
|
safe_restore(eevee, 'shadow_cube_size', eevee_settings.get('shadow_cube_size', '512'))
|
|
safe_restore(eevee, 'shadow_cascade_size', eevee_settings.get('shadow_cascade_size', '1024'))
|
|
safe_restore(eevee, 'use_shadow_high_bitdepth', eevee_settings.get('use_shadow_high_bitdepth', False))
|
|
safe_restore(eevee, 'use_soft_shadows', eevee_settings.get('use_soft_shadows', True))
|
|
safe_restore(eevee, 'use_shadows', eevee_settings.get('use_shadows', True))
|
|
safe_restore(eevee, 'light_threshold', eevee_settings.get('light_threshold', 0.01))
|
|
safe_restore(eevee, 'use_gtao', eevee_settings.get('use_gtao', False))
|
|
safe_restore(eevee, 'gtao_distance', eevee_settings.get('gtao_distance', 0.2))
|
|
safe_restore(eevee, 'gtao_factor', eevee_settings.get('gtao_factor', 1.0))
|
|
safe_restore(eevee, 'gtao_quality', eevee_settings.get('gtao_quality', 0.25))
|
|
safe_restore(eevee, 'use_overscan', eevee_settings.get('use_overscan', False))
|
|
safe_restore(eevee, 'overscan_size', eevee_settings.get('overscan_size', 3.0))
|
|
safe_restore(eevee, 'shadow_ray_count', eevee_settings.get('shadow_ray_count', 1))
|
|
safe_restore(eevee, 'shadow_step_count', eevee_settings.get('shadow_step_count', 6))
|
|
safe_restore(eevee, 'fast_gi_method', eevee_settings.get('fast_gi_method', 'GLOBAL_ILLUMINATION'))
|
|
safe_restore(eevee, 'fast_gi_ray_count', eevee_settings.get('fast_gi_ray_count', 4))
|
|
safe_restore(eevee, 'fast_gi_step_count', eevee_settings.get('fast_gi_step_count', 4))
|
|
safe_restore(eevee, 'fast_gi_quality', eevee_settings.get('fast_gi_quality', 0.25))
|
|
safe_restore(eevee, 'fast_gi_distance', eevee_settings.get('fast_gi_distance', 10.0))
|
|
|
|
print(f"ALL EEVEE settings restoration completed")
|
|
|
|
# SCENE.DISPLAY (WORKBENCH) - Always restore Workbench settings if available
|
|
if 'workbench' in original and original['workbench']:
|
|
workbench_settings = original['workbench']
|
|
print(f"Restoring ALL Workbench settings")
|
|
|
|
# Restore ALL Workbench settings comprehensively
|
|
scene.display.shading.type = workbench_settings['shading_type']
|
|
scene.display.shading.light = workbench_settings['light']
|
|
scene.display.shading.color_type = workbench_settings['color_type']
|
|
safe_restore(scene.display.shading, 'single_color', workbench_settings.get('single_color', (0.8, 0.8, 0.8)))
|
|
safe_restore(scene.display.shading, 'background_type', workbench_settings.get('background_type', 'THEME'))
|
|
safe_restore(scene.display.shading, 'background_color', workbench_settings.get('background_color', (0.05, 0.05, 0.05)))
|
|
safe_restore(scene.display.shading, 'cavity_ridge_factor', workbench_settings.get('cavity_ridge_factor', 1.0))
|
|
safe_restore(scene.display.shading, 'cavity_valley_factor', workbench_settings.get('cavity_valley_factor', 1.0))
|
|
safe_restore(scene.display.shading, 'curvature_ridge_factor', workbench_settings.get('curvature_ridge_factor', 1.0))
|
|
safe_restore(scene.display.shading, 'curvature_valley_factor', workbench_settings.get('curvature_valley_factor', 1.0))
|
|
safe_restore(scene.display, 'render_aa', workbench_settings.get('render_aa', 'FXAA'))
|
|
safe_restore(scene.display.shading, 'show_cavity', workbench_settings.get('show_cavity', False))
|
|
safe_restore(scene.display.shading, 'show_object_outline', workbench_settings.get('show_object_outline', False))
|
|
safe_restore(scene.display.shading, 'show_specular_highlight', workbench_settings.get('show_specular_highlight', True))
|
|
safe_restore(scene.display.shading, 'use_dof', workbench_settings.get('use_dof', False))
|
|
safe_restore(scene.display.shading, 'show_xray', workbench_settings.get('show_xray', False))
|
|
safe_restore(scene.display.shading, 'xray_alpha', workbench_settings.get('xray_alpha', 0.5))
|
|
safe_restore(scene.display.shading, 'show_shadows', workbench_settings.get('show_shadows', False))
|
|
safe_restore(scene.display.shading, 'shadow_intensity', workbench_settings.get('shadow_intensity', 0.5))
|
|
safe_restore(scene.display.shading, 'studio_light', workbench_settings.get('studio_light', 'DEFAULT'))
|
|
safe_restore(scene.display.shading, 'studiolight_rotate_z', workbench_settings.get('studiolight_rotate_z', 0.0))
|
|
safe_restore(scene.display.shading, 'studiolight_intensity', workbench_settings.get('studiolight_intensity', 1.0))
|
|
safe_restore(scene.display.shading, 'studiolight_background_alpha', workbench_settings.get('studiolight_background_alpha', 0.0))
|
|
safe_restore(scene.display.shading, 'studiolight_background_blur', workbench_settings.get('studiolight_background_blur', 0.0))
|
|
|
|
print(f"ALL Workbench settings restoration completed")
|
|
|
|
# Restore world if it exists
|
|
if 'world' in original and original['world']:
|
|
if original['world'] in bpy.data.worlds:
|
|
scene.world = bpy.data.worlds[original['world']]
|
|
else:
|
|
# If the exact world isn't found, create a default world
|
|
scene.world = bpy.data.worlds.new("Default")
|
|
elif 'world' in original and not original['world']:
|
|
# Original had no world
|
|
scene.world = None
|
|
|
|
|
|
|
|
# Restore any lights that were disabled
|
|
if hasattr(props, 'original_settings_extended') and props.original_settings_extended:
|
|
try:
|
|
# Convert the string back to a dictionary
|
|
light_states = ast.literal_eval(props.original_settings_extended)
|
|
|
|
# Restore each light's settings
|
|
for light_name, states in light_states.items():
|
|
if light_name in scene.objects:
|
|
light = scene.objects[light_name]
|
|
|
|
# Restore visibility states
|
|
light.hide_viewport = states['hide_viewport']
|
|
light.hide_render = states['hide_render']
|
|
light.visible_camera = states['visible_camera']
|
|
light.visible_diffuse = states['visible_diffuse']
|
|
light.visible_glossy = states['visible_glossy']
|
|
light.visible_transmission = states['visible_transmission']
|
|
light.visible_volume_scatter = states['visible_volume_scatter']
|
|
|
|
print(f"Restored light: {light_name}")
|
|
except Exception as e:
|
|
self.report({'WARNING'}, f"Could not restore light states: {str(e)}")
|
|
|
|
# Find 3D views and restore
|
|
for a in context.screen.areas:
|
|
if a.type == 'VIEW_3D':
|
|
# We don't store these per 3D view in the JSON, so just do a general reset
|
|
space = a.spaces.active
|
|
# Reset to solid (common default)
|
|
space.shading.type = 'SOLID'
|
|
# Enable overlays (common default)
|
|
space.overlay.show_overlays = True
|
|
|
|
# For any camera view, we'll reset
|
|
for region in a.regions:
|
|
if region.type == 'WINDOW':
|
|
region_3d = space.region_3d
|
|
if region_3d and region_3d.view_perspective == 'CAMERA':
|
|
# User might want perspective or ortho, but this is safer than leaving camera
|
|
region_3d.view_perspective = 'PERSP'
|
|
if hasattr(region_3d, 'use_local_camera'):
|
|
region_3d.use_local_camera = False
|
|
|
|
# Clear the stored original settings
|
|
props.original_settings = ""
|
|
if hasattr(props, 'original_settings_extended'):
|
|
props.original_settings_extended = ""
|
|
|
|
self.report({'INFO'}, "Original settings restored")
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error restoring settings: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
# UI Panel
|
|
class BPL_PT_main_panel(Panel):
|
|
bl_label = "BasedPlayblast"
|
|
bl_idname = "BPL_PT_main_panel"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "output"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
bl_order = 1 # This positions it right after the main Output panel (which has bl_order=0)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
scene = context.scene
|
|
props = scene.basedplayblast
|
|
|
|
# Main buttons - now integrated with output settings
|
|
row = layout.row(align=True)
|
|
row.scale_y = 1.5
|
|
row.operator("bpl.create_playblast", text="PLAYBLAST", icon='RENDER_ANIMATION')
|
|
row.operator("bpl.view_playblast", text="VIEW", icon='PLAY')
|
|
|
|
# Show progress if rendering
|
|
if props.is_rendering:
|
|
box = layout.box()
|
|
box.label(text=props.status_message)
|
|
box.prop(props, "render_progress", text="Progress", slider=True)
|
|
|
|
# Output settings - always visible
|
|
box = layout.box()
|
|
box.label(text="Output Settings")
|
|
|
|
# Output path with sync button
|
|
row = box.row(align=True)
|
|
row.prop(props, "output_path")
|
|
row.operator("bpl.sync_output_path", text="", icon='FILE_REFRESH')
|
|
|
|
# File name with sync button
|
|
row = box.row(align=True)
|
|
row.prop(props, "file_name")
|
|
row.operator("bpl.sync_file_name", text="", icon='FILE_REFRESH')
|
|
|
|
# MOVED BUTTONS: Add the settings apply/restore buttons here, after output settings
|
|
layout.separator()
|
|
|
|
# Settings apply/restore buttons
|
|
row = layout.row(align=True)
|
|
row.scale_y = 1.2
|
|
row.operator("bpl.apply_blast_settings", text="Apply Blast Render Settings", icon='GREASEPENCIL')
|
|
row.operator("bpl.restore_original_settings", text="Restore Original Settings", icon='LOOP_BACK')
|
|
|
|
# Properties - single collapsible section
|
|
props_box = layout.box()
|
|
row = props_box.row(align=True)
|
|
show_props = getattr(context.scene, "basedplayblast_show_properties", False)
|
|
row.prop(context.scene, "basedplayblast_show_properties", icon="TRIA_DOWN" if show_props else "TRIA_RIGHT", icon_only=True, emboss=False)
|
|
row.label(text="Properties")
|
|
row.operator("bpl.apply_user_defaults", text="", icon='PREFERENCES')
|
|
|
|
if show_props:
|
|
# 1. Display Mode
|
|
display_box = props_box.box()
|
|
display_box.label(text="Display Mode", icon='SHADING_RENDERED')
|
|
col = display_box.column(align=True)
|
|
col.prop(props, "display_mode", text="")
|
|
col.prop(props, "auto_disable_overlays")
|
|
col.prop(props, "enable_depth_of_field")
|
|
|
|
# 2. Frame Range
|
|
frame_range_box = props_box.box()
|
|
frame_range_box.label(text="Frame Range", icon='TIME')
|
|
col = frame_range_box.column(align=True)
|
|
col.prop(props, "use_scene_frame_range")
|
|
|
|
if not props.use_scene_frame_range:
|
|
row = col.row(align=True)
|
|
row.prop(props, "start_frame")
|
|
row.prop(props, "end_frame")
|
|
|
|
# 3. Resolution
|
|
resolution_box = props_box.box()
|
|
resolution_box.label(text="Resolution", icon='TEXTURE')
|
|
col = resolution_box.column(align=True)
|
|
col.prop(props, "resolution_mode", text="")
|
|
|
|
if props.resolution_mode == 'PRESET':
|
|
col.prop(props, "resolution_preset", text="")
|
|
elif props.resolution_mode == 'CUSTOM':
|
|
row = col.row(align=True)
|
|
row.prop(props, "resolution_x")
|
|
row.prop(props, "resolution_y")
|
|
|
|
col.prop(props, "resolution_percentage")
|
|
|
|
# 4. Format
|
|
format_box = props_box.box()
|
|
format_box.label(text="Format", icon='FILE_MOVIE')
|
|
col = format_box.column(align=True)
|
|
col.prop(props, "video_format", text="")
|
|
col.prop(props, "video_codec", text="")
|
|
|
|
# Custom FFmpeg arguments
|
|
col.prop(props, "use_custom_ffmpeg_args")
|
|
if props.use_custom_ffmpeg_args:
|
|
col.prop(props, "custom_ffmpeg_args", text="")
|
|
else:
|
|
col.prop(props, "video_quality", text="")
|
|
|
|
col.prop(props, "include_audio")
|
|
if props.include_audio:
|
|
row = col.row(align=True)
|
|
row.prop(props, "audio_codec", text="")
|
|
row.prop(props, "audio_bitrate")
|
|
|
|
# 5. Metadata
|
|
metadata_box = props_box.box()
|
|
metadata_box.label(text="Metadata", icon='TEXT')
|
|
col = metadata_box.column(align=True)
|
|
col.prop(props, "show_metadata", text="Show Metadata")
|
|
|
|
if props.show_metadata:
|
|
col.prop(props, "metadata_note", text="")
|
|
|
|
row = col.row(align=True)
|
|
row.prop(props, "metadata_date", toggle=True)
|
|
row.prop(props, "metadata_frame", toggle=True)
|
|
row.prop(props, "metadata_scene", toggle=True)
|
|
|
|
row = col.row(align=True)
|
|
row.prop(props, "metadata_camera", toggle=True)
|
|
row.prop(props, "metadata_lens", toggle=True)
|
|
row.prop(props, "metadata_resolution", toggle=True)
|
|
|
|
# Define the addon preferences class
|
|
class BPL_AddonPreferences(AddonPreferences):
|
|
bl_idname = __name__
|
|
|
|
default_video_quality: EnumProperty(
|
|
name="Default Video Quality",
|
|
description="Default quality setting for the add-on. This will be applied on file load.",
|
|
items=VIDEO_QUALITY_ITEMS,
|
|
default='PERC_LOSSLESS'
|
|
)
|
|
|
|
default_use_custom_ffmpeg_args: BoolProperty(
|
|
name="Enable Custom FFmpeg By Default",
|
|
description="Sets the default state for 'Use Custom FFmpeg Args' when applying user defaults.",
|
|
default=False
|
|
)
|
|
|
|
default_ffmpeg_args: StringProperty(
|
|
name="Default FFmpeg Arguments",
|
|
description="Default custom FFmpeg arguments for advanced users.",
|
|
default="-c:v h264_nvenc -preset fast -crf 0"
|
|
)
|
|
|
|
repo_initialized: BoolProperty(
|
|
name="Rainy's Extensions Added",
|
|
description="Internal flag to avoid re-adding Rainy's Extensions repository multiple times.",
|
|
default=False,
|
|
options={'HIDDEN'}
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.label(text="BasedPlayblast User Defaults")
|
|
box = layout.box()
|
|
box.prop(self, "default_video_quality")
|
|
box.prop(self, "default_use_custom_ffmpeg_args")
|
|
box.prop(self, "default_ffmpeg_args")
|
|
|
|
def on_load_post(dummy):
|
|
"""Applies user defaults after a file is loaded."""
|
|
# Using a timer ensures that the context is correct
|
|
def apply_defaults():
|
|
try:
|
|
bpy.ops.bpl.apply_user_defaults('EXEC_DEFAULT')
|
|
except Exception as e:
|
|
# This can fail if the operator is not ready, so fail silently
|
|
print(f"BasedPlayblast: Could not apply user defaults on load: {e}")
|
|
bpy.app.timers.register(apply_defaults, first_interval=0.1)
|
|
|
|
# Registration
|
|
classes = (
|
|
BPLProperties,
|
|
BPL_OT_create_playblast,
|
|
BPL_OT_view_playblast,
|
|
BPL_OT_view_latest_playblast,
|
|
BPL_OT_sync_output_path,
|
|
BPL_OT_sync_file_name,
|
|
BPL_OT_apply_user_defaults,
|
|
BPL_OT_apply_blast_settings,
|
|
BPL_OT_restore_original_settings,
|
|
BPL_PT_main_panel,
|
|
BPL_AddonPreferences,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.basedplayblast = PointerProperty(type=BPLProperties)
|
|
|
|
# Register property for collapsible properties section
|
|
bpy.types.Scene.basedplayblast_show_properties = BoolProperty(
|
|
name="Show Properties",
|
|
default=False
|
|
)
|
|
bpy.app.handlers.load_post.append(on_load_post)
|
|
ensure_rainys_extensions_repo()
|
|
|
|
def unregister():
|
|
# Safely remove handler if it exists
|
|
if on_load_post in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.remove(on_load_post)
|
|
|
|
# Unregister property for collapsible properties section
|
|
if hasattr(bpy.types.Scene, 'basedplayblast_show_properties'):
|
|
del bpy.types.Scene.basedplayblast_show_properties
|
|
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
if hasattr(bpy.types.Scene, 'basedplayblast'):
|
|
del bpy.types.Scene.basedplayblast
|
|
|
|
if __name__ == "__main__":
|
|
register() |