|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Utils for creating icosahedral meshes.""" |
|
|
|
import itertools |
|
from typing import List, NamedTuple, Sequence, Tuple |
|
|
|
import numpy as np |
|
from scipy.spatial import transform |
|
|
|
|
|
class TriangularMesh(NamedTuple): |
|
"""Data structure for triangular meshes. |
|
|
|
Attributes: |
|
vertices: spatial positions of the vertices of the mesh of shape |
|
[num_vertices, num_dims]. |
|
faces: triangular faces of the mesh of shape [num_faces, 3]. Contains |
|
integer indices into `vertices`. |
|
|
|
""" |
|
vertices: np.ndarray |
|
faces: np.ndarray |
|
|
|
|
|
def merge_meshes( |
|
mesh_list: Sequence[TriangularMesh]) -> TriangularMesh: |
|
"""Merges all meshes into one. Assumes the last mesh is the finest. |
|
|
|
Args: |
|
mesh_list: Sequence of meshes, from coarse to fine refinement levels. The |
|
vertices and faces may contain those from preceding, coarser levels. |
|
|
|
Returns: |
|
`TriangularMesh` for which the vertices correspond to the highest |
|
resolution mesh in the hierarchy, and the faces are the join set of the |
|
faces at all levels of the hierarchy. |
|
""" |
|
for mesh_i, mesh_ip1 in itertools.pairwise(mesh_list): |
|
num_nodes_mesh_i = mesh_i.vertices.shape[0] |
|
assert np.allclose(mesh_i.vertices, mesh_ip1.vertices[:num_nodes_mesh_i]) |
|
|
|
return TriangularMesh( |
|
vertices=mesh_list[-1].vertices, |
|
faces=np.concatenate([mesh.faces for mesh in mesh_list], axis=0)) |
|
|
|
|
|
def get_hierarchy_of_triangular_meshes_for_sphere( |
|
splits: int) -> List[TriangularMesh]: |
|
"""Returns a sequence of meshes, each with triangularization sphere. |
|
|
|
Starting with a regular icosahedron (12 vertices, 20 faces, 30 edges) with |
|
circumscribed unit sphere. Then, each triangular face is iteratively |
|
subdivided into 4 triangular faces `splits` times. The new vertices are then |
|
projected back onto the unit sphere. All resulting meshes are returned in a |
|
list, from lowest to highest resolution. |
|
|
|
The vertices in each face are specified in counter-clockwise order as |
|
observed from the outside the icosahedron. |
|
|
|
Args: |
|
splits: How many times to split each triangle. |
|
Returns: |
|
Sequence of `TriangularMesh`s of length `splits + 1` each with: |
|
|
|
vertices: [num_vertices, 3] vertex positions in 3D, all with unit norm. |
|
faces: [num_faces, 3] with triangular faces joining sets of 3 vertices. |
|
Each row contains three indices into the vertices array, indicating |
|
the vertices adjacent to the face. Always with positive orientation |
|
(counterclock-wise when looking from the outside). |
|
""" |
|
current_mesh = get_icosahedron() |
|
output_meshes = [current_mesh] |
|
for _ in range(splits): |
|
current_mesh = _two_split_unit_sphere_triangle_faces(current_mesh) |
|
output_meshes.append(current_mesh) |
|
return output_meshes |
|
|
|
|
|
def get_icosahedron() -> TriangularMesh: |
|
"""Returns a regular icosahedral mesh with circumscribed unit sphere. |
|
|
|
See https://en.wikipedia.org/wiki/Regular_icosahedron#Cartesian_coordinates |
|
for details on the construction of the regular icosahedron. |
|
|
|
The vertices in each face are specified in counter-clockwise order as observed |
|
from the outside of the icosahedron. |
|
|
|
Returns: |
|
TriangularMesh with: |
|
|
|
vertices: [num_vertices=12, 3] vertex positions in 3D, all with unit norm. |
|
faces: [num_faces=20, 3] with triangular faces joining sets of 3 vertices. |
|
Each row contains three indices into the vertices array, indicating |
|
the vertices adjacent to the face. Always with positive orientation ( |
|
counterclock-wise when looking from the outside). |
|
|
|
""" |
|
phi = (1 + np.sqrt(5)) / 2 |
|
vertices = [] |
|
for c1 in [1., -1.]: |
|
for c2 in [phi, -phi]: |
|
vertices.append((c1, c2, 0.)) |
|
vertices.append((0., c1, c2)) |
|
vertices.append((c2, 0., c1)) |
|
|
|
vertices = np.array(vertices, dtype=np.float32) |
|
vertices /= np.linalg.norm([1., phi]) |
|
|
|
|
|
faces = [(0, 1, 2), |
|
(0, 6, 1), |
|
(8, 0, 2), |
|
(8, 4, 0), |
|
(3, 8, 2), |
|
(3, 2, 7), |
|
(7, 2, 1), |
|
(0, 4, 6), |
|
(4, 11, 6), |
|
(6, 11, 5), |
|
(1, 5, 7), |
|
(4, 10, 11), |
|
(4, 8, 10), |
|
(10, 8, 3), |
|
(10, 3, 9), |
|
(11, 10, 9), |
|
(11, 9, 5), |
|
(5, 9, 7), |
|
(9, 3, 7), |
|
(1, 6, 5), |
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
angle_between_faces = 2 * np.arcsin(phi / np.sqrt(3)) |
|
rotation_angle = (np.pi - angle_between_faces) / 2 |
|
rotation = transform.Rotation.from_euler(seq="y", angles=rotation_angle) |
|
rotation_matrix = rotation.as_matrix() |
|
vertices = np.dot(vertices, rotation_matrix) |
|
|
|
return TriangularMesh(vertices=vertices.astype(np.float32), |
|
faces=np.array(faces, dtype=np.int32)) |
|
|
|
|
|
def _two_split_unit_sphere_triangle_faces( |
|
triangular_mesh: TriangularMesh) -> TriangularMesh: |
|
"""Splits each triangular face into 4 triangles keeping the orientation.""" |
|
|
|
|
|
|
|
|
|
|
|
new_vertices_builder = _ChildVerticesBuilder(triangular_mesh.vertices) |
|
|
|
new_faces = [] |
|
for ind1, ind2, ind3 in triangular_mesh.faces: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ind12 = new_vertices_builder.get_new_child_vertex_index((ind1, ind2)) |
|
ind23 = new_vertices_builder.get_new_child_vertex_index((ind2, ind3)) |
|
ind31 = new_vertices_builder.get_new_child_vertex_index((ind3, ind1)) |
|
|
|
|
|
|
|
|
|
new_faces.extend([[ind1, ind12, ind31], |
|
[ind12, ind2, ind23], |
|
[ind31, ind23, ind3], |
|
[ind12, ind23, ind31], |
|
]) |
|
return TriangularMesh(vertices=new_vertices_builder.get_all_vertices(), |
|
faces=np.array(new_faces, dtype=np.int32)) |
|
|
|
|
|
class _ChildVerticesBuilder(object): |
|
"""Bookkeeping of new child vertices added to an existing set of vertices.""" |
|
|
|
def __init__(self, parent_vertices): |
|
|
|
|
|
|
|
|
|
|
|
self._child_vertices_index_mapping = {} |
|
self._parent_vertices = parent_vertices |
|
|
|
self._all_vertices_list = list(parent_vertices) |
|
|
|
def _get_child_vertex_key(self, parent_vertex_indices): |
|
return tuple(sorted(parent_vertex_indices)) |
|
|
|
def _create_child_vertex(self, parent_vertex_indices): |
|
"""Creates a new vertex.""" |
|
|
|
|
|
child_vertex_position = self._parent_vertices[ |
|
list(parent_vertex_indices)].mean(0) |
|
child_vertex_position /= np.linalg.norm(child_vertex_position) |
|
|
|
|
|
|
|
child_vertex_key = self._get_child_vertex_key(parent_vertex_indices) |
|
self._child_vertices_index_mapping[child_vertex_key] = len( |
|
self._all_vertices_list) |
|
self._all_vertices_list.append(child_vertex_position) |
|
|
|
def get_new_child_vertex_index(self, parent_vertex_indices): |
|
"""Returns index for a child vertex, creating it if necessary.""" |
|
|
|
child_vertex_key = self._get_child_vertex_key(parent_vertex_indices) |
|
if child_vertex_key not in self._child_vertices_index_mapping: |
|
self._create_child_vertex(parent_vertex_indices) |
|
return self._child_vertices_index_mapping[child_vertex_key] |
|
|
|
def get_all_vertices(self): |
|
"""Returns an array with old vertices.""" |
|
return np.array(self._all_vertices_list) |
|
|
|
|
|
def faces_to_edges(faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: |
|
"""Transforms polygonal faces to sender and receiver indices. |
|
|
|
It does so by transforming every face into N_i edges. Such if the triangular |
|
face has indices [0, 1, 2], three edges are added 0->1, 1->2, and 2->0. |
|
|
|
If all faces have consistent orientation, and the surface represented by the |
|
faces is closed, then every edge in a polygon with a certain orientation |
|
is also part of another polygon with the opposite orientation. In this |
|
situation, the edges returned by the method are always bidirectional. |
|
|
|
Args: |
|
faces: Integer array of shape [num_faces, 3]. Contains node indices |
|
adjacent to each face. |
|
Returns: |
|
Tuple with sender/receiver indices, each of shape [num_edges=num_faces*3]. |
|
|
|
""" |
|
assert faces.ndim == 2 |
|
assert faces.shape[-1] == 3 |
|
senders = np.concatenate([faces[:, 0], faces[:, 1], faces[:, 2]]) |
|
receivers = np.concatenate([faces[:, 1], faces[:, 2], faces[:, 0]]) |
|
return senders, receivers |
|
|