Last session, we started to look at Data Trees and saw how they are structured and how they are formed. In this session, we will look at why Data Trees exist and what kinds of things they are useful for.
Example 6: Attractor Points
Attractor points are a useful technique when you want a particular quality to change with distance from a set of control points. To demonstrate their use we will look at a simple example – dividing up a surface and then controling the scale of the resultant panels with a set of attractor points.
First, we’ll need a surface to work from. In Rhino, draw a surface using the SrfPt command (or any other means of creating a surface – this approach should work with any surface, even 3d ones). Add several points using the Point or Points command. I will use three, but the way we will set up the definition will be flexible enough that we can use any number. These will be our attractor points.
In Grasshopper, our first act will be to reference this surface so that we can use it. Add a Surface parameter component from Params/Geometry and set it to reference our base surface. Similarly with points and curves we have referenced in previous examples this is done by right-clicking on the component and then selecting ‘Set One Surface’.
We will now divide up this surface. This is fairly simple to do but probably requires some additional explanation. To extract each sub-surface we will use the Isotrim component (Surface/Util). This can be used to extract a section of a surface specified by a two-dimensional domain.
In Rhino, curves and surfaces all posess a certain parametric domain. For example, a curve might have a domain from 0 to 50, where the domain extents represent the start and end points of the curve respectively. Any point along the curve can then be specified by a curve parameter (typically referred to as t) that lies within that domain.
Surfaces, by extension, have a two dimensional domain and any point on them can be found by specifying two parameters (typically called u and v).
The upshot of all this is that we can divide up our surface by first of all dividing up its domain and then plugging that into our Isotrim component to extract the surface designated by each sub-domain. We will divide up the domain using the Divide Domain² component (Maths/Domain). This takes in a two-dimensional domain as its first input, I – we can automatically extract the domain of our surface simply by plugging our surface directly into this input (Grasshopper is clever like that). Connect an integer Number Slider to both U and V to control the division number, then plug our surface and divided subdomain list into the Isotrim component (using S and D inputs, respectively).
We should end up with a list of smaller sub-surfaces generated from our initial surface.
Next we will turn our attention to our attractor points. Reference our three points using a Point parameter component (Params/Geometry) – right click and select ‘Set Multiple Points’ then select the points we created earlier.
Each of our attractor points will have a minimum and maximum range of effect. To make the definition easier to use and understand later, we will draw circles to graphically represent these ranges. Add two Number Sliders, one for maximum and one for minimum range. Then use two Circle components (Curve/Primitive) to draw circles around each point with the radiuses set to those ranges. The circle components’ P inputs ask for planes, but we can simply plug in our attractor points and Grasshopper will automatically convert them for us (again, Grasshopper is clever like that).
In order to decide the amount by which to scale each panel we need to know the distance between said panel and the closest of our attractor points. More specifically, we need to know the distance between the centroid of each panel and each of our attractors. We can find the centroid of each sub-surface by plugging our surface list into an Area component (Surface/Analysis) – the second output, C, will give us the centroid point.
We will find the distance between points using the Distance component (Vector/Point). Add one and plug our surface centroids into A and our attractor points into B. However, simply doing this will not quite give us the result that we want. If we give this component 100 surface centroids and 3 attractor points then we will get out 100 distance values. However, if we were getting the distance from each centroid to each point (which is what we want) we should be getting 300 distance values out.
What is happening is that when we plug two lists of points into A and B is that Grasshopper’s data matching routine is kicking in and finding the distance between pairs of points selected based on their position in the list. So the first centroid is being matched with the first attractor, the second centroid with the second attractor and then every subsequent centroid with the third (last) attractor.
In order to find the distance between each centroid and each attractor point we will need to alter this behaviour – this is where data trees come in. Data trees can override the standard data matching behaviour because when a data stream is in a tree each branch in that tree will be considered separately.
To achieve this, we are going to ‘Graft’ our list of centroids into a data tree. Grafting will take each item in a list and give it its own branch in the tree. For example, if we graft a flat list of 100 points, we will end up with a tree with 100 branches, each of which contains a single point. If we graft our centroids, then each centroid will sit in its own branch and when we put that into our Distance component each branch will be processed separately with our list of attractor points and the centroid inside will therefore be matched to all three attractor points in that list, giving us three different distance values per centroid.
We can graft using the Graft Tree component (Sets/Tree), but alternatively (and usually more conveniently) we have the option to graft inputs and outputs of other components. Right click on the S output of our Isotrim component and toggle on ‘Graft’.
Note also the ‘Flatten’ option, which is the inverse of Graft – i.e. it will take a data tree and turn it into a ‘flat’ list.
This will put each of our sub-surfaces into a separate tree branch. Our centroids will then keep the same structure. If you check the output of the Distance component you should now see (if you have 100 centroids and 3 attractors) 300 different distance values divided into 100 sub-lists/branches of 3 (here shown in a Panel).
We had to find the distance from each centroid to all of our attractors to make sure that each attractor was being considered, but really all we are interested in (in this case) is the closest attractor to each centroid – i.e. the minimum distance value in each of our sub-lists.
To find the minimum in each list we will pass our distance values through a Bounds component (Maths/Domain) in order to build a Domain around the extents of each sub-list and then through a Deconstruct Domain component (also Maths/Domain) to extract the minimum value (from the S output). Note that this is why I chose to Graft the sub-surfaces (and hence their centroids) rather than the attractor points – if we had done it the other way around we would instead be getting just the distance from each attractor point to the closest centroid instead of the distance from each centroid to the closest attractor.
Now that we have our minimum distance values, we need to map these to the range of values we want to scale our panels to. This will relate to our inner and outer radiuses. We want our transition zone to be between the inner and outer radius of each attractor – inside the minumum radius and outside of the maximum one we want the scale to be constant. To accomplish this we will limit our distance values using our minumum and maximum radiuses – i.e. if the distance is less than the minimum, we will replace it with the minimum and if it is greater than the maximum we will replace it with the maximum. We will do this by chaining up a Maximum component (Maths/Util) with the S output of our Deconstruct Domain component plugged into A and the value of our ‘Minimum’ radius slider plugged into B, followed by a Minimum component taking in the result of the Maximum and the value of our ‘Maximum’ slider.
These two components will act to ‘cap’ our distance values to the minimum and maximum values we have decided upon using the sliders.
Now we need to re-map those distance values to the range 0-1 depending on their proportional distance between the minimum and maximum radiuses. Mathematically, the formula to do this would be:
Scale Value = (Distance – Minimum Radius)/(Maximum Radius – Minimum Radius)
We could set this up using the simple maths components from Maths/Operators, but instead we will use the Remap Numbers component (Maths/Domain), which is set up to do the exact same thing for us. The first input on this component, V, is the values we want to re-map – give it the capped distance values coming out of the Minimum component.
The next, S, is the source domain – i.e. the range of numbers that we are mapping from. This will be the domain bounded by our maximum and minimum radius, so plug those sliders into the A and B inputs of a Construct Domain component (Maths/Domain) to convert them into the form of a Domain and then plug the result into S. The final input, T, is the target domain – i.e. the range of numbers that we want to map to. This should default to 0 to 1, so leave it as it is (you could however alter it if you wished to change the range of scales that we end up with).
This should give you a set of numbers which all lie between 0 and 1. We will now use those as our scaling factors for each surface panel. Add a Scale component (Transform/Affine). The Geometry to scale will be our subsurfaces from our IsoTrim component, the Centre(s) of Scaling will be our centroid points from the Area component and the Scaling Factor will be our set of remapped distance values that we just calculated.
You should now see your sub-surfaces which fall inside your maximum radius circle gradually shrinking around each attractor point until they disapper completely within the minimum radius circle. You may need to turn off the preview on some of the earlier components (select the components, right click -> ‘Preview Off’) in order to see this clearly.
A couple of things to note:
- The sub-surfaces, centroid points and remapped distance values are all in exactly the same tree structure, which allows the centroids and scale factors to be matched back up with the surfaces that they originate from at the end of the definition – this is why we grafted the subsurfaces output rather than the centroids.
- The scale component will go red and show an error. This is because we are putting in scale factors of zero. This means that a valid surface cannot be created and will be replaced with a null value in the output. Ordinarily this might be something to worry about, but in this case it is exactly the behaviour that we want and so you don’t need to worry about the ‘error’, though if we were taking the output surfaces and doing something else with them then we might want to pass them through a Clean Tree component (Sets/Tree) to remove all the nulls.
- This definition will work with any surface (including 3D ones – though they may not be as buildable!) and any number of attractors.
- We could actually have used a Closest Point component in order to find the distance to the closest attractor point for each centroid, but that wouldn’t have been as fun (or demonstrated anything about trees).
- If we wanted the opposite effect (i.e. solid panels inside the minimum radius and none outside the maximum) then all we need to is flip around the minimum and maximum sliders as they plug into the Construct Domain component:
The example files for this session can be downloaded here: