Skip to content

Commit bb7bd30

Browse files
committed
Add how to guide on getting absolute geometry
1 parent 9f204b1 commit bb7bd30

2 files changed

Lines changed: 292 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ makedocs(;
4545
"Setting up a grid cloner" => "howto/GridCloner.md",
4646
"Messages in scenes" => "howto/Message.md",
4747
"Multiple materials/colors" => "howto/Materials.md",
48-
"Advanced traversal" => "howto/Traversal.md"
48+
"Advanced traversal" => "howto/Traversal.md",
49+
"Absolute coordinates" => "howto/Coordinates.md"
4950
],
5051
"API" => [
5152
"Graphs" => "api/graphs.md",

docs/src/howto/Coordinates.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# Retrieving absolute coordinates
2+
3+
Alejandro Morales
4+
5+
Centre for Crop Systems Analysis - Wageningen University
6+
7+
In VPL, a turtle graphics approach is used to generate 3D geometry from graphs. In this
8+
approach, geometry is generated locally relative to the current position and orientation
9+
of the so-called turtle (inside `feed!()` methods specialized for each type of node by
10+
the user). However, sometimes it is required to obtain the absolute coordinates and
11+
orientation of a mesh in the scene. For example, the user may want to assign nitrogen
12+
levels to a leaf based on its absolute position inside the canopy by assuming a
13+
particular canopy nitrogen profile. Or we may want to know the angle of a branch with
14+
respect to the horizontal plane (e.g., when studying gravitropism).
15+
16+
In this little guide we will show how to extract this information from the turtle inside
17+
the `feed!()` method so that users can make use of this information. We will also show
18+
how to retrieve the absolute coordinates of the triangles both from the turtle, a mesh
19+
or a scene.
20+
21+
## Extracting state of the turtle
22+
23+
The turtle is defined by its position and three axes: `arm`, `up` and `head`. The
24+
directions defined by these vectors correspond to the `width`, `height` (if present) and
25+
`length` of geometry primitives. These geometry primitives are generated in front of the
26+
turtle, starting at its current position. If a geometry primitive is added to the turtle
27+
using the argument `move = true`, the turtle will move a distance `length` along the
28+
`head` axis. From this information one can retrieve several properties of the generated
29+
geometry (e.g., its center, orientation, etc.).
30+
31+
To retrieve the state of the turtle we use the methods `pos`, `arm`, `up` and `head`.
32+
Let's illustrate this with a modified version of the [Tree tutorial](https://virtualplantlab.com/dev/tutorials/from_tree_forest/tree/) where we will
33+
calculate the location and inclination angle (with respect to the horizontal plane) of
34+
each leaf.
35+
36+
Let's start with the definition of the types required to build a tree:
37+
38+
```julia
39+
using VirtualPlantLab
40+
using ColorTypes: RGB
41+
import GLMakie
42+
using Plots
43+
import Random: seed!
44+
import Statistics: mean
45+
using StatsPlots
46+
47+
module TreeTypes
48+
import VirtualPlantLab as VPL
49+
# Meristem
50+
struct Meristem <: VPL.Node end
51+
# Bud
52+
struct Bud <: VPL.Node end
53+
# Node
54+
struct Node <: VPL.Node end
55+
# BudNode
56+
struct BudNode <: VPL.Node end
57+
# Internode (needs to be mutable to allow for changes over time)
58+
Base.@kwdef mutable struct Internode <: VPL.Node
59+
length::Float64 = 0.10 # Internodes start at 10 cm
60+
end
61+
# Leaf
62+
Base.@kwdef mutable struct Leaf <: VPL.Node
63+
length::Float64 = 0.20 # Leaves are 20 cm long
64+
width::Float64 = 0.1 # Leaves are 10 cm wide
65+
height::Float64 = 0.0 # Height of the center of the leaf
66+
angle::Float64 = 0.0 # Angle of the leaf with respect to the horizontal plane
67+
end
68+
# Graph-level variables
69+
Base.@kwdef struct treeparams
70+
growth::Float64 = 0.1
71+
phyllotaxis::Float64 = 140.0
72+
leaf_angle::Float64 = 30.0
73+
branch_angle::Float64 = 45.0
74+
end
75+
end
76+
import .TreeTypes
77+
```
78+
79+
We now define the `feed!()` methods for each type of node. Inside these methods we will
80+
calculate the absolute height and inclination angle of each leaf and store it inside the
81+
corresponding leaf node
82+
83+
```julia
84+
function VirtualPlantLab.feed!(turtle::Turtle, i::TreeTypes.Internode, vars)
85+
# Rotate turtle around the head to implement elliptical phyllotaxis
86+
rh!(turtle, vars.phyllotaxis)
87+
HollowCylinder!(turtle, length = i.length, height = i.length/15, width = i.length/15,
88+
move = true, colors = RGB(0.5,0.4,0.0))
89+
return nothing
90+
end
91+
92+
# Create geometry + color for the leaves
93+
function VirtualPlantLab.feed!(turtle::Turtle, l::TreeTypes.Leaf, vars)
94+
# Rotate turtle around the arm for insertion angle
95+
ra!(turtle, -vars.leaf_angle)
96+
# Extract the position of the turtle and the head vector
97+
t_pos = pos(turtle)
98+
t_head = head(turtle)
99+
# The center of the leaf is length/2 in front of the turtle
100+
center = t_pos .+ 0.5*l.length*t_head
101+
l.height = center[3] # Height is the z-coordinate of the center
102+
# The inclination angle of the leaf is the same as the zenith angle of the up vector
103+
# This is given by the arc-cosine of the vertical component
104+
l.angle = acos(up(turtle)[3])*180/π # Convert to degrees
105+
l.angle = l.angle > 90 ? 180.0 - l.angle : l.angle # Correct for the angle being > 90
106+
# Now we generate the leaf
107+
Ellipse!(turtle, length = l.length, width = l.width, move = false,
108+
colors = RGB(0.2,0.6,0.2))
109+
# Rotate turtle back to original direction
110+
ra!(turtle, vars.leaf_angle)
111+
return nothing
112+
end
113+
114+
# Insertion angle for the bud nodes
115+
function VirtualPlantLab.feed!(turtle::Turtle, b::TreeTypes.BudNode, vars)
116+
# Rotate turtle around the arm for insertion angle
117+
ra!(turtle, -vars.branch_angle)
118+
end
119+
```
120+
121+
We can see that the location and inclination of the leaf is calculated from the turtle's
122+
state inside the `feed!()` method and store in the leaf node as a side effect. This is
123+
important as we will not have updated information about the leaves until the scene is
124+
generated. Let's now add the rules for the tree growth (we ignore the more complex
125+
bud break rule that was used in the original tutorial and just break each bud with a
126+
probability of 25% assuming an uniform distribution):
127+
128+
```julia
129+
meristem_rule = Rule(TreeTypes.Meristem,
130+
rhs = mer -> TreeTypes.Node() +
131+
(TreeTypes.Bud(), TreeTypes.Leaf()) +
132+
TreeTypes.Internode() + TreeTypes.Meristem())
133+
branch_rule = Rule(TreeTypes.Bud,
134+
lhs = bud -> rand() <= 0.25,
135+
rhs = bud -> TreeTypes.BudNode() + TreeTypes.Internode() + TreeTypes.Meristem())
136+
axiom = TreeTypes.Internode() + TreeTypes.Meristem()
137+
```
138+
139+
We add a function to grow the internodes over time:
140+
141+
```julia
142+
function elongate!(tree)
143+
query = Query(TreeTypes.Internode)
144+
for x in apply(tree, query)
145+
x.length = x.length*(1.0 + data(tree).growth)
146+
end
147+
end
148+
```
149+
150+
And a query to extract the position and inclination of the leaves:
151+
152+
```julia
153+
function leaf_info(tree)
154+
query = Query(TreeTypes.Leaf)
155+
heights = Float64[]
156+
angles = Float64[]
157+
for l in apply(tree, query)
158+
push!(heights, l.height)
159+
push!(angles, l.angle)
160+
end
161+
return heights, angles
162+
end
163+
```
164+
165+
We can now grow the tree:
166+
167+
```julia
168+
function growth!(tree)
169+
elongate!(tree)
170+
rewrite!(tree)
171+
end
172+
```
173+
174+
And a simulation for `n` steps is achieved with a simple loop that returns the final
175+
tree and the heights and angles of the leaves throughout the simulation:
176+
177+
```julia
178+
function simulate(n)
179+
# Initialize the tree
180+
tree = Graph(axiom = axiom, rules = (meristem_rule, branch_rule),
181+
data = TreeTypes.treeparams())
182+
# Run simulation
183+
for i in 1:n
184+
growth!(tree)
185+
end
186+
Scene(tree) # Generate the scene to trigger feed!() methods
187+
heights, angles = leaf_info(tree)
188+
return tree, heights, angles
189+
end
190+
```
191+
192+
We can now run the simulation:
193+
194+
```julia
195+
seed!(123456789);
196+
tree, heights, angles = simulate(25);
197+
```
198+
199+
We can check how the final tree looks like:
200+
201+
```julia
202+
render(Scene(tree))
203+
```
204+
205+
And we can plot the distribtuion of leaf heights and angles:
206+
207+
```julia
208+
length(heights)
209+
density(heights, bandwidth = 1, trim = true)
210+
density(angles, bandwidth = 5, trim = true)
211+
```
212+
213+
We can see that the distribution of leaves over height is not uniform but rather there is
214+
a higher density of leaves towards the middle of the tree. This is an emergent pattern of
215+
the developmental rules and growth parameters defined in the model. Similarly, the angle
216+
distribution is not uniform bur rather skewed towards more vertical leaves. This is a
217+
result of the insertion angles of leaves and branches defined in the model.
218+
219+
Note that in this example we are calculating the center and inclination angle of the
220+
leaf explicitly from the turtle's state. This is possible because the shape of the leaf
221+
is relatively simple. A more general approach is to extract the mesh from the scene and
222+
calculate the center and orientation of the leaf from the triangles. We will show how to
223+
do this in the next section.
224+
225+
## Extracting triangles
226+
227+
In VPL, all geometry is represented by triangular meshes. A mesh may be created by an
228+
user by calling any of the primitive constructors within VPL (e.g., `Rectangle!()`) or
229+
by any alternative code that generates a mesh and added using the `Mesh!()` method. This
230+
means that the turtle will internally store a single triangular mesh that combines all
231+
the geometry generated so far. This will be passed on to the scene and (when relevant)
232+
merged with other meshes.
233+
234+
One possible approach is to generate the mesh inside the `feed!()` method without adding
235+
it to the turtle, extracting all the information needed and then adding the mesh to the
236+
turtle. In order to implement this we just need to modify the `feed!()` method for the
237+
leaves as follows:
238+
239+
```julia
240+
# Create geometry + color for the leaves
241+
function VirtualPlantLab.feed!(turtle::Turtle, l::TreeTypes.Leaf, vars)
242+
# Rotate turtle around the arm for insertion angle
243+
ra!(turtle, -vars.leaf_angle)
244+
# We generate the leaf without adding it to the turtle -> just remove the "!"
245+
# And don't include colors or materials
246+
e = Ellipse(turtle, length = l.length, width = l.width, move = false)
247+
# Compute the center of the leaf
248+
verts = vertices(e) # Extract all vertices (vector of vertices)
249+
zs = getindex.(verts, 3) # Extract z-coordinate of each vertex
250+
l.height = mean(zs) # Average height of the leaf
251+
# Compute the inclination angle of the leaf (zenith of normal = inclination of plane)
252+
n = normals(e)[1] # All triangles will have the same normal so one suffices
253+
l.angle = acos(n[3])*180/π
254+
l.angle = l.angle > 90 ? 180.0 - l.angle : l.angle # Correct for the angle being > 90
255+
# Add the leaf to the turtle (important to do transform = false, deepcopy = false)
256+
Mesh!(turtle, e, colors = RGB(0.2,0.6,0.2), transform = false, deepcopy = false)
257+
# Rotate turtle back to original direction
258+
ra!(turtle, vars.leaf_angle)
259+
return nothing
260+
end
261+
```
262+
263+
We can now run the simulation:
264+
265+
```julia
266+
seed!(123456789);
267+
tree, heights2, angles2 = simulate(25);
268+
```
269+
270+
And confirm that we get the same tree:
271+
272+
```julia
273+
render(Scene(tree))
274+
length(angles) == length(angles2)
275+
```
276+
277+
The heights are the same:.
278+
279+
```julia
280+
density(heights2, bandwidth = 1, trim = true, label="Triangles")
281+
density!(heights, bandwidth = 1, trim = true, label = "Turtle")
282+
```
283+
284+
The angles are also the same (makes sense since the normals of the triangles are the same
285+
as the up vector of the turtle):
286+
287+
```julia
288+
density(angles2, bandwidth = 5, trim = true, label="Triangles")
289+
density!(angles, bandwidth = 5, trim = true, label = "Turtle")
290+
```

0 commit comments

Comments
 (0)