0

I am trying to write an addon that will give me an ability to set settings for multiple render sequences. I used to use bpy.ops.render.render(animation=True, write_still=True) function but it freezes the UI, then I used bpy.ops.render.render('INVOKE_DEFAULT', animation=True, write_still=True) but It does not work in a loop (cause it skips the order and renders the last ops call)

So I decided to use Modal Operator call to check if the render is active then do not call next render operator otherwise change the settings and call operator. It seems to work but I just need your expert advice on how to make it better, less buggy.

If you can test it in your scenario or let me know what might need some fix I will be more than glad to implement it!

import bpy

class RenderSeqOperator(bpy.types.Operator): bl_idname = "object.render_seq_operator" bl_label = "Render Sequence Operator"

index = 0
list_of_renders = []

def set_render_settings(self, rig_name, scene_name, cam_name, track_name, frame_start, frame_end, output_path):
    # Get the armature object
    if not rig_name:
        print("No armature selected!")
        return False

    armature_obj = bpy.data.objects[rig_name]
    if armature_obj.type != 'ARMATURE':
        print("Selected object is not an armature!")
        return False

    # Get the scene by name
    scene = bpy.data.scenes.get(scene_name)
    if scene is None:
        print(f"Scene '{scene_name}' not found!")
        return False

    # Set the current scene
    bpy.context.window.scene = scene

    # Mute all tracks
    for track in armature_obj.animation_data.nla_tracks:
        track.mute = True

    # Activate the specified track
    track_index = armature_obj.animation_data.nla_tracks.find(track_name)
    if track_index == -1:
        print(f"Track '{track_name}' not found!")
        return False

    armature_obj.animation_data.nla_tracks[track_index].mute = False

    #set the camera active
    cam_obj = bpy.data.objects[cam_name]
    if cam_obj.type != 'CAMERA':
        print("Selected object is not a camera!")
        return False
    bpy.context.scene.camera = cam_obj


    # Set the render frame range
    bpy.context.scene.frame_start = frame_start
    bpy.context.scene.frame_end = frame_end
    #bpy.context.scene.cycles.samples = samples
    bpy.context.scene.render.fps = 12
    bpy.context.scene.render.use_file_extension = True
    bpy.context.scene.render.image_settings.color_mode = 'RGBA'

    # Set the render output path
    bpy.context.scene.render.filepath = output_path

    return True

def modal(self, context, event):
    if event.type in {'ESC'}:
        return {'FINISHED'}

    if not bpy.app.is_job_running('RENDER'):
        if self.index < len(self.list_of_renders):
            #set render settings
            rig_name = self.list_of_renders[self.index][0]
            scene_name = self.list_of_renders[self.index][1]
            cam_name = self.list_of_renders[self.index][2]
            track_name = self.list_of_renders[self.index][3]
            frame_start = self.list_of_renders[self.index][4]
            frame_end = self.list_of_renders[self.index][5]
            output_path = self.list_of_renders[self.index][6]

            if not self.set_render_settings(rig_name, scene_name, cam_name, track_name, frame_start, frame_end, output_path):
                return {'FINISHED'}
            # Call another instance of render with animation
            bpy.ops.render.render('INVOKE_DEFAULT', animation=True, write_still=True)
            RenderSeqOperator.index += 1
        else:
            print("Nothing to Render!")
            return {'FINISHED'}

    return {'RUNNING_MODAL'}

def invoke(self, context, event):
    #set list of renders
    self.list_of_renders.append(["m2_rig", "m2","up", "walk_up", 0, 15, "C:/walk/up/"])
    self.list_of_renders.append(["m2_rig", "m2","down", "walk_down", 0, 15, "C:/walk/down/"])
    self.list_of_renders.append(["m2_rig", "m2","down", "idle_down", 0, 23, "C:/idle/down/"])
    #add modal
    context.window_manager.modal_handler_add(self)
    return {'RUNNING_MODAL'}

Register the operator

def menu_func(self, context): self.layout.operator(RenderSeqOperator.bl_idname)

def register(): bpy.utils.register_class(RenderSeqOperator) bpy.types.VIEW3D_MT_mesh_add.append(menu_func)

def unregister(): bpy.utils.unregister_class(RenderSeqOperator) bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)

if name == "main": register() # test call bpy.ops.object.render_seq_operator('INVOKE_DEFAULT')

quellenform
  • 35,177
  • 10
  • 50
  • 133

1 Answers1

0

I found another solution to my problem right here Is it possible to make a sequence of renders and give the user the option to cancel the process through the UI at any given time?

It uses timers to check in modal function if render is complete or not

import bpy

class Multi_Render(bpy.types.Operator): """Docstring""" bl_idname = "render.multi" bl_label = "Render multiple times"

# Define some variables to register
_timer = None
shots = []
stop = None
rendering = None

#render settings function
def set_render_settings(self, rig_name, scene_name, cam_name, track_name, frame_start, frame_end, output_path):
    # Get the armature object
    if not rig_name:
        print("No armature selected!")
        return False

    armature_obj = bpy.data.objects[rig_name]
    if armature_obj.type != 'ARMATURE':
        print("Selected object is not an armature!")
        return False

    # Get the scene by name
    scene = bpy.data.scenes.get(scene_name)
    if scene is None:
        print(f"Scene '{scene_name}' not found!")
        return False

    # Set the current scene
    bpy.context.window.scene = scene

    # Mute all tracks
    for track in armature_obj.animation_data.nla_tracks:
        track.mute = True

    # Activate the specified track
    track_index = armature_obj.animation_data.nla_tracks.find(track_name)
    if track_index == -1:
        print(f"Track '{track_name}' not found!")
        return False

    armature_obj.animation_data.nla_tracks[track_index].mute = False

    #set the camera active
    cam_obj = bpy.data.objects[cam_name]
    if cam_obj.type != 'CAMERA':
        print("Selected object is not a camera!")
        return False
    bpy.context.scene.camera = cam_obj


    # Set the render frame range
    bpy.context.scene.frame_start = frame_start
    bpy.context.scene.frame_end = frame_end
    #bpy.context.scene.cycles.samples = samples
    bpy.context.scene.render.fps = 12
    bpy.context.scene.render.use_file_extension = True
    bpy.context.scene.render.image_settings.color_mode = 'RGBA'

    # Set the render output path
    bpy.context.scene.render.filepath = output_path

    return True

# Define the handler functions. I use pre and
# complete to know if Blender "is rendering"
def pre(self, scene, context=None):
    self.rendering = True

def complete(self, scene, context=None):
    self.shots.pop(0) # This is just to render the next
                      # image in another path
    self.rendering = False

def cancelled(self, scene, context=None):
    self.stop = True

def execute(self, context):
    # Define the variables during execution. This allows
    # to define when called from a button
    self.stop = False
    self.rendering = False
    #set list of renders
    self.shots.append(["m2_rig", "m2","up", "walk_up", 0, 15, "C:/Users/mihail.lebedev/Downloads/VIZOR/PROJECTS/Atlantis/keepers/render/test/m2/walk/up/"])
    self.shots.append(["m2_rig", "m2","down", "walk_down", 0, 15, "C:/Users/mihail.lebedev/Downloads/VIZOR/PROJECTS/Atlantis/keepers/render/test/m2/walk/down/"])
    self.shots.append(["m2_rig", "m2","down", "idle_down", 0, 23, "C:/Users/mihail.lebedev/Downloads/VIZOR/PROJECTS/Atlantis/keepers/render/test/m2/idle/down/"])

    context.scene.render.filepath = self.path

    bpy.app.handlers.render_pre.append(self.pre)
    bpy.app.handlers.render_complete.append(self.complete)
    bpy.app.handlers.render_cancel.append(self.cancelled)

    # The timer gets created and the modal handler
    # is added to the window manager
    self._timer = context.window_manager.event_timer_add(0.5, window=context.window)
    context.window_manager.modal_handler_add(self)

    return {"RUNNING_MODAL"}

def modal(self, context, event):
    if event.type == 'TIMER': # This event is signaled every half a second
                              # and will start the render if available

        # If cancelled or no more shots to render, finish.
        if True in (not self.shots, self.stop is True): 

            # We remove the handlers and the modal timer to clean everything
            bpy.app.handlers.render_pre.remove(self.pre)
            bpy.app.handlers.render_complete.remove(self.complete)
            bpy.app.handlers.render_cancel.remove(self.cancelled)
            context.window_manager.event_timer_remove(self._timer)

            return {"FINISHED"} # I didn't separate the cancel and finish
                                # events, because in my case I don't need to,
                                # but you can create them as you need

        elif self.rendering is False: # Nothing is currently rendering.
                                      # Proceed to render.
            #set render settings
            rig_name = self.shots[0][0]
            scene_name = self.shots[0][1]
            cam_name = self.shots[0][2]
            track_name = self.shots[0][3]
            frame_start = self.shots[0][4]
            frame_end = self.shots[0][5]
            output_path = self.shots[0][6]

            self.set_render_settings(rig_name, scene_name, cam_name, track_name, frame_start, frame_end, output_path)
            bpy.ops.render.render("INVOKE_DEFAULT", animation=True, write_still=True)

    return {"PASS_THROUGH"}
    # This is very important! If we used "RUNNING_MODAL", this new modal function
    # would prevent the use of the X button to cancel rendering, because this
    # button is managed by the modal function of the render operator,
    # not this new operator!

def register(): bpy.utils.register_class(Multi_Render)

def unregister(): bpy.utils.unregister_class(Multi_Render)

if name == "main": register()

bpy.ops.render.multi() # Test call