Arcol's Parametric Geometry Engine
Modern buildings can be intricate, sophisticated structures. At Arcol we know that scalability is a crucial aspect of what we do. It's a challenge to build a compute-heavy web application that stays snappy with large assets.
We've been spending some time designing a system that can efficiently generate geometry and render it as quickly as possible. We are still in the process of integrating the new system into our main app, but the preliminary performance tests are promising. Seems like a good topic for our first technical blog post!
Arcol's geometry engine is designed for parametric modeling. This is different from direct modeling where users can only build a static mesh in its final form. With parametric modeling, the user constructs each geometric element with a series of discrete modeling operations. In this non-destructive modeling regime, the entire construction history is preserved.
To see this in action with a contrived example, try playing with the sliders in the vignette below. Each slider represents an input parameter in the parametric model.
Directed Acyclic Graph
In Arcol's geometry engine, the modeling history is not internally represented by a simple undo stack, but rather an editable graph. Each modeling operation is a node. Meshes and sketches can flow along the edges between nodes. This edge data is eagerly evicted from memory after the engine executes each node. We pay close attention to this because web applications have a limited memory budget.
The next vignette depicts a portion of the graph underpinning the example building. Each wall is generated from an edge in a floor plan sketch. Additionally, one of the walls,
Wall 3, consumes a cutting region from
Window 1. Since
Wall 3 has more than one incoming edge, the modeling graph is a directed acyclic graph (DAG) rather than a tree.
The model parameters are depicted with color coded semicircles.
The diagram is simplified in several ways. It includes only four building elements and omits all the lower level modeling operations (thicken, extrude, etc.) that comprise finer-grained graphs within each building element.
You might be wondering what kind of data the graph emits when it is done executing. At the time of this writing, the only products are triangle meshes and transformation matrices. The matrices are arranged into a scene graph hierarchy. In the future, we will likely flow BIM data though the DAG as well. For example, a list of building materials could be accumulated.
Note that Arcol's geometry engine is actually juggling several graph data structures: an element-level DAG for the entire building, the low-level modeling graphs within each element, and a transformation hierarchy!
Each time the user modifies a parameter, or adds a new modeling operation, the engine responds by executing the graph, generating new geometry, and refreshing the canvas. For snappy performance, it executes only the minimum but sufficient portion of the entire graph.
To expound further on this, when the user edits a parameter, the response of the engine roughly looks like this:
- Determine the set of visible meshes that are affected by the modified parameter.
- Gather all nodes between the modified parameter and each affected mesh.
- Perform a topological sort of the nodes; i.e. list them in dependency order.
- Iterate through the node list and invoke each one.
- Triangulate the polygon meshes that were produced by the graph.
- Send the results to WebGL.
The first three steps in the above process are similar to how a software build tool works, such as Make or esbuild.
Web workers provide another mechanism for improving performance with large assets. They allow the UI thread to stay buttery smooth even when the computational workload exceeds our "frame budget" of 16 milliseconds (60 fps).
The engine's web worker was important to enable early on, because it affected how we designed its API. Communication with workers can only be achieved with message passing. When the user changes a parameter or a performs a modeling operation, the UI layer sends a JSON snippet over to the engine worker.
When the engine is done executing the graph, it posts a reply message containing all newly generated mesh data. This might sound like a lot of data to be sending over the wire, but we store all mesh data in contiguous memory regions using typed arrays. We also use transferables so that the data doesn't need to be copied.
Occasionally we get asked why we don't just license an existing geometry kernel. The simple answer is that we're building a collaborative, web-first product, and we want to build BIM awareness into the kernel.
But, perhaps the most important reason of all: we think we can do a better job! One of our heroes, Evan Wallace of Figma, once pointed out that performance and quality are features that you only notice when they aren't there, but they can make all the difference.