Messages in scene
In VPL it is possible to generate different scenes from the same graph or collection of graphs. This is achieved by passing a message to the feed!
methods when creating the Scene
object. A message is any object (of any type) assigned to the keyword argument message
inside any Scene()
method. Within a feed!
method, the message can be from the turtle object (first argument of the method call) as turtle.message
(assuming the object is named turtle
). Examples of cases when messages can be used include:
Differentiating between geometry needed for visualization and for ray tracing (e.g. we may want to include roots in the visualization but not in the ray tracing).
Changing the color associated to some geometry (e.g. we may want to color the leaves of a plant based on the amount of light they receive).
Changing the geometry based on the message (e.g. we may want a higher level of realism for visualization that for ray tracing to reduce computational costs).
Let's illustrate how to use messages with a simple example. We will modify the Tree tutorial to allow for visualizing. Below is all the code for the tree model excluding the feed!
methods
using VirtualPlantLab
using ColorTypes
import GLMakie
# Data types
module TreeTypes
import VirtualPlantLab
# Meristem
struct Meristem <: VirtualPlantLab.Node end
# Bud
struct Bud <: VirtualPlantLab.Node end
# Node
struct Node <: VirtualPlantLab.Node end
# BudNode
struct BudNode <: VirtualPlantLab.Node end
# Internode (needs to be mutable to allow for changes over time)
Base.@kwdef mutable struct Internode <: VirtualPlantLab.Node
length::Float64 = 0.10 ## Internodes start at 10 cm
end
# Leaf
Base.@kwdef struct Leaf <: VirtualPlantLab.Node
length::Float64 = 0.30 ## Leaves are 20 cm long
width::Float64 = 0.2 ## Leaves are 10 cm wide
end
# Graph-level variables
Base.@kwdef struct treeparams
growth::Float64 = 0.1
budbreak::Float64 = 0.25
phyllotaxis::Float64 = 140.0
leaf_angle::Float64 = 30.0
branch_angle::Float64 = 45.0
end
end
# Rules
meristem_rule = Rule(TreeTypes.Meristem, rhs = mer -> TreeTypes.Node() +
(TreeTypes.Bud(), TreeTypes.Leaf()) +
TreeTypes.Internode() + TreeTypes.Meristem())
function prob_break(bud)
# We move to parent node in the branch where the bud was created
node = parent(bud)
# We count the number of internodes between node and the first Meristem
# moving down the graph
check, steps = has_descendant(node, condition = n -> data(n) isa TreeTypes.Meristem)
steps = Int(ceil(steps/2)) ## Because it will count both the nodes and the internodes
# Compute probability of bud break and determine whether it happens
if check
prob = min(1.0, steps*graph_data(bud).budbreak)
return rand() < prob
# If there is no meristem, an error happened since the model does not allow
# for this
else
error("No meristem found in branch")
end
end
branch_rule = Rule(TreeTypes.Bud,
lhs = prob_break,
rhs = bud -> TreeTypes.BudNode() + TreeTypes.Internode() + TreeTypes.Meristem())
# Graph
axiom = TreeTypes.Internode() + TreeTypes.Meristem()
tree = Graph(axiom = axiom, rules = (meristem_rule, branch_rule), data = TreeTypes.treeparams())
# Growth functions
getInternode = Query(TreeTypes.Internode)
function elongate!(tree, query)
for x in apply(tree, query)
x.length = x.length*(1.0 + data(tree).growth)
end
end
function growth!(tree, query)
elongate!(tree, query)
rewrite!(tree)
end
# Simulation
function simulate(tree, query, nsteps)
new_tree = deepcopy(tree)
for i in 1:nsteps
growth!(new_tree, query)
end
return new_tree
end
There are three types of nodes that require geometry: Leaf
, Internode
, and BudNode
, though the latter only adds the insertion angle for the branches. In the example below we will use the message to select whether to visualize the leaves or not. This would be useful if we want to visualize the branching structure of a tree with a dense canopy (to make it more meaningful we make the leaves bigger than in the original example). Note how, even when we don't generate internodes we still need to modify the state of the turtle to ensure the correct positioning of the leaves.
# Insertion angle for the bud nodes
function VirtualPlantLab.feed!(turtle::Turtle, b::TreeTypes.BudNode, vars)
# Rotate turtle around the arm for insertion angle
ra!(turtle, -vars.branch_angle)
end
# Create geometry + color for the internodes
function VirtualPlantLab.feed!(turtle::Turtle, i::TreeTypes.Internode, vars)
# Rotate turtle around the head to implement elliptical phyllotaxis
rh!(turtle, vars.phyllotaxis)
# Generate the internode or move the turtle forward
if turtle.message == "leaves"
f!(turtle, i.length)
else
HollowCylinder!(turtle, length = i.length, height = i.length/15, width = i.length/15,
move = true, colors = RGB(0.5,0.4,0.0))
end
return nothing
end
# Create geometry + color for the leaves
function VirtualPlantLab.feed!(turtle::Turtle, l::TreeTypes.Leaf, vars)
# Bypass geometry if only internodes are being rendered
turtle.message == "internodes" && return nothing
# Rotate turtle around the arm for insertion angle
ra!(turtle, -vars.leaf_angle)
# Generate the leaf
Ellipse!(turtle, length = l.length, width = l.width, move = false,
colors = RGB(0.2,0.6,0.2))
# Rotate turtle back to original direction
ra!(turtle, vars.leaf_angle)
return nothing
end
We can now generate a simulation:
newtree = simulate(tree, getInternode, 15)
For a visualization of both leaves and internodes we can actually leave the message empty given how the code above is structured:
mesh = Mesh(newtree);
render(mesh, axes = false)
For only leaves:
mesh = Mesh(newtree, message = "leaves");
render(mesh, axes = false)
And for only internodes:
mesh = Mesh(newtree, message = "internodes");
render(mesh, axes = false)