388 lines
12 KiB
Python
388 lines
12 KiB
Python
|
"""
|
||
|
A basic slow implementation of ray- triangle queries.
|
||
|
"""
|
||
|
import numpy as np
|
||
|
|
||
|
|
||
|
from .ray_util import contains_points
|
||
|
|
||
|
from ..constants import tol
|
||
|
|
||
|
from .. import util
|
||
|
from .. import caching
|
||
|
from .. import grouping
|
||
|
from .. import intersections
|
||
|
from .. import triangles as triangles_mod
|
||
|
|
||
|
|
||
|
class RayMeshIntersector(object):
|
||
|
"""
|
||
|
An object to query a mesh for ray intersections.
|
||
|
Precomputes an r-tree for each triangle on the mesh.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, mesh):
|
||
|
self.mesh = mesh
|
||
|
self._cache = caching.Cache(self.mesh.crc)
|
||
|
|
||
|
def intersects_id(self,
|
||
|
ray_origins,
|
||
|
ray_directions,
|
||
|
return_locations=False,
|
||
|
multiple_hits=True,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Find the intersections between the current mesh and an
|
||
|
array of rays.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
ray_origins : (m, 3) float
|
||
|
Ray origin points
|
||
|
ray_directions : (m, 3) float
|
||
|
Ray direction vectors
|
||
|
multiple_hits : bool
|
||
|
Consider multiple hits of each ray or not
|
||
|
return_locations : bool
|
||
|
Return hit locations or not
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
index_triangle : (h,) int
|
||
|
Index of triangles hit
|
||
|
index_ray : (h,) int
|
||
|
Index of ray that hit triangle
|
||
|
locations : (h, 3) float
|
||
|
[optional] Position of intersection in space
|
||
|
"""
|
||
|
(index_tri,
|
||
|
index_ray,
|
||
|
locations) = ray_triangle_id(
|
||
|
triangles=self.mesh.triangles,
|
||
|
ray_origins=ray_origins,
|
||
|
ray_directions=ray_directions,
|
||
|
tree=self.mesh.triangles_tree,
|
||
|
multiple_hits=multiple_hits,
|
||
|
triangles_normal=self.mesh.face_normals)
|
||
|
if return_locations:
|
||
|
if len(index_tri) == 0:
|
||
|
return index_tri, index_ray, locations
|
||
|
unique = grouping.unique_rows(
|
||
|
np.column_stack((locations, index_ray)))[0]
|
||
|
return index_tri[unique], index_ray[unique], locations[unique]
|
||
|
return index_tri, index_ray
|
||
|
|
||
|
def intersects_location(self,
|
||
|
ray_origins,
|
||
|
ray_directions,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Return unique cartesian locations where rays hit the mesh.
|
||
|
If you are counting the number of hits a ray had, this method
|
||
|
should be used as if only the triangle index is used on- edge hits
|
||
|
will be counted twice.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
ray_origins : (m, 3) float
|
||
|
Ray origin points
|
||
|
ray_directions : (m, 3) float
|
||
|
Ray direction vectors
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
locations : (n) sequence of (m,3) float
|
||
|
Intersection points
|
||
|
index_ray : (n,) int
|
||
|
Array of ray indexes
|
||
|
index_tri: (n,) int
|
||
|
Array of triangle (face) indexes
|
||
|
"""
|
||
|
(index_tri,
|
||
|
index_ray,
|
||
|
locations) = self.intersects_id(
|
||
|
ray_origins=ray_origins,
|
||
|
ray_directions=ray_directions,
|
||
|
return_locations=True,
|
||
|
**kwargs)
|
||
|
return locations, index_ray, index_tri
|
||
|
|
||
|
def intersects_any(self,
|
||
|
ray_origins,
|
||
|
ray_directions,
|
||
|
**kwargs):
|
||
|
"""
|
||
|
Find out if each ray hit any triangle on the mesh.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
ray_origins : (m, 3) float
|
||
|
Ray origin points
|
||
|
ray_directions : (m, 3) float
|
||
|
Ray direction vectors
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
hit : (m,) bool
|
||
|
Whether any ray hit any triangle on the mesh
|
||
|
"""
|
||
|
index_tri, index_ray = self.intersects_id(ray_origins,
|
||
|
ray_directions)
|
||
|
hit_any = np.zeros(len(ray_origins), dtype=np.bool)
|
||
|
hit_idx = np.unique(index_ray)
|
||
|
if len(hit_idx) > 0:
|
||
|
hit_any[hit_idx] = True
|
||
|
return hit_any
|
||
|
|
||
|
def contains_points(self, points):
|
||
|
"""
|
||
|
Check if a mesh contains a list of points, using ray tests.
|
||
|
|
||
|
If the point is on the surface of the mesh the behavior
|
||
|
is undefined.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
points : (n, 3) float
|
||
|
Points in space
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
contains : (n,) bool
|
||
|
Whether point is inside mesh or not
|
||
|
"""
|
||
|
|
||
|
return contains_points(self, points)
|
||
|
|
||
|
|
||
|
def ray_triangle_id(triangles,
|
||
|
ray_origins,
|
||
|
ray_directions,
|
||
|
triangles_normal=None,
|
||
|
tree=None,
|
||
|
multiple_hits=True):
|
||
|
"""
|
||
|
Find the intersections between a group of triangles and rays
|
||
|
|
||
|
Parameters
|
||
|
-------------
|
||
|
triangles : (n, 3, 3) float
|
||
|
Triangles in space
|
||
|
ray_origins : (m, 3) float
|
||
|
Ray origin points
|
||
|
ray_directions : (m, 3) float
|
||
|
Ray direction vectors
|
||
|
triangles_normal : (n, 3) float
|
||
|
Normal vector of triangles, optional
|
||
|
tree : rtree.Index
|
||
|
Rtree object holding triangle bounds
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
index_triangle : (h,) int
|
||
|
Index of triangles hit
|
||
|
index_ray : (h,) int
|
||
|
Index of ray that hit triangle
|
||
|
locations : (h, 3) float
|
||
|
Position of intersection in space
|
||
|
"""
|
||
|
triangles = np.asanyarray(triangles, dtype=np.float64)
|
||
|
ray_origins = np.asanyarray(ray_origins, dtype=np.float64)
|
||
|
ray_directions = np.asanyarray(ray_directions, dtype=np.float64)
|
||
|
|
||
|
# if we didn't get passed an r-tree for the bounds of each
|
||
|
# triangle create one here
|
||
|
if tree is None:
|
||
|
tree = triangles_mod.bounds_tree(triangles)
|
||
|
|
||
|
# find the list of likely triangles and which ray they
|
||
|
# correspond with, via rtree queries
|
||
|
ray_candidates, ray_id = ray_triangle_candidates(
|
||
|
ray_origins=ray_origins,
|
||
|
ray_directions=ray_directions,
|
||
|
tree=tree)
|
||
|
|
||
|
# get subsets which are corresponding rays and triangles
|
||
|
# (c,3,3) triangle candidates
|
||
|
triangle_candidates = triangles[ray_candidates]
|
||
|
# (c,3) origins and vectors for the rays
|
||
|
line_origins = ray_origins[ray_id]
|
||
|
line_directions = ray_directions[ray_id]
|
||
|
|
||
|
# get the plane origins and normals from the triangle candidates
|
||
|
plane_origins = triangle_candidates[:, 0, :]
|
||
|
if triangles_normal is None:
|
||
|
plane_normals, triangle_ok = triangles_mod.normals(
|
||
|
triangle_candidates)
|
||
|
if not triangle_ok.all():
|
||
|
raise ValueError('Invalid triangles!')
|
||
|
else:
|
||
|
plane_normals = triangles_normal[ray_candidates]
|
||
|
|
||
|
# find the intersection location of the rays with the planes
|
||
|
location, valid = intersections.planes_lines(
|
||
|
plane_origins=plane_origins,
|
||
|
plane_normals=plane_normals,
|
||
|
line_origins=line_origins,
|
||
|
line_directions=line_directions)
|
||
|
|
||
|
if (len(triangle_candidates) == 0 or
|
||
|
not valid.any()):
|
||
|
# we got no hits so return early with empty array
|
||
|
return (np.array([], dtype=np.int64),
|
||
|
np.array([], dtype=np.int64),
|
||
|
np.array([], dtype=np.float64))
|
||
|
|
||
|
# find the barycentric coordinates of each plane intersection on the
|
||
|
# triangle candidates
|
||
|
barycentric = triangles_mod.points_to_barycentric(
|
||
|
triangle_candidates[valid], location)
|
||
|
|
||
|
# the plane intersection is inside the triangle if all barycentric
|
||
|
# coordinates are between 0.0 and 1.0
|
||
|
hit = np.logical_and((barycentric > -tol.zero).all(axis=1),
|
||
|
(barycentric < (1 + tol.zero)).all(axis=1))
|
||
|
|
||
|
# the result index of the triangle is a candidate with a valid
|
||
|
# plane intersection and a triangle which contains the plane
|
||
|
# intersection point
|
||
|
index_tri = ray_candidates[valid][hit]
|
||
|
# the ray index is a subset with a valid plane intersection and
|
||
|
# contained by a triangle
|
||
|
index_ray = ray_id[valid][hit]
|
||
|
# locations are already valid plane intersections, just mask by hits
|
||
|
location = location[hit]
|
||
|
|
||
|
# only return points that are forward from the origin
|
||
|
vector = location - ray_origins[index_ray]
|
||
|
distance = util.diagonal_dot(vector, ray_directions[index_ray])
|
||
|
forward = distance > -1e-6
|
||
|
|
||
|
index_tri = index_tri[forward]
|
||
|
index_ray = index_ray[forward]
|
||
|
location = location[forward]
|
||
|
distance = distance[forward]
|
||
|
|
||
|
if multiple_hits:
|
||
|
return index_tri, index_ray, location
|
||
|
|
||
|
# since we are not returning multiple hits, we need to
|
||
|
# figure out which hit is first
|
||
|
if len(index_ray) == 0:
|
||
|
return index_tri, index_ray, location
|
||
|
|
||
|
first = np.zeros(len(index_ray), dtype=np.bool)
|
||
|
groups = grouping.group(index_ray)
|
||
|
for group in groups:
|
||
|
index = group[distance[group].argmin()]
|
||
|
first[index] = True
|
||
|
|
||
|
return index_tri[first], index_ray[first], location[first]
|
||
|
|
||
|
|
||
|
def ray_triangle_candidates(ray_origins,
|
||
|
ray_directions,
|
||
|
tree):
|
||
|
"""
|
||
|
Do broad- phase search for triangles that the rays
|
||
|
may intersect.
|
||
|
|
||
|
Does this by creating a bounding box for the ray as it
|
||
|
passes through the volume occupied by the tree
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
ray_origins: (m,3) float, ray origin points
|
||
|
ray_directions: (m,3) float, ray direction vectors
|
||
|
tree: rtree object, contains AABB of each triangle
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
ray_candidates: (n,) int, triangle indexes
|
||
|
ray_id: (n,) int, corresponding ray index for a triangle candidate
|
||
|
"""
|
||
|
ray_bounding = ray_bounds(ray_origins=ray_origins,
|
||
|
ray_directions=ray_directions,
|
||
|
bounds=tree.bounds)
|
||
|
ray_candidates = [[]] * len(ray_origins)
|
||
|
ray_id = [[]] * len(ray_origins)
|
||
|
|
||
|
for i, bounds in enumerate(ray_bounding):
|
||
|
ray_candidates[i] = np.array(list(tree.intersection(bounds)),
|
||
|
dtype=np.int)
|
||
|
ray_id[i] = np.ones(len(ray_candidates[i]), dtype=np.int) * i
|
||
|
|
||
|
ray_id = np.hstack(ray_id)
|
||
|
ray_candidates = np.hstack(ray_candidates)
|
||
|
|
||
|
return ray_candidates, ray_id
|
||
|
|
||
|
|
||
|
def ray_bounds(ray_origins,
|
||
|
ray_directions,
|
||
|
bounds,
|
||
|
buffer_dist=1e-5):
|
||
|
"""
|
||
|
Given a set of rays and a bounding box for the volume of interest
|
||
|
where the rays will be passing through, find the bounding boxes
|
||
|
of the rays as they pass through the volume.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
ray_origins: (m,3) float, ray origin points
|
||
|
ray_directions: (m,3) float, ray direction vectors
|
||
|
bounds: (2,3) bounding box (min, max)
|
||
|
buffer_dist: float, distance to pad zero width bounding boxes
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
ray_bounding: (n) set of AABB of rays passing through volume
|
||
|
"""
|
||
|
|
||
|
ray_origins = np.asanyarray(ray_origins, dtype=np.float64)
|
||
|
ray_directions = np.asanyarray(ray_directions, dtype=np.float64)
|
||
|
|
||
|
# bounding box we are testing against
|
||
|
bounds = np.asanyarray(bounds)
|
||
|
|
||
|
# find the primary axis of the vector
|
||
|
axis = np.abs(ray_directions).argmax(axis=1)
|
||
|
axis_bound = bounds.reshape((2, -1)).T[axis]
|
||
|
axis_ori = np.array([ray_origins[i][a]
|
||
|
for i, a in enumerate(axis)]).reshape((-1, 1))
|
||
|
axis_dir = np.array([ray_directions[i][a]
|
||
|
for i, a in enumerate(axis)]).reshape((-1, 1))
|
||
|
|
||
|
# parametric equation of a line
|
||
|
# point = direction*t + origin
|
||
|
# p = dt + o
|
||
|
# t = (p-o)/d
|
||
|
t = (axis_bound - axis_ori) / axis_dir
|
||
|
|
||
|
# prevent the bounding box from including triangles
|
||
|
# behind the ray origin
|
||
|
t[t < buffer_dist] = buffer_dist
|
||
|
|
||
|
# the value of t for both the upper and lower bounds
|
||
|
t_a = t[:, 0].reshape((-1, 1))
|
||
|
t_b = t[:, 1].reshape((-1, 1))
|
||
|
|
||
|
# the cartesion point for where the line hits the plane defined by
|
||
|
# axis
|
||
|
on_a = (ray_directions * t_a) + ray_origins
|
||
|
on_b = (ray_directions * t_b) + ray_origins
|
||
|
|
||
|
on_plane = np.column_stack(
|
||
|
(on_a, on_b)).reshape(
|
||
|
(-1, 2, ray_directions.shape[1]))
|
||
|
|
||
|
ray_bounding = np.hstack((on_plane.min(axis=1),
|
||
|
on_plane.max(axis=1)))
|
||
|
# pad the bounding box by TOL_BUFFER
|
||
|
# not sure if this is necessary, but if the ray is axis aligned
|
||
|
# this function will otherwise return zero volume bounding boxes
|
||
|
# which may or may not screw up the r-tree intersection queries
|
||
|
ray_bounding += np.array([-1, -1, -1, 1, 1, 1]) * buffer_dist
|
||
|
|
||
|
return ray_bounding
|