icosahedral_mesh.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # Copyright 2023 DeepMind Technologies Limited.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS-IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Utils for creating icosahedral meshes."""
  15. import itertools
  16. from typing import List, NamedTuple, Sequence, Tuple
  17. import numpy as np
  18. from scipy.spatial import transform
  19. class TriangularMesh(NamedTuple):
  20. """Data structure for triangular meshes.
  21. Attributes:
  22. vertices: spatial positions of the vertices of the mesh of shape
  23. [num_vertices, num_dims].
  24. faces: triangular faces of the mesh of shape [num_faces, 3]. Contains
  25. integer indices into `vertices`.
  26. """
  27. vertices: np.ndarray
  28. faces: np.ndarray
  29. def merge_meshes(
  30. mesh_list: Sequence[TriangularMesh]) -> TriangularMesh:
  31. """Merges all meshes into one. Assumes the last mesh is the finest.
  32. Args:
  33. mesh_list: Sequence of meshes, from coarse to fine refinement levels. The
  34. vertices and faces may contain those from preceding, coarser levels.
  35. Returns:
  36. `TriangularMesh` for which the vertices correspond to the highest
  37. resolution mesh in the hierarchy, and the faces are the join set of the
  38. faces at all levels of the hierarchy.
  39. """
  40. for mesh_i, mesh_ip1 in itertools.pairwise(mesh_list):
  41. num_nodes_mesh_i = mesh_i.vertices.shape[0]
  42. assert np.allclose(mesh_i.vertices, mesh_ip1.vertices[:num_nodes_mesh_i])
  43. return TriangularMesh(
  44. vertices=mesh_list[-1].vertices,
  45. faces=np.concatenate([mesh.faces for mesh in mesh_list], axis=0))
  46. def get_hierarchy_of_triangular_meshes_for_sphere(
  47. splits: int) -> List[TriangularMesh]:
  48. """Returns a sequence of meshes, each with triangularization sphere.
  49. Starting with a regular icosahedron (12 vertices, 20 faces, 30 edges) with
  50. circumscribed unit sphere. Then, each triangular face is iteratively
  51. subdivided into 4 triangular faces `splits` times. The new vertices are then
  52. projected back onto the unit sphere. All resulting meshes are returned in a
  53. list, from lowest to highest resolution.
  54. The vertices in each face are specified in counter-clockwise order as
  55. observed from the outside the icosahedron.
  56. Args:
  57. splits: How many times to split each triangle.
  58. Returns:
  59. Sequence of `TriangularMesh`s of length `splits + 1` each with:
  60. vertices: [num_vertices, 3] vertex positions in 3D, all with unit norm.
  61. faces: [num_faces, 3] with triangular faces joining sets of 3 vertices.
  62. Each row contains three indices into the vertices array, indicating
  63. the vertices adjacent to the face. Always with positive orientation
  64. (counterclock-wise when looking from the outside).
  65. """
  66. current_mesh = get_icosahedron()
  67. output_meshes = [current_mesh]
  68. for _ in range(splits):
  69. current_mesh = _two_split_unit_sphere_triangle_faces(current_mesh)
  70. output_meshes.append(current_mesh)
  71. return output_meshes
  72. def get_icosahedron() -> TriangularMesh:
  73. """Returns a regular icosahedral mesh with circumscribed unit sphere.
  74. See https://en.wikipedia.org/wiki/Regular_icosahedron#Cartesian_coordinates
  75. for details on the construction of the regular icosahedron.
  76. The vertices in each face are specified in counter-clockwise order as observed
  77. from the outside of the icosahedron.
  78. Returns:
  79. TriangularMesh with:
  80. vertices: [num_vertices=12, 3] vertex positions in 3D, all with unit norm.
  81. faces: [num_faces=20, 3] with triangular faces joining sets of 3 vertices.
  82. Each row contains three indices into the vertices array, indicating
  83. the vertices adjacent to the face. Always with positive orientation (
  84. counterclock-wise when looking from the outside).
  85. """
  86. phi = (1 + np.sqrt(5)) / 2
  87. vertices = []
  88. for c1 in [1., -1.]:
  89. for c2 in [phi, -phi]:
  90. vertices.append((c1, c2, 0.))
  91. vertices.append((0., c1, c2))
  92. vertices.append((c2, 0., c1))
  93. vertices = np.array(vertices, dtype=np.float32)
  94. vertices /= np.linalg.norm([1., phi])
  95. # I did this manually, checking the orientation one by one.
  96. faces = [(0, 1, 2),
  97. (0, 6, 1),
  98. (8, 0, 2),
  99. (8, 4, 0),
  100. (3, 8, 2),
  101. (3, 2, 7),
  102. (7, 2, 1),
  103. (0, 4, 6),
  104. (4, 11, 6),
  105. (6, 11, 5),
  106. (1, 5, 7),
  107. (4, 10, 11),
  108. (4, 8, 10),
  109. (10, 8, 3),
  110. (10, 3, 9),
  111. (11, 10, 9),
  112. (11, 9, 5),
  113. (5, 9, 7),
  114. (9, 3, 7),
  115. (1, 6, 5),
  116. ]
  117. # By default the top is an aris parallel to the Y axis.
  118. # Need to rotate around the y axis by half the supplementary to the
  119. # angle between faces divided by two to get the desired orientation.
  120. # /O\ (top arist)
  121. # / \ Z
  122. # (adjacent face)/ \ (adjacent face) ^
  123. # / angle_between_faces \ |
  124. # / \ |
  125. # / \ YO-----> X
  126. # This results in:
  127. # (adjacent faceis now top plane)
  128. # ----------------------O\ (top arist)
  129. # \
  130. # \
  131. # \ (adjacent face)
  132. # \
  133. # \
  134. # \
  135. angle_between_faces = 2 * np.arcsin(phi / np.sqrt(3))
  136. rotation_angle = (np.pi - angle_between_faces) / 2
  137. rotation = transform.Rotation.from_euler(seq="y", angles=rotation_angle)
  138. rotation_matrix = rotation.as_matrix()
  139. vertices = np.dot(vertices, rotation_matrix)
  140. return TriangularMesh(vertices=vertices.astype(np.float32),
  141. faces=np.array(faces, dtype=np.int32))
  142. def _two_split_unit_sphere_triangle_faces(
  143. triangular_mesh: TriangularMesh) -> TriangularMesh:
  144. """Splits each triangular face into 4 triangles keeping the orientation."""
  145. # Every time we split a triangle into 4 we will be adding 3 extra vertices,
  146. # located at the edge centres.
  147. # This class handles the positioning of the new vertices, and avoids creating
  148. # duplicates.
  149. new_vertices_builder = _ChildVerticesBuilder(triangular_mesh.vertices)
  150. new_faces = []
  151. for ind1, ind2, ind3 in triangular_mesh.faces:
  152. # Transform each triangular face into 4 triangles,
  153. # preserving the orientation.
  154. # ind3
  155. # / \
  156. # / \
  157. # / #3 \
  158. # / \
  159. # ind31 -------------- ind23
  160. # / \ / \
  161. # / \ #4 / \
  162. # / #1 \ / #2 \
  163. # / \ / \
  164. # ind1 ------------ ind12 ------------ ind2
  165. ind12 = new_vertices_builder.get_new_child_vertex_index((ind1, ind2))
  166. ind23 = new_vertices_builder.get_new_child_vertex_index((ind2, ind3))
  167. ind31 = new_vertices_builder.get_new_child_vertex_index((ind3, ind1))
  168. # Note how each of the 4 triangular new faces specifies the order of the
  169. # vertices to preserve the orientation of the original face. As the input
  170. # face should always be counter-clockwise as specified in the diagram,
  171. # this means child faces should also be counter-clockwise.
  172. new_faces.extend([[ind1, ind12, ind31], # 1
  173. [ind12, ind2, ind23], # 2
  174. [ind31, ind23, ind3], # 3
  175. [ind12, ind23, ind31], # 4
  176. ])
  177. return TriangularMesh(vertices=new_vertices_builder.get_all_vertices(),
  178. faces=np.array(new_faces, dtype=np.int32))
  179. class _ChildVerticesBuilder(object):
  180. """Bookkeeping of new child vertices added to an existing set of vertices."""
  181. def __init__(self, parent_vertices):
  182. # Because the same new vertex will be required when splitting adjacent
  183. # triangles (which share an edge) we keep them in a hash table indexed by
  184. # sorted indices of the vertices adjacent to the edge, to avoid creating
  185. # duplicated child vertices.
  186. self._child_vertices_index_mapping = {}
  187. self._parent_vertices = parent_vertices
  188. # We start with all previous vertices.
  189. self._all_vertices_list = list(parent_vertices)
  190. def _get_child_vertex_key(self, parent_vertex_indices):
  191. return tuple(sorted(parent_vertex_indices))
  192. def _create_child_vertex(self, parent_vertex_indices):
  193. """Creates a new vertex."""
  194. # Position for new vertex is the middle point, between the parent points,
  195. # projected to unit sphere.
  196. child_vertex_position = self._parent_vertices[
  197. list(parent_vertex_indices)].mean(0)
  198. child_vertex_position /= np.linalg.norm(child_vertex_position)
  199. # Add the vertex to the output list. The index for this new vertex will
  200. # match the length of the list before adding it.
  201. child_vertex_key = self._get_child_vertex_key(parent_vertex_indices)
  202. self._child_vertices_index_mapping[child_vertex_key] = len(
  203. self._all_vertices_list)
  204. self._all_vertices_list.append(child_vertex_position)
  205. def get_new_child_vertex_index(self, parent_vertex_indices):
  206. """Returns index for a child vertex, creating it if necessary."""
  207. # Get the key to see if we already have a new vertex in the middle.
  208. child_vertex_key = self._get_child_vertex_key(parent_vertex_indices)
  209. if child_vertex_key not in self._child_vertices_index_mapping:
  210. self._create_child_vertex(parent_vertex_indices)
  211. return self._child_vertices_index_mapping[child_vertex_key]
  212. def get_all_vertices(self):
  213. """Returns an array with old vertices."""
  214. return np.array(self._all_vertices_list)
  215. def faces_to_edges(faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
  216. """Transforms polygonal faces to sender and receiver indices.
  217. It does so by transforming every face into N_i edges. Such if the triangular
  218. face has indices [0, 1, 2], three edges are added 0->1, 1->2, and 2->0.
  219. If all faces have consistent orientation, and the surface represented by the
  220. faces is closed, then every edge in a polygon with a certain orientation
  221. is also part of another polygon with the opposite orientation. In this
  222. situation, the edges returned by the method are always bidirectional.
  223. Args:
  224. faces: Integer array of shape [num_faces, 3]. Contains node indices
  225. adjacent to each face.
  226. Returns:
  227. Tuple with sender/receiver indices, each of shape [num_edges=num_faces*3].
  228. """
  229. assert faces.ndim == 2
  230. assert faces.shape[-1] == 3
  231. senders = np.concatenate([faces[:, 0], faces[:, 1], faces[:, 2]])
  232. receivers = np.concatenate([faces[:, 1], faces[:, 2], faces[:, 0]])
  233. return senders, receivers