I am running baking operation for blender through python script but its taking a lot of time due to update image operations running for same mesh. (total time 23 mins)
I need to optimize the process as same scene takes <5mins in the GUI to bake.
The thing is I run blender in background because I don't need any GUI.
Here is the snippet which helps me prepare mesh and bake.
def process_meshes_for_baking(output_dir, args):
"""
Process and bake each mesh individually, without using collections.
Uses batched operations to minimize synchronization issues.
Args:
output_dir: Directory where the output files will be saved
args: Arguments from parse_args() function
Returns:
bool: True if successful
"""
all_meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH' and obj.visible_get() and not obj.hide_render]
print(f"\n=== Found {len(all_meshes)} meshes to process ===")
metallic_materials_exist = handle_metallic_materials(output_dir)
textures_dir = os.path.join(output_dir, "textures")
if not os.path.exists(textures_dir):
os.makedirs(textures_dir)
prepared_meshes = []
for mesh in all_meshes:
print(f"\n=== Preparing Mesh: {mesh.name} ===")
if mesh.get("skipped_lightmap"):
print(f" Skipping {mesh.name} - marked to skip: {mesh.get('skip_reason', 'unknown')}")
continue
is_glass = False
for slot in mesh.material_slots:
if slot.material and is_glass_material(slot.material):
is_glass = True
break
if is_glass:
print(f" Skipping glass object: {mesh.name}")
continue
irradiance_img = prepare_mesh_revised_batch(mesh, output_dir, args)
if not irradiance_img:
print(f"Failed to prepare mesh: {mesh.name}")
continue
prepared_meshes.append((mesh, irradiance_img))
print(f"\n=== Batch assigning images to {len(prepared_meshes)} meshes ===")
batch_assign_images_to_materials(prepared_meshes)
baked_blend_path = os.path.join(output_dir, "after_prepare_mesh_revised.blend")
save_blender_file(baked_blend_path)
print(f"Saved prepared state to {baked_blend_path}")
print(f"\n=== Batch baking {len(prepared_meshes)} meshes ===")
batch_bake_meshes(prepared_meshes, output_dir, args)
connect_material_nodes_after_baking_revised()
baked_blend_path = os.path.join(output_dir, "reconnected.blend")
save_blender_file(baked_blend_path)
restore_metallic_materials(output_dir)
switch_off_all_lights(debug=False)
return True
def batch_assign_images_to_materials(prepared_meshes):
"""
Batch assign images to all material nodes to avoid synchronization issues.
Args:
prepared_meshes: List of (mesh, irradiance_img) tuples
"""
print(" Batch assigning images to avoid sync issues...")
for mesh, irradiance_img in prepared_meshes:
for slot in mesh.material_slots:
if not slot.material:
continue
mat = slot.material
if not mat.use_nodes:
continue
nodes = mat.node_tree.nodes
for node in nodes:
if node.type == 'TEX_IMAGE' and ('Irradiance' in node.name or 'Irradiance' in node.label):
node.image = irradiance_img
print(f" Assigned image to {node.name} in {mat.name}")
def batch_bake_meshes(prepared_meshes, output_dir, args):
"""
Batch bake all meshes to minimize synchronization overhead.
Args:
prepared_meshes: List of (mesh, irradiance_img) tuples
output_dir: Output directory
args: Command line arguments
"""
print(" Starting batch baking process...")
bpy.ops.object.select_all(action='DESELECT')
for mesh, _ in prepared_meshes:
mesh.select_set(True)
if prepared_meshes:
bpy.context.view_layer.objects.active = prepared_meshes[0][0]
meshes = [mesh for mesh, _ in prepared_meshes]
select_texture_nodes_for_baking(meshes, texture_type='irradiance')
print(" Batch baking irradiance textures...")
successful_bakes = []
i = 0
for mesh, irradiance_img in prepared_meshes:
print(f" Baking irradiance for {mesh.name}")
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh
irradiance_success = run_cycles_bake(
bake_type='IRRADIANCE',
samples=int(args.samples),
margin=int(args.margin_px),
use_denoising=True,
MAX_BOUNCES=int(args.max_bounces),
DIFFUSE_BOUNCES=int(args.diffuse_bounces),
output_dir=output_dir,
image=irradiance_img
)
if irradiance_success:
save_texture(irradiance_img, irradiance_img.name, os.path.join(output_dir, "textures"), ['PNG'])
print(f" ✅ Saved irradiance texture for {mesh.name}")
save_texture(irradiance_img, irradiance_img.name, os.path.join(output_dir, "textures"), ['HDR'])
print(f" ✅ Saved HDR irradiance texture for {mesh.name}")
successful_bakes.append(irradiance_img)
else:
print(f" ❌ Irradiance baking failed for {mesh.name}")
if successful_bakes:
print(f" Batch updating {len(successful_bakes)} images...")
batch_update_images(successful_bakes)
print(f" Batch denoising {len(successful_bakes)} textures...")
batch_denoise_textures(successful_bakes, output_dir)
print(" Batch baking complete")
def run_cycles_bake(bake_type, samples, margin=4, use_denoising=True, debug=True, MAX_BOUNCES=12, DIFFUSE_BOUNCES=12, output_dir=None, image=None):
"""
Runs Cycles baking process for objects with active texture nodes.
Args:
bake_type (str): Type of bake to perform ('ATLAS' for diffuse color, 'IRRADIANCE' for lighting)
samples (int): Number of render samples for baking
margin (int): Bake margin in pixels
use_denoising (bool): Enable denoising for baking
debug (bool): Whether to print debug information
MAX_BOUNCES (int): Maximum light bounces
DIFFUSE_BOUNCES (int): Maximum diffuse bounces
output_dir (str): Directory to save baked textures
image (bpy.types.Image): The image to bake into and save
Returns:
bool: True if baking completed successfully
"""
start_time = time.time()
if debug:
print(f"\n=== Starting Cycles Bake: {bake_type} ===")
if hasattr(bpy.context.preferences.addons['cycles'], 'preferences'):
cycles_prefs = bpy.context.preferences.addons['cycles'].preferences
if hasattr(cycles_prefs, 'compute_device_type'):
cycles_prefs.compute_device_type = 'OPTIX'
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.device = 'GPU'
bpy.context.scene.cycles.samples = samples
bpy.context.view_layer.cycles.denoising_store_passes = True
bpy.context.scene.cycles.use_preview_denoising = False
bpy.context.scene.cycles.use_denoising = False
bpy.context.scene.cycles.caustics_reflective = False
bpy.context.scene.cycles.caustics_refractive = False
bpy.context.scene.render.bake.use_clear = False
bpy.context.scene.render.bake.margin = margin
bpy.context.scene.render.bake.margin_type = 'ADJACENT_FACES'
bpy.context.scene.cycles.max_bounces = MAX_BOUNCES # Increased from 12
bpy.context.scene.cycles.diffuse_bounces = DIFFUSE_BOUNCES # Increased from 12
bpy.context.scene.cycles.glossy_bounces = 4
bpy.context.scene.cycles.transmission_bounces = 1
bpy.context.scene.cycles.transparent_max_bounces = 8
bpy.context.scene.cycles.filter_width = 3
if bake_type == 'ATLAS':
bpy.context.scene.render.bake.use_pass_direct = False
bpy.context.scene.render.bake.use_pass_indirect = False
bpy.context.scene.render.bake.use_pass_color = True
elif bake_type == 'IRRADIANCE':
bpy.context.scene.render.bake.use_pass_direct = True
bpy.context.scene.render.bake.use_pass_indirect = True
bpy.context.scene.render.bake.use_pass_color = False
bpy.context.scene.render.bake.use_selected_to_active = False
selected_objects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
if debug:
print(f"Found {len(selected_objects)} selected mesh objects for baking")
print(f"Bake settings for {bake_type}:")
print(f" - use_pass_direct: {bpy.context.scene.render.bake.use_pass_direct}")
print(f" - use_pass_indirect: {bpy.context.scene.render.bake.use_pass_indirect}")
print(f" - use_pass_color: {bpy.context.scene.render.bake.use_pass_color}")
print(f" - bounce settings: max={bpy.context.scene.cycles.max_bounces}, diffuse={bpy.context.scene.cycles.diffuse_bounces}")
if not selected_objects:
print("Error: No objects selected for baking.")
return False
try:
if image and output_dir:
if bake_type == 'ATLAS':
filepath = os.path.join(output_dir, f"{bake_type}_Texture.png")
image.filepath_raw = filepath
image.colorspace_settings.name = 'sRGB'
if debug:
print(f"Set target filepath: {filepath}")
elif bake_type == 'IRRADIANCE':
filepath = os.path.join(output_dir, f"{bake_type}_Texture.exr")
image.filepath_raw = filepath
image.colorspace_settings.name = 'Linear Rec.709'
if debug:
print(f"Set target filepath: {filepath}")
if debug:
print(f"Starting DIFFUSE bake with {samples} samples and {margin}px margin...")
bpy.ops.object.bake(type='DIFFUSE', save_mode='EXTERNAL')
time.sleep(1)
if image:
if not image.packed_file:
image.pack()
if output_dir:
if debug:
print(f"Saving {bake_type} texture immediately after baking...")
if bake_type == 'ATLAS':
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.image_settings.color_depth = '16'
bpy.context.scene.render.image_settings.compression = 15
filepath = os.path.join(output_dir, f"{bake_type}_Texture.png")
image.filepath_raw = filepath
image.colorspace_settings.name = 'sRGB'
try:
image.save_render(filepath, scene=bpy.context.scene)
print(f"✅ Saved {bake_type} texture to {filepath}")
except Exception as e:
print(f"❌ Failed to save {bake_type} texture: {e}")
elif bake_type == 'IRRADIANCE':
bpy.context.scene.render.image_settings.file_format = 'PNG'
filepath_png = os.path.join(output_dir, f"{bake_type}_Texture.png")
image.filepath_raw = filepath_png
image.colorspace_settings.name = 'Linear Rec.709'
try:
image.save_render(filepath_png, scene=bpy.context.scene)
print(f"✅ Saved {bake_type} PNG texture to {filepath_png}")
except Exception as e:
print(f"❌ Failed to save {bake_type} PNG texture: {e}")
bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR'
bpy.context.scene.render.image_settings.color_depth = '32'
filepath_exr = os.path.join(output_dir, f"{bake_type}_Texture.exr")
image.filepath_raw = filepath_exr
try:
image.save_render(filepath_exr, scene=bpy.context.scene)
print(f"✅ Saved {bake_type} EXR texture to {filepath_exr}")
except Exception as e:
print(f"❌ Failed to save {bake_type} EXR texture: {e}")
if debug:
print(f"Baking completed successfully in {time.time() - start_time:.2f} seconds")
return True
except Exception as e:
print(f"Error during baking: {e}")
return False
finally:
if debug:
print(f"\n=== Baking process finished ===")
print(f"Total time: {time.time() - start_time:.2f} seconds")
i believe this is the part causing unecessary syncronization and updating logs
for node in nodes:
if node.type == 'TEX_IMAGE' and ('Irradiance' in node.name or 'Irradiance' in node.label):
node.image = irradiance_img
print(f" Assigned image to {node.name} in {mat.name}")
