I am trying to write a script to remove the vertical perspective from a camera relative to the center of the view frame (e.g. I want all vertical lines to be parallel when viewed from the camera). In order to achieve this, I set the camera's local x rotation to 90, and then need to compensate using the y-shift is order to keep what was exactly at the center of the frame in the same position.
I am struggling with figuring out how to determine the proper y-shift to achieve this. This is my current attempt, which unfortunately does not work:
def auto_remove_camera_vertical_perspective(cam_obj): # ==Add plane and align to cam frustum== bpy.ops.mesh.primitive_plane_add( enter_editmode=False, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1), ) view_plane = bpy.context.object # Align rotation bpy.ops.object.constraint_add(type="COPY_ROTATION") view_plane.constraints["Copy Rotation"].target = cam_obj bpy.ops.constraint.apply(constraint="Copy Rotation", owner="OBJECT") # Align location using the cam view frame plane coords cam_data = cam_obj.data matrix = cam_obj.matrix_world.normalized() frame_points = [matrix @ v for v in cam_data.view_frame(scene=bpy.context.scene)] x = sum(p.x for p in frame_points) / 4 y = sum(p.y for p in frame_points) / 4 z = sum(p.z for p in frame_points) / 4 center_point = Vector((x, y, z)) view_plane.location = center_point # Scale plane scale_factor = 10_000 view_plane.scale = Vector([scale_factor] * 3) # Apply transforms bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # ==Ray cast to plane== # Get the camera's location and rotation ray_origin = cam_obj.location cam_rot = cam_obj.rotation_euler.to_matrix() # Calculate the ray direction from camera's facing ray_direction = cam_rot @ Vector((0, 0, -1)) # Cast the ray from the camera origin onto the object ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction) ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction) loc_1 = ray_cast_result bpy.ops.object.empty_add(location=loc_1) bpy.context.object.name = "ray_hit" # Local x rotation set to 90 vector cam_rot_corrected = Euler(cam_obj.rotation_euler) cam_rot_corrected.x = math.radians(90.0) cam_rot_corrected = cam_rot_corrected.to_matrix() ray_direction = cam_rot_corrected @ Vector((0, 0, -1)) # Cast the ray from the camera origin onto the object ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction) ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction) loc_2 = ray_cast_result bpy.ops.object.empty_add(location=loc_2) bpy.context.object.name = "ray_hit" # ==Calculate y-shift== shift_length = abs(loc_2.z - loc_1.z) shift_sign = -1.0 if loc_2.z > loc_1.z else 1.0 # NOTE: Since the sensor is square and the aspect ratio is 16:9 (1920x1080), the width of the # view frame should correspond to the full length of the sensor. # TODO: I think this is probably the broken area. # the sensor and the view frame. sensor_length = (frame_points - frame_points).length shift_amount = shift_sign * (shift_length / sensor_length) # ==Apply y-shift and remove cam local x rotation cam_obj.rotation_euler.x = math.radians(90.0) cam_data.shift_y = shift_amount # ==Cleanup== bpy.ops.object.select_all(action="DESELECT") view_plane.select_set(True) bpy.ops.object.delete( use_global=False, confirm=False, ) ```