0
$\begingroup$

I have been trying to get a custom render button to work. The purpose is to render all markers in a timeline. There are examples that use handlers but I could not make it work properly:

import bpy
import os
import time
from bpy.types import Operator, Panel

class Marker_OT_Render(bpy.types.Operator):
    bl_idname = "render.markers"
    bl_label = "Render markers"

    def execute(self, context):
        # Variable to continue or stop rendering
        complete = False
        rendering = True

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

        #for filenaming 
        itemmarkers = bpy.context.scene.timeline_markers.items()
        rendername = []
        rendername.clear
        for m in itemmarkers:
            rendername.append(m[0])

        #render limit verification
        maxframe = bpy.context.scene.frame_end + 1

        #position frame for the modal script
        bpy.context.scene.frame_set(bpy.context.scene.frame_start - 1)

        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if complete == False:
            if rendering == False:   
                sc = bpy.context.scene
                sc.frame_set(sc.frame_current + 1)
                if sc.frame_current != maxframe:
                    sc.render.filepath = os.path.join(output_path, rendername[sc.frame_current] + ".png")
                    bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)
                else:
                     complete = True
                return {"PASS_THROUGH"}
            else:
                time.sleep(5)
                return {"PASS_THROUGH"}
        else:
            return {"FINISHED"}



class Render_PT_Buttontest(Panel): 

    bl_label = "Render Button Test" 
    bl_idname = "SCENE_PT_RENDERBUTTON" 
    bl_category = "Tool"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout 

        col = layout.column(align=True)
        row = layout.row() 
        row.operator('render.markers', text="Render Button")
        layout.separator()

def register():
    bpy.utils.register_class(Marker_OT_Render)
    bpy.utils.register_class(Render_PT_Buttontest)

def unregister():
    bpy.utils.unregister_class(Marker_OT_Render)
    bpy.utils.unregister_class(Render_PT_Buttontest)

if __name__ == "__main__":
    register()

Q: Is there any way to get the render window when rendering these frames in the timeline?

$\endgroup$
  • 2
    $\begingroup$ In general a tricky topic so: Why do you need that extra operator at all? What's your ultimate goal? Rendering all markers in the timeline? Might be helpful: blender.stackexchange.com/a/153254/31447 Also it would be nice answering our questions from yesterday... $\endgroup$ – brockmann Apr 17 '20 at 10:22
  • $\begingroup$ The end goal to get a script that works in a panel with other functions. And yes, the purpose is to render all markers in a timeline. The script you linked to has a error. It keeps looping the render of the same frame and I cannot stop it. Maybe am using it wrong. What question from yesterday are you referring too?? $\endgroup$ – Sharl Apr 17 '20 at 10:41
  • $\begingroup$ I don't know where it comes from. I was trying to see it there were additional render options to 2.82 in the python console. There it was.Have you have the function in your version? I haven't figured out if it is a plugin. $\endgroup$ – Sharl Apr 17 '20 at 11:07
  • $\begingroup$ Yes, I need the usual render window. Because other users will be using it. Since the rendering can take 10mins for each frame, people need to know that the render is in progress. The way blender freeze is not useful. $\endgroup$ – Sharl Apr 17 '20 at 11:41
2
$\begingroup$

The whole picture

Currently there is no way executing the render process in the background using bpy.ops.render.render() and even worse, the operator doesn't provide any arguments to pass a frame or the output path. It is like it is, and hopefully someone is going to implement that in the near future.

The limitations aside, we need to know whether the user started the rendering, if the user interrupted the process and if the rendering is finished to get the usual behavior of the render window, in any case.

In order to catch these 'states' we can wrap the render operator into a modal operator and use some of the available render handlers to get the information we need. The modal also keeps the UI responsive (non-blocking) and we can trigger a function in a very short (unnoticeable) interval by adding a timer to get the user interaction and handle all other events.

Read: 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?


Render camera markers in the timeline

To get the usual behavior and window for rendering the markers in the timeline, I would suggest the following changes to the operator of the linked answer:

enter image description here Modal operator which allows to render all camera markers (base color of the shader is keyframed)

Assign the marker references to the shot variable in the execute method. That's simple enough and you still have access to all marker properties in the modal method:

def execute(self, context):
    # Define the variables during execution. This allows
    # to define when called from a button
    self.stop = False
    self.rendering = False

    # Get all markers in a list and assign them to shot variable
    self.shots = [m for m in context.scene.timeline_markers if m.camera is not None]
    ...

Read the first element of the shot-list to get TimeLineMarker.frame and TimeLineMarker.camera attribute in order to set the frame and assemble the output file path based on that:

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        if self.shots:  
            # Get the first marker in the list
            marker = self.shots[0]
            frame_number = marker.frame
            camera_name = marker.camera.name

            # Assemble the file path 
            # -> output path + camera name + frame
            scn.render.filepath = os.path.join(
                        self.path, 
                        "{}_{}.jpg".format(camera_name, frame_number))

            # Set the frame
            scn.frame_set(frame_number)

            # Call the render operator
            bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)                           

return {"PASS_THROUGH"}

Finally reset the output path in case the user interrupts the rendering:

if event.type == 'TIMER':
    scn = context.scene

    # No frames in the list or user hits ESC
    if True in (not self.shots, self.stop is True):

        ...

        # Reset the output path
        scn.render.filepath = self.path
        return {"FINISHED"}

Note: As of 2.81+ you'll have to add new depsgraph arguments to all handler methods:

# Set up the handler methods
def pre(self, scene, depsgraph):
    self.rendering = True

def post(self, scene, depsgraph):
    self.shots.pop(0)
    self.rendering = False

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

Otherwise the setup will cause an infinite loop for whatever reason.

$\endgroup$
  • $\begingroup$ Thanks, the breakdown is very helpful. First question, why does the script render frame 1 only? Wouldn't it be best to render each frame then take the marker of that frame regardless of the camera name. Anyway, i'll learn from what you have post and try to adopt it to my needs. Many Thanks $\endgroup$ – Sharl Apr 20 '20 at 11:52
  • $\begingroup$ As you can see from the trusty gif the operator actually renders all frames which have a marker and a camera assigned to them. If the rendering is finished for that frame the post handler is called to remove that frame from the list and the next first item of the list is going to be rendered. What do you mean by "each frame of the camera name..."? The bare minimum is the frame number to save the image properly. However, for convenience I think it's nice to know which camera was used to render the frame (that's a common reason why using markers at all). Please explain what else @Sharl $\endgroup$ – brockmann Apr 20 '20 at 12:20
  • $\begingroup$ The script renders each marker yes, but the renders stays on frame 1. For example, we a cube that changes color on each frame, Red, Yellow, Green, Purple and Blue. With a marker for the corresponding color. The since the script renders only the Red cube. For the Camera, I only need one Camera. I'll post my modification. Should I post it here or as a new question/answer? $\endgroup$ – Sharl Apr 20 '20 at 13:02
  • $\begingroup$ I guess you're just missing a tiny detail. Suggest upload a basic scene, add that to your question: blend-exchange.giantcowfilms.com and I'll take a look @Sharl $\endgroup$ – brockmann Apr 20 '20 at 13:04
  • $\begingroup$ Tested the operator re the colors and it's working fine: i.stack.imgur.com/4xK9d.png Make sure the scene and material is set up properly by adding the keyframes to the shader (basecolor property in my case): i.stack.imgur.com/y6Fn7.png @Sharl $\endgroup$ – brockmann Apr 20 '20 at 13:28
0
$\begingroup$

So this a response to the answer. My progress report :)

The main difference is that I am working with one camera and I am not be the end user.

Secondly, the user should be able to - render part of the timeline with the start and end frame - add additional text to the beginning and end of the filename

The start is pretty much the same.

class RenderMarkers_OT_LIST(bpy.types.Operator):
    bl_idname = "render.colorways"
    bl_label = "Render list"
    bl_description = "Render colorways with marker in filename"

    # Define some variables to register
    _timer = None
    shots = None
    stop = None
    rendering = None
    path = "/tmp/"

    # Set up the handler methods
    def pre(self, scene, depsgraph):
        self.rendering = True

    def post(self, scene, depsgraph):
        #self.shots.pop(0)
        self.rendering = False

    def cancelled(self, scene, depsgraph):
        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

I changed the self.shots list to include only the string information of the marker

        # Get all markers in a list and assign them to shots variable
        itemmarkers = bpy.context.scene.timeline_markers.items()
        self.shots = []
        self.shots.clear
        for m in itemmarkers:
            self.shots.append(m[0])

        #don't know why but I need to begin 1 frame before the start
        bpy.context.scene.frame_set(bpy.context.scene.frame_start - 1)

        self.path = context.scene.render.filepath

        bpy.app.handlers.render_pre.append(self.pre)
        bpy.app.handlers.render_post.append(self.post)
        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"}

the main difference is the filenaming code

    def modal(self, context, event):
        if event.type == 'TIMER':
            scn = context.scene

            # filename prefix ans suffix
            firstpart = bpy.context.scene.TextInputProps.nameprefix
            lastpart = bpy.context.scene.TextInputProps.namesuffix

            # No frames in the list or user hits ESC
            if True in (scn.frame_current >= scn.frame_end, 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_post.remove(self.post)
                bpy.app.handlers.render_cancel.remove(self.cancelled)
                context.window_manager.event_timer_remove(self._timer)

                # Reset the output path
                scn.render.filepath = self.path
                return {"FINISHED"}

            elif self.rendering is False: # Nothing is currently rendering.
                                          # Proceed to render.
                # Get the marker from the list
                marker_name  = self.shots[scn.frame_current]

                #Create filename variable
                if firstpart == "" and lastpart == "":
                    finalfilename = marker_name
                elif firstpart == "" and lastpart != "":
                    finalfilename = marker_name +"_"+ lastpart
                elif firstpart != "" and lastpart == "":
                    finalfilename = firstpart +"_"+ marker_name
                else:
                    finalfilename = firstpart +"_"+ marker_name +"_"+ lastpart

                # Assemble the file path and filename
                scn.render.filepath = os.path.join(self.path, finalfilename + "." + scn.render.image_settings.file_format)

                # Call the render operator
                bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

Because I don't link the camera to the markers, the code needs to jump one frame

                # Set the frame
                scn.frame_set(scn.frame_current + 1)

        return {"PASS_THROUGH"}

I am sure I can simplify the code some more. But it is a good start for me.....

$\endgroup$
  • $\begingroup$ Consider, that this is not a regular forum and you've posted your progress into the answer section of this site, I don't think that's useful for future visitors. A few beginner mistakes too: You already have a context reference in operator methods so you don't need bpy.context.scene*, just write context.scene*for all references instead. And again, the marker has a frame attribute, use it. You don't need scn.render.image_settings.file_format, use .jpg it's just a placeholder and will be overwritten. Next, to test a string you don't need ==, variable names should be lower_case. $\endgroup$ – brockmann Apr 22 '20 at 9:07
  • $\begingroup$ How to check if the string is empty? $\endgroup$ – brockmann Apr 22 '20 at 9:09
  • $\begingroup$ The error comes from using bpy.context.scene.frame_set(bpy.context.scene.frame_start - 1) - wrong references, see the comment above. Again to make my operator work for only one camera, get all markers self.shots = [m for m in context.scene.timeline_markers] and set the name for the camera to something like: camera_name = context.scene.camera.name (name of the scene camera) or just remove the variable, that's waaaayyy easier than your frame_current-approach. $\endgroup$ – brockmann Apr 22 '20 at 9:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.