From 72f1c6f641ee8fae9dbda5bc0beacadc6d8eb568 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 00:17:18 +0900 Subject: [PATCH 01/14] Add loop subdivision function in remesh file --- trimesh/remesh.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index d162eb4ff..131d8e97e 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -4,11 +4,12 @@ Deal with re- triangulation of existing meshes. """ - +from itertools import zip_longest import numpy as np from . import util from . import grouping +from . import graph from .geometry import faces_to_edges @@ -213,3 +214,84 @@ def subdivide_to_size(vertices, return final_vertices, final_faces, final_index return final_vertices, final_faces + + +def loop(vertices, + faces): + # find the unique edges of our faces + edges, edges_face = faces_to_edges(faces, return_index=True) + edges = np.sort(edges, axis=1) + unique, inverse = grouping.unique_rows(edges) + + # set interior edges if there are two edges + edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) + # set mask for the interior edges. This mask works for edges[unique] + edge_inter_mask = (edges[unique][:, None, :] == edges[edge_inter[:, 0][None, :]]).all(-1).any(-1) + # non-interior edges are boundary edges + edge_bound_mask = ~edge_inter_mask + + # find the opposite face for each edge + edge_pair = np.zeros(len(edges)).astype(int) + edge_pair[edge_inter[:, 0]] = edge_inter[:, 1] + edge_pair[edge_inter[:, 1]] = edge_inter[:, 0] + opposite_face1 = edges_face[unique] + opposite_face2 = edges_face[edge_pair[unique]] + + # set odd vertices to the middle of each edge (default as boundary case). + odd = vertices[edges[unique]].mean(axis=1) + # modify the odd vertices for the interior case + e = edges[unique[edge_inter_mask]] + e_v0 = vertices[e][:, 0] + e_v1 = vertices[e][:, 1] + e_f0 = faces[opposite_face1[edge_inter_mask]] + e_f1 = faces[opposite_face2[edge_inter_mask]] + e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)] + e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)] + e_v2 = vertices[e_v2_idx] + e_v3 = vertices[e_v3_idx] + odd[edge_inter_mask] = 3/8 * (e_v0 + e_v1) + 1/8 * (e_v2 + e_v3) + + # find vertex neighbors of each vertex + neighbors = graph.neighbors(edges=edges[unique], max_index=len(vertices)) + # convert list type of array into a fixed-shaped numpy array (set -1 to empties) + neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T + # if the neighbor has -1 index, its point is (0, 0, 0), so that + # it is not included in the summation of neighbors when calculating the even values + vertices_ = np.vstack([vertices, [0, 0, 0]]) + # number of neighbors + k = (neighbors + 1).astype(bool).sum(-1) + + # calculate even vertices for the interior case + even = np.zeros_like(vertices) + beta = 1/k * (5/8 - (3/8 + 1/4 * np.cos(2 * np.pi / k)) ** 2) + even = beta[:, None] * vertices_[neighbors].sum(1) + (1 - k[:, None] * beta[:, None]) * vertices + + # calculate even vertices for the boundary case + if True in edge_bound_mask: + # boundary vertices from boundary edges + vrt_bound_mask = np.zeros(len(vertices), dtype=bool) + vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True + # one boundary vertex has two neighbor boundary vertices (set others as -1) + boundary_neighbors = neighbors[vrt_bound_mask] + boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 + even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) + 3/4 * vertices[vrt_bound_mask] + + # the new faces with correct winding + odd_idx = inverse.reshape((-1, 3)) + len(vertices) + new_faces = np.column_stack([faces[:, 0], + odd_idx[:, 0], + odd_idx[:, 2], + odd_idx[:, 0], + faces[:, 1], + odd_idx[:, 1], + odd_idx[:, 2], + odd_idx[:, 1], + faces[:, 2], + odd_idx[:, 0], + odd_idx[:, 1], + odd_idx[:, 2]]).reshape((-1, 3)) + + # stack the new even vertices and odd vertices + new_vertices = np.vstack((even, odd)) + + return new_vertices, new_faces \ No newline at end of file From 4841ecccc19a7028a33fe3997b1418472ccc93d1 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 00:28:43 +0900 Subject: [PATCH 02/14] add function docstring --- trimesh/remesh.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index 131d8e97e..8c9de9025 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -218,6 +218,26 @@ def subdivide_to_size(vertices, def loop(vertices, faces): + """ + Subdivide a mesh by dividing each triangle into four triangles + with approximation scheme of its smoothed surface. + + Will return a triangle soup, not a nicely structured mesh. + + Parameters + ------------ + vertices : (n, 3) float + Vertices in space + faces : (m, 3) int + Indices of vertices which make up triangles + + Returns + ------------ + vertices : (j, 3) float + Vertices in space + faces : (q, 3) int + Indices of vertices + """ # find the unique edges of our faces edges, edges_face = faces_to_edges(faces, return_index=True) edges = np.sort(edges, axis=1) From 75c146433d26fb145e7c638660ccb7cbb46df3d8 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 00:46:16 +0900 Subject: [PATCH 03/14] edit function docstring --- trimesh/remesh.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index 8c9de9025..a57d5dc4d 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -221,9 +221,30 @@ def loop(vertices, """ Subdivide a mesh by dividing each triangle into four triangles with approximation scheme of its smoothed surface. + Overall process: + 1. Calculate odd vertices. + Assign a new odd vertex on each edge and + calculate the value for the boundary case and the interior case. + The value is calculated as follows. + v2 + / f0 \ 0 + v0--e--v1 / \ + \ f1 / v0--e--v1 + v3 + - interior case : 3:1 ratio of mean(v0,v1) and mean(v2,v3) + - boundary case : mean(v0,v1) + 2. Calculate even vertices. + The new even vertices are calculated with the existing + vertices and their adjacent vertices. + 1---2 + / \ / \ 0---1 + 0---v---3 / \ / \ + \ / \ / b0---v---b1 + k...4 + - interior case : (1-kβ):β ratio of v and k adjacencies + - boundary case : 3:1 ratio of v and mean(b0,b1) + 3. Compose new faces with new vertices. - Will return a triangle soup, not a nicely structured mesh. - Parameters ------------ vertices : (n, 3) float @@ -296,7 +317,7 @@ def loop(vertices, boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) + 3/4 * vertices[vrt_bound_mask] - # the new faces with correct winding + # the new faces with odd vertices odd_idx = inverse.reshape((-1, 3)) + len(vertices) new_faces = np.column_stack([faces[:, 0], odd_idx[:, 0], From 2e951a0caf1b336b8d5311de8ffa8c117612c077 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 01:07:20 +0900 Subject: [PATCH 04/14] fix the slowest part --- trimesh/remesh.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index a57d5dc4d..98dca4ae8 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -221,6 +221,9 @@ def loop(vertices, """ Subdivide a mesh by dividing each triangle into four triangles with approximation scheme of its smoothed surface. + This function is an array-based implementation of loop subdivision, + which avoids slow for loop and enables faster calculation. + Overall process: 1. Calculate odd vertices. Assign a new odd vertex on each edge and @@ -264,12 +267,15 @@ def loop(vertices, edges = np.sort(edges, axis=1) unique, inverse = grouping.unique_rows(edges) - # set interior edges if there are two edges + # set interior edges if there are two edges and boundary if there is one. edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) - # set mask for the interior edges. This mask works for edges[unique] - edge_inter_mask = (edges[unique][:, None, :] == edges[edge_inter[:, 0][None, :]]).all(-1).any(-1) - # non-interior edges are boundary edges - edge_bound_mask = ~edge_inter_mask + edge_bound = grouping.group_rows(edges, require_count=1) + + # set interior, boundary mask for unique edges + edge_bound_mask = np.zeros(len(edges), dtype=bool) + edge_bound_mask[edge_bound] = True + edge_bound_mask = edge_bound_mask[unique] + edge_inter_mask = ~edge_bound_mask # find the opposite face for each edge edge_pair = np.zeros(len(edges)).astype(int) From b220156b413241893f25bf8e030da1fd2238b10b Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 01:41:47 +0900 Subject: [PATCH 05/14] add loop subdivision into Trimesh class --- trimesh/base.py | 20 ++++++++++++++++++++ trimesh/remesh.py | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/trimesh/base.py b/trimesh/base.py index 5e63e6bc0..af4d5a072 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1996,6 +1996,26 @@ def subdivide_to_size(self, max_edge, max_iter=10, return_index=False): return result, final_index return result + + def loop(self, iterations=1): + """ + Subdivide a mesh by dividing each triangle into four triangles + and approximating their smoothed surface (loop subdivision). + + Parameters + ------------ + iterations : int + Number of iterations to run subdivisio + """ + vertices = self.vertices + faces = self.faces + for _index in range(iterations): + vertices, faces = remesh.loop(vertices, faces) + result = Trimesh( + vertices=vertices, + faces=faces) + return result + @log_time def smoothed(self, **kwargs): diff --git a/trimesh/remesh.py b/trimesh/remesh.py index 98dca4ae8..a2addf04f 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -220,7 +220,7 @@ def loop(vertices, faces): """ Subdivide a mesh by dividing each triangle into four triangles - with approximation scheme of its smoothed surface. + and approximating their smoothed surface (loop subdivision). This function is an array-based implementation of loop subdivision, which avoids slow for loop and enables faster calculation. @@ -270,6 +270,8 @@ def loop(vertices, # set interior edges if there are two edges and boundary if there is one. edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) edge_bound = grouping.group_rows(edges, require_count=1) + # make sure that one edge is shared by only one or two faces. + assert len(edge_inter) * 2 + len(edge_bound) == len(edges) # set interior, boundary mask for unique edges edge_bound_mask = np.zeros(len(edges), dtype=bool) From be6c5b5a8375ccd38c5acec1481c7d65a8097755 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 20:35:49 +0900 Subject: [PATCH 06/14] edit loop subdivision for multibody case --- trimesh/base.py | 40 ++++++++--- trimesh/remesh.py | 172 ++++++++++++++++++++++++---------------------- 2 files changed, 121 insertions(+), 91 deletions(-) diff --git a/trimesh/base.py b/trimesh/base.py index af4d5a072..95dbb052c 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1997,7 +1997,7 @@ def subdivide_to_size(self, max_edge, max_iter=10, return_index=False): return result - def loop(self, iterations=1): + def loop(self, iterations=1, multibody=False): """ Subdivide a mesh by dividing each triangle into four triangles and approximating their smoothed surface (loop subdivision). @@ -2006,16 +2006,38 @@ def loop(self, iterations=1): ------------ iterations : int Number of iterations to run subdivisio - """ - vertices = self.vertices - faces = self.faces - for _index in range(iterations): - vertices, faces = remesh.loop(vertices, faces) + multibody : bool + If True will try to subdivide for each submesh + """ + if multibody: + splited_meshes = self.split(only_watertight=False) + if len(splited_meshes) > 1: + new_meshes = [] + # perform subdivision for all submesh + for splited_mesh in splited_meshes: + new_vertices, new_faces = remesh.loop( + vertices=splited_mesh.vertices, + faces=splited_mesh.faces, + iterations=iterations) + # create new mesh + new_mesh = Trimesh( + vertices=new_vertices, + faces=new_faces) + new_meshes.append(new_mesh) + # concatenate all meshes into one + result = util.concatenate(new_meshes) + return result + + # perform subdivision for one mesh + new_vertices, new_faces = remesh.loop( + vertices=self.vertices, + faces=self.faces, + iterations=iterations) + # create new mesh result = Trimesh( - vertices=vertices, - faces=faces) + vertices=new_vertices, + faces=new_faces) return result - @log_time def smoothed(self, **kwargs): diff --git a/trimesh/remesh.py b/trimesh/remesh.py index a2addf04f..0aaf4cddc 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -217,7 +217,8 @@ def subdivide_to_size(vertices, def loop(vertices, - faces): + faces, + iterations=1): """ Subdivide a mesh by dividing each triangle into four triangles and approximating their smoothed surface (loop subdivision). @@ -261,86 +262,93 @@ def loop(vertices, Vertices in space faces : (q, 3) int Indices of vertices + iterations : int + Number of iterations to run subdivision """ - # find the unique edges of our faces - edges, edges_face = faces_to_edges(faces, return_index=True) - edges = np.sort(edges, axis=1) - unique, inverse = grouping.unique_rows(edges) - - # set interior edges if there are two edges and boundary if there is one. - edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) - edge_bound = grouping.group_rows(edges, require_count=1) - # make sure that one edge is shared by only one or two faces. - assert len(edge_inter) * 2 + len(edge_bound) == len(edges) - - # set interior, boundary mask for unique edges - edge_bound_mask = np.zeros(len(edges), dtype=bool) - edge_bound_mask[edge_bound] = True - edge_bound_mask = edge_bound_mask[unique] - edge_inter_mask = ~edge_bound_mask - - # find the opposite face for each edge - edge_pair = np.zeros(len(edges)).astype(int) - edge_pair[edge_inter[:, 0]] = edge_inter[:, 1] - edge_pair[edge_inter[:, 1]] = edge_inter[:, 0] - opposite_face1 = edges_face[unique] - opposite_face2 = edges_face[edge_pair[unique]] - - # set odd vertices to the middle of each edge (default as boundary case). - odd = vertices[edges[unique]].mean(axis=1) - # modify the odd vertices for the interior case - e = edges[unique[edge_inter_mask]] - e_v0 = vertices[e][:, 0] - e_v1 = vertices[e][:, 1] - e_f0 = faces[opposite_face1[edge_inter_mask]] - e_f1 = faces[opposite_face2[edge_inter_mask]] - e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)] - e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)] - e_v2 = vertices[e_v2_idx] - e_v3 = vertices[e_v3_idx] - odd[edge_inter_mask] = 3/8 * (e_v0 + e_v1) + 1/8 * (e_v2 + e_v3) - - # find vertex neighbors of each vertex - neighbors = graph.neighbors(edges=edges[unique], max_index=len(vertices)) - # convert list type of array into a fixed-shaped numpy array (set -1 to empties) - neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T - # if the neighbor has -1 index, its point is (0, 0, 0), so that - # it is not included in the summation of neighbors when calculating the even values - vertices_ = np.vstack([vertices, [0, 0, 0]]) - # number of neighbors - k = (neighbors + 1).astype(bool).sum(-1) + def _subdivide(vertices, faces): + # find the unique edges of our faces + edges, edges_face = faces_to_edges(faces, return_index=True) + edges = np.sort(edges, axis=1) + unique, inverse = grouping.unique_rows(edges) + + # set interior edges if there are two edges and boundary if there is one. + edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) + edge_bound = grouping.group_rows(edges, require_count=1) + # make sure that one edge is shared by only one or two faces. + assert len(edge_inter) * 2 + len(edge_bound) == len(edges) + + # set interior, boundary mask for unique edges + edge_bound_mask = np.zeros(len(edges), dtype=bool) + edge_bound_mask[edge_bound] = True + edge_bound_mask = edge_bound_mask[unique] + edge_inter_mask = ~edge_bound_mask + + # find the opposite face for each edge + edge_pair = np.zeros(len(edges)).astype(int) + edge_pair[edge_inter[:, 0]] = edge_inter[:, 1] + edge_pair[edge_inter[:, 1]] = edge_inter[:, 0] + opposite_face1 = edges_face[unique] + opposite_face2 = edges_face[edge_pair[unique]] + + # set odd vertices to the middle of each edge (default as boundary case). + odd = vertices[edges[unique]].mean(axis=1) + # modify the odd vertices for the interior case + e = edges[unique[edge_inter_mask]] + e_v0 = vertices[e][:, 0] + e_v1 = vertices[e][:, 1] + e_f0 = faces[opposite_face1[edge_inter_mask]] + e_f1 = faces[opposite_face2[edge_inter_mask]] + e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)] + e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)] + e_v2 = vertices[e_v2_idx] + e_v3 = vertices[e_v3_idx] + odd[edge_inter_mask] = 3/8 * (e_v0 + e_v1) + 1/8 * (e_v2 + e_v3) + + # find vertex neighbors of each vertex + neighbors = graph.neighbors(edges=edges[unique], max_index=len(vertices)) + # convert list type of array into a fixed-shaped numpy array (set -1 to empties) + neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T + # if the neighbor has -1 index, its point is (0, 0, 0), so that + # it is not included in the summation of neighbors when calculating the even values + vertices_ = np.vstack([vertices, [0, 0, 0]]) + # number of neighbors + k = (neighbors + 1).astype(bool).sum(-1) + + # calculate even vertices for the interior case + even = np.zeros_like(vertices) + beta = 1/k * (5/8 - (3/8 + 1/4 * np.cos(2 * np.pi / k)) ** 2) + even = beta[:, None] * vertices_[neighbors].sum(1) + (1 - k[:, None] * beta[:, None]) * vertices + + # calculate even vertices for the boundary case + if True in edge_bound_mask: + # boundary vertices from boundary edges + vrt_bound_mask = np.zeros(len(vertices), dtype=bool) + vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True + # one boundary vertex has two neighbor boundary vertices (set others as -1) + boundary_neighbors = neighbors[vrt_bound_mask] + boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 + even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) + 3/4 * vertices[vrt_bound_mask] + + # the new faces with odd vertices + odd_idx = inverse.reshape((-1, 3)) + len(vertices) + new_faces = np.column_stack([faces[:, 0], + odd_idx[:, 0], + odd_idx[:, 2], + odd_idx[:, 0], + faces[:, 1], + odd_idx[:, 1], + odd_idx[:, 2], + odd_idx[:, 1], + faces[:, 2], + odd_idx[:, 0], + odd_idx[:, 1], + odd_idx[:, 2]]).reshape((-1, 3)) + + # stack the new even vertices and odd vertices + new_vertices = np.vstack((even, odd)) + + return new_vertices, new_faces - # calculate even vertices for the interior case - even = np.zeros_like(vertices) - beta = 1/k * (5/8 - (3/8 + 1/4 * np.cos(2 * np.pi / k)) ** 2) - even = beta[:, None] * vertices_[neighbors].sum(1) + (1 - k[:, None] * beta[:, None]) * vertices - - # calculate even vertices for the boundary case - if True in edge_bound_mask: - # boundary vertices from boundary edges - vrt_bound_mask = np.zeros(len(vertices), dtype=bool) - vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True - # one boundary vertex has two neighbor boundary vertices (set others as -1) - boundary_neighbors = neighbors[vrt_bound_mask] - boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 - even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) + 3/4 * vertices[vrt_bound_mask] - - # the new faces with odd vertices - odd_idx = inverse.reshape((-1, 3)) + len(vertices) - new_faces = np.column_stack([faces[:, 0], - odd_idx[:, 0], - odd_idx[:, 2], - odd_idx[:, 0], - faces[:, 1], - odd_idx[:, 1], - odd_idx[:, 2], - odd_idx[:, 1], - faces[:, 2], - odd_idx[:, 0], - odd_idx[:, 1], - odd_idx[:, 2]]).reshape((-1, 3)) - - # stack the new even vertices and odd vertices - new_vertices = np.vstack((even, odd)) - - return new_vertices, new_faces \ No newline at end of file + for _index in range(iterations): + vertices, faces = _subdivide(vertices, faces) + return vertices, faces \ No newline at end of file From b65681796fd09c6d358eb32165ba825033cc7fb2 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Tue, 15 Nov 2022 21:59:18 +0900 Subject: [PATCH 07/14] add test cases for loop subdivision --- tests/test_remesh.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_remesh.py b/tests/test_remesh.py index 6411e338a..91ca9c575 100644 --- a/tests/test_remesh.py +++ b/tests/test_remesh.py @@ -106,6 +106,56 @@ def test_sub(self): # volume should be the same assert g.np.isclose(m.volume, s.volume) + def test_loop(self): + meshes = [ + g.get_mesh('soup.stl'), # a soup of random triangles + g.get_mesh('featuretype.STL')] # a mesh with a single body + + for m in meshes: + sub = m.loop(iterations=1) + # number of faces should increase + assert len(sub.faces) > len(m.faces) + # subdivided faces are smaller + assert sub.area_faces.mean() < m.area_faces.mean() + + def test_loop_multibody(self): + mesh = g.get_mesh('cycloidal.ply') # a mesh with multiple bodies + sub = mesh.loop(iterations=1, multibody=True) + # number of faces should increase + assert len(sub.faces) > len(mesh.faces) + # subdivided faces are smaller + assert sub.area_faces.mean() < mesh.area_faces.mean() + + def test_loop_correct(self): + box = g.trimesh.creation.box() + big_sphere = g.trimesh.creation.icosphere(radius=0.5) + small_sphere = g.trimesh.creation.icosphere(radius=0.4) + sub = box.loop(iterations=2) + # smaller than 0.5 sphere + assert big_sphere.contains(sub.vertices).all() + # bigger than 0.4 sphere + assert (~small_sphere.contains(sub.vertices)).all() + + def test_loop_bound(self): + def _get_boundary_vertices(mesh): + boundary_groups = g.trimesh.grouping.group_rows( + mesh.edges_sorted, require_count=1) + return mesh.vertices[g.np.unique(mesh.edges_sorted[boundary_groups])] + + box = g.trimesh.creation.box() + bottom_mask = g.np.zeros(len(box.faces), dtype=bool) + bottom_faces = [1,5] + bottom_mask[bottom_faces] = True + # eliminate bottom of the box + box.update_faces(~bottom_mask) + bottom_vrts = _get_boundary_vertices(box) + # subdivide box + sub = box.loop(iterations=2) + sub_bottom_vrts = _get_boundary_vertices(sub) + epsilon = 1e-5 + # y value of bottom boundary vertices should not be changed + assert (bottom_vrts[:, 1].mean() - sub_bottom_vrts[:, 1].mean()) < epsilon + def test_uv(self): # get a mesh with texture m = g.get_mesh('fuze.obj') @@ -200,3 +250,4 @@ def test(fidx): if __name__ == '__main__': g.trimesh.util.attach_to_log() g.unittest.main() + From 4adc252e38bfd1c01c4e7fd800eb4ea4f40f7d41 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Wed, 16 Nov 2022 23:26:10 +0900 Subject: [PATCH 08/14] add error message --- trimesh/remesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index 0aaf4cddc..b9c5d8c0a 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -275,7 +275,8 @@ def _subdivide(vertices, faces): edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) edge_bound = grouping.group_rows(edges, require_count=1) # make sure that one edge is shared by only one or two faces. - assert len(edge_inter) * 2 + len(edge_bound) == len(edges) + if not len(edge_inter)*2 + len(edge_bound) == len(edges): + raise ValueError('Some edges are shared by more than 2 faces') # set interior, boundary mask for unique edges edge_bound_mask = np.zeros(len(edges), dtype=bool) From 6a886da152e2c326850b9a6071a4721be47602f4 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 00:50:23 +0900 Subject: [PATCH 09/14] fix formatting --- tests/test_remesh.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_remesh.py b/tests/test_remesh.py index 91ca9c575..39f709715 100644 --- a/tests/test_remesh.py +++ b/tests/test_remesh.py @@ -117,7 +117,7 @@ def test_loop(self): assert len(sub.faces) > len(m.faces) # subdivided faces are smaller assert sub.area_faces.mean() < m.area_faces.mean() - + def test_loop_multibody(self): mesh = g.get_mesh('cycloidal.ply') # a mesh with multiple bodies sub = mesh.loop(iterations=1, multibody=True) @@ -125,7 +125,7 @@ def test_loop_multibody(self): assert len(sub.faces) > len(mesh.faces) # subdivided faces are smaller assert sub.area_faces.mean() < mesh.area_faces.mean() - + def test_loop_correct(self): box = g.trimesh.creation.box() big_sphere = g.trimesh.creation.icosphere(radius=0.5) @@ -135,8 +135,8 @@ def test_loop_correct(self): assert big_sphere.contains(sub.vertices).all() # bigger than 0.4 sphere assert (~small_sphere.contains(sub.vertices)).all() - - def test_loop_bound(self): + + def test_loop_bound(self): def _get_boundary_vertices(mesh): boundary_groups = g.trimesh.grouping.group_rows( mesh.edges_sorted, require_count=1) @@ -144,7 +144,7 @@ def _get_boundary_vertices(mesh): box = g.trimesh.creation.box() bottom_mask = g.np.zeros(len(box.faces), dtype=bool) - bottom_faces = [1,5] + bottom_faces = [1, 5] bottom_mask[bottom_faces] = True # eliminate bottom of the box box.update_faces(~bottom_mask) @@ -155,7 +155,7 @@ def _get_boundary_vertices(mesh): epsilon = 1e-5 # y value of bottom boundary vertices should not be changed assert (bottom_vrts[:, 1].mean() - sub_bottom_vrts[:, 1].mean()) < epsilon - + def test_uv(self): # get a mesh with texture m = g.get_mesh('fuze.obj') @@ -249,5 +249,4 @@ def test(fidx): if __name__ == '__main__': g.trimesh.util.attach_to_log() - g.unittest.main() - + g.unittest.main() \ No newline at end of file From 026fba0b136b537c14288f47eb87ab426e48188c Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 01:06:39 +0900 Subject: [PATCH 10/14] Revert "fix formatting" This reverts commit 6a886da152e2c326850b9a6071a4721be47602f4. --- tests/test_remesh.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_remesh.py b/tests/test_remesh.py index 39f709715..91ca9c575 100644 --- a/tests/test_remesh.py +++ b/tests/test_remesh.py @@ -117,7 +117,7 @@ def test_loop(self): assert len(sub.faces) > len(m.faces) # subdivided faces are smaller assert sub.area_faces.mean() < m.area_faces.mean() - + def test_loop_multibody(self): mesh = g.get_mesh('cycloidal.ply') # a mesh with multiple bodies sub = mesh.loop(iterations=1, multibody=True) @@ -125,7 +125,7 @@ def test_loop_multibody(self): assert len(sub.faces) > len(mesh.faces) # subdivided faces are smaller assert sub.area_faces.mean() < mesh.area_faces.mean() - + def test_loop_correct(self): box = g.trimesh.creation.box() big_sphere = g.trimesh.creation.icosphere(radius=0.5) @@ -135,8 +135,8 @@ def test_loop_correct(self): assert big_sphere.contains(sub.vertices).all() # bigger than 0.4 sphere assert (~small_sphere.contains(sub.vertices)).all() - - def test_loop_bound(self): + + def test_loop_bound(self): def _get_boundary_vertices(mesh): boundary_groups = g.trimesh.grouping.group_rows( mesh.edges_sorted, require_count=1) @@ -144,7 +144,7 @@ def _get_boundary_vertices(mesh): box = g.trimesh.creation.box() bottom_mask = g.np.zeros(len(box.faces), dtype=bool) - bottom_faces = [1, 5] + bottom_faces = [1,5] bottom_mask[bottom_faces] = True # eliminate bottom of the box box.update_faces(~bottom_mask) @@ -155,7 +155,7 @@ def _get_boundary_vertices(mesh): epsilon = 1e-5 # y value of bottom boundary vertices should not be changed assert (bottom_vrts[:, 1].mean() - sub_bottom_vrts[:, 1].mean()) < epsilon - + def test_uv(self): # get a mesh with texture m = g.get_mesh('fuze.obj') @@ -249,4 +249,5 @@ def test(fidx): if __name__ == '__main__': g.trimesh.util.attach_to_log() - g.unittest.main() \ No newline at end of file + g.unittest.main() + From fd38dc1ca0e07d779b7bc95d4d1c169f4f76449d Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 01:08:16 +0900 Subject: [PATCH 11/14] fix format --- tests/test_remesh.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_remesh.py b/tests/test_remesh.py index 91ca9c575..7e0848d2d 100644 --- a/tests/test_remesh.py +++ b/tests/test_remesh.py @@ -117,7 +117,7 @@ def test_loop(self): assert len(sub.faces) > len(m.faces) # subdivided faces are smaller assert sub.area_faces.mean() < m.area_faces.mean() - + def test_loop_multibody(self): mesh = g.get_mesh('cycloidal.ply') # a mesh with multiple bodies sub = mesh.loop(iterations=1, multibody=True) @@ -125,7 +125,7 @@ def test_loop_multibody(self): assert len(sub.faces) > len(mesh.faces) # subdivided faces are smaller assert sub.area_faces.mean() < mesh.area_faces.mean() - + def test_loop_correct(self): box = g.trimesh.creation.box() big_sphere = g.trimesh.creation.icosphere(radius=0.5) @@ -135,8 +135,8 @@ def test_loop_correct(self): assert big_sphere.contains(sub.vertices).all() # bigger than 0.4 sphere assert (~small_sphere.contains(sub.vertices)).all() - - def test_loop_bound(self): + + def test_loop_bound(self): def _get_boundary_vertices(mesh): boundary_groups = g.trimesh.grouping.group_rows( mesh.edges_sorted, require_count=1) @@ -144,7 +144,7 @@ def _get_boundary_vertices(mesh): box = g.trimesh.creation.box() bottom_mask = g.np.zeros(len(box.faces), dtype=bool) - bottom_faces = [1,5] + bottom_faces = [1, 5] bottom_mask[bottom_faces] = True # eliminate bottom of the box box.update_faces(~bottom_mask) @@ -155,7 +155,7 @@ def _get_boundary_vertices(mesh): epsilon = 1e-5 # y value of bottom boundary vertices should not be changed assert (bottom_vrts[:, 1].mean() - sub_bottom_vrts[:, 1].mean()) < epsilon - + def test_uv(self): # get a mesh with texture m = g.get_mesh('fuze.obj') @@ -250,4 +250,3 @@ def test(fidx): if __name__ == '__main__': g.trimesh.util.attach_to_log() g.unittest.main() - From 2ed14ca91c71d54c25ba27038a161e633d28f51f Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 01:52:36 +0900 Subject: [PATCH 12/14] fix format --- trimesh/base.py | 46 +++++------ trimesh/remesh.py | 203 +++++++++++++++++++++++----------------------- 2 files changed, 126 insertions(+), 123 deletions(-) diff --git a/trimesh/base.py b/trimesh/base.py index 95dbb052c..bd101225f 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1996,10 +1996,10 @@ def subdivide_to_size(self, max_edge, max_iter=10, return_index=False): return result, final_index return result - + def loop(self, iterations=1, multibody=False): """ - Subdivide a mesh by dividing each triangle into four triangles + Subdivide a mesh by dividing each triangle into four triangles and approximating their smoothed surface (loop subdivision). Parameters @@ -2008,30 +2008,30 @@ def loop(self, iterations=1, multibody=False): Number of iterations to run subdivisio multibody : bool If True will try to subdivide for each submesh - """ + """ if multibody: - splited_meshes = self.split(only_watertight=False) - if len(splited_meshes) > 1: - new_meshes = [] - # perform subdivision for all submesh - for splited_mesh in splited_meshes: - new_vertices, new_faces = remesh.loop( - vertices=splited_mesh.vertices, - faces=splited_mesh.faces, - iterations=iterations) - # create new mesh - new_mesh = Trimesh( - vertices=new_vertices, - faces=new_faces) - new_meshes.append(new_mesh) - # concatenate all meshes into one - result = util.concatenate(new_meshes) - return result - + splited_meshes = self.split(only_watertight=False) + if len(splited_meshes) > 1: + new_meshes = [] + # perform subdivision for all submesh + for splited_mesh in splited_meshes: + new_vertices, new_faces = remesh.loop( + vertices=splited_mesh.vertices, + faces=splited_mesh.faces, + iterations=iterations) + # create new mesh + new_mesh = Trimesh( + vertices=new_vertices, + faces=new_faces) + new_meshes.append(new_mesh) + # concatenate all meshes into one + result = util.concatenate(new_meshes) + return result + # perform subdivision for one mesh new_vertices, new_faces = remesh.loop( - vertices=self.vertices, - faces=self.faces, + vertices=self.vertices, + faces=self.faces, iterations=iterations) # create new mesh result = Trimesh( diff --git a/trimesh/remesh.py b/trimesh/remesh.py index b9c5d8c0a..a52e1f717 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -220,35 +220,35 @@ def loop(vertices, faces, iterations=1): """ - Subdivide a mesh by dividing each triangle into four triangles + Subdivide a mesh by dividing each triangle into four triangles and approximating their smoothed surface (loop subdivision). This function is an array-based implementation of loop subdivision, which avoids slow for loop and enables faster calculation. - + Overall process: 1. Calculate odd vertices. - Assign a new odd vertex on each edge and + Assign a new odd vertex on each edge and calculate the value for the boundary case and the interior case. The value is calculated as follows. - v2 - / f0 \ 0 - v0--e--v1 / \ - \ f1 / v0--e--v1 - v3 + v2 + / f0 \\ 0 + v0--e--v1 / \\ + \\f1 / v0--e--v1 + v3 - interior case : 3:1 ratio of mean(v0,v1) and mean(v2,v3) - boundary case : mean(v0,v1) 2. Calculate even vertices. The new even vertices are calculated with the existing vertices and their adjacent vertices. 1---2 - / \ / \ 0---1 - 0---v---3 / \ / \ - \ / \ / b0---v---b1 - k...4 + / \\/ \\ 0---1 + 0---v---3 / \\/ \\ + \\ /\\/ b0---v---b1 + k...4 - interior case : (1-kβ):β ratio of v and k adjacencies - boundary case : 3:1 ratio of v and mean(b0,b1) 3. Compose new faces with new vertices. - + Parameters ------------ vertices : (n, 3) float @@ -265,91 +265,94 @@ def loop(vertices, iterations : int Number of iterations to run subdivision """ - def _subdivide(vertices, faces): - # find the unique edges of our faces - edges, edges_face = faces_to_edges(faces, return_index=True) - edges = np.sort(edges, axis=1) - unique, inverse = grouping.unique_rows(edges) - - # set interior edges if there are two edges and boundary if there is one. - edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) - edge_bound = grouping.group_rows(edges, require_count=1) - # make sure that one edge is shared by only one or two faces. - if not len(edge_inter)*2 + len(edge_bound) == len(edges): - raise ValueError('Some edges are shared by more than 2 faces') - - # set interior, boundary mask for unique edges - edge_bound_mask = np.zeros(len(edges), dtype=bool) - edge_bound_mask[edge_bound] = True - edge_bound_mask = edge_bound_mask[unique] - edge_inter_mask = ~edge_bound_mask - - # find the opposite face for each edge - edge_pair = np.zeros(len(edges)).astype(int) - edge_pair[edge_inter[:, 0]] = edge_inter[:, 1] - edge_pair[edge_inter[:, 1]] = edge_inter[:, 0] - opposite_face1 = edges_face[unique] - opposite_face2 = edges_face[edge_pair[unique]] - - # set odd vertices to the middle of each edge (default as boundary case). - odd = vertices[edges[unique]].mean(axis=1) - # modify the odd vertices for the interior case - e = edges[unique[edge_inter_mask]] - e_v0 = vertices[e][:, 0] - e_v1 = vertices[e][:, 1] - e_f0 = faces[opposite_face1[edge_inter_mask]] - e_f1 = faces[opposite_face2[edge_inter_mask]] - e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)] - e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)] - e_v2 = vertices[e_v2_idx] - e_v3 = vertices[e_v3_idx] - odd[edge_inter_mask] = 3/8 * (e_v0 + e_v1) + 1/8 * (e_v2 + e_v3) - - # find vertex neighbors of each vertex - neighbors = graph.neighbors(edges=edges[unique], max_index=len(vertices)) - # convert list type of array into a fixed-shaped numpy array (set -1 to empties) - neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T - # if the neighbor has -1 index, its point is (0, 0, 0), so that - # it is not included in the summation of neighbors when calculating the even values - vertices_ = np.vstack([vertices, [0, 0, 0]]) - # number of neighbors - k = (neighbors + 1).astype(bool).sum(-1) - - # calculate even vertices for the interior case - even = np.zeros_like(vertices) - beta = 1/k * (5/8 - (3/8 + 1/4 * np.cos(2 * np.pi / k)) ** 2) - even = beta[:, None] * vertices_[neighbors].sum(1) + (1 - k[:, None] * beta[:, None]) * vertices - - # calculate even vertices for the boundary case - if True in edge_bound_mask: - # boundary vertices from boundary edges - vrt_bound_mask = np.zeros(len(vertices), dtype=bool) - vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True - # one boundary vertex has two neighbor boundary vertices (set others as -1) - boundary_neighbors = neighbors[vrt_bound_mask] - boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 - even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) + 3/4 * vertices[vrt_bound_mask] - - # the new faces with odd vertices - odd_idx = inverse.reshape((-1, 3)) + len(vertices) - new_faces = np.column_stack([faces[:, 0], - odd_idx[:, 0], - odd_idx[:, 2], - odd_idx[:, 0], - faces[:, 1], - odd_idx[:, 1], - odd_idx[:, 2], - odd_idx[:, 1], - faces[:, 2], - odd_idx[:, 0], - odd_idx[:, 1], - odd_idx[:, 2]]).reshape((-1, 3)) - - # stack the new even vertices and odd vertices - new_vertices = np.vstack((even, odd)) - - return new_vertices, new_faces - + def _subdivide(vertices, faces): + # find the unique edges of our faces + edges, edges_face = faces_to_edges(faces, return_index=True) + edges = np.sort(edges, axis=1) + unique, inverse = grouping.unique_rows(edges) + + # set interior edges if there are two edges and boundary if there is one. + edge_inter = np.sort(grouping.group_rows(edges, require_count=2), axis=1) + edge_bound = grouping.group_rows(edges, require_count=1) + # make sure that one edge is shared by only one or two faces. + if not len(edge_inter)*2 + len(edge_bound) == len(edges): + raise ValueError('Some edges are shared by more than 2 faces') + + # set interior, boundary mask for unique edges + edge_bound_mask = np.zeros(len(edges), dtype=bool) + edge_bound_mask[edge_bound] = True + edge_bound_mask = edge_bound_mask[unique] + edge_inter_mask = ~edge_bound_mask + + # find the opposite face for each edge + edge_pair = np.zeros(len(edges)).astype(int) + edge_pair[edge_inter[:, 0]] = edge_inter[:, 1] + edge_pair[edge_inter[:, 1]] = edge_inter[:, 0] + opposite_face1 = edges_face[unique] + opposite_face2 = edges_face[edge_pair[unique]] + + # set odd vertices to the middle of each edge (default as boundary case). + odd = vertices[edges[unique]].mean(axis=1) + # modify the odd vertices for the interior case + e = edges[unique[edge_inter_mask]] + e_v0 = vertices[e][:, 0] + e_v1 = vertices[e][:, 1] + e_f0 = faces[opposite_face1[edge_inter_mask]] + e_f1 = faces[opposite_face2[edge_inter_mask]] + e_v2_idx = e_f0[~(e_f0[:, :, None] == e[:, None, :]).any(-1)] + e_v3_idx = e_f1[~(e_f1[:, :, None] == e[:, None, :]).any(-1)] + e_v2 = vertices[e_v2_idx] + e_v3 = vertices[e_v3_idx] + odd[edge_inter_mask] = 3/8 * (e_v0 + e_v1) + 1/8 * (e_v2 + e_v3) + + # find vertex neighbors of each vertex + neighbors = graph.neighbors(edges=edges[unique], max_index=len(vertices)) + # convert list type of array into a fixed-shaped numpy array (set -1 to empties) + neighbors = np.array(list(zip_longest(*neighbors, fillvalue=-1))).T + # if the neighbor has -1 index, its point is (0, 0, 0), so that + # it is not included in the summation of neighbors when calculating the even + vertices_ = np.vstack([vertices, [0, 0, 0]]) + # number of neighbors + k = (neighbors + 1).astype(bool).sum(-1) + + # calculate even vertices for the interior case + even = np.zeros_like(vertices) + beta = 1/k * (5/8 - (3/8 + 1/4 * np.cos(2 * np.pi / k)) ** 2) + even = beta[:, None] * vertices_[neighbors].sum(1) \ + + (1 - k[:, None] * beta[:, None]) * vertices + + # calculate even vertices for the boundary case + if True in edge_bound_mask: + # boundary vertices from boundary edges + vrt_bound_mask = np.zeros(len(vertices), dtype=bool) + vrt_bound_mask[np.unique(edges[unique][~edge_inter_mask])] = True + # one boundary vertex has two neighbor boundary vertices (set others as -1) + boundary_neighbors = neighbors[vrt_bound_mask] + boundary_neighbors[~vrt_bound_mask[neighbors[vrt_bound_mask]]] = -1 + even[vrt_bound_mask] = 1/8 * vertices_[boundary_neighbors].sum(1) \ + + 3/4 * vertices[vrt_bound_mask] + + # the new faces with odd vertices + odd_idx = inverse.reshape((-1, 3)) + len(vertices) + new_faces = np.column_stack([ + faces[:, 0], + odd_idx[:, 0], + odd_idx[:, 2], + odd_idx[:, 0], + faces[:, 1], + odd_idx[:, 1], + odd_idx[:, 2], + odd_idx[:, 1], + faces[:, 2], + odd_idx[:, 0], + odd_idx[:, 1], + odd_idx[:, 2]]).reshape((-1, 3)) + + # stack the new even vertices and odd vertices + new_vertices = np.vstack((even, odd)) + + return new_vertices, new_faces + for _index in range(iterations): - vertices, faces = _subdivide(vertices, faces) - return vertices, faces \ No newline at end of file + vertices, faces = _subdivide(vertices, faces) + return vertices, faces From 6e5474e33072515f7aead2d168b05dd12d6b9de7 Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 02:11:53 +0900 Subject: [PATCH 13/14] fix import zip_longest for py2 --- trimesh/remesh.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index a52e1f717..5693c407f 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -4,7 +4,6 @@ Deal with re- triangulation of existing meshes. """ -from itertools import zip_longest import numpy as np from . import util @@ -265,6 +264,11 @@ def loop(vertices, iterations : int Number of iterations to run subdivision """ + try: + from itertools import zip_longest + except: + from itertools import izip_longest as zip_longest # python2 + def _subdivide(vertices, faces): # find the unique edges of our faces edges, edges_face = faces_to_edges(faces, return_index=True) From d7d064ce6b7ffc9faa7c8d9b339db85cb2063f6e Mon Sep 17 00:00:00 2001 From: Hyeonseo Nam Date: Thu, 17 Nov 2022 02:16:54 +0900 Subject: [PATCH 14/14] fix format --- trimesh/remesh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trimesh/remesh.py b/trimesh/remesh.py index 5693c407f..60bcb8234 100644 --- a/trimesh/remesh.py +++ b/trimesh/remesh.py @@ -264,11 +264,11 @@ def loop(vertices, iterations : int Number of iterations to run subdivision """ - try: + try: from itertools import zip_longest - except: - from itertools import izip_longest as zip_longest # python2 - + except BaseException: + from itertools import izip_longest as zip_longest # python2 + def _subdivide(vertices, faces): # find the unique edges of our faces edges, edges_face = faces_to_edges(faces, return_index=True)