thallada.github.io/_posts/2020-02-01-generating-icosahedrons-and-hexspheres-in-rust.md

759 lines
27 KiB
Markdown

---
title: "Generating icosahedrons and hexspheres in Rust"
layout: post
image: /img/blog/hexsphere_colored_7.png
---
I've been trying to learn [Rust](https://www.rust-lang.org/) lately, the hot new
systems programming language. One of the projects I wanted to tackle with the
speed of Rust was generating 3D polyhedron shapes. Specifically, I wanted to
implement something like the [Three.js
`IcosahedronGeometry`](https://threejs.org/docs/#api/en/geometries/IcosahedronGeometry)
in Rust. If you try to generate
[icosahedron](https://en.wikipedia.org/wiki/Icosahedron)s in Three.js over any
detail level over 5 the whole browser will slow to a crawl. I think we can do
better in Rust!
Furthermore, I wanted to generate a hexsphere: a sphere composed of hexagon
faces and 12 pentagon faces, otherwise known as a truncated icosahedron or the
[Goldberg polyhedron](https://en.wikipedia.org/wiki/Goldberg_polyhedron). The
shape would be ideal for a game since (almost) every tile would have the same
area and six sides to defend or attack from. There's a few [Javascript projects
for generating hexspheres](https://www.robscanlon.com/hexasphere/). Most of them
generate the shape by starting with a subdivided icosahedron and then truncating
the sides into hexagons. Though, there [exist other methods for generating the
hexsphere
shape](https://stackoverflow.com/questions/46777626/mathematically-producing-sphere-shaped-hexagonal-grid).
**Play around with all of these shapes in your browser at:
[https://www.hallada.net/planet/](https://www.hallada.net/planet/).**
So, how would we go about generating a hexsphere from scratch?
<!--excerpt-->
### The Icosahedron Seed
To start our sculpture, we need our ball of clay. The most basic shape that we
start with can be defined by its 20 triangle faces and 12 vertices: the regular
icosahedron. If you've ever played Dungeons and Dragons, this is the 20-sided
die.
To define this basic shape in Rust, we first need to define a few structs. The
most basic unit we need is a 3D vector which describes a single point in 3D
space with a X, Y, and Z float values. I could have defined this myself, but to
avoid having to implement a bunch of vector operations (like add, subtract,
multiply, etc.) I chose to import
[`Vector3`](https://docs.rs/cgmath/0.17.0/cgmath/struct.Vector3.html) from the
[cgmath crate](https://crates.io/crates/cgmath).
The next struct we need is `Triangle`. This will define a face between three
vertices:
```rust
#[derive(Debug)]
pub struct Triangle {
pub a: usize,
pub b: usize,
pub c: usize,
}
impl Triangle {
fn new(a: usize, b: usize, c: usize) -> Triangle {
Triangle { a, b, c }
}
}
```
We use `usize` for the three points of the triangle because they are indices
into a [`Vec`](https://doc.rust-lang.org/std/vec/struct.Vec.html) of `Vector3`s.
To keep these all together, I'll define a `Polyhedron` struct:
```rust
#[derive(Debug)]
pub struct Polyhedron {
pub positions: Vec<Vector3>,
pub cells: Vec<Triangle>,
}
```
With this, we can define the regular icosahedron:
```rust
impl Polyhedron {
pub fn regular_isocahedron() -> Polyhedron {
let t = (1.0 + (5.0 as f32).sqrt()) / 2.0;
Polyhedron {
positions: vec![
Vector3::new(-1.0, t, 0.0),
Vector3::new(1.0, t, 0.0),
Vector3::new(-1.0, -t, 0.0),
Vector3::new(1.0, -t, 0.0),
Vector3::new(0.0, -1.0, t),
Vector3::new(0.0, 1.0, t),
Vector3::new(0.0, -1.0, -t),
Vector3::new(0.0, 1.0, -t),
Vector3::new(t, 0.0, -1.0),
Vector3::new(t, 0.0, 1.0),
Vector3::new(-t, 0.0, -1.0),
Vector3::new(-t, 0.0, 1.0),
],
cells: vec![
Triangle::new(0, 11, 5),
Triangle::new(0, 5, 1),
Triangle::new(0, 1, 7),
Triangle::new(0, 7, 10),
Triangle::new(0, 10, 11),
Triangle::new(1, 5, 9),
Triangle::new(5, 11, 4),
Triangle::new(11, 10, 2),
Triangle::new(10, 7, 6),
Triangle::new(7, 1, 8),
Triangle::new(3, 9, 4),
Triangle::new(3, 4, 2),
Triangle::new(3, 2, 6),
Triangle::new(3, 6, 8),
Triangle::new(3, 8, 9),
Triangle::new(4, 9, 5),
Triangle::new(2, 4, 11),
Triangle::new(6, 2, 10),
Triangle::new(8, 6, 7),
Triangle::new(9, 8, 1),
],
}
}
}
```
### JSON Serialization
To prove this works, we need to be able to output our shape to some format that
will be able to be rendered. Coming from a JS background, I'm only familiar with
rendering shapes with WebGL. So, I need to be able to serialize the shape to
JSON so I can load it in JS.
There's an amazing library in Rust called
[serde](https://crates.io/crates/serde) that will make this very
straightforward. We just need to import it and `impl Serialize` for all of our
structs.
The JSON structure we want will look like this. This is what Three.js expects
when initializing
[`BufferGeometry`](https://threejs.org/docs/#api/en/core/BufferGeometry).
```json
{
"positions": [
[
-0.8506508,
0,
0.5257311
],
...
],
"cells": [
[
0,
1,
2,
],
...
],
}
```
For the `"cells"` array, we'll need to serialize `Triangle` into an array of 3
integer arrays:
```rust
impl Serialize for Triangle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let vec_indices = vec![self.a, self.b, self.c];
let mut seq = serializer.serialize_seq(Some(vec_indices.len()))?;
for index in vec_indices {
seq.serialize_element(&index)?;
}
seq.end()
}
}
```
I had some trouble serializing the `cgmath::Vector3` to an array, so I made my
own type that wrapped `Vector3` that could be serialized to an array of 3
floats.
```rust
#[derive(Debug)]
pub struct ArraySerializedVector(pub Vector3<f32>);
impl Serialize for ArraySerializedVector {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let values = vec![self.0.x, self.0.y, self.0.z];
let mut seq = serializer.serialize_seq(Some(values.len()))?;
for value in values {
seq.serialize_element(&value)?;
}
seq.end()
}
}
```
And now `Polyhedron` needs to use this new type and implement `Serialize` for
the whole shape to get serialized:
```rust
#[derive(Serialize, Debug)]
pub struct Polyhedron {
pub positions: Vec<ArraySerializedVector>,
pub cells: Vec<Triangle>,
}
```
The actual serialization is done with:
```rust
fn write_to_json_file(polyhedron: Polyhedron, path: &Path) {
let mut json_file = File::create(path).expect("Can't create file");
let json = serde_json::to_string(&polyhedron).expect("Problem serializing");
json_file
.write_all(json.as_bytes())
.expect("Can't write to file");
}
```
On the JS side, the `.json` file can be read and simply fed into either Three.js
or [regl](https://github.com/regl-project/reg) to be rendered in WebGL ([more on
that later](#rendering-in-webgl-with-regl)).
![Regular Icosahedron](/img/blog/icosahedron_colored_1.png)
## Subdivided Icosahedron
Now, we need to take our regular icosahedron and subdivide its faces N number of
times to generate an icosahedron with a detail level of N.
I pretty much copied must of [the subdividing code from
Three.js](https://github.com/mrdoob/three.js/blob/34dc2478c684066257e4e39351731a93c6107ef5/src/geometries/PolyhedronGeometry.js#L90)
directly into Rust.
I won't bore you with the details here, you can find the function
[here](https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/lib.rs#L160-L205).
![Subdivided Icosahedron](/img/blog/icosahedron_colored_3.png)
### Truncated Icosahedron
Now we get to the meat of this project. Transforming an icosahedron into a
hexsphere by
[truncating](https://en.wikipedia.org/wiki/Truncation_%28geometry%29) the points
of the icosahedron into hexagon and pentagon faces.
You can imagine this operation as literally cutting off the points of the
subdivided icosahedron at exactly the midpoint between the point and it's six or
five neighboring points.
![Image of biggest dodecahedron inside
icosahedron](/img/blog/dodecahedron_in_icosahedron.png)
([image source](http://www.oz.nthu.edu.tw/~u9662122/DualityProperty.html))
In this image you can see the regular icosahedron (0 subdivisions) in wireframe
with a yellow shape underneath which is the result of all 12 points truncated to
12 pentagon faces, in other words: the [regular
dodecahedron](https://en.wikipedia.org/wiki/Dodecahedron).
You can see that the points of the new pentagon faces will be the exact center
of the original triangular faces. It should now make sense why truncating a
shape with 20 faces of 3 edges each results in a shape with 12 faces of 5 edges
each. Each pair multiplied still equals 60.
#### Algorithm
There are many different algorithms you could use to generate the truncated
shape, but this is roughly what I came up with:
1. Store a map of every icosahedron vertex to faces composed from that vertex
(`vert_to_faces`).
2. Calculate and cache the [centroid](https://en.wikipedia.org/wiki/Centroid) of
every triangle in the icosahedron (`triangle_centroids`).
3. For every vertex in the original icosahedron:
4. Find the center point between all of centroids of all of the faces for that
vertex (`center_point`). This is essentially the original icosahedron point
but lowered towards the center of the polygon since it will eventually be the
center of a new flat hexagon face.
![hexagon center point in red with original icosahedron faces fanning
out](/img/blog/hexagon_fan.png)
5. For every triangle face composed from the original vertex:
![hexagon fan with selected triangle face in
blue](/img/blog/hexagon_fan_triangle_selected.png)
6. Sort the vertices of the triangle face so there is a vertex `A` in the center
of the fan like in the image, and two other vertices `B` and `C` at the edges
of the hexagon.
7. Find the centroid of the selected face. This will be one of the five or six
points of the new pentagon or hexagon (in brown in diagram below:
`triangleCentroid`).
8. Find the mid point between `AB` and `AC` (points `midAB` and `midAC` in
diagram).
9. With these mid points and the face centroid, we now have two new triangles
(in orange below) that form one-fifth or one-sixth of the final pentagon or
hexagon face. Add the points of the triangle to the `positions` array. Add
the two new triangles composed from those vertices as indexes into the
`positions` array to the `cells` array. We need to compose the pentagon or
hexagon out of triangles because in graphics everything is a triangle, and
this is the simplest way to tile either shape with triangles:
![hexagon fan ](/img/blog/hexagon_fan_construct.png)
10. Go to step 5 until all faces of the icosahedron vertex have been visited.
Save indices to all new triangles in the `cells` array, which now form a
complete pentagon or hexagon face, to the `faces` array.
![hexagons tiling on icosahedron faces](/img/blog/hexagon_tiling.png)
11. Go to step 3 until all vertices in the icosahedron have been visited. The
truncated icosahedron is now complete.
![colored hexsphere of detail level 3](/img/blog/hexsphere_colored_3.png)
#### Code
The `truncate` function calls out to a bunch of other functions, so [here's a
link to the function within the context of the whole
file](https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/lib.rs#L227).
### Calculating Normals
It took me a surprisingly long time to figure out how to compute
[normals](https://en.wikipedia.org/wiki/Normal_(geometry)) for the truncated
icosahedron. I tried just using an out-of-the-box solution like
[angle-normals](https://github.com/mikolalysenko/angle-normals/blob/master/angle-normals.js)
which could supposedly calculate the normal vectors for you, but they came out
all wrong.
![hexsphere with bad normals](/img/blog/bad_hexsphere_normals.png)
So, I tried doing it myself. Most tutorials on computing normal vectors for a
mesh assume that it is tiled in a particular way. But, my algorithm spins around
icosahedron points in all different directions, and so the triangle points are
not uniformly in clockwise or counter-clockwise order.
I could have sorted these points into the correct order, but I found it easier
to instead just detect when the normal was pointing the wrong way and just
invert it.
```rust
pub fn compute_triangle_normals(&mut self) {
let origin = Vector3::new(0.0, 0.0, 0.0);
for i in 0..self.cells.len() {
let vertex_a = &self.positions[self.cells[i].a].0;
let vertex_b = &self.positions[self.cells[i].b].0;
let vertex_c = &self.positions[self.cells[i].c].0;
let e1 = vertex_a - vertex_b;
let e2 = vertex_c - vertex_b;
let mut no = e1.cross(e2);
// detect and correct inverted normal
let dist = vertex_b - origin;
if no.dot(dist) < 0.0 {
no *= -1.0;
}
let normal_a = self.normals[self.cells[i].a].0 + no;
let normal_b = self.normals[self.cells[i].b].0 + no;
let normal_c = self.normals[self.cells[i].c].0 + no;
self.normals[self.cells[i].a] = ArraySerializedVector(normal_a);
self.normals[self.cells[i].b] = ArraySerializedVector(normal_b);
self.normals[self.cells[i].c] = ArraySerializedVector(normal_c);
}
for normal in self.normals.iter_mut() {
*normal = ArraySerializedVector(normal.0.normalize());
}
}
```
### Assigning Random Face Colors
Finally, all that's left to generate is the face colors. The only way I could
figure out how to individually color a shape's faces in WebGL was to pass a
color per vertex. The issue with this is that each vertex of the generated
shapes could be shared between many different faces.
How can we solve this? At the cost of memory, we can just duplicate a vertex
every time it's used by a different triangle. That way no vertex is shared.
This can be done after a shape has been generated with shared vertices.
```rust
pub fn unique_vertices(&mut self, other: Polyhedron) {
for triangle in other.cells {
let vertex_a = other.positions[triangle.a].0;
let vertex_b = other.positions[triangle.b].0;
let vertex_c = other.positions[triangle.c].0;
let normal_a = other.normals[triangle.a].0;
let normal_b = other.normals[triangle.b].0;
let normal_c = other.normals[triangle.c].0;
self.positions.push(ArraySerializedVector(vertex_a));
self.positions.push(ArraySerializedVector(vertex_b));
self.positions.push(ArraySerializedVector(vertex_c));
self.normals.push(ArraySerializedVector(normal_a));
self.normals.push(ArraySerializedVector(normal_b));
self.normals.push(ArraySerializedVector(normal_c));
self.colors
.push(ArraySerializedVector(Vector3::new(1.0, 1.0, 1.0)));
self.colors
.push(ArraySerializedVector(Vector3::new(1.0, 1.0, 1.0)));
self.colors
.push(ArraySerializedVector(Vector3::new(1.0, 1.0, 1.0)));
let added_index = self.positions.len() - 1;
self.cells
.push(Triangle::new(added_index - 2, added_index - 1, added_index));
}
self.faces = other.faces;
}
```
With unique vertices, we can now generate a random color per face with the [rand
crate](https://crates.io/crates/rand).
```rust
pub fn assign_random_face_colors(&mut self) {
let mut rng = rand::thread_rng();
for i in 0..self.faces.len() {
let face_color = Vector3::new(rng.gen(), rng.gen(), rng.gen());
for c in 0..self.faces[i].len() {
let face_cell = &self.cells[self.faces[i][c]];
self.colors[face_cell.a] = ArraySerializedVector(face_color);
self.colors[face_cell.b] = ArraySerializedVector(face_color);
self.colors[face_cell.c] = ArraySerializedVector(face_color);
}
}
}
```
### Binary Serialization
Now that we have to duplicate vertices for individual face colors, the size of
our JSON outputs are getting quite big:
| File | Size |
|---|---|
| icosahedron_r1_d6.json | 28 MB |
| icosahedron_r1_d7.json | 113 MB |
| hexsphere_r1_d5.json | 42 MB |
| hexsphere_r1_d6.json | 169 MB |
Since all of our data is just floating point numbers, we could reduce the size
of the output considerably by using a binary format instead.
I used the [byteorder](https://docs.rs/byteorder/1.3.2/byteorder/) crate to
write out all of the `Vec`s in my `Polyhedron` struct to a binary file in
little-endian order.
The binary format is laid out as:
1. 1 32 bit unsigned integer specifying the number of vertices (`V`)
2. 1 32 bit unsigned integer specifying the number of triangles (`T`)
3. `V` * 3 number of 32 bit floats for every vertex's x, y, and z coordinate
4. `V` * 3 number of 32 bit floats for the normals of every vertex
5. `V` * 3 number of 32 bit floats for the color of every vertex
6. `T` * 3 number of 32 bit unsigned integers for the 3 indices into the vertex
array that make every triangle
The `write_to_binary_file` function which does all that is
[here](https://github.com/thallada/icosahedron/blob/9643757df245e29f5ecfbb25f9a2c06b3a4e1217/src/bin.rs#L13).
That's a lot better:
| File | Size |
|---|---|
| icosahedron_r1_d6.bin | 9.8 MB |
| icosahedron_r1_d7.bin | 11 MB |
| hexsphere_r1_d5.bin | 14 MB |
| hexsphere_r1_d6.bin | 58 MB |
On the JavaScript side, the binary files can be read into `Float32Array`s like
this:
```javascript
fetch(binaryFile)
.then(response => response.arrayBuffer())
.then(buffer => {
let reader = new DataView(buffer);
let numVertices = reader.getUint32(0, true);
let numCells = reader.getUint32(4, true);
let shape = {
positions: new Float32Array(buffer, 8, numVertices * 3),
normals: new Float32Array(buffer, numVertices * 12 + 8, numVertices * 3),
colors: new Float32Array(buffer, numVertices * 24 + 8, numVertices * 3),
cells: new Uint32Array(buffer, numVertices * 36 + 8, numCells * 3),
})
```
### Rendering in WebGL with Regl
I was initially rendering the shapes with Three.js but switched to
[regl](https://github.com/regl-project/regl) because it seemed like a more
direct abstraction over WebGL. It makes setting up a WebGL renderer incredibly
easy compared to all of the dozens cryptic function calls you'd have to
otherwise use.
This is pretty much all of the rendering code using regl in my [3D hexsphere and
icosahedron viewer project](https://github.com/thallada/planet).
```javascript
const drawShape = hexsphere => regl({
vert: `
precision mediump float;
uniform mat4 projection, view;
attribute vec3 position, normal, color;
varying vec3 fragNormal, fragPosition, fragColor;
void main() {
fragNormal = normal;
fragPosition = position;
fragColor = color;
gl_Position = projection * view * vec4(position, 1.0);
}`,
frag: `
precision mediump float;
struct Light {
vec3 color;
vec3 position;
};
uniform Light lights[1];
varying vec3 fragNormal, fragPosition, fragColor;
void main() {
vec3 normal = normalize(fragNormal);
vec3 light = vec3(0.1, 0.1, 0.1);
for (int i = 0; i < 1; i++) {
vec3 lightDir = normalize(lights[i].position - fragPosition);
float diffuse = max(0.0, dot(lightDir, normal));
light += diffuse * lights[i].color;
}
gl_FragColor = vec4(fragColor * light, 1.0);
}`,
attributes: {
position: hexsphere.positions,
normal: hexsphere.normals,
color: hexsphere.colors,
},
elements: hexsphere.cells,
uniforms: {
"lights[0].color": [1, 1, 1],
"lights[0].position": ({ tick }) => {
const t = 0.008 * tick
return [
1000 * Math.cos(t),
1000 * Math.sin(t),
1000 * Math.sin(t)
]
},
},
})
```
I also imported [regl-camera](https://github.com/regl-project/regl-camera) which
handled all of the complex viewport code for me.
It was fairly easy to get a simple renderer working quickly in regl, but I
couldn't find many examples of more complex projects using regl. Unfortunately,
the project looks a bit unmaintained these days as well. If I'm going to
continue with rendering in WebGL, I think I will try out
[Babylon.js](https://www.babylonjs.com/) instead.
### Running in WebAssembly
Since rust can be compiled down to wasm and then run in the browser, I briefly
tried getting the project to run completely in the browser.
The [wasm-pack](https://github.com/rustwasm/wasm-pack) tool made it pretty easy
to get started. My main struggle was figuring out an efficient way to get the
megabytes of generated shape data into the JavaScript context so it could be
rendered in WebGL.
The best I could come up with was to export all of my structs into flat
`Vec<f32>`s and then create `Float32Array`s from the JS side that are views into
wasm's memory.
To export:
```rust
pub fn fill_exports(&mut self) {
for position in &self.positions {
self.export_positions.push(position.0.x);
self.export_positions.push(position.0.y);
self.export_positions.push(position.0.z);
}
for normal in &self.normals {
self.export_normals.push(normal.0.x);
self.export_normals.push(normal.0.y);
self.export_normals.push(normal.0.z);
}
for color in &self.colors {
self.export_colors.push(color.0.x);
self.export_colors.push(color.0.y);
self.export_colors.push(color.0.z);
}
for cell in &self.cells {
self.export_cells.push(cell.a as u32);
self.export_cells.push(cell.b as u32);
self.export_cells.push(cell.c as u32);
}
}
```
And then the wasm `lib.rs`:
```rust
use byteorder::{LittleEndian, WriteBytesExt};
use js_sys::{Array, Float32Array, Uint32Array};
use wasm_bindgen::prelude::*;
use web_sys::console;
mod icosahedron;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
Ok(())
}
#[wasm_bindgen]
pub struct Hexsphere {
positions: Float32Array,
normals: Float32Array,
colors: Float32Array,
cells: Uint32Array,
}
#[wasm_bindgen]
pub fn shape_data() -> Result<Array, JsValue> {
let radius = 1.0;
let detail = 7;
let mut hexsphere = icosahedron::Polyhedron::new_truncated_isocahedron(radius, detail);
hexsphere.compute_triangle_normals();
let mut unique_hexsphere = icosahedron::Polyhedron::new();
unique_hexsphere.unique_vertices(hexsphere);
unique_hexsphere.assign_random_face_colors();
unique_hexsphere.fill_exports();
let positions = unsafe { Float32Array::view(&unique_hexsphere.export_positions) };
let normals = unsafe { Float32Array::view(&unique_hexsphere.export_normals) };
let colors = unsafe { Float32Array::view(&unique_hexsphere.export_colors) };
let cells = unsafe { Uint32Array::view(&unique_hexsphere.export_cells) };
Ok(Array::of4(&positions, &normals, &colors, &cells))
}
```
With wasm-pack, I could import the wasm package, run the `shape_data()`
function, and then read the contents as any other normal JS array.
```javascript
let rust = import("../pkg/index.js")
rust.then(module => {
const shapeData = module.shape_data()
const shape = {
positions: shapeData[0],
normals: shapeData[1],
colors: shapeData[2],
cells: shapeData[3],
}
...
})
```
I could side-step the issue of transferring data from Rust to JavaScript
entirely by programming literally everything in WebAssembly. But the bindings
from rust wasm to the WebGL API are still way too complicated compared to just
using regl. Plus, I'd have to implement my own camera from scratch.
### The Stats
So how much faster is Rust than JavaScript in generating icosahedrons and
hexspheres?
Here's how long it took with generating shapes in JS with Three.js in Firefox
versus in native Rust with a i5-2500K 3.3 GHz CPU.
| Shape | JS generate time | Rust generate time |
|---|---|---|
| Icosahedron detail 6 | 768 ms | 28.23 ms |
| Icosahedron detail 7 | 4.25 s | 128.81 ms |
| Hexsphere detail 6 | 11.37 s | 403.10 ms |
| Hexsphere detail 7 | 25.49 s | 1.85 s |
So much faster!
### Todo
* Add a process that alters the shape post-generation. Part of the reason why I
decided to fan the hexagon faces with so many triangles is that it also allows
me to control the height of the faces better. This could eventually allow me
to create mountain ranges and river valleys on a hexsphere planet. Stretching
and pulling the edges of the polygon faces in random directions could add
variation and make for a more organic looking hexsphere.
* Conversely, it would be nice to be able to run a process post-generation that
could reduce the number of triangles by tiling the hexagons more efficiently
when face elevation isn't needed.
* Add parameters to the generation that allows generating sections of the
hexsphere / icosahedron. This will be essential for rendering very detailed
polyhedrons since at a certain detail level it becomes impossible to render
the entire shape at once.
In WebGL, figure out what part of the shape is in the current viewport and
pass these parameters to the generation.
* Render the shapes in a native Rust graphics library instead of WebGL. I'm
curious how much slower WebGL is making things.
* Parallelize the generation. Right now the generation is very CPU bound and
each subdivide/truncate iteration is mostly independent from each other, so I
think I could get some decent speed-up by allowing the process to run on
multiple cores. Perhaps the [rayon](https://github.com/rayon-rs/rayon) crate
could make this pretty straightforward.
* Find some way to avoid unique vertices. The size of the shape is *much* bigger
because of this. There might be a way to keep shared vertices while also
having a separate color per face by using texture mapping.
* In the renderer, implement face selection (point and click face and show an
outline around selected face).
* In the renderer, implement fly-to-face zooming: given a face, fly the camera
around the sphere in an orbit and then zoom in on the face.