Animate Camera Resolution

Hi everyone!

I’d like to animate the resolution of the camera so I can automate rendering out different vantage points of my scene at unique resolutions. I know this isn’t possible by default in Blender so I wrote some code to remedy that but I’m still having issues.

When I play the timeline in my viewport, the camera cycles through the correct resolution per frame as intended. But when I render out this animation, all frames are exported using the resolution of frame one.

Here is the code I used.

import bpy

res_carousel1 = [2560,941]
res_carousel2 = [2560,941]
res_carousel3 = [2560,941]

res_banner4 = [1700,540]
res_banner5 = [840,530]
res_banner6 = [840,530]
res_dotm7 = [840,1090]

res_banner8 = [313,550]
res_banner9 = [635,270]
res_banner10 = [312,270]
res_banner11 = [312,270]
res_banner12 = [312,270]
res_banner13 = [312,270]

res_banner14 = [635,270]
res_banner15 = [635,270]

res_banner16 = [313,349]
res_banner17 = [312,349]
res_banner18 = [313,349]
res_banner19 = [312,349]


frame_res = {
    1: res_carousel1,
    2: res_carousel2,
    3: res_carousel3,
    4: res_banner4,
    5: res_banner5,
    6: res_banner6,
    7: res_dotm7,
    8: res_banner8,
    9: res_banner9,
    10: res_banner10,
    11: res_banner11,
    12: res_banner12,
    13: res_banner13,
    14: res_banner14,
    15: res_banner15,
    16: res_banner16,
    17: res_banner17,
    18: res_banner18,
    19: res_banner19,
    }


# Define a callback function to call when scene current frame changes
def chg_res(scene):
    # Get frame number
    f = scene.frame_current


    if frame_res[f] :
        scene.render.resolution_x = frame_res[f][0]
        scene.render.resolution_y = frame_res[f][1]
    else:
        scene.render.resolution_x = 100
        scene.render.resolution_y = 100

# Register callback in Blender event system (once)
bpy.app.handlers.frame_change_pre.append(chg_res)
1 Like

IIRC when you hit render, it only reads the scene variables once. Rather than every frame.

The way I would tackle it, is to manually render and save out the frames via python, setting the frame size in between each render

The renderer is only set up once. Instead of calling the render operator to render all frames, you’ll need to loop the frames and render them one-by-one.

Thanks carlo and sybren!

Given my inexperience in writing code, could you point me in the direction I need to accomplished that?

This script has a similar problem where it only takes the resolution of the first frame.

import bpy

res_carousel1 = [2560,941]
res_carousel2 = [2560,941]
res_carousel3 = [2560,941]

res_banner4 = [1700,540]
res_banner5 = [840,530]
res_banner6 = [840,530]
res_dotm7 = [840,1090]

res_banner8 = [313,550]
res_banner9 = [635,270]
res_banner10 = [312,270]
res_banner11 = [312,270]
res_banner12 = [312,270]
res_banner13 = [312,270]

res_banner14 = [635,270]
res_banner15 = [635,270]

res_banner16 = [313,349]
res_banner17 = [312,349]
res_banner18 = [313,349]
res_banner19 = [312,349]


frame_res = {
    1: res_carousel1,
    2: res_carousel2,
    3: res_carousel3,
    4: res_banner4,
    5: res_banner5,
    6: res_banner6,
    7: res_dotm7,
    8: res_banner8,
    9: res_banner9,
    10: res_banner10,
    11: res_banner11,
    12: res_banner12,
    13: res_banner13,
    14: res_banner14,
    15: res_banner15,
    16: res_banner16,
    17: res_banner17,
    18: res_banner18,
    19: res_banner19,
    }


scene = bpy.context.scene
fp = scene.render.filepath

f = scene.frame_current
i = 1

while i <= max(frame_res):
  
    scene.frame_set(i)
    if frame_res[f] :
        scene.render.resolution_x = frame_res[f][0]
        scene.render.resolution_y = frame_res[f][1]
    else:
        scene.render.resolution_x = 100
        scene.render.resolution_y = 100
    scene.render.filepath = fp + str(i)
    bpy.ops.render.render(write_still=True)
    scene.render.filepath = fp
    i += 1

I didn’t try your code but it seems the problem is in your conditional statement. The current scene frame will always remain 1. remember you’re not rendering animation.
Here’s a code that works:

import bpy

res_carousel1 = [2560,941]
res_carousel2 = [2560,941]
res_carousel3 = [2560,941]

res_banner4 = [1700,540]
res_banner5 = [840,530]
res_banner6 = [840,530]
res_dotm7 = [840,1090]

res_banner8 = [313,550]
res_banner9 = [635,270]
res_banner10 = [312,270]
res_banner11 = [312,270]
res_banner12 = [312,270]
res_banner13 = [312,270]

res_banner14 = [635,270]
res_banner15 = [635,270]

res_banner16 = [313,349]
res_banner17 = [312,349]
res_banner18 = [313,349]
res_banner19 = [312,349]

f_path = bpy.context.scene.render.filepath

frame_res = {
    1: res_carousel1,
    2: res_carousel2,
    3: res_carousel3,
    4: res_banner4,
    5: res_banner5,
    6: res_banner6,
    7: res_dotm7,
    8: res_banner8,
    9: res_banner9,
    10: res_banner10,
    11: res_banner11,
    12: res_banner12,
    13: res_banner13,
    14: res_banner14,
    15: res_banner15,
    16: res_banner16,
    17: res_banner17,
    18: res_banner18,
    19: res_banner19,
    }
    
for i in frame_res.keys():
    bpy.context.scene.render.resolution_x = frame_res[i][0]
    bpy.context.scene.render.resolution_y = frame_res[i][1]
    bpy.context.scene.render.filepath = f_path + str(i)
    
    bpy.ops.render.render(write_still = True)
bpy.context.scene.render.filepath = f_path #EDIT: forgot to reset the file path lol

Ah! I thought once the ‘scene.frame_set’ was defined, the ‘scene.frame_current’ would reference it.

Thanks for the more elegant and (more importantly) functional code.

My pleasure.
I didn’t try 'scene.frame_set’ yet so maybe it doesn’t work in a loop?

1 Like

Just tested it, I need to add ‘bpy.context.scene.frame_set(i)’ back in so the timeline would advance. It seems to be working as intended now. Thanks again!

for i in frame_res.keys():

    bpy.context.scene.frame_set(i)

    bpy.context.scene.render.resolution_x = frame_res[i][0]
    bpy.context.scene.render.resolution_y = frame_res[i][1]
    bpy.context.scene.render.filepath = f_path + str(i)
    
    bpy.ops.render.render(write_still = True)
bpy.context.scene.render.filepath = f_path
1 Like

A little refactor of the code:

scene = bpy.context.scene
render = scene.render
f_path = render.filepath

for frame, resolution in frame_res.items():
    scene.frame_set(frame)

    render.resolution_x, render.resolution_y = resolution
    render.filepath = f'{f_path}{frame}'
    
    bpy.ops.render.render(write_still=True)
render.filepath = f_path
  • This avoids doing repeated property lookups for bpy.context.scene and b.c.s.render, as Python doesn’t optimise these.
  • Use .items() instead of .keys() to get the key and the value simultaneously. This avoids the two dictionary lookups frame_res[i].
  • Use tuple assignment (res_x, res_y) = resolution. Not only is it shorter, it will also cause an error when the resolution has more elements than expected (the old code would silently ignore such cases).
  • Use string formatting to format the string, instead of explicit conversion to string and concatenation with +. This makes it easier to adjust to your particular needs (like prepending leading zeroes to the frame numbers).
2 Likes

Wow thanks, Sybren!

I appreciate the refactored code and the detailed explanation. You and everyone in the blender community has been so helpful in my journey to learning coding.

1 Like

IIRC when you hit render, it only reads the scene variables once. Rather than every frame.

The way I would tackle it, is to manually render and save out the frames via python, setting the auto clicker frame size in between each render

The renderer is only set up once. Instead of calling the render operator to render all frames, you’ll need to loop the frames and render them one-by-one. --Yes this helps.