Setting a render border for each frame of an animation

I’m making an animation and I wrote a Python script to help improve the render times, but it’s not quite working.

My main character has a lot of detail, but the background is relatively simple. I want to render the background at a lower quality setting to save time and then add the main character on top in a separate layer. (I’m going to render a few hundred frames, so every little bit of optimization helps.)

What I’d like to do is isolate the main character and set a border around them, so I don’t waste time rendering the background. I was able to do that with a Python script: It loops over all vertices in a model, projects the coordinates into screen-space, and then finds a bounding box that contains those points.

If I run the function once it will set the border around the object, and if I add it to a scene update callback it will update the border every time I change the frame in the UI.

This almosts works, the only problem is that it doesn’t update when I’m rendering an animation. It will keep whatever border was set on the first frame. I can see that it triggers the scene update callback and sets the correct border, but something in the render settings is getting cached and not updating properly in between frames.

I guess I should start by asking: Is this even necessary? There’s not some feature built into Blender that I’m missing, is there?

Secondly, is it possible to do what I’m trying to do with a Python script? I’m looking for some way to refresh the render settings before it starts rendering.

(*It is possible to render each frame inside a Python loop, but my plan is to use a render farm service with their default animation rendering setup. I also don’t want to hide the background outright because it has to cast shadows and reflect onto the main character.)

Here is what the script looks like so far:

# Test scene for setting the render border for each frame in Python
import bpy
import bpy_extras.object_utils

# Options
UpdateFrames = 1

# UpdateBorder
def UpdateBorder(camera, scene, object):
    
    # Update the render border to wrap around a single object
    print("\nUpdateBorder Frame "+str(scene.frame_current))
    matrix = object.matrix_world
    mesh = object.data

    # Get matrix components (Blender 2.8)
    col0 = matrix.col[0]
    col1 = matrix.col[1]
    col2 = matrix.col[2]
    col3 = matrix.col[3]

    # Find a bounding box that fits around the vertices
    minX = 1
    maxX = 0
    minY = 1
    maxY = 0
    numVertices = len(object.data.vertices)
    for t in range(0, numVertices):

        co = mesh.vertices[t].co

        # Apply the object transformation
        # 2.79 version (doesn't work in 2.8 for some reason?)
        #pos = matrix * mesh.vertices[t].co

        # 2.8 version
        pos = (col0 * co.x) + (col1 * co.y) + (col2 * co.z) + col3
        
        # Apply the perspective transformation
        pos = bpy_extras.object_utils.world_to_camera_view(scene, camera, pos)
        
        # Expand the bounding box
        if (pos.x < minX):
            minX = pos.x
        if (pos.y < minY):
            minY = pos.y
        if (pos.x > maxX):
            maxX = pos.x
        if (pos.y > maxY):
            maxY = pos.y

    # Print border to console
    render = scene.render
    pMinX = str(int(minX*render.resolution_x))
    pMinY = str(int(minY*render.resolution_y))
    pMaxX = str(int(maxX*render.resolution_x))
    pMaxY = str(int(maxY*render.resolution_y))
    print("  ("+pMinX+", "+pMinY+") - ("+pMaxX+", "+pMaxY+")")
    
    # Set the render border
    render.border_min_x = minX
    render.border_min_y = minY
    render.border_max_x = maxX
    render.border_max_y = maxY

    # Various things that don't fix the border problem
    #render.resolution_x = 1920
    #render.resolution_y = 1080
    #render.use_border = True
    #scene.update()
    #bpy.context.update()

def UpdateFrame(scene):
    camera = bpy.data.objects['Camera']
    object = bpy.data.objects['Cube']
    UpdateBorder(camera, scene, object)

print("Initializing")

# Run the function for the first time (optional)
scene = bpy.context.scene
camera = bpy.data.objects['Camera']
object = bpy.data.objects['Cube']
UpdateBorder(camera, scene, object)

# Set a callback for when the scene is rendered
# (Other possible callback functions: render_pre, render_init, scene_update_pre, etc.)
bpy.app.handlers.frame_change_post.clear()
bpy.app.handlers.frame_change_post.append(UpdateFrame)

Blender file:
BorderTest.blend (643.4 KB)