TransWikia.com

How to find all objects in the Camera's view with Python?

Blender Asked by nantille on November 8, 2021

I have a scene with a many objects and one camera.

I would like to select the objects that the camera sees, even partially.

Is there a way to do that in Python that accounts for the objects bounds?


Note, I know that KD trees and Octrees are used for the lookup. There is mathutils.kdtree but the example only suggests how to query objects close to a point. I’m not familiar with the math for combining camera frustrum and binary trees lookup.

2 Answers

First I will consider here:

  • A list of objects, a scene, a camera, assuming you already defined these
  • we only care about mesh or bezier curves (I include mesh for testing, you can skip it)
  • we consider all splines inside a curve object (you can count only for first if the case)

  • a tolerance: this helps with bevel. so we consider a bit before the spline point is in view

  • a LOD, just for example (make your rules there)

Note that the tolerance here has to be related to a 0 to 1 factor and I just give a 0.03 out of the blue. this can be computed in relation to distance and a radius if u need more precision.

The steps are:

  • for each object
    • create a list of points, mesh points or by subdividing curves
    • test if the point is inside rendered area, by screen factor 0 to 1
    • create 0 if off screen or LOD (1, 2, 3 ...)
  • produce a list of integers that store the state of each object

Key moments:

  • for splines use mathutils.geometry.interpolate_bezier to subdivide it (we use points, so we need segments to be little here)
  • for on screen use bpy_extras.object_utils.world_to_camera_view that gives a factor related to screen (0-1 for on screen, rest is off)

The code example would then be something like this:

# check if mesh or spline points are in view, (o.g. variant 1, 2016)

from mathutils.geometry import interpolate_bezier
from bpy_extras.object_utils import world_to_camera_view

def pointsOfMesh_or_subdivideSpline(object):
    points = []

    if getattr(object, 'type', '') == 'MESH':
        points = [v.co for v in object.data.vertices]

    elif getattr(object, 'type', '') ==  "CURVE":
        splines = object.data.splines
        for spline in splines:

            if getattr(spline, 'type', '') == 'BEZIER':
                resolution = spline.resolution_u + 1 
                last = 0 if spline.use_cyclic_u else 1

                lenP = len(spline.bezier_points)
                for i, p1 in enumerate(spline.bezier_points):
                    if i < lenP - last:
                        p2 = spline.bezier_points[(i + 1) % lenP]

                        knot1 = p1.co
                        handle1 = p1.handle_right
                        handle2 = p2.handle_left
                        knot2 = p2.co
                        _points = interpolate_bezier(knot1, handle1, handle2, knot2, resolution)
                        points.extend(_points)
    return points

def testInView(coord, tolerance):
    pointInView = False
    z = -1

    if scene is not None and camera is not None and coord is not None:
        xFactor, yFactor, z = world_to_camera_view(scene, camera, coord)

        # add this if you use ortho !!!:
        #if camera.data.type != "PERSPECTIVE":
        #    sx, sy = camera.data.shift_x, camera.data.shift_y
        #    xFactor, yFactor = xFactor - 2 * sx, yFactor - 2 * sy

        # !! tolerance can be computed with above z and radius or so
        if -tolerance < xFactor < 1 + tolerance and  -tolerance < yFactor < 1 + tolerance and z > 0:
            pointInView = True

    return pointInView, z



objectsInViewList = []
LOD2limit = max(LOD2limit, LOD3limit)   #clamp lod 2 to lod 3

# mesh points or spline subdivision points with a tolerance
for object in ObjectList:
    if object is None or getattr(object, 'type', '') not in ['MESH', 'CURVE']:
        objectsInViewList.append(0)

    else:
        matrix = object.matrix_world
        zlist = []

        # see if any point is in view
        points = pointsOfMesh_or_subdivideSpline(object)
        for point in points:
            pointInView, z = testInView(matrix * point, tolerance)
            if pointInView: zlist.append(z)

        # these LODs are just an example
        if zlist == []: LOD = 0
        elif min(zlist) < LOD3limit: LOD = 3
        elif LOD3limit <= min(zlist) < LOD2limit: LOD = 2
        else: LOD = 1

        objectsInViewList.append(LOD)

so you get a list of 0 (hide) or LOD for each object in list

Again, note that tolerance is related to screen factor, so I just use a 0.03 here, but you can further calculate it related to z


Testing, just to see

All inputs there, I assume you have defined somehow. (I intentionally use None and lamp to see..)

Compare the result 0,1,2, list with the obvious state of objects Note that bezier 001 is slightly off screen, but I still get a 1, cause of the tolerance.

enter image description here

Note that I use Animation Nodes/ script node for convenience here, skips a lot of formalities, registering etc and I can use bogus obj list etc, plus it updates real time.

However, just to illustrate how the code works

Answered by o.g. on November 8, 2021

There are a few different ways to handle this, you could...

  • project into 2d space, then detect which objects are in the 2d frame.
  • calculate the camera bounds in 3d and detect which objects are inside it.

Here is some sample code that demonstrates the second method. It uses a set of planes (4 or 5 for orthographic cameras) and finds all objects that have any part of their bounding boxes within the planes:


Note, this isn't all that elegant, we could for example have a single function that intersects 2 sets of planes - one for the camera - another for the bound-box. However this is at least working and can give you some starting point - others may like to improve or post a method that uses projection.


def camera_as_planes(scene, obj):
    """
    Return planes in world-space which represent the camera view bounds.
    """
    from mathutils.geometry import normal

    camera = obj.data
    # normalize to ignore camera scale
    matrix = obj.matrix_world.normalized()
    frame = [matrix @ v for v in camera.view_frame(scene=scene)]
    origin = matrix.to_translation()

    planes = []
    from mathutils import Vector
    is_persp = (camera.type != 'ORTHO')
    for i in range(4):
        # find the 3rd point to define the planes direction
        if is_persp:
            frame_other = origin
        else:
            frame_other = frame[i] + matrix.col[2].xyz

        n = normal(frame_other, frame[i - 1], frame[i])
        d = -n.dot(frame_other)
        planes.append((n, d))

    if not is_persp:
        # add a 5th plane to ignore objects behind the view
        n = normal(frame[0], frame[1], frame[2])
        d = -n.dot(origin)
        planes.append((n, d))

    return planes


def side_of_plane(p, v):
    return p[0].dot(v) + p[1]


def is_segment_in_planes(p1, p2, planes):
    dp = p2 - p1

    p1_fac = 0.0
    p2_fac = 1.0

    for p in planes:
        div = dp.dot(p[0])
        if div != 0.0:
            t = -side_of_plane(p, p1)
            if div > 0.0:
                # clip p1 lower bounds
                if t >= div:
                    return False
                if t > 0.0:
                    fac = (t / div)
                    p1_fac = max(fac, p1_fac)
                    if p1_fac > p2_fac:
                        return False
            elif div < 0.0:
                # clip p2 upper bounds
                if t > 0.0:
                    return False
                if t > div:
                    fac = (t / div)
                    p2_fac = min(fac, p2_fac)
                    if p1_fac > p2_fac:
                        return False

    ## If we want the points
    # p1_clip = p1.lerp(p2, p1_fac)
    # p2_clip = p1.lerp(p2, p2_fac)        
    return True


def point_in_object(obj, pt):
    xs = [v[0] for v in obj.bound_box]
    ys = [v[1] for v in obj.bound_box]
    zs = [v[2] for v in obj.bound_box]
    pt = obj.matrix_world.inverted() @ pt
    return (min(xs) <= pt.x <= max(xs) and
            min(ys) <= pt.y <= max(ys) and
            min(zs) <= pt.z <= max(zs))


def object_in_planes(obj, planes):
    from mathutils import Vector

    matrix = obj.matrix_world
    box = [matrix @ Vector(v) for v in obj.bound_box]
    for v in box:
        if all(side_of_plane(p, v) > 0.0 for p in planes):
            # one point was in all planes
            return True

    # possible one of our edges intersects
    edges = ((0, 1), (0, 3), (0, 4), (1, 2),
             (1, 5), (2, 3), (2, 6), (3, 7),
             (4, 5), (4, 7), (5, 6), (6, 7))
    if any(is_segment_in_planes(box[e[0]], box[e[1]], planes)
           for e in edges):
        return True


    return False


def objects_in_planes(objects, planes, origin):
    """
    Return all objects which are inside (even partially) all planes.
    """
    return [obj for obj in objects
            if point_in_object(obj, origin) or
               object_in_planes(obj, planes)]

def select_objects_in_camera():
    from bpy import context
    scene = context.scene
    origin = scene.camera.matrix_world.to_translation()
    planes = camera_as_planes(scene, scene.camera)
    objects_in_view = objects_in_planes(scene.objects, planes, origin)

    for obj in objects_in_view:
        obj.select_set(True)

if __name__ == "__main__":
    select_objects_in_camera()

Answered by ideasman42 on November 8, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP