From 9db0348acb7ae5911594d3f848c347afa20db60b Mon Sep 17 00:00:00 2001
From: andersct <andersct@umich.edu>
Date: Sat, 24 Oct 2020 20:22:37 -0400
Subject: [PATCH] Add asteroids for space examples

- add display for asteroid
- generate random convex asteroids by sampling normals
- add functions to fix surface triangulations
- add figure setting for space background
---
 examples/display_asteroid.py              | 50 +++++++++++++++++++++++
 examples/display_shuttle_model.py         |  7 +++-
 rigid_body_models/shuttle.py              |  1 +
 rigid_body_models/triangulated_surface.py | 39 ++++++++++++++++++
 scene_elements_3d/__init__.py             |  0
 scene_elements_3d/asteroid.py             | 45 ++++++++++++++++++++
 vis/triangulated_surface.py               |  6 ++-
 vis/xyz_utils.py                          |  9 ++++
 8 files changed, 154 insertions(+), 3 deletions(-)
 create mode 100644 examples/display_asteroid.py
 create mode 100644 scene_elements_3d/__init__.py
 create mode 100644 scene_elements_3d/asteroid.py

diff --git a/examples/display_asteroid.py b/examples/display_asteroid.py
new file mode 100644
index 0000000..2666f78
--- /dev/null
+++ b/examples/display_asteroid.py
@@ -0,0 +1,50 @@
+import numpy as np
+import plotly.graph_objects as go
+import scene_elements_3d.asteroid as ast
+from vis import triangulated_surface as vtrs
+from vis.xyz_utils import set_space_background
+
+
+def main(is_surface_normals=False):
+    seed = np.random.randint(1000)
+    print('seed: {}'.format(seed))
+    # seed = 0
+    np.random.seed(seed)
+    mu = np.array([
+        [0., 0, 0],
+        [2, 0, 1],
+    ])
+    sigma = np.array([
+        [
+            [2., 0, 0],
+            [0, 1, 0],
+            [0, 0, 1],
+        ],
+        [
+            [1., 0, .2],
+            [0, 1, 0],
+            [.2, 0, 1],
+        ],
+    ])
+    model = ast.generate_convex_asteroid(mu, sigma, 100)
+
+    fig = go.Figure()
+    fig.add_traces([vtrs.make_model_mesh(model)])
+    if is_surface_normals:
+        fig.add_traces(vtrs.make_surface_normals(
+            model.vertices, model.triangles))
+    title_str = 'Asteroid'
+    fig.update_layout(
+        title=title_str, autosize=False,
+        width=1200, height=600,
+        scene=dict(
+            xaxis=dict(range=[-8, 8]),
+            yaxis=dict(range=[-6, 6]),
+            zaxis=dict(range=[-6, 6]),
+        ), showlegend=False)
+    set_space_background(fig)
+    fig.show()
+
+
+if __name__ == '__main__':
+    main(is_surface_normals=False)
diff --git a/examples/display_shuttle_model.py b/examples/display_shuttle_model.py
index 829955e..b4366fe 100644
--- a/examples/display_shuttle_model.py
+++ b/examples/display_shuttle_model.py
@@ -2,9 +2,10 @@ import plotly.graph_objects as go
 from rigid_body_models.shuttle import Shuttle
 from vis import triangulated_surface as vtrs
 from vis import triangulated_thruster_model as vttm
+from vis.xyz_utils import set_space_background
 
 
-def main_display_mesh(is_thrusters=True):
+def main_display_mesh(is_thrusters=True, is_space_bg=True):
     model = Shuttle(rho=1e6)
     fig = go.Figure()
     fig.add_traces([vtrs.make_model_mesh(model)])
@@ -23,8 +24,10 @@ def main_display_mesh(is_thrusters=True):
             yaxis=dict(range=[-6, 6]),
             zaxis=dict(range=[-6, 6]),
         ), showlegend=is_thrusters)
+    if is_space_bg:
+        set_space_background(fig)
     fig.show()
 
 
 if __name__ == '__main__':
-    main_display_mesh(is_thrusters=True)
+    main_display_mesh(is_thrusters=True, is_space_bg=True)
diff --git a/rigid_body_models/shuttle.py b/rigid_body_models/shuttle.py
index 24e8a3b..4a44bbe 100644
--- a/rigid_body_models/shuttle.py
+++ b/rigid_body_models/shuttle.py
@@ -86,3 +86,4 @@ class Shuttle(trs.TriangulatedSurface):
         _, _, im = trs.calculate_mass_properties(
             self.vertices, self.triangles, rho)
         self.im = im
+        self.color = 'white'
diff --git a/rigid_body_models/triangulated_surface.py b/rigid_body_models/triangulated_surface.py
index 3a9a0e7..010089d 100644
--- a/rigid_body_models/triangulated_surface.py
+++ b/rigid_body_models/triangulated_surface.py
@@ -69,3 +69,42 @@ def calculate_mass_properties(vertices, triangles, rho):
         [-s[6], -s[5], s[7] + s[8]],
     ])
     return m, center, im
+
+
+def remove_unused_vertices(vertices, triangles):
+    """
+    Remove vertices that do not form a triangle, and relabel triangle vertices
+    to their new indices.
+    :param vertices: n_points, 3 | includes all points forming triangle faces
+    :param triangles: n_faces, 3 | indices of vertices that form each triangle
+    :return: vertices, triangles
+    """
+    used_inds = np.unique(triangles.ravel())
+    vertices_new = vertices[used_inds]
+    triangles_new = triangles.copy()
+    for i in range(used_inds.size):
+        triangles_new[triangles_new == used_inds[i]] = i
+    return vertices_new, triangles_new
+
+
+def make_convex_shape_triangles_consistent(vertices, triangles):
+    """
+    For a convex shape described by a triangulated surface, ensure
+    each triangle's normal faces outward.
+    :param vertices: n_points, 3 | includes all points forming triangle faces
+    :param triangles: n_faces, 3 | indices of vertices that form each triangle
+    :return: triangles: n_faces, 3 | triangles made consistent
+    """
+    used_inds = np.unique(triangles.ravel())
+    center = vertices[used_inds].mean(axis=0)
+    triangle_centers = np.array([
+        vertices[triangles[i]].mean(axis=0) for i in range(len(triangles))
+    ])
+    outward_vec = triangle_centers - center
+    triangle_normals = np.cross(
+        vertices[triangles[:, 1]] - vertices[triangles[:, 0]],
+        vertices[triangles[:, 2]] - vertices[triangles[:, 0]],
+    )
+    is_mismatch = np.einsum('ij,ij->i', outward_vec, triangle_normals) < 0
+    triangles[is_mismatch, :] = triangles[is_mismatch, :][:, [0, 2, 1]]
+    return triangles
diff --git a/scene_elements_3d/__init__.py b/scene_elements_3d/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scene_elements_3d/asteroid.py b/scene_elements_3d/asteroid.py
new file mode 100644
index 0000000..ed1b98f
--- /dev/null
+++ b/scene_elements_3d/asteroid.py
@@ -0,0 +1,45 @@
+import numpy as np
+from rigid_body_models import triangulated_surface as trs
+from scipy.spatial import ConvexHull
+
+
+def generate_convex_asteroid(mu_list, sigma_list, n_samples_per, **kwargs):
+    """
+    Generate asteroid from the convex hull of samples taken
+    from a list of normals.
+    :param mu_list: n, 3 | means of multivariate normals
+    :param sigma_list: n, 3, 3 | covariance matrix of multivariate normals
+    :param n_samples_per: number of samples per normal
+    :return:
+        vertices:
+        triangles:
+    """
+    samples = []
+    for mu, sigma in zip(mu_list, sigma_list):
+        L = np.linalg.cholesky(sigma)
+        samples_i = (L.dot(np.random.randn(3, n_samples_per))).T + mu
+        samples.append(samples_i)
+    samples = np.vstack(samples)  # n*n_samples_per, 3
+    hull = ConvexHull(samples)
+    triangles = hull.simplices
+    vertices = samples
+    vertices, triangles = trs.remove_unused_vertices(vertices, triangles)
+    triangles = trs.make_convex_shape_triangles_consistent(vertices, triangles)
+    asteroid = Asteroid(vertices, triangles, **kwargs)
+    return asteroid
+
+
+class Asteroid(trs.TriangulatedSurface):
+
+    def __init__(self, vertices, triangles, rho=2000.):
+        super(Asteroid, self).__init__(rho, vertices, triangles)
+        m, center, im = trs.calculate_mass_properties(
+            self.vertices, self.triangles, rho)
+        self.m = m
+        self.im = im
+        self.center = 0 * center
+        self.vertices -= center
+        _, _, im = trs.calculate_mass_properties(
+            self.vertices, self.triangles, rho)
+        self.im = im
+        self.color = 'grey'
diff --git a/vis/triangulated_surface.py b/vis/triangulated_surface.py
index 3f48e81..87acdd3 100644
--- a/vis/triangulated_surface.py
+++ b/vis/triangulated_surface.py
@@ -15,10 +15,14 @@ def make_model_mesh(model, Rt=()):
         Rt = Rt_3d()
     xyz = Rt[:3, :3].dot(model.vertices.T).T + Rt[:3, -1]
     ijk = model.triangles
+    try:
+        color = model.color
+    except AttributeError:
+        color = None
     mesh = go.Mesh3d(
         x=xyz[:, 0], y=xyz[:, 1], z=xyz[:, 2],
         i=ijk[:, 0], j=ijk[:, 1], k=ijk[:, 2],
-        color='white', flatshading=True
+        color=color, flatshading=True
     )
     return mesh
 
diff --git a/vis/xyz_utils.py b/vis/xyz_utils.py
index cdc9b97..dc538ee 100644
--- a/vis/xyz_utils.py
+++ b/vis/xyz_utils.py
@@ -29,3 +29,12 @@ def make_3d_vector(root, vector, scale=1., name=''):
         name=name,
     )
     return line_obj, arrow_head_obj
+
+
+def set_space_background(fig):
+    """
+    :param fig: plotly figure
+    :return:
+    """
+    fig.update_layout(paper_bgcolor='rgb(10,10,10)')
+    fig.update_scenes(xaxis_visible=False, yaxis_visible=False, zaxis_visible=False)
-- 
GitLab