Skip to content

Developer's Overview

Carl Davidson edited this page Dec 11, 2017 · 14 revisions

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

Quickstart

Tectonics.js is a completely static web app - no server required! Tectonics.js also abstains from webdev tools like Node, NPM, and Gulp, so if you want to run Tectonics.js locally, simply clone the repo and load up "./index.html" in the browser of your choice:

git clone https://github.com/davidson16807/tectonics.js.git
chrome tectonics.js/index.html

Code for Tectonics.js resides in three folders under the project's root directory:

  • precompiled
  • postcompiled
  • noncompiled

Virtually all model code resides in the "noncompiled" folder. You can edit code under this folder just as you would any Javascript.

The "precompiled" and "postcompiled" folders are only meant for high-performance utility code and graphics shaders. If you want to edit code under these folders, you'll have to run a Makefile. This Makefile takes Javascript from the "precompiled" folder and transforms it to what you see under the "postcompiled" folder. To run the Makefile, go to the root project folder and type:

make

High Level Overview

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 only includes small, stateless functions that can be used in circumstances outside 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.

Raster

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.

Raster functions

Functions within the raster layer all have a common format:

Namespace.raster_function(raster1, raster2, result){
    result = result || Float32Raster(raster1.grid);

    ASSERT_IS_ARRAY(raster1, Float32Array)
    ASSERT_IS_ARRAY(raster2, Float32Array)
    ASSERT_IS_ARRAY(result, Float32Array)

    ...
    return result
}

Let's break it down:

  • Namespace is a CamelCase name that indicates the raster data type and paradigm, for instance: BinaryMorphology, ScalarField, or VectorRasterImageAnalysis.

  • raster_function is a underscore_delimited name that indicates the operation to perform, for instance: add_field, or average

  • result is an optional parameter that will store the return value of the function, if passed. The reason we use this parameter is namely due to performance. For performance reasons, all rasters are implemented as Javascript TypedArrays. TypedArrays can be very performant for read/write operations, but they can be very slow to create. To avoid creating an unnecessary TypedArray, we allow the developer to pass a TypedArray that already exists. Usually, you don't need to worry about passing a value to the result parameter, but sometimes it can be really helpful while writing performant code.

  • ASSERT_IS_ARRAY is a macro we can use to perform type checking on code during development. In production, a flag is set that turns off type checking for performance reasons. Please note: it is a macro, not a function! Tectonics.js comes with a Makefile that passes Javascript from the "precompiled" subfolder to a cpp preprocessor, which turns it into proper, browser-readable code under the "postcompiled" folder.