Digital Trees
This tree is in my backyard, right behind our cabana. Go ahead, move it around and I’ll explain below how an iPhone + claude + plus some cool geometry math made this happen.
I’ve always been fascinated by trees. I love how they grow in seemingly random ways, yet also obey a fixed set of rules. They form the backdrop of so many of my favorite places, always telling a story about the soil they are in and the geography around them.
In building a 3-D model of my house, I had to figure out some way to model the trees in my yard. Two things make a full model of a tree hard. First, it’s not feasible (and stupid) to get up 40 ft and do a laser scan of every branch. Second, a native scan would be huge and not useful in an architecture model.
This turns out to be a mixed-fidelity problem. You can build a mesh pretty easily, using an AI-powered tool like TRELLIS or Meshy to generate an actual mesh, resulting in hundreds of thousands of triangles without a lot of associated meaning. At the top of the tree, that may be perfectly fine. The canopy needs to be dimensionally correct and visually plausible, but the higher you go, the less it matters where each branch is located.
At the base of the tree, though, it matters a lot. A major split in the trunk, the direction of a large branch, or a limb extending over a building I’m working on can be important. The bottom of the tree needs to be real, while the top just needs to look right and be close to the correct height.
Ultimately, Revit and then rendering tools like Enscape or Trellis need the right fidelity, not maximum fidelity. And the most useful representation may not be a mesh at all. It may be a set of nodes, connections, dimensions, and rough geometry.
The ideal geometry is just a bunch of connected, tapered cylinders. Turns out someone has thought of this before the the internet is full of tree making programs. Enter Quantitative Structure Models, or QSMs. Instead of storing a tree as a giant triangle mesh, a QSM represents the trunk and branches as a connected graph of tapered cylinders: each segment has a start point, end point, radius, and parent-child relationship. That gives us exactly the right data structure for this problem because the measured lower part of the tree can remain geometrically accurate, while the unmeasured upper branches can be generated by Claude. This both looks accurate, but renders fast.
The iPhone and SiteScape can get me a good point cloud. No top of the tree, not even a mesh, just a bunch of dots and only observed at my height and below.

The approach that works is measurement-first, inference-last. A phone/terrestrial scan of a backyard tree only captures the lower 5–6 m, so the pipeline splits the problem: everything the scanner saw is reconstructed and locked, and everything above the scan ceiling is grown by a AI with a constraint engine rather than invented freehand.

To get this right we want to give Claude all the data with an open-source tool (AdTree) that automatically reconstructs a tree’s branch skeleton and geometry from a laser-scanned point cloud. The constraints are: species (cedar elm), no central leader, sympodial forking, scaffolds arching outward), a photo-derived crown envelope, a surveyed height (46 ft by yardstick and ChatGPT), pipe-model taper at every fork, and a tapered-cantilever beam check that rejects branches that couldn’t carry their own weight. The LLM’s job is proposing and tuning those constraints and deciding which branches matter, not drawing geometry.
Forestry research tools (TreeQSM, AdTree, TreeAIBox, 3DFin) are mature at turning point clouds into cylinder models, but they reconstruct everything for biomass estimation: thousands of cylinders, collapsing radii where the scan is sparse. They are awesome, and mostly built to reconstruct a forest. My architectural models need the opposite: a few hundred faces with believable structure. So the pipeline deliberately inserts a decimation step (keep scaffolds, drop twigs) and re-derives upper radii from the trustworthy dense-zone measurements instead of the QSM’s guesses. Everything below ran locally: the reconstruction chain on a Mac (AdTree built from source, Blender via MCP for geometry), with CloudCompare’s Windows build adding TreeIso, 3DFin and TreeAIBox as a second, independent QSM to cross-check the first.

The output is pretty cool. I get a real tree that is watertight, parametric, computationally simple and structurally similar to the actual tree.

AdTree (Tech Details)
I took a look under the hood to see how AdTree works. It’s pretty cool.
AdTree converts a single-tree point set \( P=\lbrace\mathbf{p}_{i}\rbrace_{i=1}^{N}\subset\mathbb{R}^{3}\) into a rooted skeletal graph rather than reconstructing a surface directly. It first identifies points likely to lie on major branches from locally stable point density and contracts them toward branch centerlines using mean-shift. It then constructs a 3-D Delaunay proximity graph \(G_D=(V,E_D)\), assigns each edge the Euclidean cost \(\ell_{ij}=\lVert\mathbf{p}_{i}-\mathbf{p}_{j}\rVert_{2}\), and applies Dijkstra’s algorithm from the tree base to form what the paper calls a minimum spanning tree—more precisely, a rooted single-source shortest-path tree embedded in the cloud. Each skeletal vertex receives an importance \(W(v)=\sum_{e\in T_v}\ell_e\), defined as the total length of the edges in its descendant subtree, while each edge receives the mean importance of its two endpoints; low-importance basal artifacts are then pruned. Degree-two vertices are removed using a Douglas–Peucker-type test \(\alpha=d/r\leq\sigma\), where \(d\) is the vertex’s distance from the parent–child chord and \(r\) is a local edge scale, implemented using the parent-edge radius. At bifurcations, two child vertices are eligible to be merged when \(\alpha=\min\left(\ell_1\sin\theta/r_2,\ell_2\sin\theta/r_1\right)\leq\sigma\), with the replacement vertex positioned at \(\mathbf{p}_{\mathrm{new}}=(W_1\mathbf{p}_1+W_2\mathbf{p}_2)/(W_1+W_2)\). In the source implementation, merging is additionally restricted to child edges separated by an angle of approximately \(26^\circ \) or less and having lengths within a factor of two. This procedure repeatedly contracts redundant graph structure while attempting to preserve the tree’s major branching topology.
Geometry is then attached to the simplified skeleton by fitting generalized cylinders. Points near the well-sampled lower trunk are retrieved using a spatial or \(k\)-d-tree query. The trunk-cylinder parameters—an axis unit vector \(\mathbf{a}\), a point \(\mathbf{c}\) on the axis, and a radius \(r\)—are estimated by Levenberg–Marquardt minimization of the radial residual:
$$
e_i(\mathbf{a},\mathbf{c},r)= \left\lVert(\mathbf{p}_{i}-\mathbf{c})-\left[(\mathbf{p}_{i}-\mathbf{c})\cdot\mathbf{a}\right]\mathbf{a}\right\rVert_{2}-r,\qquad \min_{\mathbf{a},\mathbf{c},r}\sum_i e_i^2.
$$
AdTree then performs a second, robustified fit using \(q_i=1-\lvert e_i\rvert/\max_j\lvert e_j\rvert\). In the source implementation, \(q_i\) multiplies the residual passed to the least-squares solver, so the implemented objective is \(\sum_i(q_i e_i)^2\), reducing the influence of distant outliers. Because crown points are generally too sparse and noisy for reliable independent cylinder fitting, the remaining edge radii are inferred allometrically from subtree importance. The paper states the linear relationship \(r_e=r_t(W_e/W_t)\), where \(r_t\) and \(W_t\) belong to the fitted trunk; the source implementation instead uses \(r_e=r_t(W_e/W_t)^{1.1}\). After skeleton smoothing, generalized cylinders with varying endpoint radii are constructed along the skeletal paths and triangulated into the final OBJ mesh. Leaves are synthesized near terminal branches rather than reconstructed from LiDAR. You can think of AdTree as a graph extraction + topology-preserving simplification + partial geometric fitting + allometric completion algorithm, not a neural or direct point-to-mesh method.
I had to take this and make a hybrid technique. First, I derive all parameters diameter at breast height (DBH) is fitted by RANSAC circle regression during stem inventory; the crown envelope, branching rules, and mechanical limits come from a species template scaled to the input height; and growth seed points are computed from the measured skeleton rather than authored by hand. A single orchestrator (maketree.py) executes the nine stages as subprocesses of the repository’s stage scripts, prints a per-stage status line with wall-clock time, and halts on any gate failure. On current hardware (Apple M-series) a five-million-point scan completes in roughly twelve minutes, of which the AdTree skeletonization stage accounts for approximately nine.
The design principle is measure where the scan is dense, infer where it is not, and let physics referee the boundary. Below 3.05 m (10 ft), where point density is highest, the trunk union is meshed directly from an occupancy grid by marching cubes, no cylinder abstraction at all, because a multi-stem base is a fluted solid that swept tubes cannot represent. Above that, AdTree’s skeleton is reduced to a canonical branch graph, repaired (single-rootedness, child-to-parent attachment gap ≤ 5 mm, cloud-support pruning of spurious stubs), and extended to the surveyed height by a constrained grower whose proposals must survive a tapered-cantilever beam check and a slenderness floor \( r ≥ L/90 \) before emission. The completed skeleton is exported in TreeQSM’s cylinder interchange format, deliberately reusing the Inverse Tampere Blender add-on rather than a bespoke mesher, and the tube crown and fitted base are fused by a single voxel remesh, which yields one watertight component and makes the seam, pinch, and open-cap defect classes structurally impossible rather than individually patched.





















