Files
jiggablend/pkg/scripts/scripts/render_blender.py.template

654 lines
31 KiB
Plaintext

import bpy
import sys
import os
import json
# Make all file paths relative to the blend file location FIRST
# This must be done immediately after file load, before any other operations
# to prevent Blender from trying to access external files with absolute paths
try:
bpy.ops.file.make_paths_relative()
print("Made all file paths relative to blend file")
except Exception as e:
print(f"Warning: Could not make paths relative: {e}")
# Auto-enable addons from blender_addons folder in context
# Supports .zip files (installed via Blender API) and already-extracted addons
blend_dir = os.path.dirname(bpy.data.filepath) if bpy.data.filepath else os.getcwd()
addons_dir = os.path.join(blend_dir, "blender_addons")
if os.path.isdir(addons_dir):
print(f"Found blender_addons folder: {addons_dir}")
for item in os.listdir(addons_dir):
item_path = os.path.join(addons_dir, item)
try:
if item.endswith('.zip'):
# Install and enable zip addon using Blender's API
bpy.ops.preferences.addon_install(filepath=item_path)
# Get module name from zip (usually the folder name inside)
import zipfile
with zipfile.ZipFile(item_path, 'r') as zf:
# Find the top-level module name
names = zf.namelist()
if names:
module_name = names[0].split('/')[0]
if module_name.endswith('.py'):
module_name = module_name[:-3]
bpy.ops.preferences.addon_enable(module=module_name)
print(f" Installed and enabled addon: {module_name}")
elif item.endswith('.py') and not item.startswith('__'):
# Single-file addon
bpy.ops.preferences.addon_install(filepath=item_path)
module_name = item[:-3]
bpy.ops.preferences.addon_enable(module=module_name)
print(f" Installed and enabled addon: {module_name}")
elif os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, '__init__.py')):
# Multi-file addon directory - add to path and enable
if addons_dir not in sys.path:
sys.path.insert(0, addons_dir)
bpy.ops.preferences.addon_enable(module=item)
print(f" Enabled addon: {item}")
except Exception as e:
print(f" Error with addon {item}: {e}")
else:
print(f"No blender_addons folder found at: {addons_dir}")
{{UNHIDE_CODE}}
# Read output format from file (created by Go code)
format_file_path = {{FORMAT_FILE_PATH}}
output_format_override = None
if os.path.exists(format_file_path):
try:
with open(format_file_path, 'r') as f:
output_format_override = f.read().strip().upper()
print(f"Read output format from file: '{output_format_override}'")
except Exception as e:
print(f"Warning: Could not read output format file: {e}")
else:
print(f"Warning: Output format file does not exist: {format_file_path}")
# Read render settings from JSON file (created by Go code)
render_settings_file = {{RENDER_SETTINGS_FILE}}
render_settings_override = None
if os.path.exists(render_settings_file):
try:
with open(render_settings_file, 'r') as f:
render_settings_override = json.load(f)
print(f"Loaded render settings from job metadata")
except Exception as e:
print(f"Warning: Could not read render settings file: {e}")
# Get current scene settings (preserve blend file preferences)
scene = bpy.context.scene
current_engine = scene.render.engine
current_device = scene.cycles.device if hasattr(scene, 'cycles') and scene.cycles else None
current_output_format = scene.render.image_settings.file_format
print(f"Blend file render engine: {current_engine}")
if current_device:
print(f"Blend file device setting: {current_device}")
print(f"Blend file output format: {current_output_format}")
# Override output format if specified
# The format file always takes precedence (it's written specifically for this job)
if output_format_override:
print(f"Overriding output format from '{current_output_format}' to '{output_format_override}'")
# Map common format names to Blender's format constants
# For video formats, we render as appropriate frame format first
format_to_use = output_format_override.upper()
if format_to_use in ['EXR_264_MP4', 'EXR_AV1_MP4', 'EXR_VP9_WEBM']:
format_to_use = 'EXR' # Render as EXR for EXR video formats
format_map = {
'PNG': 'PNG',
'JPEG': 'JPEG',
'JPG': 'JPEG',
'EXR': 'OPEN_EXR',
'OPEN_EXR': 'OPEN_EXR',
'TARGA': 'TARGA',
'TIFF': 'TIFF',
'BMP': 'BMP',
}
blender_format = format_map.get(format_to_use, format_to_use)
try:
scene.render.image_settings.file_format = blender_format
print(f"Successfully set output format to: {blender_format}")
except Exception as e:
print(f"Warning: Could not set output format to {blender_format}: {e}")
print(f"Using blend file's format: {current_output_format}")
else:
print(f"Using blend file's output format: {current_output_format}")
# Apply render settings from job metadata if provided
# Note: output_format is NOT applied from render_settings_override - it's already set from format file above
if render_settings_override:
engine_override = render_settings_override.get('engine', '').upper()
engine_settings = render_settings_override.get('engine_settings', {})
# Switch engine if specified
if engine_override and engine_override != current_engine.upper():
print(f"Switching render engine from '{current_engine}' to '{engine_override}'")
try:
scene.render.engine = engine_override
current_engine = engine_override
print(f"Successfully switched to {engine_override} engine")
except Exception as e:
print(f"Warning: Could not switch engine to {engine_override}: {e}")
print(f"Using blend file's engine: {current_engine}")
# Apply engine-specific settings
if engine_settings:
if current_engine.upper() == 'CYCLES':
cycles = scene.cycles
print("Applying Cycles render settings from job metadata...")
for key, value in engine_settings.items():
try:
if hasattr(cycles, key):
setattr(cycles, key, value)
print(f" Set Cycles.{key} = {value}")
else:
print(f" Warning: Cycles has no attribute '{key}'")
except Exception as e:
print(f" Warning: Could not set Cycles.{key} = {value}: {e}")
elif current_engine.upper() in ['EEVEE', 'EEVEE_NEXT']:
eevee = scene.eevee
print("Applying EEVEE render settings from job metadata...")
for key, value in engine_settings.items():
try:
if hasattr(eevee, key):
setattr(eevee, key, value)
print(f" Set EEVEE.{key} = {value}")
else:
print(f" Warning: EEVEE has no attribute '{key}'")
except Exception as e:
print(f" Warning: Could not set EEVEE.{key} = {value}: {e}")
# Apply resolution if specified
if 'resolution_x' in render_settings_override:
try:
scene.render.resolution_x = render_settings_override['resolution_x']
print(f"Set resolution_x = {render_settings_override['resolution_x']}")
except Exception as e:
print(f"Warning: Could not set resolution_x: {e}")
if 'resolution_y' in render_settings_override:
try:
scene.render.resolution_y = render_settings_override['resolution_y']
print(f"Set resolution_y = {render_settings_override['resolution_y']}")
except Exception as e:
print(f"Warning: Could not set resolution_y: {e}")
# Only override device selection if using Cycles (other engines handle GPU differently)
if current_engine == 'CYCLES':
# Check if CPU rendering is forced
force_cpu = False
if render_settings_override and render_settings_override.get('force_cpu'):
force_cpu = render_settings_override.get('force_cpu', False)
print("Force CPU rendering is enabled - skipping GPU detection")
# Ensure Cycles addon is enabled
try:
if 'cycles' not in bpy.context.preferences.addons:
bpy.ops.preferences.addon_enable(module='cycles')
print("Enabled Cycles addon")
except Exception as e:
print(f"Warning: Could not enable Cycles addon: {e}")
# If CPU is forced, skip GPU detection and set CPU directly
if force_cpu:
scene.cycles.device = 'CPU'
print("Forced CPU rendering (skipping GPU detection)")
else:
# Access Cycles preferences
prefs = bpy.context.preferences
try:
cycles_prefs = prefs.addons['cycles'].preferences
except (KeyError, AttributeError):
try:
cycles_addon = prefs.addons.get('cycles')
if cycles_addon:
cycles_prefs = cycles_addon.preferences
else:
raise Exception("Cycles addon not found")
except Exception as e:
print(f"ERROR: Could not access Cycles preferences: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Check all devices and choose the best GPU type
# Device type preference order (most performant first)
device_type_preference = ['OPTIX', 'CUDA', 'HIP', 'ONEAPI', 'METAL']
gpu_available = False
best_device_type = None
best_gpu_devices = []
devices_by_type = {} # {device_type: [devices]}
seen_device_ids = set() # Track device IDs to avoid duplicates
print("Checking for GPU availability...")
# Try to get all devices - try each device type to see what's available
for device_type in device_type_preference:
try:
cycles_prefs.compute_device_type = device_type
cycles_prefs.refresh_devices()
# Get devices for this type
devices = None
if hasattr(cycles_prefs, 'devices'):
try:
devices_prop = cycles_prefs.devices
if devices_prop:
devices = list(devices_prop) if hasattr(devices_prop, '__iter__') else [devices_prop]
except Exception as e:
pass
if not devices or len(devices) == 0:
try:
devices = cycles_prefs.get_devices()
except Exception as e:
pass
if devices and len(devices) > 0:
# Categorize devices by their type attribute, avoiding duplicates
for device in devices:
if hasattr(device, 'type'):
device_type_str = str(device.type).upper()
device_id = getattr(device, 'id', None)
# Use device ID to avoid duplicates (same device appears when checking different compute_device_types)
if device_id and device_id in seen_device_ids:
continue
if device_id:
seen_device_ids.add(device_id)
if device_type_str not in devices_by_type:
devices_by_type[device_type_str] = []
devices_by_type[device_type_str].append(device)
except (ValueError, AttributeError, KeyError, TypeError):
# Device type not supported, continue
continue
except Exception as e:
# Other errors - log but continue
print(f" Error checking {device_type}: {e}")
continue
# Print what we found
print(f"Found devices by type: {list(devices_by_type.keys())}")
for dev_type, dev_list in devices_by_type.items():
print(f" {dev_type}: {len(dev_list)} device(s)")
for device in dev_list:
device_name = getattr(device, 'name', 'Unknown')
print(f" - {device_name}")
# Choose the best GPU type based on preference
for preferred_type in device_type_preference:
if preferred_type in devices_by_type:
gpu_devices = [d for d in devices_by_type[preferred_type] if preferred_type in ['CUDA', 'OPENCL', 'OPTIX', 'HIP', 'METAL', 'ONEAPI']]
if gpu_devices:
best_device_type = preferred_type
best_gpu_devices = [(d, preferred_type) for d in gpu_devices]
print(f"Selected {preferred_type} as best GPU type with {len(gpu_devices)} device(s)")
break
# Second pass: Enable the best GPU we found
if best_device_type and best_gpu_devices:
print(f"\nEnabling GPU devices for {best_device_type}...")
try:
# Set the device type again
cycles_prefs.compute_device_type = best_device_type
cycles_prefs.refresh_devices()
# First, disable all CPU devices to ensure only GPU is used
print(f" Disabling CPU devices...")
all_devices = cycles_prefs.devices if hasattr(cycles_prefs, 'devices') else cycles_prefs.get_devices()
if all_devices:
for device in all_devices:
if hasattr(device, 'type') and str(device.type).upper() == 'CPU':
try:
device.use = False
device_name = getattr(device, 'name', 'Unknown')
print(f" Disabled CPU: {device_name}")
except Exception as e:
print(f" Warning: Could not disable CPU device {getattr(device, 'name', 'Unknown')}: {e}")
# Enable all GPU devices
enabled_count = 0
for device, device_type in best_gpu_devices:
try:
device.use = True
enabled_count += 1
device_name = getattr(device, 'name', 'Unknown')
print(f" Enabled: {device_name}")
except Exception as e:
print(f" Warning: Could not enable device {getattr(device, 'name', 'Unknown')}: {e}")
# Enable ray tracing acceleration for supported device types
try:
if best_device_type == 'HIP':
# HIPRT (HIP Ray Tracing) for AMD GPUs
if hasattr(cycles_prefs, 'use_hiprt'):
cycles_prefs.use_hiprt = True
print(f" Enabled HIPRT (HIP Ray Tracing) for faster rendering")
elif hasattr(scene.cycles, 'use_hiprt'):
scene.cycles.use_hiprt = True
print(f" Enabled HIPRT (HIP Ray Tracing) for faster rendering")
else:
print(f" HIPRT not available (requires Blender 4.0+)")
elif best_device_type == 'OPTIX':
# OptiX is already enabled when using OPTIX device type
# But we can check if there are any OptiX-specific settings
if hasattr(scene.cycles, 'use_optix_denoising'):
scene.cycles.use_optix_denoising = True
print(f" Enabled OptiX denoising")
print(f" OptiX ray tracing is active (using OPTIX device type)")
elif best_device_type == 'CUDA':
# CUDA can use OptiX if available, but it's usually automatic
# Check if we can prefer OptiX over CUDA
if hasattr(scene.cycles, 'use_optix_denoising'):
scene.cycles.use_optix_denoising = True
print(f" Enabled OptiX denoising (if OptiX available)")
print(f" CUDA ray tracing active")
elif best_device_type == 'METAL':
# MetalRT for Apple Silicon (if available)
if hasattr(scene.cycles, 'use_metalrt'):
scene.cycles.use_metalrt = True
print(f" Enabled MetalRT (Metal Ray Tracing) for faster rendering")
elif hasattr(cycles_prefs, 'use_metalrt'):
cycles_prefs.use_metalrt = True
print(f" Enabled MetalRT (Metal Ray Tracing) for faster rendering")
else:
print(f" MetalRT not available")
elif best_device_type == 'ONEAPI':
# Intel oneAPI - Embree might be available
if hasattr(scene.cycles, 'use_embree'):
scene.cycles.use_embree = True
print(f" Enabled Embree for faster CPU ray tracing")
print(f" oneAPI ray tracing active")
except Exception as e:
print(f" Could not enable ray tracing acceleration: {e}")
print(f"SUCCESS: Enabled {enabled_count} GPU device(s) for {best_device_type}")
gpu_available = True
except Exception as e:
print(f"ERROR: Failed to enable GPU devices: {e}")
import traceback
traceback.print_exc()
# Set device based on availability (prefer GPU, fallback to CPU)
if gpu_available:
scene.cycles.device = 'GPU'
print(f"Using GPU for rendering (blend file had: {current_device})")
# Auto-enable GPU denoising when using GPU (OpenImageDenoise supports all GPUs)
try:
view_layer = bpy.context.view_layer
if hasattr(view_layer, 'cycles') and hasattr(view_layer.cycles, 'denoising_use_gpu'):
view_layer.cycles.denoising_use_gpu = True
print("Auto-enabled GPU denoising (OpenImageDenoise)")
except Exception as e:
print(f"Could not auto-enable GPU denoising: {e}")
else:
scene.cycles.device = 'CPU'
print(f"GPU not available, using CPU for rendering (blend file had: {current_device})")
# Ensure GPU denoising is disabled when using CPU
try:
view_layer = bpy.context.view_layer
if hasattr(view_layer, 'cycles') and hasattr(view_layer.cycles, 'denoising_use_gpu'):
view_layer.cycles.denoising_use_gpu = False
print("Using CPU denoising")
except Exception as e:
pass
# Verify device setting
if current_engine == 'CYCLES':
final_device = scene.cycles.device
print(f"Final Cycles device: {final_device}")
else:
# For other engines (EEVEE, etc.), respect blend file settings
print(f"Using {current_engine} engine - respecting blend file settings")
# Enable GPU acceleration for EEVEE viewport rendering (if using EEVEE)
if current_engine == 'EEVEE' or current_engine == 'EEVEE_NEXT':
try:
if hasattr(bpy.context.preferences.system, 'gpu_backend'):
bpy.context.preferences.system.gpu_backend = 'OPENGL'
print("Enabled OpenGL GPU backend for EEVEE")
except Exception as e:
print(f"Could not set EEVEE GPU backend: {e}")
# Enable GPU acceleration for compositing (if compositing is enabled)
try:
if scene.use_nodes and hasattr(scene, 'node_tree') and scene.node_tree:
if hasattr(scene.node_tree, 'use_gpu_compositing'):
scene.node_tree.use_gpu_compositing = True
print("Enabled GPU compositing")
except Exception as e:
print(f"Could not enable GPU compositing: {e}")
# CRITICAL: Initialize headless rendering to prevent black images
# This ensures the render engine is properly initialized before rendering
print("Initializing headless rendering context...")
try:
# Ensure world exists and has proper settings
if not scene.world:
# Create a default world if none exists
world = bpy.data.worlds.new("World")
scene.world = world
print("Created default world")
# Ensure world has a background shader (not just black)
if scene.world:
# Enable nodes if not already enabled
if not scene.world.use_nodes:
scene.world.use_nodes = True
print("Enabled world nodes")
world_nodes = scene.world.node_tree
if world_nodes:
# Find or create background shader
bg_shader = None
for node in world_nodes.nodes:
if node.type == 'BACKGROUND':
bg_shader = node
break
if not bg_shader:
bg_shader = world_nodes.nodes.new(type='ShaderNodeBackground')
# Connect to output
output = world_nodes.nodes.get('World Output')
if not output:
output = world_nodes.nodes.new(type='ShaderNodeOutputWorld')
output.name = 'World Output'
if output and bg_shader:
# Connect background to surface input
if 'Surface' in output.inputs and 'Background' in bg_shader.outputs:
world_nodes.links.new(bg_shader.outputs['Background'], output.inputs['Surface'])
print("Created background shader for world")
# Ensure background has some color (not pure black)
if bg_shader:
# Only set if it's pure black (0,0,0)
if hasattr(bg_shader.inputs, 'Color'):
color = bg_shader.inputs['Color'].default_value
if len(color) >= 3 and color[0] == 0.0 and color[1] == 0.0 and color[2] == 0.0:
# Set to a very dark gray instead of pure black
bg_shader.inputs['Color'].default_value = (0.01, 0.01, 0.01, 1.0)
print("Adjusted world background color to prevent black renders")
else:
# Fallback: use legacy world color if nodes aren't working
if hasattr(scene.world, 'color'):
color = scene.world.color
if len(color) >= 3 and color[0] == 0.0 and color[1] == 0.0 and color[2] == 0.0:
scene.world.color = (0.01, 0.01, 0.01)
print("Adjusted legacy world color to prevent black renders")
# For EEVEE, force viewport update to initialize render engine
if current_engine in ['EEVEE', 'EEVEE_NEXT']:
# Force EEVEE to update its internal state
try:
# Update depsgraph to ensure everything is initialized
depsgraph = bpy.context.evaluated_depsgraph_get()
if depsgraph:
# Force update
depsgraph.update()
print("Forced EEVEE depsgraph update for headless rendering")
except Exception as e:
print(f"Warning: Could not force EEVEE update: {e}")
# Ensure EEVEE settings are applied
try:
# Force a material update to ensure shaders are compiled
for obj in scene.objects:
if obj.type == 'MESH' and obj.data.materials:
for mat in obj.data.materials:
if mat and mat.use_nodes:
# Touch the material to force update
mat.use_nodes = mat.use_nodes
print("Forced material updates for EEVEE")
except Exception as e:
print(f"Warning: Could not update materials: {e}")
# For Cycles, ensure proper initialization
if current_engine == 'CYCLES':
# Ensure samples are set (even if 1 for preview)
if not hasattr(scene.cycles, 'samples') or scene.cycles.samples < 1:
scene.cycles.samples = 1
print("Set minimum Cycles samples")
# Check for lights in the scene
lights = [obj for obj in scene.objects if obj.type == 'LIGHT']
print(f"Found {len(lights)} light(s) in scene")
if len(lights) == 0:
print("WARNING: No lights found in scene - rendering may be black!")
print(" Consider adding lights or ensuring world background emits light")
# Ensure world background emits light (critical for Cycles)
if scene.world and scene.world.use_nodes:
world_nodes = scene.world.node_tree
if world_nodes:
bg_shader = None
for node in world_nodes.nodes:
if node.type == 'BACKGROUND':
bg_shader = node
break
if bg_shader:
# Check and set strength - Cycles needs this to emit light!
if hasattr(bg_shader.inputs, 'Strength'):
strength = bg_shader.inputs['Strength'].default_value
if strength <= 0.0:
bg_shader.inputs['Strength'].default_value = 1.0
print("Set world background strength to 1.0 for Cycles lighting")
else:
print(f"World background strength: {strength}")
# Also ensure color is not pure black
if hasattr(bg_shader.inputs, 'Color'):
color = bg_shader.inputs['Color'].default_value
if len(color) >= 3 and color[0] == 0.0 and color[1] == 0.0 and color[2] == 0.0:
bg_shader.inputs['Color'].default_value = (1.0, 1.0, 1.0, 1.0)
print("Set world background color to white for Cycles lighting")
# Check film_transparent setting - if enabled, background will be transparent/black
if hasattr(scene.cycles, 'film_transparent') and scene.cycles.film_transparent:
print("WARNING: film_transparent is enabled - background will be transparent")
print(" If you see black renders, try disabling film_transparent")
# Force Cycles to update/compile materials and shaders
try:
# Update depsgraph to ensure everything is initialized
depsgraph = bpy.context.evaluated_depsgraph_get()
if depsgraph:
depsgraph.update()
print("Forced Cycles depsgraph update")
# Force material updates to ensure shaders are compiled
for obj in scene.objects:
if obj.type == 'MESH' and obj.data.materials:
for mat in obj.data.materials:
if mat and mat.use_nodes:
# Force material update
mat.use_nodes = mat.use_nodes
print("Forced Cycles material updates")
except Exception as e:
print(f"Warning: Could not force Cycles updates: {e}")
# Verify device is actually set correctly
if hasattr(scene.cycles, 'device'):
actual_device = scene.cycles.device
print(f"Cycles device setting: {actual_device}")
if actual_device == 'GPU':
# Try to verify GPU is actually available
try:
prefs = bpy.context.preferences
cycles_prefs = prefs.addons['cycles'].preferences
devices = cycles_prefs.devices
enabled_devices = [d for d in devices if d.use]
if len(enabled_devices) == 0:
print("WARNING: GPU device set but no GPU devices are enabled!")
print(" Falling back to CPU may cause issues")
except Exception as e:
print(f"Could not verify GPU devices: {e}")
# Ensure camera exists and is active
if scene.camera is None:
# Find first camera in scene
for obj in scene.objects:
if obj.type == 'CAMERA':
scene.camera = obj
print(f"Set active camera: {obj.name}")
break
print("Headless rendering initialization complete")
except Exception as e:
print(f"Warning: Headless rendering initialization had issues: {e}")
import traceback
traceback.print_exc()
# Final verification before rendering
print("\n=== Pre-render verification ===")
try:
scene = bpy.context.scene
print(f"Render engine: {scene.render.engine}")
print(f"Active camera: {scene.camera.name if scene.camera else 'None'}")
if scene.render.engine == 'CYCLES':
print(f"Cycles device: {scene.cycles.device}")
print(f"Cycles samples: {scene.cycles.samples}")
lights = [obj for obj in scene.objects if obj.type == 'LIGHT']
print(f"Lights in scene: {len(lights)}")
if scene.world:
if scene.world.use_nodes:
world_nodes = scene.world.node_tree
if world_nodes:
bg_shader = None
for node in world_nodes.nodes:
if node.type == 'BACKGROUND':
bg_shader = node
break
if bg_shader:
if hasattr(bg_shader.inputs, 'Strength'):
strength = bg_shader.inputs['Strength'].default_value
print(f"World background strength: {strength}")
if hasattr(bg_shader.inputs, 'Color'):
color = bg_shader.inputs['Color'].default_value
print(f"World background color: ({color[0]:.2f}, {color[1]:.2f}, {color[2]:.2f})")
else:
print("World exists but nodes are disabled")
else:
print("WARNING: No world in scene!")
print("=== Verification complete ===\n")
except Exception as e:
print(f"Warning: Verification failed: {e}")
print("Device configuration complete - blend file settings preserved, device optimized")
sys.stdout.flush()