Created
November 4, 2021 02:35
-
-
Save semagnum/f896124d6a28ddf60c945eff5dd2e0bc to your computer and use it in GitHub Desktop.
Culls objects whose bounding boxes are not intersecting or within camera frustrum
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from mathutils.geometry import normal | |
from mathutils import Vector | |
from bpy import context | |
def camera_as_planes(scene, obj): | |
""" | |
Return planes in world-space which represent the camera view bounds. | |
""" | |
camera = obj.data | |
# normalize to ignore camera scale | |
matrix = obj.matrix_world | |
frame = [matrix @ v for v in camera.view_frame(scene=scene)] | |
origin = matrix.to_translation() | |
planes = [] | |
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 | |
p1_clip = p1.lerp(p2, p1_fac) | |
p2_clip = p1.lerp(p2, p2_fac) | |
epsilon = -0.5 | |
return all(side_of_plane(p, p1_clip) > epsilon and side_of_plane(p, p2_clip) > epsilon for p in planes) | |
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): | |
matrix = obj.matrix_world | |
box = [matrix @ Vector(v) for v in obj.bound_box] | |
epsilon = -0.00001 | |
for v in box: | |
if all(side_of_plane(p, v) > epsilon 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 visible_objects(objects, planes, origin, camera): | |
""" | |
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(): | |
scene = context.scene | |
camera = context.scene.objects['Camera.002'] | |
origin = camera.matrix_world.to_translation() | |
planes = camera_as_planes(scene, camera) | |
objects_in_view = visible_objects(scene.objects, planes, origin, camera) | |
for obj in objects_in_view: | |
obj.select_set(True) | |
if __name__ == "__main__": | |
select_objects_in_camera() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment