Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Ray Refactor #1108

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
run: |
sudo apt-get update -qq -y
sudo apt-get install -qq -y libgeos-dev
# install binvox and openctm for tests
sudo bash docker/builds/binvox.bash
sudo bash docker/builds/openctm.bash
- name: Install Brew On Mac
if: matrix.os == 'macos-latest'
run: |
Expand Down
4 changes: 0 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ ARG INCLUDE_X=false
COPY docker/builds/apt.bash /tmp/
RUN bash /tmp/apt.bash ${INCLUDE_X}

# Install `embree`, Intel's fast ray checking engine
COPY docker/builds/embree.bash /tmp/
RUN bash /tmp/embree.bash

# XVFB runs in the background if you start supervisor.
COPY docker/config/xvfb.supervisord.conf /etc/supervisor/conf.d/

Expand Down
Binary file modified docs/_static/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/_static/images/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/_static/images/logotype-a.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 1 addition & 138 deletions docs/_static/images/logotype-b.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/_static/images/trimesh-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/data/ray_data.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions tests/test_minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def test_path_exc(self):
assert offset.shape == (2, 2)
assert density > .833

def test_path_imports(self):
# check various utility functions that should
# import cleanly even if there's no shapely/etc
from trimesh.path import packing
from trimesh.path.segments import resample

def test_load(self):
# kinds of files we should be able to
# load even with a minimal install
Expand Down
106 changes: 57 additions & 49 deletions tests/test_ray.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,40 @@ def test_rays(self):
broken = hit_any[i].astype(g.np.int64).ptp(axis=0).sum()
assert broken == 0

def test_rps(self):
for use_embree in [True, False]:
dimension = (10000, 3)
sphere = g.get_mesh('unit_sphere.STL',
use_embree=use_embree)

ray_origins = g.np.random.random(dimension)
ray_directions = g.np.tile([0, 0, 1], (dimension[0], 1))
ray_origins[:, 2] = -5

# force ray object to allocate tree before timing it
# tree = sphere.ray.tree
def test_rps(self, count=50000):
# do a rudimentary benchmark
count = int(count)
mesh = g.trimesh.creation.icosphere(subdivisions=5)
origins = g.np.random.random((count, 3))
origins[:, 2] = -5
vectors = g.np.tile([0, 0, 1], (count, 1))
rps = {}

for engine in g.trimesh.ray.engines:
# collect allocation time
tic = [g.time.time()]
a = sphere.ray.intersects_id(
ray_origins, ray_directions)
e = engine(mesh)
# it might be doing lazy creation of acceleration
# structures so assume it's caching and do a single
# ray check to include in the allocation bucket
e.intersects_id(origins=[[0, 0, 0]],
vectors=[[0, 0, 1]])
tic.append(g.time.time())
b = sphere.ray.intersects_location(
ray_origins, ray_directions)

# collect the rays/sec on a full query
a = e.intersects_id(origins=origins, vectors=vectors)

tic.append(g.time.time())

# make sure ray functions always return numpy arrays
assert all(len(i.shape) >= 0 for i in a)
assert all(len(i.shape) >= 0 for i in b)

rps = dimension[0] / g.np.diff(tic)
# store the allocation and ray-check time
rps[str(e)] = g.np.diff(tic)

g.log.info('Measured %s rays/second with embree %d',
str(rps),
use_embree)
g.log.info('\n'.join(
f'\n\n{k}\n{v[0]:0.4f}s allocation\n{sum(v):0.4f}s total\n{count/v[1]:0.1f} rays/sec'
for k, v in rps.items()))

def test_empty(self):
"""
Expand All @@ -66,19 +71,19 @@ def test_empty(self):
sphere = g.get_mesh('unit_sphere.STL',
use_embree=use_embree)
# should never hit the sphere
ray_origins = g.np.random.random(dimension)
ray_directions = g.np.tile([0, 1, 0], (dimension[0], 1))
ray_origins[:, 2] = -5
origins = g.np.random.random(dimension)
vectors = g.np.tile([0, 1, 0], (dimension[0], 1))
origins[:, 2] = -5

# make sure ray functions always return numpy arrays
# these functions return multiple results all of which
# should always be a numpy array
assert all(len(i.shape) >= 0 for i in
sphere.ray.intersects_id(
ray_origins, ray_directions))
origins, vectors))
assert all(len(i.shape) >= 0 for i in
sphere.ray.intersects_location(
ray_origins, ray_directions))
origins, vectors))

def test_contains(self):
scale = 1.5
Expand Down Expand Up @@ -109,13 +114,13 @@ def test_on_vertex(self):
origins = g.np.zeros_like(m.vertices)
vectors = m.vertices.copy()

assert m.ray.intersects_any(ray_origins=origins,
ray_directions=vectors).all()
assert m.ray.intersects_any(origins=origins,
vectors=vectors).all()

(locations,
index_ray,
index_tri) = m.ray.intersects_location(ray_origins=origins,
ray_directions=vectors)
index_tri) = m.ray.intersects_location(origins=origins,
vectors=vectors)

hit_count = g.np.bincount(index_ray,
minlength=len(origins))
Expand All @@ -128,7 +133,7 @@ def test_on_edge(self):

points = [[4.5, 0, -23], [4.5, 0, -2], [0, 0, -1e-6], [0, 0, -1]]
truth = [False, True, True, True]
result = g.trimesh.ray.ray_util.contains_points(m.ray, points)
result = g.trimesh.ray.util.contains_points(m.ray, points)

assert (result == truth).all()

Expand All @@ -139,20 +144,20 @@ def test_multiple_hits(self):
f = g.np.array([1000., 1000.])
h, w = 256, 256

# Set up a list of ray directions - one for each pixel in our (256,
# Set up a list of ray vectors - one for each pixel in our (256,
# 256) output image.
ray_directions = g.trimesh.util.grid_arange(
vectors = g.trimesh.util.grid_arange(
[[-h / 2, -w / 2],
[h / 2, w / 2]],
step=2.0)
ray_directions = g.np.column_stack(
(ray_directions,
g.np.ones(len(ray_directions)) * f[0]))
vectors = g.np.column_stack(
(vectors,
g.np.ones(len(vectors)) * f[0]))

# Initialize the camera origin to be somewhere behind the cube.
cam_t = g.np.array([0, 0, -15.])
# Duplicate to ensure we have an camera_origin per ray direction
ray_origins = g.np.tile(cam_t, (ray_directions.shape[0], 1))
origins = g.np.tile(cam_t, (vectors.shape[0], 1))

for use_embree in [True, False]:
# Generate a 1 x 1 x 1 cube using the trimesh box primitive
Expand All @@ -162,14 +167,14 @@ def test_multiple_hits(self):
# Perform 256 * 256 raycasts, one for each pixel on the image
# plane. We only want the 'first' hit.
index_triangles, index_ray = cube_mesh.ray.intersects_id(
ray_origins=ray_origins,
ray_directions=ray_directions,
origins=origins,
vectors=vectors,
multiple_hits=False)
assert len(g.np.unique(index_triangles)) == 2

index_triangles, index_ray = cube_mesh.ray.intersects_id(
ray_origins=ray_origins,
ray_directions=ray_directions,
origins=origins,
vectors=vectors,
multiple_hits=True)
assert len(g.np.unique(index_triangles)) > 2

Expand Down Expand Up @@ -213,8 +218,8 @@ def test_box(self):
# (n,) int, index of original ray
# (m,) int, index of mesh.faces
pos, ray, tri = mesh.ray.intersects_location(
ray_origins=origins,
ray_directions=vectors)
origins=origins,
vectors=vectors)

for p, r in zip(pos, ray):
# intersect location XY should match ray origin XY
Expand All @@ -228,22 +233,25 @@ def test_broken(self):
Test a mesh with badly defined face normals
"""

ray_origins = g.np.array([[0.12801793, 24.5030052, -5.],
[0.12801793, 24.5030052, -5.]])
ray_directions = g.np.array([[-0.13590759, -0.98042506, 0.],
[0.13590759, 0.98042506, -0.]])
origins = g.np.array([[0.12801793, 24.5030052, -5.],
[0.12801793, 24.5030052, -5.]])
vectors = g.np.array([[-0.13590759, -0.98042506, 0.],
[0.13590759, 0.98042506, -0.]])

for kwargs in [{'use_embree': True},
{'use_embree': False}]:
mesh = g.get_mesh('broken.STL', **kwargs)

locations, index_ray, index_tri = mesh.ray.intersects_location(
ray_origins=ray_origins, ray_directions=ray_directions)
origins=origins, vectors=vectors)

# should be same number of location hits
assert len(locations) == len(ray_origins)
assert len(locations) == len(origins)


if __name__ == '__main__':
import faulthandler

faulthandler.enable()
g.trimesh.util.attach_to_log()
g.unittest.main()
2 changes: 1 addition & 1 deletion tests/test_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_procrustes(self):

# every combination of possible boolean options
# a_flip and a_scale are apply-to-test-data
opt = g.itertools.combinations([True, False] * 6, 6)
opt = set(g.itertools.combinations([True, False] * 6, 6))
for reflection, translation, scale, a_flip, a_scale, weight in opt:
# create random points in space
points_a = (g.np.random.random((1000, 3)) - .5) * 1000
Expand Down
12 changes: 5 additions & 7 deletions trimesh/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,12 @@ def __init__(self,
# embree is a much, much faster raytracer written by Intel
# if you have pyembree installed you should use it
# although both raytracers were designed to have a common API
if ray.has_embree and use_embree:
self.ray = ray.ray_pyembree.RayMeshIntersector(self)
if use_embree:
# engines is a sorted list of available ray backends
self.ray = ray.engines[0](self)
else:
# create a ray-mesh query object for the current mesh
# initializing is very inexpensive and object is convenient to have.
# On first query expensive bookkeeping is done (creation of r-tree),
# and is cached for subsequent queries
self.ray = ray.ray_triangle.RayMeshIntersector(self)
# the vanilla ray tracer is the last engine
self.ray = ray.engines[-1](self)

# a quick way to get permuted versions of the current mesh
self.permutate = permutate.Permutator(self)
Expand Down
10 changes: 5 additions & 5 deletions trimesh/intersections.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def plane_lines(plane_origin,
def planes_lines(plane_origins,
plane_normals,
line_origins,
line_directions,
line_vectors,
return_distance=False,
return_denom=False):
"""
Expand All @@ -376,7 +376,7 @@ def planes_lines(plane_origins,
Normal vector of each plane
line_origins : (n,3) float
Point at origin of each line
line_directions : (n,3) float
line_vectors : (n,3) float
Direction vector of each line
return_distance : bool
Return distance from origin to point also
Expand All @@ -399,20 +399,20 @@ def planes_lines(plane_origins,
plane_origins = np.asanyarray(plane_origins, dtype=np.float64)
plane_normals = np.asanyarray(plane_normals, dtype=np.float64)
line_origins = np.asanyarray(line_origins, dtype=np.float64)
line_directions = np.asanyarray(line_directions, dtype=np.float64)
line_vectors = np.asanyarray(line_vectors, dtype=np.float64)

# vector from line to plane
origin_vectors = plane_origins - line_origins

projection_ori = util.diagonal_dot(origin_vectors, plane_normals)
projection_dir = util.diagonal_dot(line_directions, plane_normals)
projection_dir = util.diagonal_dot(line_vectors, plane_normals)

valid = np.abs(projection_dir) > 1e-5

distance = np.divide(projection_ori[valid],
projection_dir[valid])

on_plane = line_directions[valid] * distance.reshape((-1, 1))
on_plane = line_vectors[valid] * distance.reshape((-1, 1))
on_plane += line_origins[valid]

result = [on_plane, valid]
Expand Down
Loading