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

690 lines
21 KiB
Python
Raw Normal View History

"""
triangles.py
-------------
Functions for dealing with triangle soups in (n, 3, 3) float form.
"""
import numpy as np
from . import util
from .util import unitize, diagonal_dot
from .points import point_plane_distance
from .constants import tol
def cross(triangles):
"""
Returns the cross product of two edges from input triangles
Parameters
--------------
triangles: (n, 3, 3) float
Vertices of triangles
Returns
--------------
crosses : (n, 3) float
Cross product of two edge vectors
"""
vectors = np.diff(triangles, axis=1)
crosses = np.cross(vectors[:, 0], vectors[:, 1])
return crosses
def area(triangles=None, crosses=None, sum=False):
"""
Calculates the sum area of input triangles
Parameters
----------
triangles : (n, 3, 3) float
Vertices of triangles
crosses : (n, 3) float or None
As a speedup don't re- compute cross products
sum : bool
Return summed area or individual triangle area
Returns
----------
area : (n,) float or float
Individual or summed area depending on `sum` argument
"""
if crosses is None:
crosses = cross(triangles)
area = (np.sum(crosses**2, axis=1)**.5) * .5
if sum:
return np.sum(area)
return area
def normals(triangles=None, crosses=None):
"""
Calculates the normals of input triangles
Parameters
------------
triangles : (n, 3, 3) float
Vertex positions
crosses : (n, 3) float
Cross products of edge vectors
Returns
------------
normals : (m, 3) float
Normal vectors
valid : (n,) bool
Was the face nonzero area or not
"""
if crosses is None:
crosses = cross(triangles)
# unitize the cross product vectors
unit, valid = unitize(crosses, check_valid=True)
return unit, valid
def angles(triangles):
"""
Calculates the angles of input triangles.
Parameters
------------
triangles : (n, 3, 3) float
Vertex positions
Returns
------------
angles : (n, 3) float
Angles at vertex positions in radians
Degenerate angles will be returned as zero
"""
# don't copy triangles
triangles = np.asanyarray(triangles, dtype=np.float64)
# get a unit vector for each edge of the triangle
u = unitize(triangles[:, 1] - triangles[:, 0])
v = unitize(triangles[:, 2] - triangles[:, 0])
w = unitize(triangles[:, 2] - triangles[:, 1])
# run the cosine and per-row dot product
result = np.zeros((len(triangles), 3), dtype=np.float64)
# clip to make sure we don't float error past 1.0
result[:, 0] = np.arccos(np.clip(diagonal_dot(u, v), -1, 1))
result[:, 1] = np.arccos(np.clip(diagonal_dot(-u, w), -1, 1))
# the third angle is just the remaining
result[:, 2] = np.pi - result[:, 0] - result[:, 1]
# a triangle with any zero angles is degenerate
# so set all of the angles to zero in that case
result[(result < tol.merge).any(axis=1), :] = 0.0
return result
def all_coplanar(triangles):
"""
Check to see if a list of triangles are all coplanar
Parameters
----------------
triangles: (n, 3, 3) float
Vertices of triangles
Returns
---------------
all_coplanar : bool
True if all triangles are coplanar
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
test_normal = normals(triangles)[0]
test_vertex = triangles[0][0]
distances = point_plane_distance(points=triangles[1:].reshape((-1, 3)),
plane_normal=test_normal,
plane_origin=test_vertex)
all_coplanar = np.all(np.abs(distances) < tol.zero)
return all_coplanar
def any_coplanar(triangles):
"""
For a list of triangles if the FIRST triangle is coplanar
with ANY of the following triangles, return True.
Otherwise, return False.
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
test_normal = normals(triangles)[0]
test_vertex = triangles[0][0]
distances = point_plane_distance(points=triangles[1:].reshape((-1, 3)),
plane_normal=test_normal,
plane_origin=test_vertex)
any_coplanar = np.any(
np.all(np.abs(distances.reshape((-1, 3)) < tol.zero), axis=1))
return any_coplanar
def mass_properties(triangles,
crosses=None,
density=1.0,
center_mass=None,
skip_inertia=False):
"""
Calculate the mass properties of a group of triangles.
Implemented from:
http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf
Parameters
----------
triangles : (n, 3, 3) float
Triangle vertices in space
crosses : (n,) float
Optional cross products of triangles
density : float
Optional override for density
center_mass : (3,) float
Optional override for center mass
skip_inertia : bool
if True will not return moments matrix
Returns
---------
info : dict
Mass properties
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
if crosses is None:
crosses = cross(triangles)
# these are the subexpressions of the integral
# this is equvilant but 7x faster than triangles.sum(axis=1)
f1 = triangles[:, 0, :] + triangles[:, 1, :] + triangles[:, 2, :]
# for the the first vertex of every triangle:
# triangles[:,0,:] will give rows like [[x0, y0, z0], ...]
# for the x coordinates of every triangle
# triangles[:,:,0] will give rows like [[x0, x1, x2], ...]
f2 = (triangles[:, 0, :]**2 +
triangles[:, 1, :]**2 +
triangles[:, 0, :] * triangles[:, 1, :] +
triangles[:, 2, :] * f1)
f3 = ((triangles[:, 0, :]**3) +
(triangles[:, 0, :]**2) * (triangles[:, 1, :]) +
(triangles[:, 0, :]) * (triangles[:, 1, :]**2) +
(triangles[:, 1, :]**3) +
(triangles[:, 2, :] * f2))
g0 = (f2 + (triangles[:, 0, :] + f1) * triangles[:, 0, :])
g1 = (f2 + (triangles[:, 1, :] + f1) * triangles[:, 1, :])
g2 = (f2 + (triangles[:, 2, :] + f1) * triangles[:, 2, :])
integral = np.zeros((10, len(f1)))
integral[0] = crosses[:, 0] * f1[:, 0]
integral[1:4] = (crosses * f2).T
integral[4:7] = (crosses * f3).T
for i in range(3):
triangle_i = np.mod(i + 1, 3)
integral[i + 7] = crosses[:, i] * (
(triangles[:, 0, triangle_i] * g0[:, i]) +
(triangles[:, 1, triangle_i] * g1[:, i]) +
(triangles[:, 2, triangle_i] * g2[:, i]))
coefficients = 1.0 / np.array(
[6, 24, 24, 24, 60, 60, 60, 120, 120, 120],
dtype=np.float64)
integrated = integral.sum(axis=1) * coefficients
volume = integrated[0]
if center_mass is None:
if np.abs(volume) < tol.zero:
center_mass = np.zeros(3)
else:
center_mass = integrated[1:4] / volume
mass = density * volume
result = {'density': density,
'mass': mass,
'volume': volume,
'center_mass': center_mass}
if skip_inertia:
return result
inertia = np.zeros((3, 3))
inertia[0, 0] = integrated[5] + integrated[6] - \
(volume * (center_mass[[1, 2]]**2).sum())
inertia[1, 1] = integrated[4] + integrated[6] - \
(volume * (center_mass[[0, 2]]**2).sum())
inertia[2, 2] = integrated[4] + integrated[5] - \
(volume * (center_mass[[0, 1]]**2).sum())
inertia[0, 1] = (
integrated[7] - (volume * np.product(center_mass[[0, 1]])))
inertia[1, 2] = (
integrated[8] - (volume * np.product(center_mass[[1, 2]])))
inertia[0, 2] = (
integrated[9] - (volume * np.product(center_mass[[0, 2]])))
inertia[2, 0] = inertia[0, 2]
inertia[2, 1] = inertia[1, 2]
inertia[1, 0] = inertia[0, 1]
inertia *= density
result['inertia'] = inertia
return result
def windings_aligned(triangles, normals_compare):
"""
Given a list of triangles and a list of normals determine if the
two are aligned
Parameters
----------
triangles : (n, 3, 3) float
Vertex locations in space
normals_compare : (n, 3) float
List of normals to compare
Returns
----------
aligned : (n,) bool
Are normals aligned with triangles
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3), allow_zeros=True):
raise ValueError(
'triangles must have shape (n, 3, 3), got %s' % str(triangles.shape))
calculated, valid = normals(triangles)
difference = diagonal_dot(calculated,
normals_compare[valid])
aligned = np.zeros(len(triangles), dtype=np.bool)
aligned[valid] = difference > 0.0
return aligned
def bounds_tree(triangles):
"""
Given a list of triangles, create an r-tree for broad- phase
collision detection
Parameters
---------
triangles : (n, 3, 3) float
Triangles in space
Returns
---------
tree : rtree.Rtree
One node per triangle
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
# the (n,6) interleaved bounding box for every triangle
triangle_bounds = np.column_stack((triangles.min(axis=1),
triangles.max(axis=1)))
tree = util.bounds_tree(triangle_bounds)
return tree
def nondegenerate(triangles, areas=None, height=None):
"""
Find all triangles which have an oriented bounding box
where both of the two sides is larger than a specified height.
Degenerate triangles can be when:
1) Two of the three vertices are colocated
2) All three vertices are unique but colinear
Parameters
----------
triangles : (n, 3, 3) float
Triangles in space
height : float
Minimum edge length of a triangle to keep
Returns
----------
nondegenerate : (n,) bool
True if a triangle meets required minimum height
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
if height is None:
height = tol.merge
# if both edges of the triangles OBB are longer than tol.merge
# we declare them to be nondegenerate
ok = (extents(triangles=triangles,
areas=areas) > height).all(axis=1)
return ok
def extents(triangles, areas=None):
"""
Return the 2D bounding box size of each triangle.
Parameters
----------
triangles : (n, 3, 3) float
Triangles in space
areas : (n,) float
Optional area of input triangles
Returns
----------
box : (n, 2) float
The size of each triangle's 2D oriented bounding box
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
if areas is None:
areas = area(triangles=triangles,
sum=False)
# the edge vectors which define the triangle
a = triangles[:, 1] - triangles[:, 0]
b = triangles[:, 2] - triangles[:, 0]
# length of the edge vectors
length_a = (a**2).sum(axis=1)**.5
length_b = (b**2).sum(axis=1)**.5
# which edges are acceptable length
nonzero_a = length_a > tol.merge
nonzero_b = length_b > tol.merge
# find the two heights of the triangle
# essentially this is the side length of an
# oriented bounding box, per triangle
box = np.zeros((len(triangles), 2), dtype=np.float64)
box[:, 0][nonzero_a] = (areas[nonzero_a] * 2) / length_a[nonzero_a]
box[:, 1][nonzero_b] = (areas[nonzero_b] * 2) / length_b[nonzero_b]
return box
def barycentric_to_points(triangles, barycentric):
"""
Convert a list of barycentric coordinates on a list of triangles
to cartesian points.
Parameters
------------
triangles : (n, 3, 3) float
Triangles in space
barycentric : (n, 2) float
Barycentric coordinates
Returns
-----------
points : (m, 3) float
Points in space
"""
barycentric = np.asanyarray(barycentric, dtype=np.float64)
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
if barycentric.shape == (2,):
barycentric = np.ones((len(triangles), 2),
dtype=np.float64) * barycentric
if util.is_shape(barycentric, (len(triangles), 2)):
barycentric = np.column_stack((barycentric,
1.0 - barycentric.sum(axis=1)))
elif not util.is_shape(barycentric, (len(triangles), 3)):
raise ValueError('Barycentric shape incorrect!')
barycentric /= barycentric.sum(axis=1).reshape((-1, 1))
points = (triangles * barycentric.reshape((-1, 3, 1))).sum(axis=1)
return points
def points_to_barycentric(triangles,
points,
method='cramer'):
"""
Find the barycentric coordinates of points relative to triangles.
The Cramer's rule solution implements:
http://blackpawn.com/texts/pointinpoly
The cross product solution implements:
https://www.cs.ubc.ca/~heidrich/Papers/JGT.05.pdf
Parameters
-----------
triangles : (n, 3, 3) float
Triangles vertices in space
points : (n, 3) float
Point in space associated with a triangle
method : str
Which method to compute the barycentric coordinates with:
- 'cross': uses a method using cross products, roughly 2x slower but
different numerical robustness properties
- anything else: uses a cramer's rule solution
Returns
-----------
barycentric : (n, 3) float
Barycentric coordinates of each point
"""
def method_cross():
n = np.cross(edge_vectors[:, 0], edge_vectors[:, 1])
denominator = diagonal_dot(n, n)
barycentric = np.zeros((len(triangles), 3), dtype=np.float64)
barycentric[:, 2] = diagonal_dot(
np.cross(edge_vectors[:, 0], w), n) / denominator
barycentric[:, 1] = diagonal_dot(
np.cross(w, edge_vectors[:, 1]), n) / denominator
barycentric[:, 0] = 1 - barycentric[:, 1] - barycentric[:, 2]
return barycentric
def method_cramer():
dot00 = diagonal_dot(edge_vectors[:, 0], edge_vectors[:, 0])
dot01 = diagonal_dot(edge_vectors[:, 0], edge_vectors[:, 1])
dot02 = diagonal_dot(edge_vectors[:, 0], w)
dot11 = diagonal_dot(edge_vectors[:, 1], edge_vectors[:, 1])
dot12 = diagonal_dot(edge_vectors[:, 1], w)
inverse_denominator = 1.0 / (dot00 * dot11 - dot01 * dot01)
barycentric = np.zeros((len(triangles), 3), dtype=np.float64)
barycentric[:, 2] = (dot00 * dot12 - dot01 *
dot02) * inverse_denominator
barycentric[:, 1] = (dot11 * dot02 - dot01 *
dot12) * inverse_denominator
barycentric[:, 0] = 1 - barycentric[:, 1] - barycentric[:, 2]
return barycentric
# establish that input triangles and points are sane
triangles = np.asanyarray(triangles, dtype=np.float64)
points = np.asanyarray(points, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('triangles shape incorrect')
if not util.is_shape(points, (len(triangles), 3)):
raise ValueError('triangles and points must correspond')
edge_vectors = triangles[:, 1:] - triangles[:, :1]
w = points - triangles[:, 0].reshape((-1, 3))
if method == 'cross':
return method_cross()
return method_cramer()
def closest_point(triangles, points):
"""
Return the closest point on the surface of each triangle for a
list of corresponding points.
Implements the method from "Real Time Collision Detection" and
use the same variable names as "ClosestPtPointTriangle" to avoid
being any more confusing.
Parameters
----------
triangles : (n, 3, 3) float
Triangle vertices in space
points : (n, 3) float
Points in space
Returns
----------
closest : (n, 3) float
Point on each triangle closest to each point
"""
# check input triangles and points
triangles = np.asanyarray(triangles, dtype=np.float64)
points = np.asanyarray(points, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('triangles shape incorrect')
if not util.is_shape(points, (len(triangles), 3)):
raise ValueError('need same number of triangles and points!')
# store the location of the closest point
result = np.zeros_like(points)
# which points still need to be handled
remain = np.ones(len(points), dtype=np.bool)
# if we dot product this against a (n, 3)
# it is equivalent but faster than array.sum(axis=1)
ones = [1.0, 1.0, 1.0]
# get the three points of each triangle
# use the same notation as RTCD to avoid confusion
a = triangles[:, 0, :]
b = triangles[:, 1, :]
c = triangles[:, 2, :]
# check if P is in vertex region outside A
ab = b - a
ac = c - a
ap = points - a
# this is a faster equivalent of:
# diagonal_dot(ab, ap)
d1 = np.dot(ab * ap, ones)
d2 = np.dot(ac * ap, ones)
# is the point at A
is_a = np.logical_and(d1 < tol.zero, d2 < tol.zero)
if is_a.any():
result[is_a] = a[is_a]
remain[is_a] = False
# check if P in vertex region outside B
bp = points - b
d3 = np.dot(ab * bp, ones)
d4 = np.dot(ac * bp, ones)
# do the logic check
is_b = (d3 > -tol.zero) & (d4 <= d3) & remain
if is_b.any():
result[is_b] = b[is_b]
remain[is_b] = False
# check if P in edge region of AB, if so return projection of P onto A
vc = (d1 * d4) - (d3 * d2)
is_ab = ((vc < tol.zero) &
(d1 > -tol.zero) &
(d3 < tol.zero) & remain)
if is_ab.any():
v = (d1[is_ab] / (d1[is_ab] - d3[is_ab])).reshape((-1, 1))
result[is_ab] = a[is_ab] + (v * ab[is_ab])
remain[is_ab] = False
# check if P in vertex region outside C
cp = points - c
d5 = np.dot(ab * cp, ones)
d6 = np.dot(ac * cp, ones)
is_c = (d6 > -tol.zero) & (d5 <= d6) & remain
if is_c.any():
result[is_c] = c[is_c]
remain[is_c] = False
# check if P in edge region of AC, if so return projection of P onto AC
vb = (d5 * d2) - (d1 * d6)
is_ac = (vb < tol.zero) & (d2 > -tol.zero) & (d6 < tol.zero) & remain
if is_ac.any():
w = (d2[is_ac] / (d2[is_ac] - d6[is_ac])).reshape((-1, 1))
result[is_ac] = a[is_ac] + w * ac[is_ac]
remain[is_ac] = False
# check if P in edge region of BC, if so return projection of P onto BC
va = (d3 * d6) - (d5 * d4)
is_bc = ((va < tol.zero) &
((d4 - d3) > - tol.zero) &
((d5 - d6) > -tol.zero) & remain)
if is_bc.any():
d43 = d4[is_bc] - d3[is_bc]
w = (d43 / (d43 + (d5[is_bc] - d6[is_bc]))).reshape((-1, 1))
result[is_bc] = b[is_bc] + w * (c[is_bc] - b[is_bc])
remain[is_bc] = False
# any remaining points must be inside face region
if remain.any():
# point is inside face region
denom = 1.0 / (va[remain] + vb[remain] + vc[remain])
v = (vb[remain] * denom).reshape((-1, 1))
w = (vc[remain] * denom).reshape((-1, 1))
# compute Q through its barycentric coordinates
result[remain] = a[remain] + (ab[remain] * v) + (ac[remain] * w)
return result
def to_kwargs(triangles):
"""
Convert a list of triangles to the kwargs for the Trimesh
constructor.
Parameters
---------
triangles : (n, 3, 3) float
Triangles in space
Returns
---------
kwargs : dict
Keyword arguments for the trimesh.Trimesh constructor
Includes keys 'vertices' and 'faces'
Examples
---------
>>> mesh = trimesh.Trimesh(**trimesh.triangles.to_kwargs(triangles))
"""
triangles = np.asanyarray(triangles, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError('Triangles must be (n, 3, 3)!')
vertices = triangles.reshape((-1, 3))
faces = np.arange(len(vertices)).reshape((-1, 3))
kwargs = {'vertices': vertices,
'faces': faces}
return kwargs