Skip to content

Commit d2ed696

Browse files
committed
Add how to guide on using the slicer
1 parent 5a35bbf commit d2ed696

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ makedocs(;
4747
"Multiple materials/colors" => "howto/Materials.md",
4848
"Advanced traversal" => "howto/Traversal.md",
4949
"Absolute coordinates" => "howto/Coordinates.md",
50-
"Creating light sources" => "howto/LightSources.md"
50+
"Creating light sources" => "howto/LightSources.md",
51+
"Using the slicer" => "howto/Slicer.md"
5152
],
5253
"API" => [
5354
"Graphs" => "api/graphs.md",

docs/src/howto/Slicer.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Slicing a mesh by planes
2+
3+
Alejandro Morales
4+
5+
Centre for Crop Systems Analysis - Wageningen University
6+
7+
In this example we will learn how to use the slicer functionality to modify existing meshes
8+
according to specific slicing planes. The idea of the slicer is to cut a mesh by one or
9+
more planes, generating a new version of the mesh such that none the triangles intersect
10+
with the planes. For example, if we use multiple horizontal planes (to define layers in the
11+
soil or aboveground) this wil result in any triangle in the new mesh belonging to only one
12+
layer. The slicer can be used when modeling plant-environment interactions, since the physics
13+
of the environment will often be simulated in layers or voxels, though the user is free to
14+
apply in other contexts.
15+
16+
To introduce the use of the slicer, we will first create a simple mesh and then slice it
17+
using a single plane. A more detailed example will be provided in the next section.
18+
19+
## Slicing a rectangle with a single plane
20+
21+
Let's start by creating a simple rectangle.
22+
23+
```julia
24+
using VirtualPlantLab
25+
r = Rectangle(length = 1.0, width = 1.0)
26+
```
27+
28+
We can check that the rectangle is implemented as two triangles:
29+
30+
```julia
31+
ntriangles(r)
32+
```
33+
34+
Now we will slice it by a horizontal plane at 0.5 units above the origin. We will use the
35+
function `slice!` and specify the plane as `Z = 0.5`:
36+
37+
```julia
38+
slice!(r, Z = 0.5)
39+
```
40+
41+
We can check that the rectangle has been sliced into six triangles (each of the original
42+
triangles has been split into three):
43+
44+
```julia
45+
ntriangles(r)
46+
```
47+
48+
The slicer adds a property `slices` to the mesh, which indicates to which layer or voxel
49+
each triangle belongs to:
50+
51+
```julia
52+
properties(r)[:slices]
53+
```
54+
55+
As you can see, each triangle has been assigned three numbers, corresponding to the their
56+
position relative to slicing planes perpendicular to the X, Y and Z axis respectively. A
57+
`0` implies no actual slicing in a particular dimension (in this case both `X` and `Y`).
58+
For `Z` axis, we have a `1` for the triangles below the slicing plane (first layer) and a
59+
`2` for the triangles above the plane (second layer).
60+
61+
To visualize the results, let's color each triangle according to the layer it belongs to:
62+
63+
```julia
64+
import ColorTypes: RGB
65+
colors = [RGB(1, 0, 0), RGB(0, 1, 0)]
66+
all_colors = RGB{Float64}[]
67+
for slice in properties(r)[:slices]
68+
push!(all_colors, colors[slice[3]])
69+
end
70+
add_property!(r, :colors, all_colors)
71+
```
72+
73+
And now we can render the results:
74+
75+
```julia
76+
import GLMakie
77+
render(r)
78+
```
79+
80+
## Slicing a rectangle with multiple planes
81+
82+
Let's repeat the previous example but now we slice the rectangly with multiple planes along
83+
the `Y` and `Z` axes. Note how we can pass an array of values to the `slice!` function:
84+
85+
```julia
86+
r = Rectangle(length = 1.0, width = 1.0)
87+
Ycuts = collect(-0.25:0.25:0.5)
88+
ny = length(Ycuts)
89+
Zcuts = collect(0.25:0.25:1)
90+
nz = length(Zcuts)
91+
slice!(r, Y = Ycuts, Z = Zcuts);
92+
```
93+
94+
We now have a large number of triangles:
95+
96+
```julia
97+
ntriangles(r)
98+
```
99+
100+
We can visualize the results as before, assigning a random color to each pixel that results
101+
from the intersection of the slicing planes. Notice that now we have to use the second and
102+
third elements of the `slice` array to identify the exact location of the triangle in the
103+
grid of pixels:
104+
105+
106+
```julia
107+
colors = rand(RGB, ny, nz)
108+
all_colors = RGB{Float64}[]
109+
for slice in properties(r)[:slices]
110+
push!(all_colors, colors[slice[2], slice[3]])
111+
end
112+
add_property!(r, :colors, all_colors)
113+
render(r)
114+
```
115+
116+
## Slicing geometry inside the `feed!` method
117+
118+
Using the slicer inside a `feed!` method is straightforward, though it requires some
119+
adjustments. Firstly, we do not know a priori the number of triangles that will result from
120+
the slicing, so we need to adjust the way we store colors or optical properties. Secondly,
121+
we cannot add a geometric primitive directly to the turtle, as that will prevents use from
122+
applying the slicer to it. Instead, we need to create a local version of the primitive,
123+
slice it, add any properties we want to store (adjusting to the actual number of triangles)
124+
and then feed it to the turtle using the more generic `Mesh!()`.
125+
126+
Let's illustrate this with an example reusing the rectangle we created before. This time
127+
the rectangle represents the geometry of a node in a graph (e.g. a leaf of a plant or a soil
128+
tile).
129+
130+
As usual, we create a module to contain the data structure and associated methods:
131+
132+
```julia
133+
module MyRectangle
134+
using VirtualPlantLab
135+
import ColorTypes: RGB
136+
137+
# Object that will be rendered as rectangle
138+
struct Tile <: Node
139+
length::Float64
140+
width::Float64
141+
colors::Matrix{RGB{Float64}}
142+
end
143+
144+
function VirtualPlantLab.feed!(turtle::Turtle, t::Tile, data)
145+
# Create the rectangle
146+
r = Rectangle(turtle, length = t.length, width = t.width)
147+
# Slice it (YCUTS and XCUTS are global variables, could be in data too)
148+
slice!(r, Y = YCUTS, Z = ZCUTS)
149+
# Add the colors (the colors per pixel are stored in the node)
150+
all_colors = RGB{Float64}[]
151+
for slice in properties(r)[:slices]
152+
push!(all_colors, t.colors[slice[2], slice[3]])
153+
end
154+
# Add the mesh to the turtle while defining the colors as properties
155+
Mesh!(turtle, r, colors = all_colors)
156+
return nothing
157+
end
158+
159+
# Define the cuts and functions to modify them
160+
YCUTS::Vector{Float64} = collect(-0.25:0.25:0.5)
161+
set_ycuts!(cuts) = (global YCUTS = cuts)
162+
ZCUTS::Vector{Float64} = collect(0.25:0.25:1)
163+
set_zcuts!(cuts) = (global ZCUTS = cuts)
164+
end
165+
```
166+
167+
Let's now create a simple static graph and make sure that we can generate the rectangular
168+
tile:
169+
170+
```julia
171+
import .MyRectangle as MR
172+
nz = length(MR.ZCUTS)
173+
ny = length(MR.YCUTS)
174+
graph = Graph(axiom = MR.Tile(1.0, 1.0, rand(RGB, ny, nz)))
175+
render(Mesh(graph))
176+
```

0 commit comments

Comments
 (0)