2
$\begingroup$
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?

$\endgroup$
5
  • $\begingroup$ Don't invoke default? If you are after user input could write an operator or macro to invoke and feed arguments into bake op. $\endgroup$
    – batFINGER
    Commented Mar 21, 2020 at 9:48
  • $\begingroup$ But this freezes the UI and no progress bar is shown $\endgroup$ Commented Mar 21, 2020 at 10:00
  • $\begingroup$ @RobertGützkow sounds good, but I can't find a handler for "bake_post" or something similar $\endgroup$ Commented Mar 21, 2020 at 11:00
  • $\begingroup$ @RobertGützkow unfortunately, neither render_post nor render_complete is triggered $\endgroup$ Commented Mar 21, 2020 at 11:31
  • 1
    $\begingroup$ The options to save the baked image externally doesn't appear to work either. I'll put it on my to do list. $\endgroup$ Commented Mar 21, 2020 at 12:21

3 Answers 3

6
$\begingroup$

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()
$\endgroup$
0
$\begingroup$

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
$\endgroup$
0
$\begingroup$

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
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .