""" creation.py -------------- Create meshes from primitives, or with operations. """ from .base import Trimesh from .constants import log, tol from .geometry import faces_to_edges, align_vectors, plane_transform from . import util from . import grouping from . import triangles from . import transformations as tf import numpy as np import collections try: # shapely is a soft dependency from shapely.geometry import Polygon from shapely.wkb import loads as load_wkb except BaseException as E: # shapely will sometimes raise OSErrors # on import rather than just ImportError from . import exceptions # re-raise the exception when someone tries # to use the module that they don't have Polygon = exceptions.closure(E) load_wkb = exceptions.closure(E) def revolve(linestring, angle=None, sections=None, transform=None, **kwargs): """ Revolve a 2D line string around the 2D Y axis, with a result with the 2D Y axis pointing along the 3D Z axis. This function is intended to handle the complexity of indexing and is intended to be used to create all radially symmetric primitives, eventually including cylinders, annular cylinders, capsules, cones, and UV spheres. Note that if your linestring is closed, it needs to be counterclockwise if you would like face winding and normals facing outwards. Parameters ------------- linestring : (n, 2) float Lines in 2D which will be revolved angle : None or float Angle in radians to revolve curve by sections : None or int Number of sections result should have If not specified default is 32 per revolution transform : None or (4, 4) float Transform to apply to mesh after construction **kwargs : dict Passed to Trimesh constructor Returns -------------- revolved : Trimesh Mesh representing revolved result """ linestring = np.asanyarray(linestring, dtype=np.float64) # linestring must be ordered 2D points if len(linestring.shape) != 2 or linestring.shape[1] != 2: raise ValueError('linestring must be 2D!') if angle is None: # default to closing the revolution angle = np.pi * 2 closed = True else: # check passed angle value closed = angle >= ((np.pi * 2) - 1e-8) if sections is None: # default to 32 sections for a full revolution sections = int(angle / (np.pi * 2) * 32) # change to face count sections += 1 # create equally spaced angles theta = np.linspace(0, angle, sections) # 2D points around the revolution points = np.column_stack((np.cos(theta), np.sin(theta))) # how many points per slice per = len(linestring) # use the 2D X component as radius radius = linestring[:, 0] # use the 2D Y component as the height along revolution height = linestring[:, 1] # a lot of tiling to get our 3D vertices vertices = np.column_stack(( np.tile(points, (1, per)).reshape((-1, 2)) * np.tile(radius, len(points)).reshape((-1, 1)), np.tile(height, len(points)))) if closed: # should be a duplicate set of vertices assert np.allclose(vertices[:per], vertices[-per:]) # chop off duplicate vertices vertices = vertices[:-per] if transform is not None: # apply transform to vertices vertices = tf.transform_points(vertices, transform) # how many slices of the pie slices = len(theta) - 1 # start with a quad for every segment # this is a superset which will then be reduced quad = np.array([0, per, 1, 1, per, per + 1]) # stack the faces for a single slice of the revolution single = np.tile(quad, per).reshape((-1, 3)) # `per` is basically the stride of the vertices single += np.tile(np.arange(per), (2, 1)).T.reshape((-1, 1)) # remove any zero-area triangle # this covers many cases without having to think too much single = single[triangles.area(vertices[single]) > tol.merge] # how much to offset each slice # note arange multiplied by vertex stride # but tiled by the number of faces we actually have offset = np.tile(np.arange(slices) * per, (len(single), 1)).T.reshape((-1, 1)) # stack a single slice into N slices stacked = np.tile(single.ravel(), slices).reshape((-1, 3)) if tol.strict: # make sure we didn't screw up stacking operation assert np.allclose(stacked.reshape((-1, single.shape[0], 3)) - single, 0) # offset stacked and wrap vertices faces = (stacked + offset) % len(vertices) # create the mesh from our vertices and faces mesh = Trimesh(vertices=vertices, faces=faces) # strict checks run only in unit tests if (tol.strict and np.allclose(radius[[0, -1]], 0.0) or np.allclose(linestring[0], linestring[-1])): # if revolved curve starts and ends with zero radius # it should really be a valid volume, unless the sign # reversed on the input linestring assert mesh.is_volume return mesh def extrude_polygon(polygon, height, transform=None, triangle_args=None, **kwargs): """ Extrude a 2D shapely polygon into a 3D mesh Parameters ---------- polygon : shapely.geometry.Polygon 2D geometry to extrude height : float Distance to extrude polygon along Z triangle_args : str or None Passed to triangle **kwargs: passed to Trimesh Returns ---------- mesh : trimesh.Trimesh Resulting extrusion as watertight body """ # create a triangulation from the polygon vertices, faces = triangulate_polygon( polygon, triangle_args=triangle_args, **kwargs) # extrude that triangulation along Z mesh = extrude_triangulation(vertices=vertices, faces=faces, height=height, transform=transform, **kwargs) return mesh def sweep_polygon(polygon, path, angles=None, **kwargs): """ Extrude a 2D shapely polygon into a 3D mesh along an arbitrary 3D path. Doesn't handle sharp curvature well. Parameters ---------- polygon : shapely.geometry.Polygon Profile to sweep along path path : (n, 3) float A path in 3D angles : (n,) float Optional rotation angle relative to prior vertex at each vertex Returns ------- mesh : trimesh.Trimesh Geometry of result """ path = np.asanyarray(path, dtype=np.float64) if not util.is_shape(path, (-1, 3)): raise ValueError('Path must be (n, 3)!') # Extract 2D vertices and triangulation verts_2d = np.array(polygon.exterior)[:-1] base_verts_2d, faces_2d = triangulate_polygon(polygon, **kwargs) n = len(verts_2d) # Create basis for first planar polygon cap x, y, z = util.generate_basis(path[0] - path[1]) tf_mat = np.ones((4, 4)) tf_mat[:3, :3] = np.c_[x, y, z] tf_mat[:3, 3] = path[0] # Compute 3D locations of those vertices verts_3d = np.c_[verts_2d, np.zeros(n)] verts_3d = tf.transform_points(verts_3d, tf_mat) base_verts_3d = np.c_[base_verts_2d, np.zeros(len(base_verts_2d))] base_verts_3d = tf.transform_points(base_verts_3d, tf_mat) # keep matching sequence of vertices and 0- indexed faces vertices = [base_verts_3d] faces = [faces_2d] # Compute plane normals for each turn -- # each turn induces a plane halfway between the two vectors v1s = util.unitize(path[1:-1] - path[:-2]) v2s = util.unitize(path[1:-1] - path[2:]) norms = np.cross(np.cross(v1s, v2s), v1s + v2s) norms[(norms == 0.0).all(1)] = v1s[(norms == 0.0).all(1)] norms = util.unitize(norms) final_v1 = util.unitize(path[-1] - path[-2]) norms = np.vstack((norms, final_v1)) v1s = np.vstack((v1s, final_v1)) # Create all side walls by projecting the 3d vertices into each plane # in succession for i in range(len(norms)): verts_3d_prev = verts_3d # Rotate if needed if angles is not None: tf_mat = tf.rotation_matrix(angles[i], norms[i], path[i]) verts_3d_prev = tf.transform_points(verts_3d_prev, tf_mat) # Project vertices onto plane in 3D ds = np.einsum('ij,j->i', (path[i + 1] - verts_3d_prev), norms[i]) ds = ds / np.dot(v1s[i], norms[i]) verts_3d_new = np.einsum('i,j->ij', ds, v1s[i]) + verts_3d_prev # Add to face and vertex lists new_faces = [[i + n, (i + 1) % n, i] for i in range(n)] new_faces.extend([[(i - 1) % n + n, i + n, i] for i in range(n)]) # save faces and vertices into a sequence faces.append(np.array(new_faces)) vertices.append(np.vstack((verts_3d, verts_3d_new))) verts_3d = verts_3d_new # do the main stack operation from a sequence to (n,3) arrays # doing one vstack provides a substantial speedup by # avoiding a bunch of temporary allocations vertices, faces = util.append_faces(vertices, faces) # Create final cap x, y, z = util.generate_basis(path[-1] - path[-2]) vecs = verts_3d - path[-1] coords = np.c_[np.einsum('ij,j->i', vecs, x), np.einsum('ij,j->i', vecs, y)] base_verts_2d, faces_2d = triangulate_polygon(Polygon(coords)) base_verts_3d = (np.einsum('i,j->ij', base_verts_2d[:, 0], x) + np.einsum('i,j->ij', base_verts_2d[:, 1], y)) + path[-1] faces = np.vstack((faces, faces_2d + len(vertices))) vertices = np.vstack((vertices, base_verts_3d)) return Trimesh(vertices, faces) def extrude_triangulation(vertices, faces, height, transform=None, **kwargs): """ Extrude a 2D triangulation into a watertight mesh. Parameters ---------- vertices : (n, 2) float 2D vertices faces : (m, 3) int Triangle indexes of vertices height : float Distance to extrude triangulation **kwargs : dict Passed to Trimesh constructor Returns --------- mesh : trimesh.Trimesh Mesh created from extrusion """ vertices = np.asanyarray(vertices, dtype=np.float64) height = float(height) faces = np.asanyarray(faces, dtype=np.int64) if not util.is_shape(vertices, (-1, 2)): raise ValueError('Vertices must be (n,2)') if not util.is_shape(faces, (-1, 3)): raise ValueError('Faces must be (n,3)') if np.abs(height) < tol.merge: raise ValueError('Height must be nonzero!') # make sure triangulation winding is pointing up normal_test = triangles.normals( [util.stack_3D(vertices[faces[0]])])[0] normal_dot = np.dot(normal_test, [0.0, 0.0, np.sign(height)])[0] # make sure the triangulation is aligned with the sign of # the height we've been passed if normal_dot < 0.0: faces = np.fliplr(faces) # stack the (n,3) faces into (3*n, 2) edges edges = faces_to_edges(faces) edges_sorted = np.sort(edges, axis=1) # edges which only occur once are on the boundary of the polygon # since the triangulation may have subdivided the boundary of the # shapely polygon, we need to find it again edges_unique = grouping.group_rows( edges_sorted, require_count=1) # (n, 2, 2) set of line segments (positions, not references) boundary = vertices[edges[edges_unique]] # we are creating two vertical triangles for every 2D line segment # on the boundary of the 2D triangulation vertical = np.tile(boundary.reshape((-1, 2)), 2).reshape((-1, 2)) vertical = np.column_stack((vertical, np.tile([0, height, 0, height], len(boundary)))) vertical_faces = np.tile([3, 1, 2, 2, 1, 0], (len(boundary), 1)) vertical_faces += np.arange(len(boundary)).reshape((-1, 1)) * 4 vertical_faces = vertical_faces.reshape((-1, 3)) # stack the (n,2) vertices with zeros to make them (n, 3) vertices_3D = util.stack_3D(vertices) # a sequence of zero- indexed faces, which will then be appended # with offsets to create the final mesh faces_seq = [faces[:, ::-1], faces.copy(), vertical_faces] vertices_seq = [vertices_3D, vertices_3D.copy() + [0.0, 0, height], vertical] # append sequences into flat nicely indexed arrays vertices, faces = util.append_faces(vertices_seq, faces_seq) if transform is not None: # apply transform here to avoid later bookkeeping vertices = tf.transform_points( vertices, transform) # if the transform flips the winding flip faces back # so that the normals will be facing outwards if tf.flips_winding(transform): # fliplr makes arrays non-contiguous faces = np.ascontiguousarray(np.fliplr(faces)) # create mesh object with passed keywords mesh = Trimesh(vertices=vertices, faces=faces, **kwargs) # only check in strict mode (unit tests) if tol.strict: assert mesh.volume > 0.0 return mesh def triangulate_polygon(polygon, triangle_args=None, **kwargs): """ Given a shapely polygon create a triangulation using a python interface to `triangle.c`: > `pip install triangle` Parameters --------- polygon : Shapely.geometry.Polygon Polygon object to be triangulated triangle_args : str or None Passed to triangle.triangulate i.e: 'p', 'pq30' Returns -------------- vertices : (n, 2) float Points in space faces : (n, 3) int Index of vertices that make up triangles """ # do the import here for soft requirement from triangle import triangulate # set default triangulation arguments if not specified if triangle_args is None: triangle_args = 'p' # turn the polygon in to vertices, segments, and hole points arg = _polygon_to_kwargs(polygon) # run the triangulation result = triangulate(arg, triangle_args) return result['vertices'], result['triangles'] def _polygon_to_kwargs(polygon): """ Given a shapely polygon generate the data to pass to the triangle mesh generator Parameters --------- polygon : Shapely.geometry.Polygon Input geometry Returns -------- result : dict Has keys: vertices, segments, holes """ if not polygon.is_valid: raise ValueError('invalid shapely polygon passed!') def round_trip(start, length): """ Given a start index and length, create a series of (n, 2) edges which create a closed traversal. Examples --------- start, length = 0, 3 returns: [(0,1), (1,2), (2,0)] """ tiled = np.tile(np.arange(start, start + length).reshape((-1, 1)), 2) tiled = tiled.reshape(-1)[1:-1].reshape((-1, 2)) tiled = np.vstack((tiled, [tiled[-1][-1], tiled[0][0]])) return tiled def add_boundary(boundary, start): # coords is an (n, 2) ordered list of points on the polygon boundary # the first and last points are the same, and there are no # guarantees on points not being duplicated (which will # later cause meshpy/triangle to shit a brick) coords = np.array(boundary.coords) # find indices points which occur only once, and sort them # to maintain order unique = np.sort(grouping.unique_rows(coords)[0]) cleaned = coords[unique] vertices.append(cleaned) facets.append(round_trip(start, len(cleaned))) # holes require points inside the region of the hole, which we find # by creating a polygon from the cleaned boundary region, and then # using a representative point. You could do things like take the mean of # the points, but this is more robust (to things like concavity), if # slower. test = Polygon(cleaned) holes.append(np.array(test.representative_point().coords)[0]) return len(cleaned) # sequence of (n,2) points in space vertices = collections.deque() # sequence of (n,2) indices of vertices facets = collections.deque() # list of (2) vertices in interior of hole regions holes = collections.deque() start = add_boundary(polygon.exterior, 0) for interior in polygon.interiors: try: start += add_boundary(interior, start) except BaseException: log.warning('invalid interior, continuing') continue # create clean (n,2) float array of vertices # and (m, 2) int array of facets # by stacking the sequence of (p,2) arrays vertices = np.vstack(vertices) facets = np.vstack(facets).tolist() # shapely polygons can include a Z component # strip it out for the triangulation if vertices.shape[1] == 3: vertices = vertices[:, :2] result = {'vertices': vertices, 'segments': facets} # holes in meshpy lingo are a (h, 2) list of (x,y) points # which are inside the region of the hole # we added a hole for the exterior, which we slice away here holes = np.array(holes)[1:] if len(holes) > 0: result['holes'] = holes return result def box(extents=None, transform=None, **kwargs): """ Return a cuboid. Parameters ------------ extents : float, or (3,) float Edge lengths transform: (4, 4) float Transformation matrix **kwargs: passed to Trimesh to create box Returns ------------ geometry : trimesh.Trimesh Mesh of a cuboid """ # vertices of the cube vertices = [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1] vertices = np.array(vertices, order='C', dtype=np.float64).reshape((-1, 3)) vertices -= 0.5 # resize cube based on passed extents if extents is not None: extents = np.asanyarray(extents, dtype=np.float64) if extents.shape != (3,): raise ValueError('Extents must be (3,)!') vertices *= extents # hardcoded face indices faces = [1, 3, 0, 4, 1, 0, 0, 3, 2, 2, 4, 0, 1, 7, 3, 5, 1, 4, 5, 7, 1, 3, 7, 2, 6, 4, 2, 2, 7, 6, 6, 5, 4, 7, 5, 6] faces = np.array(faces, order='C', dtype=np.int64).reshape((-1, 3)) face_normals = [-1, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 1, 0, -1, 0, 0, 0, 1, 0, 1, 0, 0, 0, -1, 0, 1, 0, 1, 0, 0, 1, 0, 0] face_normals = np.asanyarray(face_normals, order='C', dtype=np.float64).reshape(-1, 3) box = Trimesh(vertices=vertices, faces=faces, face_normals=face_normals, process=False, **kwargs) # do the transform here to preserve face normals if transform is not None: box.apply_transform(transform) return box def icosahedron(): """ Create an icosahedron, a 20 faced polyhedron. Returns ------------- ico : trimesh.Trimesh Icosahederon centered at the origin. """ t = (1.0 + 5.0**.5) / 2.0 vertices = [-1, t, 0, 1, t, 0, -1, -t, 0, 1, -t, 0, 0, -1, t, 0, 1, t, 0, -1, -t, 0, 1, -t, t, 0, -1, t, 0, 1, -t, 0, -1, -t, 0, 1] faces = [0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8, 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1] # scale vertices so each vertex radius is 1.0 vertices = np.reshape(vertices, (-1, 3)) / np.sqrt(2.0 + t) faces = np.reshape(faces, (-1, 3)) mesh = Trimesh(vertices=vertices, faces=faces, process=False) return mesh def icosphere(subdivisions=3, radius=1.0, color=None): """ Create an isophere centered at the origin. Parameters ---------- subdivisions : int How many times to subdivide the mesh. Note that the number of faces will grow as function of 4 ** subdivisions, so you probably want to keep this under ~5 radius : float Desired radius of sphere color: (3,) float or uint8 Desired color of sphere Returns --------- ico : trimesh.Trimesh Meshed sphere """ def refine_spherical(): vectors = ico.vertices scalar = (vectors ** 2).sum(axis=1)**.5 unit = vectors / scalar.reshape((-1, 1)) offset = radius - scalar ico.vertices += unit * offset.reshape((-1, 1)) ico = icosahedron() ico._validate = False for j in range(subdivisions): ico = ico.subdivide() refine_spherical() ico._validate = True if color is not None: ico.visual.face_colors = color return ico def uv_sphere(radius=1.0, count=[32, 32], theta=None, phi=None): """ Create a UV sphere (latitude + longitude) centered at the origin. Roughly one order of magnitude faster than an icosphere but slightly uglier. Parameters ---------- radius : float Radius of sphere count : (2,) int Number of latitude and longitude lines theta : (n,) float Optional theta angles in radians phi : (n,) float Optional phi angles in radians Returns ---------- mesh : trimesh.Trimesh Mesh of UV sphere with specified parameters """ count = np.array(count, dtype=np.int) count += np.mod(count, 2) count[1] *= 2 # generate vertices on a sphere using spherical coordinates if theta is None: theta = np.linspace(0, np.pi, count[0]) if phi is None: phi = np.linspace(0, np.pi * 2, count[1])[:-1] spherical = np.dstack((np.tile(phi, (len(theta), 1)).T, np.tile(theta, (len(phi), 1)))).reshape((-1, 2)) vertices = util.spherical_to_vector(spherical) * radius # generate faces by creating a bunch of pie wedges c = len(theta) # a quad face as two triangles pairs = np.array([[c, 0, 1], [c + 1, c, 1]]) # increment both triangles in each quad face by the same offset incrementor = np.tile(np.arange(c - 1), (2, 1)).T.reshape((-1, 1)) # create the faces for a single pie wedge of the sphere strip = np.tile(pairs, (c - 1, 1)) strip += incrementor # the first and last faces will be degenerate since the first # and last vertex are identical in the two rows strip = strip[1:-1] # tile pie wedges into a sphere faces = np.vstack([strip + (i * c) for i in range(len(phi))]) # poles are repeated in every strip, so a mask to merge them mask = np.arange(len(vertices)) # the top pole are all the same vertex mask[0::c] = 0 # the bottom pole are all the same vertex mask[c - 1::c] = c - 1 # faces masked to remove the duplicated pole vertices # and mod to wrap to fill in the last pie wedge faces = mask[np.mod(faces, len(vertices))] # we save a lot of time by not processing again # since we did some bookkeeping mesh is watertight mesh = Trimesh(vertices=vertices, faces=faces, process=False) return mesh def capsule(height=1.0, radius=1.0, count=[32, 32]): """ Create a mesh of a capsule, or a cylinder with hemispheric ends. Parameters ---------- height : float Center to center distance of two spheres radius : float Radius of the cylinder and hemispheres count : (2,) int Number of sections on latitude and longitude Returns ---------- capsule : trimesh.Trimesh Capsule geometry with: - cylinder axis is along Z - one hemisphere is centered at the origin - other hemisphere is centered along the Z axis at height """ height = float(height) radius = float(radius) count = np.array(count, dtype=np.int) count += np.mod(count, 2) # create a theta where there is a double band around the equator # so that we can offset the top and bottom of a sphere to # get a nicely meshed capsule theta = np.linspace(0, np.pi, count[0]) center = np.clip(np.arctan(tol.merge / radius), tol.merge, np.inf) offset = np.array([-center, center]) + (np.pi / 2) theta = np.insert(theta, int(len(theta) / 2), offset) capsule = uv_sphere(radius=radius, count=count, theta=theta) top = capsule.vertices[:, 2] > tol.zero capsule.vertices[top] += [0, 0, height] return capsule def cone(radius, height, sections=None, transform=None, **kwargs): """ Create a mesh of a cone along Z centered at the origin. Parameters ---------- radius : float The radius of the cylinder height : float The height of the cylinder sections : int or None How many pie wedges per revolution transform : (4, 4) float or None Transform to apply after creation **kwargs : dict Passed to Trimesh constructor Returns ---------- cone: trimesh.Trimesh Resulting mesh of a cone """ # create the 2D outline of a cone linestring = [[0, 0], [radius, 0], [0, height]] # revolve the profile to create a cone cone = revolve(linestring=linestring, sections=sections, transform=transform, **kwargs) return cone def cylinder(radius, height, sections=None, segment=None, transform=None, **kwargs): """ Create a mesh of a cylinder along Z centered at the origin. Parameters ---------- radius : float The radius of the cylinder height : float The height of the cylinder sections : int How many pie wedges should the cylinder have segment : (2, 3) float Endpoints of axis, overrides transform and height transform : (4, 4) float Transform to apply **kwargs: passed to Trimesh to create cylinder Returns ---------- cylinder: trimesh.Trimesh Resulting mesh of a cylinder """ if segment is not None: segment = np.asanyarray(segment, dtype=np.float64) if segment.shape != (2, 3): raise ValueError('segment must be 2 3D points!') vector = segment[1] - segment[0] # override height with segment length height = np.linalg.norm(vector) # point in middle of line midpoint = segment[0] + (vector * 0.5) # align Z with our desired direction rotation = align_vectors([0, 0, 1], vector) # translate to midpoint of segment translation = tf.translation_matrix(midpoint) # compound the rotation and translation transform = np.dot(translation, rotation) half = abs(float(height)) / 2.0 # create a profile to revolve linestring = [[0, -half], [radius, -half], [radius, half], [0, half]] # generate cylinder through simple revolution return revolve(linestring=linestring, sections=sections, transform=transform) return cylinder def annulus(r_min, r_max, height, sections=None, transform=None, **kwargs): """ Create a mesh of an annular cylinder along Z centered at the origin. Parameters ---------- r_min : float The inner radius of the annular cylinder r_max : float The outer radius of the annular cylinder height : float The height of the annular cylinder sections : int or None How many pie wedges should the annular cylinder have transform : (4, 4) float or None Transform to apply to move result from the origin **kwargs: passed to Trimesh to create annulus Returns ---------- annulus : trimesh.Trimesh Mesh of annular cylinder """ r_min = abs(float(r_min)) # if center radius is zero this is a cylinder if r_min < tol.merge: return cylinder(radius=r_max, height=height, sections=sections, transform=transform) r_max = abs(float(r_max)) # we're going to center at XY plane so take half the height half = abs(float(height)) / 2.0 # create counter-clockwise rectangle linestring = [[r_min, -half], [r_max, -half], [r_max, half], [r_min, half], [r_min, -half]] # revolve the curve annulus = revolve(linestring=linestring, sections=sections, transform=transform, **kwargs) return annulus def random_soup(face_count=100): """ Return random triangles as a Trimesh Parameters ----------- face_count : int Number of faces desired in mesh Returns ----------- soup : trimesh.Trimesh Geometry with face_count random faces """ vertices = np.random.random((face_count * 3, 3)) - 0.5 faces = np.arange(face_count * 3).reshape((-1, 3)) soup = Trimesh(vertices=vertices, faces=faces) return soup def axis(origin_size=0.04, transform=None, origin_color=None, axis_radius=None, axis_length=None): """ Return an XYZ axis marker as a Trimesh, which represents position and orientation. If you set the origin size the other parameters will be set relative to it. Parameters ---------- transform : (4, 4) float Transformation matrix origin_size : float Radius of sphere that represents the origin origin_color : (3,) float or int, uint8 or float Color of the origin axis_radius : float Radius of cylinder that represents x, y, z axis axis_length: float Length of cylinder that represents x, y, z axis Returns ------- marker : trimesh.Trimesh Mesh geometry of axis indicators """ # the size of the ball representing the origin origin_size = float(origin_size) # set the transform and use origin-relative # sized for other parameters if not specified if transform is None: transform = np.eye(4) if origin_color is None: origin_color = [255, 255, 255, 255] if axis_radius is None: axis_radius = origin_size / 5.0 if axis_length is None: axis_length = origin_size * 10.0 # generate a ball for the origin axis_origin = uv_sphere(radius=origin_size, count=[10, 10]) axis_origin.apply_transform(transform) # apply color to the origin ball axis_origin.visual.face_colors = origin_color # create the cylinder for the z-axis translation = tf.translation_matrix( [0, 0, axis_length / 2]) z_axis = cylinder( radius=axis_radius, height=axis_length, transform=transform.dot(translation)) # XYZ->RGB, Z is blue z_axis.visual.face_colors = [0, 0, 255] # create the cylinder for the y-axis translation = tf.translation_matrix( [0, 0, axis_length / 2]) rotation = tf.rotation_matrix(np.radians(-90), [1, 0, 0]) y_axis = cylinder( radius=axis_radius, height=axis_length, transform=transform.dot(rotation).dot(translation)) # XYZ->RGB, Y is green y_axis.visual.face_colors = [0, 255, 0] # create the cylinder for the x-axis translation = tf.translation_matrix( [0, 0, axis_length / 2]) rotation = tf.rotation_matrix(np.radians(90), [0, 1, 0]) x_axis = cylinder( radius=axis_radius, height=axis_length, transform=transform.dot(rotation).dot(translation)) # XYZ->RGB, X is red x_axis.visual.face_colors = [255, 0, 0] # append the sphere and three cylinders marker = util.concatenate([axis_origin, x_axis, y_axis, z_axis]) return marker def camera_marker(camera, marker_height=0.4, origin_size=None): """ Create a visual marker for a camera object, including an axis and FOV. Parameters --------------- camera : trimesh.scene.Camera Camera object with FOV and transform defined marker_height : float How far along the camera Z should FOV indicators be origin_size : float Sphere radius of the origin (default: marker_height / 10.0) Returns ------------ meshes : list Contains Trimesh and Path3D objects which can be visualized """ # append the visualizations to an array meshes = [axis(origin_size=marker_height / 10.0)] try: # path is a soft dependency from .path.exchange.load import load_path except ImportError: # they probably don't have shapely installed log.warning('unable to create FOV visualization!', exc_info=True) return meshes # create sane origin size from marker height if origin_size is None: origin_size = marker_height / 10.0 # calculate vertices from camera FOV angles x = marker_height * np.tan(np.deg2rad(camera.fov[0]) / 2.0) y = marker_height * np.tan(np.deg2rad(camera.fov[1]) / 2.0) z = marker_height # combine the points into the vertices of an FOV visualization points = np.array( [(0, 0, 0), (-x, -y, z), (x, -y, z), (x, y, z), (-x, y, z)], dtype=float) # create line segments for the FOV visualization # a segment from the origin to each bound of the FOV segments = np.column_stack( (np.zeros_like(points), points)).reshape( (-1, 3)) # add a loop for the outside of the FOV then reshape # the whole thing into multiple line segments segments = np.vstack((segments, points[[1, 2, 2, 3, 3, 4, 4, 1]])).reshape((-1, 2, 3)) # add a single Path3D object for all line segments meshes.append(load_path(segments)) return meshes def truncated_prisms(tris, origin=None, normal=None): """ Return a mesh consisting of multiple watertight prisms below a list of triangles, truncated by a specified plane. Parameters ------------- triangles : (n, 3, 3) float Triangles in space origin : None or (3,) float Origin of truncation plane normal : None or (3,) float Unit normal vector of truncation plane Returns ----------- mesh : trimesh.Trimesh Triangular mesh """ if origin is None: transform = np.eye(4) else: transform = plane_transform(origin=origin, normal=normal) # transform the triangles to the specified plane transformed = tf.transform_points( tris.reshape((-1, 3)), transform).reshape((-1, 9)) # stack triangles such that every other one is repeated vs = np.column_stack((transformed, transformed)).reshape((-1, 3, 3)) # set the Z value of the second triangle to zero vs[1::2, :, 2] = 0 # reshape triangles to a flat array of points and transform back to original frame vertices = tf.transform_points( vs.reshape((-1, 3)), matrix=np.linalg.inv(transform)) # face indexes for a *single* truncated triangular prism f = np.array([[2, 1, 0], [3, 4, 5], [0, 1, 4], [1, 2, 5], [2, 0, 3], [4, 3, 0], [5, 4, 1], [3, 5, 2]]) # find the projection of each triangle with the normal vector cross = np.dot([0, 0, 1], triangles.cross(transformed.reshape((-1, 3, 3))).T) # stack faces into one prism per triangle f_seq = np.tile(f, (len(transformed), 1)).reshape((-1, len(f), 3)) # if the normal of the triangle was positive flip the winding f_seq[cross > 0] = np.fliplr(f) # offset stacked faces to create correct indices faces = (f_seq + (np.arange(len(f_seq)) * 6).reshape((-1, 1, 1))).reshape((-1, 3)) # create a mesh from the data mesh = Trimesh(vertices=vertices, faces=faces, process=False) return mesh