Skip to content

Developer's Overview

Carl Davidson edited this page Oct 23, 2017 · 14 revisions

This is a general overview of the model for people who want to develop for it.

Tectonics.js is organized into several layers:

Layer Description
"Raster" layer implements fundamental data structures used throughout to the model: the raster, the grid, and the voronoi sphere. This layer contains functions that can be used in many circumstances that may not have anything to do with modeling, like vector calculus and image analysis.
"Academic" layer takes raster code and uses it to express the academic aspects of the model. This layer separates academic concerns (like how to model something mathematically) from architectural concerns (like how a model is represented in object-oriented programming). It's composed entirely of small, stateless, static functions that usually rely heavily on math.
"Model" layer takes academic code and ties it together into object-oriented classes
"View" layer takes the model and represents it visually in some way

Each layer is able to use the layers below it, e.g. the "view" layer can directly invoke code from the "model" layer, the "academic" layer, and the "raster" layer, as it sees fit. In addition, the javascript used to handle the html has it's own model/view distinction using Vue.js, but this is completely unrelated to the model and view layers mentioned above.

Let's start by describing the most fundamental data structures in the raster layer: the raster, the grid, the fibonacci sphere and the voronoi sphere

Grid

A grid is a representation of a surface using an array of interconnected, equidistant vertices. In theory, a grid could represent the surface of any 3d object: a sphere, a cube, a donut, anything. For practical purposes, though, tectonics only uses the grid class for spheres.

FibonacciSphere

More specifically, the model uses spherical fibonacci grids.

What is a Fibonacci grid? A Fibonacci grid is an approximation for a grid that is formed from an arbitrary number of roughly equidistant vertices. Pass it any positive number and you'll get a mesh back containing that exact number of vertices for each hemisphere. Every vertex in a fibonacci grid is roughly equidistant to its neighbors, no matter how many vertices there are. This makes it ideal for us, because we really want equidistant vertices.

How does it do this? Vertices start out at the meridian and are spaced at regular intervals along the z axis. With that, each vertex is rotated away from its predecessor by a certain angle. That angle is 360 degrees divided by the [golden ratio].

It might seem strange why this would produce equidistant vertices. For a gentle (and fascinating) introduction as to why this algorithm works, I highly recommend this Vihart video.

Raster

A raster takes ever vertex on a grid and maps a value to it. Any number of data types can be used for the value - it can be a vector, a float, or an integer. In practice, Tectonics.js only implements rasters for vectors, float32s, uint8s, and uint16s.

We can manipulate rasters using many different operational paradigms. We can perform vector calculus, binary morphology, image analysis, etc. We don't want to implement all this functionality in a single class because it violates separation of concerns. We don't care about vector calculus when we're writing code for binary morphology, so why should we store their code in the same place? Nor do we want to implement all this functionality across multiple raster subclasses, because we have to switch between these paradigms effortlessly, and we don't want to constantly coerce rasters to different subclasses.

So we split functionality across several namespaces. Each namespace provides functions that operate on a single "raster" data structure.

Rasters are simple data structures. I want them to be almost like primitive objects. They're really just TypedArrays with an extra "grid" attribute that represents the grid they work with. I don't use multi-dimensional array because most raster operations amount to doing the same operation over many vertices, without having to look up values for neighboring vertices. This sort of stuff can be easily done with a single dimensional array - all it requires is a single for loop.

VoronoiSphere

Given a raster and a point in space, we want to very quickly identify the value that appears at that point in the raster. This is one of the most fundamental operations in our model. There are normally 10,000 vertices in a standard grid cell, and they each perform this operation multiple times per frame, so we could easily be doing this operation millions of times per second. It's really important to get right. So how do we do this?

We could write some complex function that maps xyz coordinates back to a vertex id in a fibonacci grid but 1.) that's hard, 2.) that's probably slow, and 3.) that doesn't work with non-fibonacci grids.

We could write a 3d hash map that directly maps xyz coordinates to vertex ids. This would be very fast, but in practice it consumes too much memory and it takes too long to generate. It's O(n^3) complexity, where n is our hash map's resolution.

Since we already work exclusively with spheres, we could write a 2d hash map that maps lat-long coordinates to vertex ids. We'd first map xyz to lat-long, then map lat-long to vertex id. This is a smart approach, but conversion to lat-long involves sine() and cosine(), and these are very slow functions.

So instead, what Tectonics.js does is use a 6 sided cube-sphere as its hash map. We first map xyz to one of the six sides of a cube. If z is positive and is the largest of the xyz coordinates, then we map to xy coordinates on the +z side. Then, we plug the xy coordinates into the +z hash map, and this gives us vertex id.

It's important to note this approach only works well for spheres. Grid is design to work for any 3d surface, so our Grid class can't depend on VoronoiSphere directly. Instead, we use the inversion of control pattern - we pass a function to Grid that generates a VoronoiSphere given a list of vertices, then Grid invokes that function and stores the return value as its hash map.

Clone this wiki locally