Skip to content

Commit 6e47f84

Browse files
committed
Add guide on how to create a light source
1 parent bb7bd30 commit 6e47f84

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ makedocs(;
4646
"Messages in scenes" => "howto/Message.md",
4747
"Multiple materials/colors" => "howto/Materials.md",
4848
"Advanced traversal" => "howto/Traversal.md",
49-
"Absolute coordinates" => "howto/Coordinates.md"
49+
"Absolute coordinates" => "howto/Coordinates.md",
50+
"Creating light sources" => "howto/LightSources.md"
5051
],
5152
"API" => [
5253
"Graphs" => "api/graphs.md",

docs/src/howto/LightSources.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
**Redo tutorial using a tiled floor, we should be able to see spatial distribution better**
2+
3+
4+
# Constructing light sources
5+
6+
Alejandro Morales
7+
8+
Centre for Crop Systems Analysis - Wageningen University
9+
10+
## What makes a light source?
11+
12+
In VPL, a light (or radiation) source is responsible for generating rays that will be
13+
traced throughout a 3D scene as they interact with surfaces. A ray is composed of three
14+
components: a direction, an origin, and a vector of radiant power.
15+
16+
Both the origin and the direction of a ray are generated randomly by the light source
17+
according to specific distributions, whereas the radiant power per ray and the total number
18+
of rays is determined by the user during the simulation. Therefore, defining a new type of
19+
light source consists of defining the distributions for the origin and direction of the rays.
20+
21+
The origin of the rain is determined by the geometry component of a light source (field
22+
`geom` of the type `Source`). VPL currently supports the following geometry components:
23+
24+
- `PointSource`: A single point in space from which all rays are generated.
25+
26+
- `LineSource`: A line segment in space from which all rays are sampled randomly.
27+
28+
- `AreaSource`: A 3D mesh representing a surface from which all rays are sampled randomly.
29+
30+
- `DirectionalSource`: A special case for directional light sources that should cover the entire scene (assuming one is using a grid cloner).
31+
32+
The direction of the rays is determined by the direction component of a light source (field
33+
`angle` of the type `Source`). VPL currently implements two types of direction components:
34+
35+
- `FixedSource`: All rays have the same direction (generally combined with `DirectionalSource` to represent solar radiation).
36+
37+
- `LambertianSource`: The direction of the rays is sampled from a Lambertian distribution.
38+
39+
Users may want to defined their own light sources, often by specifying different distributions
40+
of directions to the ones provided by VPL in order to capture more realistic the light
41+
sources used in certain productive contexts (e.g., greenhouses and vertical farms). This
42+
guide illustrates how to define a new light source in VPL by specifying the direction
43+
component assuming a Gaussian distribution.
44+
45+
## Creating your own direction component of a light source
46+
47+
Creating a new direction component in VPL is straightforward. The user needs to define a
48+
new type that inherits from the abstract class `SourceAngle` and defined a method for the
49+
function `generate_direction` that takes as first argument the user-defined type and as
50+
second a random number generator. The method should return a 3D unit vector representing the
51+
direction of the ray. The user is given complete freedom as to how the direction is
52+
generated.
53+
54+
Typically, the distribution of possible distributions is encoded in polar coordinates, that
55+
is, the azimuth ($Phi$, orientation of the ray) and zenith angles ($theta$, inclination of
56+
the ray) with respect to the plane that contains the light source. This requires defining a
57+
local coordinate system for the light source, that is, an XYZ coordinate system where the XY
58+
plane is the plane that contains the light source and the Z axis is perpendicular to the
59+
plane.
60+
61+
Given the azimuth and zenith angles and the local coordinate system, the direction of the
62+
ray can be computed using the `polar_to_cartesian` function provided by the package
63+
PlantRayTracer within VPL.
64+
65+
```julia
66+
import PlantRayTracer as PRT
67+
dir = PRT.polar_to_cartesian((e1 = X, e2 = Y, n = Z), θ, Φ)
68+
```
69+
70+
The function `polar_to_cartesian` will perform the necessary conversions and ensure that the
71+
resulting direction vector meets the expectations of the ray tracer within VPL. The user
72+
thus only needs to specify distributions for the azimuth and zenith angles and generate the
73+
samples accordingly. Let's illustrate this with a Gaussian light source.
74+
75+
## Example: Gaussian light source
76+
77+
A Gaussian light source is a light source where the azimuth angle follows an uniform
78+
distribution (from 0 to 2π radians) and the zenith angle follows a Gaussian distribution
79+
constrained within the interval [-π/2, π/2] radians centered at 0. This Gaussian distribution
80+
will be parameterized by a standard deviation $\sigma$. First we define the type for the
81+
Gaussian light source to store the axes of the local coordinate system and the standard
82+
deviation of the zenith angle.
83+
84+
```julia
85+
using VirtualPlantLab
86+
import PlantRayTracer as PRT
87+
struct GaussianSource <: PRT.SourceAngle
88+
x::Vec{Float64}
89+
y::Vec{Float64}
90+
z::Vec{Float64}
91+
σ::Float64
92+
end
93+
```
94+
95+
We will then use the Distributions.jl package to generate the samples for the zenith angle by
96+
defining a method to the function `generate_direction`:
97+
98+
```julia
99+
using Distributions
100+
import ColorTypes: RGB
101+
function PRT.generate_direction(source::GaussianSource, rng)
102+
dist = Truncated(Normal(0, source.σ), -π/2, π/2)
103+
θ = rand(rng, dist)
104+
Φ = 2π*rand(rng)
105+
dir = PRT.polar_to_cartesian((e1 = source.x, e2 = source.y, n = source.z), θ, Φ)
106+
return dir
107+
end
108+
```
109+
110+
We also need to define the geometry of the light source. Let's assume that the light source
111+
is just a point in space at coordinates [0.5, 0.5, 1.0], that it is pointing downwards, the
112+
radiant power is 1 and we want to generate one million rays. We create the light source as
113+
follows:
114+
115+
```julia
116+
source = Source(PointSource(Vec(0.5, 0.5, 1.0)), GaussianSource(X(), Y(), -Z(), 0.1), 1.0, 1_000_000)
117+
```
118+
119+
This light source is rather narrow as we use a standar deviation of 0.1. We can visualize the
120+
distribution of zenith angles as follows:
121+
122+
```julia
123+
using Plots
124+
dist = Truncated(Normal(0, 0.1), -π/2, π/2)
125+
θ = rand(dist, 1_000_000)
126+
histogram(θ, label = "", xlabel = "Zenith angle", ylabel = "Prob. density", normalize=:pdf)
127+
vline!([-π/2, π/2], label = "")
128+
```
129+
130+
The light source is now ready to be used in a ray tracer. We will test this light source by
131+
creating multiple sensors below the light source and measuring the light intensity at each
132+
sensor.
133+
134+
```julia
135+
scene = Scene()
136+
sensors = [Sensor(1) for _ in 1:400]
137+
c = 1
138+
for i in 1:20
139+
for j in 1:20
140+
r = Rectangle(length = 0.05, width = 0.05)
141+
rotatey!(r, π/2) ## To put it in the XY plane
142+
VirtualPlantLab.translate!(r, Vec((i - 1)*0.05, (j - 1)*0.05 + 0.025, 0.0))
143+
add!(scene, mesh = r, materials = sensors[c], colors = rand(RGB))
144+
c += 1
145+
end
146+
end
147+
```
148+
149+
```julia
150+
import GLMakie
151+
render(scene)
152+
```
153+
154+
We can now create a ray tracing object without a grid cloner (note that since all the
155+
surfaces are sensors we can just set `maxiter = 1`):
156+
157+
```julia
158+
settings = RTSettings(pkill = 0.9, maxiter = 1, parallel = true)
159+
rtobj = RayTracer(scene, source, settings = settings);
160+
trace!(rtobj)
161+
```
162+
163+
We can now visualize the results by plotting the light intensity at each sensor:
164+
165+
```julia
166+
p_narrow = [power(s)[1]/1_000_000 for s in sensors]
167+
coords = collect(0.025:0.05:0.975)
168+
heatmap(coords, coords, reshape(p_narrow,20,20))
169+
```
170+
171+
We can see a straight line meaning that the light intensity is practical constant at each
172+
sensor since the light source is so narrow. Let's now test a wider light source by setting
173+
the standard deviation to 1.5. Let's first look at the histogram
174+
175+
```julia
176+
dist = Truncated(Normal(0, 1.0), -π/2, π/2)
177+
θ = rand(dist, 1_000_000)
178+
histogram(θ, label = "", xlabel = "Zenith angle", ylabel = "Prob. density", normalize=:pdf)
179+
vline!([-π/2, π/2], label = "")
180+
```
181+
182+
Now we expect a lot of rays to escaped from the sides of the scene and not hit the sensors.
183+
Let's test this by creating a new light source and tracing the rays again.
184+
185+
```julia
186+
source = Source(PointSource(Vec(0.5, 0.5, 1.0)), GaussianSource(X(), Y(), -Z(), 1.0), 1.0, 1_000_000)
187+
settings = RTSettings(pkill = 0.9, maxiter = 1, parallel = true)
188+
rtobj = RayTracer(scene, source, settings = settings);
189+
trace!(rtobj)
190+
```
191+
192+
If we plot the light intensity at each sensor we will see that the sensors receive less light
193+
than before but also the gradient of light intensity towards the shaded regions is less
194+
pronounced.
195+
196+
```julia
197+
p_wide = [power(s)[1]/1_000_000 for s in sensors]
198+
heatmap(coords, coords, reshape(p_wide,20,20))
199+
```

0 commit comments

Comments
 (0)