hub/venv/lib/python3.7/site-packages/trimesh/ray/ray_util.py

133 lines
4.5 KiB
Python

import numpy as np
from .. import util
from .. import bounds
from .. import constants
@constants.log_time
def contains_points(intersector,
points,
check_direction=None):
"""
Check if a mesh contains a set of points, using ray tests.
If the point is on the surface of the mesh, behavior is
undefined.
Parameters
---------
mesh: Trimesh object
points: (n,3) points in space
Returns
---------
contains : (n) bool
Whether point is inside mesh or not
"""
# convert points to float and make sure they are 3D
points = np.asanyarray(points, dtype=np.float64)
if not util.is_shape(points, (-1, 3)):
raise ValueError('points must be (n,3)')
# placeholder result with no hits we'll fill in later
contains = np.zeros(len(points), dtype=np.bool)
# cull points outside of the axis aligned bounding box
# this avoids running ray tests unless points are close
inside_aabb = bounds.contains(intersector.mesh.bounds,
points)
# if everything is outside the AABB, exit early
if not inside_aabb.any():
return contains
# default ray direction is random, but we are not generating
# uniquely each time so the behavior of this function is easier to debug
default_direction = np.array([0.4395064455,
0.617598629942,
0.652231566745])
if check_direction is None:
# if no check direction is specified use the default
# stack it only for points inside the AABB
ray_directions = np.tile(default_direction,
(inside_aabb.sum(), 1))
else:
# if a direction is passed use it
ray_directions = np.tile(
np.array(check_direction).reshape(3),
(inside_aabb.sum(), 1))
# cast a ray both forwards and backwards
location, index_ray, c = intersector.intersects_location(
np.vstack(
(points[inside_aabb],
points[inside_aabb])),
np.vstack(
(ray_directions,
-ray_directions)))
# if we hit nothing in either direction just return with no hits
if len(index_ray) == 0:
return contains
# reshape so bi_hits[0] is the result in the forward direction and
# bi_hits[1] is the result in the backwards directions
bi_hits = np.bincount(
index_ray,
minlength=len(ray_directions) * 2).reshape((2, -1))
# a point is probably inside if it hits a surface an odd number of times
bi_contains = np.mod(bi_hits, 2) == 1
# if the mod of the hit count is the same in both
# directions, we can save that result and move on
agree = np.equal(*bi_contains)
# in order to do an assignment we can only have one
# level of boolean indexes, for example this doesn't work:
# contains[inside_aabb][agree] = bi_contains[0][agree]
# no error is thrown, but nothing gets assigned
# to get around that, we create a single mask for assignment
mask = inside_aabb.copy()
mask[mask] = agree
# set contains flags for things inside the AABB and who have
# ray tests that agree in both directions
contains[mask] = bi_contains[0][agree]
# if one of the rays in either direction hit nothing
# it is a very solid indicator we are in free space
# as the edge cases we are working around tend to
# add hits rather than miss hits
one_freespace = (bi_hits == 0).any(axis=0)
# rays where they don't agree and one isn't in free space
# are deemed to be broken
broken = np.logical_and(np.logical_not(agree),
np.logical_not(one_freespace))
# if all rays agree return
if not broken.any():
return contains
# try to run again with a new random vector
# only do it if check_direction isn't specified
# to avoid infinite recursion
if check_direction is None:
# we're going to run the check again in a random direction
new_direction = util.unitize(np.random.random(3) - .5)
# do the mask trick again to be able to assign results
mask = inside_aabb.copy()
mask[mask] = broken
contains[mask] = contains_points(
intersector,
points[inside_aabb][broken],
check_direction=new_direction)
constants.log.debug(
'detected %d broken contains test, attempted to fix',
broken.sum())
return contains