Step 1: Set up a list of points
Before we can start calculating surfaces between peak points, we need to assemble the list of points we’re going to work with.
The most straightforward way to do this is to specify the X, Y, and Z coordinate of each peak point. This gives you full control over exactly where each point is located, and only requires a single node per point.
A second method — which is probably more likely in a holistic architectural design workflow — is to consider the rest of the building mass while assembling the list of roof peaks.
You wouldn’t think so by looking only up at the Elbphilharmonie’s roof, but this building’s footprint is surprisingly regular: four straight edges arranged in a trapezoid.
Let’s look past the fact that this particular project’s footprint was an existing warehouse building to remain, and consider that you might want the ability to change the building perimeter at some time in the future, and watch your roof geometry adjust itself accordingly.
Notice that most — all but one, in fact — of the Elbphilharmonie’s peaks are located at the building perimeter. We can start by drawing four lines that represent the building footprint, and then use them to host any peak points that should live on the building edge. Constraining the points so that they follow these lines builds a level of intelligence about the design intent into our own Elbphilharmonie Dynamo script.
So, rather than setting each peak’s X and Y coordinates individually, we use the Curve.PointAtParameter node to locate most of the perimeter points with a decimal that describes how far along the line it should go — 0 being at the very beginning, 1 being at the very end. Then we move all of these points up to the Z coordinates of our choice with the Geometry.Translate node.
The whole workflow looks like this:
Both of these methods are included in the Dynamo file for you to download and explore.
Step 2: Build the first roof section
According to our roadmap, we’ll pick the four peak points we’d like to bound this roof section, connect them with lines, find their mid-points and push them down in the Z direction, calculate perimeter arcs, and generate surfaces.
Step 3: Build the next roof section
We can replicate the same process for drawing arcs between points as we did in the last step, but since the remaining roof sections will all be triangular, we only need three peak points per section instead of four. There are many, many techniques for having Dynamo gather the lists of three points for each section of roof — some that require some careful (and manual) arrangement of points, and plenty of others that require far more intense programming chops than the average architect has.
Here’s a method that probably strikes a good balance between the two: we can have Dynamo take one point that hasn’t yet been incorporated into the roof, find the nearest arc that has been incorporated into the roof, and then consider the start point and end point of the arc (now we have three points!) to calculate the three-sided surface. Here is the sequence of nodes that finds the nearest arc to a point, then extracts its endpoints resulting in a list of three points:
And here’s the next sequence that constructs one triangular roof section from that list of three points:
If you’d like, you can now copy-paste this group of nodes a bunch more times (one for each section of roof you’d like to model), reconnecting each node group’s output from Arc.ByThreePoints to the next node group’s input to the Flatten node.
But what happens when you want to add or remove peak points to the design? You’ll have to come back and modify this portion of the Dynamo script to accommodate every time the number of points changes. It’s important to build scripts that scale properly as your project develops, so instead of ending this post with instructions to copy-paste large chunks of your graph…
Step 4: Turn the previous step into a Loop
Now for one of my favorite nodes in Dynamo: LoopWhile. Anytime I work with LoopWhile, I start by creating a custom node that built in a very specific way.
First, the node needs to be built so that it accepts the entire list of inputs that it needs to iterate through. So, even though we have a workflow that only considers one peak point at a time, we need to anticipate that after it finishes working with the first step, it will progress to the second step — so rather than providing just a single point as the input, we build the custom node to accept the entire list of points, then pick the first point in the list to work on.
Next, the custom node we build needs to be completely self-contained, such that when several of them are strung together one after another like in the image below, the outcome automatically develops one step further. The node will need to have the same exact set of outputs as it has inputs, and constructed so that the outputs update and structure whatever data is going to be needed for the next iteration.
Here’s a look inside the custom node:
A few things to note:
- Our list of points enters the node and is immediately split into two parts: we get the first point (List.FirstItem), which the function will use to calculate the roof section. The other part of the list (List.RestOfItems) removes the first point, because it’s setting up the list of points that will be used for the next section of roof, and is then routed directly to the Output labeled “Points”.
- After the roof Surface and list of Arcs are both generated, we add them to the ends of the input lists. Unlike our list of points, which we want to reduce by 1 each time the node runs, we want to see our list of roof surfaces grow after each run (it’s why we built this script in the first place!). If we fed the newly-calculated Surface or Arcs straight to the node’s output, we’d only send the current iteration to the next iteration, and we’d lose all of the iterations before this one.
- The number that sets the depth of the arcs doesn’t change from iteration to iteration, so it is pretty much routed straight from Input to Output, while providing the information it needs to the Translate node, the one place it’s needed.
After making sure the custom node runs properly in sequence, we need to make one more adjustment to get it ready to use with LoopWhile. Instead of having four inputs, we should only have one.
The solution is to pack all of our input lists up into a single, nested list with List.Create. Once the nested list flows into our custom node, the we can adjust the node to unpack the data with GetItemAtIndex, re-creating the same streams of data as if they came from their own inputs.
We’ll apply the same logic to the end of the custom node’s function as well — rather than routing data to multiple outputs, we pack the data back up into a single nested list that matches the data structure coming in.
Now we’re ready to connect our highly-tailored custom node to LoopWhile.
LoopWhile is going to run the function connected to the loopBody input over and over and over again. Each time a run is about to take place, it performs whatever function is hooked up to continueWhile, looking for a Boolean value. If the answer returned by this continueWhile function is false, the loopBody will run, and then perform the continueWhile function again with its output. As soon as LoopWhile encounters a true value, the loop ends, and displays whatever data came out of the function during its last iteration.
So, in this case, we’re telling LoopWhile to run our custom node function until the first item in our nested list, which is our list of peak points, is empty.
The init input is the data we’d like to feed our custom node for the first iteration, and for each following iteration it will use the data that came out of the last run.
And now it’s time to play! Experiment with the form by adding, delecting, and changing the locations of the peak points, and changing the depths of the arcs between them.