10
$\begingroup$

I'm am currently writing a script that changes a node property (cycles value node) for fast switching between different materiallayers. I also want to be able to render multiple materiallayers into a folder automatically. The problem is that while I render the multiple images, either the UI freezes (it's rendering fine but while it does, blender doesn't respond) or when I use

py.ops.render.render('INVOKE_DEFAULT', write_still=True)

only the last image I called gets rendered (in the UI without freezing).

Here is an image thats shows what the script does

enter image description here

and the full code of the renderscript

import bpy
from bpy.props import *
#ml_filepath = 'C:/Users/render/Pictures/test'

def renderLayer(context, layer):    #renders
    scn = bpy.context.scene
    ml_filepath = '//{}/'.format(scn['ml_renderfolder_name'])   #generate filepath
    name = bpy.context.scene['mln{}'.format(layer)] #get name for the image
    bpy.data.node_groups["MLN Layercontroll"].nodes["layer"].outputs[0].default_value = layer #set materiallayer
    bpy.context.scene.render.filepath = ml_filepath + scn['ml_renderfolder_name'] + '_ml{}_{}'.format(layer, name) #generate filename
    bpy.ops.render.render(write_still=True)  #renders layer - ui freeze while rendering
    #bpy.ops.render.render('INVOKE_DEFAULT', write_still=True) #only the last layer gets rendered :(

def initSceneProperties(scn):       #initiert renderliste
    scn['ml_renderlist'] = "1,2,3,4,5"
    scn['ml_renderfolder_name'] = "mlRender"

    bpy.types.Scene.ml_renderlist = StringProperty(name = "")    
    bpy.types.Scene.ml_renderfolder_name = StringProperty(name = "")    

initSceneProperties(bpy.context.scene)

class MlRenderPanel(bpy.types.Panel):           #Panel
    bl_label = "ML Render"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "render"

    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene       
        layout.label(text="choose materiallayers to render:")  
        layout.prop(scn, 'ml_renderlist')
        layout.label(text="choose foldername:")
        layout.prop(scn, 'ml_renderfolder_name')
        layout.operator("render.mlrender")
        layout.label(text="UI will freeze while rendering")

class MlRenderOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "render.mlrender"
    bl_label = "render materiallayers"

    def execute(self, context):
        scn = bpy.context.scene
        list = scn['ml_renderlist'].split(",")  #splits renderlist string into list
        listInts = map(int, list)

        for number in listInts:            #calls renderlayer() for every number in the renderlist
            renderLayer(context, number)

        return {'FINISHED'}


bpy.utils.register_module(__name__)

also here is the code for switching between materiallayers in viewport and renaming them. Probably not necessary for this but I better just post it.

import bpy
from bpy.props import *
layerRange = 5   #Range of layers


# set value in nodegroup "MLN LAYERCONTROLL" to layer property-------------
def execute(self, context):
    scn = context.scene
    val = scn['materiallayer']
    layerName = scn['layerName']
    bpy.data.node_groups["MLN Layercontroll"].nodes["layer"].outputs[0].default_value = val
    print("set materiallayer to",val)

    currentname = scn['mln{}'.format(val)]          #rename textfield when switching between layers
    scn.layerName = currentname

    for i in range(0, layerRange):                  #rename nodeinputs
        print("named nodegroup input",i)
        newname = scn['mln{}'.format(i+1)]
        bpy.data.node_groups["MLN x5"].inputs[i].name = newname

#raname node inputs + properties   
def renameNodeInputs(self, context):               
    scn = context.scene
    textfieldName = scn['layerName']
    val = scn['materiallayer']
    scn['mln{}'.format(val)] = textfieldName        #rename layername property


    for i in range(0, layerRange):                  #alle inputs benennen
        print("named nodegroup input",i)
        newname = scn['mln{}'.format(i+1)]
        bpy.data.node_groups["MLN x5"].inputs[i].name = newname

#initialize scene properties
def initSceneProperties(scn):
    scn['materiallayer'] = 1
    bpy.types.Scene.materiallayer = IntProperty(                  #current Materiallayer (number)
        name = "materiallayer", update = execute,
        description = "choose materiallayer",
        min = 1, max = layerRange,)

    bpy.types.Scene.layerName = StringProperty(name = "Name",
    update = renameNodeInputs)                                   #layername

    for i in range(0, layerRange):
            scn['mln{}'.format(i+1)] = "materiallayer {}".format(i+1)   #create layernamen
            print("created mln property")
#

    layername = bpy.context.scene.layerName
    test =  scn['materiallayer'] 
    scn["layerName"] = layername

initSceneProperties(bpy.context.scene)

#Panel in UI region
class ML_Panel(bpy.types.Panel):
    bl_label = "Materiallayer"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'materiallayer', icon='BLENDER')
        layout.prop(scn, 'layerName')


#Registration
bpy.utils.register_module(__name__)

sorry when my code is a total mess - I just started learning python and bpy.

tldr: How can I render multiple images from a for loop without freezing the UI while rendering?

$\endgroup$
5
  • $\begingroup$ Are you rendering animations or stills? If stills you could make an operator to animate your value node and render your "material layers" as animation. If it's an animation you will probably have to run blender render in separate thread. $\endgroup$ Commented Nov 2, 2016 at 13:54
  • $\begingroup$ Yeah i thought about that for stills - but i want to render animations with it too. I will try this separate thread thing but i guess it still won't show the UI for seing the rendering progress. $\endgroup$
    – Simon S.
    Commented Nov 2, 2016 at 14:41
  • 1
    $\begingroup$ Then this is related: blender.stackexchange.com/questions/1050/…. You could have a custom progress bar somewhere and update it from handler or modal operator based on the progress of rendering in the other thread. $\endgroup$ Commented Nov 2, 2016 at 17:25
  • 1
    $\begingroup$ I suggest scripting this through rendring an animation where there is proper support for scripting multiple renders together. Using the frame change handler you can set up the materials between renders and you retain responsive UI with native progress bars. OFC this would mean you can't really render actual keyframed animations with the script. $\endgroup$
    – kheetor
    Commented Nov 3, 2016 at 7:14
  • $\begingroup$ does the INVOKE_DEFAULT cause the last layer to get rendered even if you eliminate the loop over layers and only call it once for the first layer? Is it possible that because INVOKE_DEFAULT makes the render call non-blocking, you're telling blender to start the next render before the previous one is finished, so every one of them tried to start, but only the last one finished? $\endgroup$ Commented Apr 30, 2017 at 6:25

2 Answers 2

6
$\begingroup$

If you want to render multiple images in a row (without rendering an animation), you need to run a separate thread that renders the scene.

Here an example to have a stoppable thread (it's a bit more complicated but quite useful).

import bpy
import threading
class StoppableThread(threading.Thread):
    """Thread class with a stop() method. The thread itself has to check
    regularly for the stopped() condition."""
    def __init__(self):
        super(StoppableThread, self).__init__()
        self._stop = threading.Event()
    def stop(self):
        self._stop.set()
    def stopped(self):
        return self._stop.isSet()

class RenderStop(bpy.types.Operator):
    """Stop the rendering"""
    bl_idname = "render.stop"
    bl_label = "Stop the rendering"

    def execute(self, context):
        threads = threading.enumerate()
        for th in threads:
            if th.name == 'render':
                th.stop()

        def draw(self, context):
            self.layout.label("Render will be stop after the current frame finishes")
        context.window_manager.popup_menu(draw, title="Stop Render", icon='ERROR')
        return {'FINISHED'}


def render():
    start = bpy.context.scene.frame_start
    end = bpy.context.scene.frame_end
    for i in range(start, end + 1):
        bpy.context.scene.frame_set(i)
        stop = False
        threads = threading.enumerate()
        for th in threads:
            if th.name == 'render_th':
                if th.stopped():
                    break
            bpy.ops.render.render(write_still=True) 

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

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

if __name__ == "__main__":
    register()
    print("Test Run")
    ### Then you can lunch the rendering by setting up a new thread
    ### t.run define the function that the thread call
    t = StoppableThread()
    t.run = render  
    t.setDaemon(True)
    t.setName("render")
    t.start()

By the way, if you want you can render an animation, and use a handlers to change the material at each frame and use an other handler to remove the 1rst one when the render is completed or cancel import bpy; bpy.app.handlers.frame_change_post.append(changemat)

def change_mat(context):
   ####do your stuff

bpy.app.handlers.render_complete.append(remove_frame_change_post_handlers)
bpy.app.handlers.render_cancel.append(remove_frame_change_post_handlers)

def remove_frame_change_post_handlers(context):
    if remove_frame_change_post_handlers in bpy.app.handlers.frame_change_post:
        bpy.app.handlers.frame_change_post.remove(remove_frame_change_post_handlers);


### Then you just need to lunch the render in animation 
bpy.ops.render.render('INVOKE_DEFAULT',animation=True);
$\endgroup$
2
  • 1
    $\begingroup$ Could you please edit your initial script to make it a running version, to make it easier to test your solution. Eg import bpy , import threading instead of import thread , I assume start and end are scene.frame_range.x and y.... etc. Also recommend using scene.frame_set(frame) rather than setting scene.frame_current. $\endgroup$
    – batFINGER
    Commented Aug 15, 2017 at 4:45
  • $\begingroup$ Edited to make runable in text editor. It renders without a filename? eg "Saved: '/tmp/.png' Time: 00:00.26 (Saving: 00:00.13)" and henceforth overwrites each time? $\endgroup$
    – batFINGER
    Commented Aug 15, 2017 at 13:46
0
$\begingroup$

are you using gpu rendering? if yes, then use cpu for rendering...the UI should still function properly...otoh if you are using all the system resources(likely with multiple rendering) then you need to cut down the number of renders running.

$\endgroup$

You must log in to answer this question.

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