After a couple of quick experiments I determined that for a square camera the following coordinates (expressed in the camera's local coordinate system) would appear in the corners of the image:
x = y = sensor_width/lens /2
[±x, ±y, -1]
For a rectangular image, whichever dimension is larger becomes sensor_width/lens/2, and the smaller dimension is proportionally adjusted.
To define the half-planes in the camera's coordinate system, it's enough to calculate the cross products of the vectors pointing at the vertices of each edge, so
lr = [ x,-y,-1]
ur = [ x, y,-1]
ll = [-x,-y,-1]
ul = [-x, y,-1]
n1 = | lr × ll |
n2 = | ll × ul |
n3 = | ul × ur |
n4 = | ur × lr |
We normalize the vectors to length 1 so that we can use them in distance calculations later on.
When you want to determine if a particular coordinate c is in the camera's view cone convert it to camera local coordinates using
M = cam.matrix_world.inverted()
c2 = M c
Now that we have c2 (in the camera's coordinate system) we can compute the dot product between it and the various half-plane normals.
zi = ni ∙ c2
If all those zis are >=0 then the point is in the camera's view cone. If the original coordinate is for an object, it's useful to incorporate a fudge factor like the radius of the object's bounding sphere (centered on the coordinate) and make sure that zi >= -fudge .
And this is the python class based on all that math:
class CameraCone:
def __init__(self, matrix, sensor_width, lens, resolution_x, resolution_y):
self.matrix = matrix.inverted()
self.sensor_width = sensor_width
self.lens = lens
w = 0.5* sensor_width / lens
if resolution_x> resolution_y:
x = w
y = w*resolution_y/resolution_x
else:
x = w*resolution_x/resolution_y
y = w
lr = Vector([x,-y,-1])
ur = Vector([x,y,-1])
ll = Vector([-x,-y,-1])
ul = Vector([-x,y,-1])
self.half_plane_normals = [
lr.cross(ll).normalized(),
ll.cross(ul).normalized(),
ul.cross(ur).normalized(),
ur.cross(lr).normalized()
]
def from_camera(cam, scn):
return CameraCone(cam.matrix_world, cam.data.sensor_width, cam.data.lens, scn.render.resolution_x, scn.render.resolution_y)
def isVisible(self, loc, fudge=0):
loc2 = self.matrix * loc
for norm in self.half_plane_normals:
z2 = loc2.dot(norm)
if z2 < -fudge:
return False
return True