Operating system: Windows 10
Graphics card: GTX 1070
Short description of error
When a camera has shift_x or shift_y != 0, world_to_camera_view does not work.
We can test this by comparing points projected manually with camera.calc_matrix_camera(), and with the utility world_to_camera_view. When there is no shift_x or shift_y, they line up. But when we change shift_x or shift_y, they are no longer equal.
Projection using the matrix from camera.calc_matrix_camera() seems to be correct. So I think world_to_camera_view() has a bug. This is suprising since the docs suggest that world_to_camera_view() "Takes shift-x/y ... into account"
Exact steps for others to reproduce the error
- Open new blend file with default camera and cube
- Run this script
import bpy import numpy as np from bpy_extras.object_utils import world_to_camera_view D, C = bpy.data, bpy.context camera = C.scene.camera cube = D.objects["Cube"] def project_with_matrix(): width, height = C.scene.render.resolution_x, C.scene.render.resolution_y projection_matrix = camera.calc_matrix_camera(C.evaluated_depsgraph_get(), x=width, y=height) projection_matrix = np.array([list(row) for row in projection_matrix]) # Cube vertices in camera space verts_camspace = np.array([list(camera.matrix_world.inverted() @ v.co) for v in cube.data.vertices]) # Homogenize verts_camspace_h = np.hstack([verts_camspace, np.ones((len(verts_camspace), 1))]) # Project projected = verts_camspace_h.dot(projection_matrix.T) # Dehomogenize projected = projected[:, :2] / projected[:, 3, None] # [-1, 1] to [0, 1] projected = (projected + 1.0) / 2.0 return projected def project_with_world_to_camera_view(): projected =  for v in cube.data.vertices: projected.append(list(world_to_camera_view(C.scene, camera, v.co).xy)) return np.array(projected) if __name__ == "__main__": camera.data.shift_y = 0.0 camera.data.shift_x = 0.0 print("shift_x=0.0, shift_y=0.0:", np.allclose(project_with_matrix(), project_with_world_to_camera_view())) camera.data.shift_x = 0.1 camera.data.shift_y = 0.0 print("shift_x=0.1, shift_y=0.0:", np.allclose(project_with_matrix(), project_with_world_to_camera_view())) camera.data.shift_x = 0.0 camera.data.shift_y = 0.1 print("shift_x=0.0, shift_y=0.1:", np.allclose(project_with_matrix(), project_with_world_to_camera_view()))
See this output:
shift_x=0.0, shift_y=0.0: True shift_x=0.1, shift_y=0.0: False shift_x=0.0, shift_y=0.1: False
Note: this assumes the default cube has identity transform, so we skip applying it's world matrix