bpy.ops.object.bake('INVOKE_DEFAULT', type='DIFFUSE')
save_image()
Does not work because the bake uses 'INVOKE_DEFAULT'
and thus save_image
will be executed directly.
How to run save_image
after the baking has finished?
Although the question is old, can help more developers.
You can create a modal operator and check the state of the 'is_dirty' property of the image.
import bpy
class ModalTimerOperator(bpy.types.Operator):
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
_img = None
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
if self._img.is_dirty: # <--- Wait until the image is marked as dirty
self.finish(context)
return {'FINISHED'}
return {'PASS_THROUGH'}
def execute(self, context: bpy.context):
if context.scene.render.engine != 'CYCLES':
context.scene.render.engine = 'CYCLES'
if not context.active_object.select_get() or not context.active_object.type == 'MESH':
self.report({'WARNING'}, "No valid selected objects")
return {'FINISHED'}
obj = context.active_object
mats = obj.data.materials
mats_len = len(mats)
if mats_len == 0 or not mats[0]:
mat_new = bpy.data.materials.new('BakeMaterial')
if mats_len == 0:
mats.append(mat_new)
else:
mats[0] = mat_new
pass
mat = mats[0]
mat.use_nodes = True
nodes = mat.node_tree.nodes
tex_node = nodes.new('ShaderNodeTexImage')
tex_node.name = 'BakeNode'
tex_node.select = True
nodes.active = tex_node
self._img = bpy.data.images.new('BakeResult', 1024, 1024)
tex_node.image = self._img
self.report({'INFO'}, "Execute")
result = bpy.ops.object.bake('INVOKE_DEFAULT', type='DIFFUSE')
if result != {'RUNNING_MODAL'}: # <--- Important to check the result
self.report({'WARNING'}, "Failed to start baking")
return {'FINISHED'}
wm = context.window_manager
self._timer = wm.event_timer_add(0.5, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
self.report({'INFO'}, "Baking map cancelled")
def finish(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self.report({'INFO'}, "Baking map completed")
#save_image() # <--- Call the function after baking
def register():
bpy.utils.register_class(ModalTimerOperator)
def unregister():
bpy.utils.unregister_class(ModalTimerOperator)
if __name__ == "__main__":
register()
bpy.ops.wm.modal_timer_operator()
Thank you. Great script. As a side note, once the image data is baked, is_dity returns true. If you want to bake that image over again, you need to pack the image once. In
self._img.pack()
, the value of is_dirty returns false. Now you can create a modus operandi that bakes images on top of each other.
When overlapping bakes, scene settings should also be reviewed.
bpy.context.scene.render.bake.use_clear = False
To add another answer to an old question...
@Carneiro's solution worked really nicely for a single bake, but unfortunately I found that calling bpy.ops.object.bake
more than once in a for loop did not work with INVOKE_DEFAULT
, the first would work fine but the rest would fail. I also needed to dynamically define the number of bakes.
Thanks to a user called kaio in this thread (plus others) on the Blender dev talk forum I managed to piece together a solution using a macro instead, a feature that seems little documented.
I had setup code that needed to happen in the scene before each bake also, and I wanted to manually save the image afterwards and report the progress. Calling bpy.ops.object.bake(...)
from a macro operator did not work. I had to add OBJECT_OT_bake
as a step in the macro.
Below is a template approach that can run multiple steps in a macro, showing a toy example of baking at different margin settings. It's essentially a summarisation of kaio's code from that other thread, with the one major difference being that it doesn't need _bpy.ops.macro_define
from the hidden _bpy
module, as you can use .define
on the macro class (this may be new since 2019, I'm not sure). Leaving it here in case it helps someone!
from typing import Optional, Type
import bpy
class DEMOProperties(bpy.types.PropertyGroup):
progress: bpy.props.IntProperty(default=0)
total: bpy.props.IntProperty(default=0)
def get_macro() -> Type[bpy.types.Macro]:
# need to redefine the class to clear any previous steps assigned to the macro
class DEMO_OT_bake_macro(bpy.types.Macro):
bl_idname = "demo.bake_macro"
bl_label = "Bake Macro"
bl_options = {"INTERNAL", "MACRO"}
# unregister any previous macro
if hasattr(bpy.types, "DEMO_OT_bake_macro"):
bpy.utils.unregister_class(bpy.types.DEMO_OT_bake_macro)
bpy.utils.register_class(DEMO_OT_bake_macro)
return DEMO_OT_bake_macro
class DEMO_OT_bake_setup(bpy.types.Operator):
bl_idname = "demo.bake_setup"
bl_label = "Individual setup for bake"
bl_options = {"INTERNAL"}
out_path: bpy.props.StringProperty(subtype="DIR_PATH")
def execute(self, context):
# do before-bake scene setup
output_image = bpy.data.images.new("out_image", 1024, 1024)
output_image.filepath = str(self.out_path)
# ...
# insert into material, etc
# ...
return {"FINISHED"}
class DEMO_OT_post_bake(bpy.types.Operator):
bl_idname = "demo.post_bake"
bl_label = "Save previous bake, remove image for next time, handle progress"
bl_options = {"INTERNAL"}
def execute(self, context):
image = bpy.data.images["out_image"]
image.save()
bpy.data.images.remove(image, do_unlink=True)
context.scene.demo.progress += 1
return {"FINISHED"}
class DEMO_OT_transfer(bpy.types.Operator):
bl_idname = "demo.transfer"
bl_label = "Bake Images"
bl_description = "Set up and start a series of bakes"
bl_options = {"INTERNAL"}
_timer: Optional[bpy.types.Timer] = None
def modal(self, context, event):
if context.scene.demo.progress == context.scene.demo.total:
self.cancel(context)
return {"FINISHED"}
if event.type in {"RIGHTMOUSE", "ESC"}:
self.cancel(context)
return {"CANCELLED"}
return {"PASS_THROUGH"}
def cancel(self, context):
context.window_manager.event_timer_remove(self._timer)
self._timer = None
context.scene.demo.progress = 0
context.scene.demo.total = 0
def invoke(self, context, event):
# set up the macro
macro = get_macro()
margins = [4, 8, 16, 32]
context.scene.demo.progress = 0
context.scene.demo.total = len(margins)
out_folder = "./"
for margin in margins:
setup_step = macro.define("DEMO_OT_bake_setup")
setup_step.properties.out_path = out_folder + f"padding_{margin}.png"
bake_step = macro.define("OBJECT_OT_bake")
bake_step.properties.type = "DIFFUSE"
bake_step.properties.save_mode = "INTERNAL"
bake_step.properties.margin = margin
save_and_remove_step = macro.define("DEMO_OT_post_bake")
# start the macro
bpy.ops.demo.bake_macro("INVOKE_DEFAULT")
# need a timer to kick off the macro and clean up after it's finished
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def register():
bpy.utils.register_class(DEMOProperties)
bpy.utils.register_class(DEMO_OT_bake_setup)
bpy.utils.register_class(DEMO_OT_post_bake)
bpy.utils.register_class(DEMO_OT_transfer)
bpy.types.Scene.demo = bpy.props.PointerProperty(type=DEMOProperties)
def unregister():
bpy.utils.unregister_class(DEMOProperties)
bpy.utils.unregister_class(DEMO_OT_bake_setup)
bpy.utils.unregister_class(DEMO_OT_post_bake)
bpy.utils.unregister_class(DEMO_OT_transfer)
del bpy.types.Scene.demo
render_post
norrender_complete
is triggered $\endgroup$