Building Plant Models
Overview
This guide explains how to build complete plant geometries by combining reference meshes with transformations and associating them with nodes in a MultiScaleTreeGraph (MTG) structure. PlantGeom makes it easy to create realistic 3D plant models by leveraging reference meshes for different organ types and applying appropriate transformations.
Geometry in MTG Nodes
In PlantGeom, 3D geometries are attached to MTG nodes through the :geometry
attribute. Each node's geometry is represented by a Geometry
object, which typically contains:
- A reference to a
RefMesh
that defines the base shape - A transformation that positions, scales, and orients the mesh
# The core Geometry type
PlantGeom.Geometry(;
ref_mesh::RefMesh, # The reference mesh to use
transformation=Identity(), # Transformation to apply to the reference mesh (here, no transformation)
)
Geometry
also has the dUp
and dDwn
arguments for appyling tapering to the geometry (i.e. make it pointy), but it is rarely used in practice, and is there for backward compatibility with the OPF file format. It also has the mesh
field, which is used to store the mesh resulting from applying the transformation to the reference mesh. This one is lazily computed because we prefer not store it unless we really have to.
Creating Plant Organ Geometries
The typical workflow for building a plant geometry is:
- Create reference meshes for each organ type (e.g., stem, leaf, root)
- Traverse the MTG structure
- For each node, create a Geometry with the appropriate reference mesh and transformation
Step 1: Create Reference Meshes
First, define a reference mesh for each type of plant organ:
using PlantGeom
using Meshes, Colors
# Create a cylinder reference mesh for internodes and roots, oriented towards the z direction
cylinder = Meshes.CylinderSurface(Meshes.Point(0.0, 0.0, 0.0), Meshes.Point(0.0, 0.0, 1.0), 0.5) |>
Meshes.discretize |> Meshes.simplexify
refmesh_cylinder = RefMesh("Cylinder", cylinder, RGB(0.5, 0.5, 0.5))
# RGB(0, 0.5, 0), RGB(0, 0.5, 0.1), RGB(0.1, 0.5, 0)]
# Make a reference mesh for the leaves:
w = l = 1 # Leaf width and length are set to 1.0 to facilitate transformations
p = 0.2 * l # Petiole length is set to 20% of the leaf length
points = [
(0.0, -0.05 * w, 0.0), # Petiole base 1
(0.0, 0.05 * w, 0.0), # Base 2. NB: the petiole base width is 5% of the leaf width
(p, 0.0, 0.0), # End of petiole, first point of leaf blade
(p + l, 0.0, 0.0), # tip of the leaf
(p + l / 2.0, -w / 2.0, 0.0),
(p + l / 2.0, w / 2.0, 0.0)
]
connec = Meshes.connect.(
[
(1, 2, 3), # Petiole
(3, 5, 4), # left part of the leaf
(3, 6, 4) # right part of the leaf
],
Triangle
)
refmesh_leaf = RefMesh("Leaf", Meshes.SimpleMesh(points, connec), RGB(0.1, 0.5, 0))
RefMesh{String, SimpleMesh{𝔼{3}, CoordRefSystems.Cartesian3D{CoordRefSystems.NoDatum, Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}, Vector{Meshes.Point{𝔼{3}, CoordRefSystems.Cartesian3D{CoordRefSystems.NoDatum, Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}}}, SimpleTopology{Connectivity{Triangle, 3}}}, ColorTypes.RGB{Float64}, Vector{Meshes.Vec{3, Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}}, Nothing}("Leaf", 3 SimpleMesh, Meshes.Vec{3, Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}[Vec(0.0 m, 0.0 m, -1.0 m), Vec(0.0 m, 0.0 m, -1.0 m), Vec(0.0 m, 0.0 m, -1.0 m), Vec(0.0 m, 0.0 m, -1.0 m), Vec(-0.0 m, 0.0 m, 1.0 m), Vec(0.0 m, 0.0 m, -1.0 m)], nothing, RGB{Float64}(0.1, 0.5, 0.0), false)
Step 2: Make a plant graph
For this example, we'll create a very simple plant that has only one meristem going upward, producing one internode and one leaf per internode, and one root meristem going downward.
using MultiScaleTreeGraph
function build_mtg(n_internode=5, n_roots = 3)
# Starting with the plant scale:
mtg = Node(NodeMTG("/", "Plant", 1, 1))
# Then, adding internodes and leaves recursively:
last_node = mtg
for i in 1:n_internode
internode = Node(last_node, NodeMTG(i == 1 ? "/" : "<", "Internode", i, 2))
Node(internode, NodeMTG("+", "Leaf", i, 2)) # Branching with a leaf
last_node = internode
end
# And finally, adding the roots:
last_root = mtg
for i in 1:n_roots
last_root = Node(last_root, NodeMTG(i == 1 ? "/" : "<", "RootSegment", i, 2))
end
return mtg
end
# Now let's use our function to create a plant:
mtg = build_mtg()
/ 1: Plant
├─ / 2: Internode
│ ├─ + 3: Leaf
│ └─ < 4: Internode
│ ├─ + 5: Leaf
│ └─ < 6: Internode
│ ├─ + 7: Leaf
│ └─ < 8: Internode
│ ├─ + 9: Leaf
│ └─ < 10: Internode
│ └─ + 11: Leaf
└─ / 12: RootSegment
└─ < 13: RootSegment
└─ < 14: RootSegment
Step 3: Associate Geometries with MTG Nodes
Next, traverse the MTG and assign the appropriate geometry to each node:
using MultiScaleTreeGraph
import TransformsBase: → # The → operator from TransformsBase composes transformations
using Rotations
function add_geometry!(mtg, refmesh_cylinder, refmesh_leaf)
# Track the current height for positioning internodes
current_height = 0.0
internode_width = 0.1
internode_length = 0.3
root_width = 0.05
root_length = 0.5
root_depth = -0.5 # Start below ground
phyllotaxy = 0.0
traverse!(mtg) do node
if symbol(node) == "Internode"
# Scale and position the internode
transformation = Meshes.Scale(internode_width, internode_width, internode_length) →
Meshes.Translate(0.0, 0.0, current_height)
# Attach geometry to this node
node.geometry = PlantGeom.Geometry(ref_mesh=refmesh_cylinder, transformation=transformation)
# Update the height for the next internode
current_height += internode_length
phyllotaxy += π/2
elseif symbol(node) == "Leaf"
# Scale, rotate, and position the leaf
leaf_length = 0.20 + 0.10*current_height
leaf_width = 0.5 * leaf_length
transformation = Meshes.Scale(leaf_length, leaf_width, 1e-6) →
Meshes.Rotate(RotY(-π/4)) → # Give an insertion angle to the leaf
Meshes.Translate(internode_width/2.0, 0.0, current_height) → # Position on the stem
Meshes.Rotate(RotZ(phyllotaxy))
# Attach geometry to this node
node.geometry = PlantGeom.Geometry(ref_mesh=refmesh_leaf, transformation=transformation)
elseif symbol(node) == "RootSegment"
# Scale and position the root (going downward)
transformation = Meshes.Scale(root_width, root_width, root_length) →
Meshes.Translate(0.0, 0.0, root_depth) →
Meshes.Rotate(RotZ(π)) # Point downward
# Attach geometry to this node
node.geometry = PlantGeom.Geometry(ref_mesh=refmesh_cylinder,
transformation=transformation)
# Update the depth for the next root segment
root_depth -= root_length
end
end
end
add_geometry! (generic function with 1 method)
Step 4: Visualize the Plant
We can build a plant and compute the geometry of each node using the previous two functions:
mtg = build_mtg()
add_geometry!(mtg, refmesh_cylinder, refmesh_leaf)
And finally, we can visualize our plant:
using PlantGeom, CairoMakie # or GLMakie for interactive 3D, or WGLMakie for web
# Visualize the plant
fig = Figure()
ax = Axis3(fig[1, 1], aspect=:data)
viz!(ax, mtg)
fig
Working with Direct Meshes
In some cases, you may want to use the transformed mesh directly rather than reference meshes. This is particularly useful for complex or unique organ shapes such as grass leaves. In this case, you can provide the mesh as a reference mesh:
# Direct mesh approach
complex_leaf_mesh = load_mesh("complex_leaf.obj")
# During MTG traversal:
node.geometry = PlantGeom.Geometry(ref_mesh=complex_leaf_mesh)
By default, Geometry
uses the Identity
transformation, which means no transformation.
Tips for Building Realistic Plant Geometries
- Understand mesh orientation: Know the default orientation of your reference meshes to apply transformations correctly
- Use transformation composition: The
→
operator allows clean composition of multiple transformations - Scale appropriately: Make sure organ scales match realistically with each other
- Phyllotaxy patterns: Implement botanical phyllotaxy patterns (opposite, alternate, whorled, etc.)
- Calibrate transformations: You may need to experiment with transformation parameters to get realistic positioning
Conclusion
By combining reference meshes with appropriate transformations, you can build complex and realistic 3D plant models efficiently. The use of reference meshes saves memory and computational resources while still allowing for detailed and visually appealing plant representations.
More examples
You can look at VPalm.jl to get an idea of a more complex 3D reconstruction of a plant using sequential architectural allometries.