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.
First I will consider here:
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
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:
Key moments:
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.
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...
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
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP