1
$\begingroup$

I am currently working on a project where I am using the camera FOV to place objects visible within a scene, using the view_frame (How to get camera view bounds on 2D plane, not Viewport limits). The images are rendered in a square, 1:1 aspect ratio. When I create the camera, I specify the X & Y FOV angles as so:

lens_angle = 40 # degrees
scn = bpy.context.scene
cam = bpy.data.cameras.new("Camera")
cam_obj = bpy.data.objects.new("Camera", cam)
cam_obj.data.angle_x = math.radians(lens_angle)
cam_obj.data.angle_y = math.radians(lens_angle)

The bpy documentation for cameras lists angle_x and angle_y as separate parameters (https://docs.blender.org/api/current/bpy.types.Camera.html), and to me this heavily implies they should be separately settable, independent of each other. However, I was consistently getting objects placed outside the FOV, so I checked what the actual FOV angle is for each angle later in the script as so:

cam_angle_x = cam_obj.data.angle_x
cam_angle_y = cam_obj.data.angle_y
print(cam_angle_x)
print(cam_angle_y)

With the result:

0.9994651675224304
0.6981316804885864

This seems like very poor API practise, to have 2 exposed parameters that do not actually set the thing that any user would assume they set.

Am I misunderstanding how blender deals with camera angles here (and is there a good reason for this way of doing things that I am missing?). Most importantly, how can I set-up a square aspect ratio camera to use for my object placement code (all within bpy scripting)?

$\endgroup$
1
  • $\begingroup$ Field of view depend of lens focal distance and sensor size, and in 3D output case the image resolution. I don't think you can arbitrarily set horizontal and vertical fields of view independently without changing output image size $\endgroup$ Sep 7, 2023 at 10:57

1 Answer 1

1
$\begingroup$

Setting angle_x and angle_y directly to create a square camera does not work as you have experienced.

Quoting you (emphasis mine):

The bpy documentation for cameras lists angle_x and angle_y as separate parameters (https://docs.blender.org/api/current/bpy.types.Camera.html), and to me this heavily implies they should be separately settable, independent of each other.

Well, this may be a valid first assumption, but it's not. Do not forget that we deal with an Blender object and the attributes you see are Properties, in the angle case it's FloatProperties. You can verify this with

for p in bpy.data.objects["Camera"].data.bl_rna.properties:
    print(p)

which gives

...
<bpy_struct, FloatProperty("angle_x") at 0x00007FF656F3BF70>
<bpy_struct, FloatProperty("angle_y") at 0x00007FF656F3C0B0>
<bpy_struct, FloatProperty("angle") at 0x00007FF656F3C1F0>
...

Now, the thing is, the getter/setter of Blender properties can be custom functions doing more stuff behind the scenes. If you're not interested in a source code dive, skip to section 2.

Section 1 In Blender's source code the getter/setter definition for angle_x is

RNA_def_property_float_funcs(prop, "rna_Camera_angle_x_get", "rna_Camera_angle_x_set", nullptr);

with RNA_def_property_float_funcs declared as

void RNA_def_property_float_funcs(PropertyRNA *prop, const char *get, const char *set, const char *range)

Don't care about the C++ syntax, what you only need to see is that there are a custom getter rna_Camera_angle_x_get and setter rna_Camera_angle_x_set implemented. Looking these up gives

static float rna_Camera_angle_x_get(PointerRNA *ptr)
{
  Camera *cam = (Camera *)ptr->owner_id;
  return focallength_to_fov(cam->lens, cam->sensor_x);
}

static void rna_Camera_angle_x_set(PointerRNA *ptr, float value)
{
  Camera *cam = (Camera *)ptr->owner_id;
  cam->lens = fov_to_focallength(value, cam->sensor_x);
}

with two helper functions

float focallength_to_fov(float focal_length, float sensor)
{
  return 2.0f * atanf((sensor / 2.0f) / focal_length);
}

float fov_to_focallength(float hfov, float sensor)
{
  return (sensor / 2.0f) / tanf(hfov * 0.5f);
}

Now we have all together to see what Blender does internally.

Section 2

What Blender does in the custom setter/getter is when setting angle_x it just recalculates the the focal length and thats it! When you retrieve angle_x, Blender recalculates it with the current focal length.

The general relationship for focal length $f$, sensor size $s$ and field of view $v$ is $ v = 2 \cdot \arctan{(\frac{s}{2f})} $, so when you write

anglex = 40
cam.data.angle_x = math.radians(anglex)

Blender does

cam.data.lens = (cam.data.sensor_width / 2.0) / math.tan(anglex * 0.5)

and when you write

print(cam.data.angle_x)

Blender does

print(2.0 * math.atan((cam.data.sensor_width / 2.0) / cam.data.lens))

The behaviour is analogous with angle_y, just using sensor_height instead (note that Blender keeps the focal length in cam.data.lens in Millimeters. Changing the Lens Unit to Field of View only changes the UI presenting the Field of View in degrees. This is only a convenience for users).

So, when you do

# update focal length such that angle_x fits the given value
cam_obj.data.angle_x = math.radians(lens_angle)  
# update focal length such that angle_y fits the given value
cam_obj.data.angle_y = math.radians(lens_angle) 

you're actually adjusting the camera's focal length two times in a row! angle, angle_x and angle_y will be recalculated after that, therefore your prints will not show the same values, but the newest ones.

Section 3

The logic behind all this is that Blender takes the sensor width and height as given physical properties which don't change when you want to change the angles. Physically you would realize a change in horizontal/vertical field of view by adjusting the focal length and that is what Blender emulates. Changing the sensor values is like saying: hey Blender I got a new camera for you! Setting the angle is just indirectly adjusting the same camera.

So, how to get angle_x and angle_y the same? You just need to align sensor_with and sensor_height. If you set them equal, the angles will be too (because of the formula above). And luckily, you don't need to do anything of this! Although Blender mimics physical cameras (thats useful/necessary for a lot of people) it's still a virtual one, which means it can render more than a real camera could capture. Just set the render resolution square and you get your square aspect ratio rendering, no matter the sensor settings!

In the screenshot you can see that the rendered image is always the same (orange border 400x400) but the sensor width and height are different (grey border). The field of view has always been adjusted to $70°$ again after changing the sensor values (note that the focal length values when selecting Millimeters must not be the same!).

enter image description here

TL;DR To make a square camera just make the resolution square. If you need for whatever reason a physical square camera, make the sensor width and height equal too and calculate the focal length from the field of view.

cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
sensorsize = 40
cam.data.sensor_width = sensorsize
cam.data.sensor_height = sensorsize
fov = 70
cam.data.lens = (sensorsize / 2.0) / math.tan(math.radians(fov) * 0.5)
bpy.context.collection.objects.link(cam)

I also suggest to read this great and informative posts:

Blender camera sensor size physical equivalent?

Relationship between the sensor size and focal length

How to set camera horizontal and vertical FOV

What is the point of Blenders camera sensor width?

$\endgroup$

You must log in to answer this question.

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