Skip to content

Commit 373f8ae

Browse files
authored
Merge pull request #20 from JuanDiegoMontoya/main
Added MiePlot article
2 parents 8da5c16 + edef508 commit 373f8ae

17 files changed

+38058
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
---
2+
title: 'Generating Phase Function LUTs with MiePlot'
3+
description: 'MiePlot tutorial for graphics programmers who just want a darn LUT for their cool volume renderer.'
4+
date: '2024-10-30'
5+
authors: ['jaker']
6+
tags: ['intermediate', 'techniques', 'theory', 'fog', 'mieplot', 'volumetric']
7+
image: 'render_fog_2.png'
8+
---
9+
10+
In computer graphics, Mie theory describes the interactions between light and large (relative to the wavelength of light) particles in a medium. Simulating it is crucial in rendering convincing volumetric effects like fog, clouds, and smoke.
11+
12+
This article is primarily aimed at graphics programmers who already have a volume renderer that has a pluggable phase function. In other words, this is a tutorial about producing phase functions that you can easily use in a volume renderer.
13+
14+
<!-- truncate -->
15+
## Phase Functions
16+
17+
[Phase functions](https://www.pbr-book.org/3ed-2018/Volume_Scattering/Phase_Functions) are used to quantify the amount of light that this light is [scattered into](https://pbr-book.org/3ed-2018/Volume_Scattering/Volume_Scattering_Processes#In-scattering) the subject ray (such as a camera ray).
18+
19+
Real phase functions are extremely complex. For example, take this phase function for fog generated by MiePlot:
20+
21+
![A polar plot of the phase function for fog in MiePlot](mieplot_polar_01.png)
22+
23+
The graph essentially depicts the distribution of light when it collides with a particle. It's kind of like the volumetric equivalent to a [BRDF](https://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function).
24+
25+
Analytical approximations for real-time applications exist, but miss high-frequency information that is essential for capturing effects like [fog bows](https://en.wikipedia.org/wiki/Fog_bow) (blue) and [glories](https://en.wikipedia.org/wiki/Glory_(optical_phenomenon)) (green).
26+
27+
![Composited image of polar phase function plot and foggy photos, with arrows pointing at notable effects like fogbows and glories](polar_vs_fogbow.png)
28+
29+
To recover some of these features, a more complex approximation might layer multiple simpler approximations or use a bespoke polynomial. However, even those can't capture everything we care about without excessive runtime costs. What we need is a way to precompute the phase function and store it in memory.
30+
31+
## MiePlot
32+
33+
[MiePlot](http://www.philiplaven.com/mieplot.htm) is a program for simulating light scattering with Mie theory. With it, we can compute high-quality phase function LUTs for various atmospheric phenomena such as rain, fog, clouds, and smoke.
34+
35+
## Procedure
36+
37+
The basic procedure we'll be using to generate Mie scattering LUTs will be the following:
38+
39+
1. Set simulation parameters in MiePlot, then generate a plot
40+
2. Save the plot to a file
41+
3. Lightly process the data in spreadsheet software (this step could be done in your app as well)
42+
4. Load the processed data in your app, then normalize it with Monte Carlo integration
43+
5. Sample the LUT in your atmosphere simulation
44+
45+
## Fog
46+
47+
To simulate "typical" foggy conditions, configure MiePlot as such:
48+
49+
![MiePlot settings for fog](mieplot_settings_fog.png)
50+
51+
- To set the medium, go to Advanced -> Refractive Index -> Surrounding medium -> Air -> 5°C, 101325 Pa.
52+
- In Particle size, use the first drop-down menu to change Normal to Gamma, then select the first equation to get the shape below.
53+
54+
A graph on the right will show the particle size distribution.
55+
56+
![A second image of the MiePlot settings for fog](mieplot_settings_fog_2.png)
57+
58+
With these parameters, I'm trying to recreate the continental fog distribution presented in [Typical droplet distribution for different kinds of fog](https://www.researchgate.net/figure/Typical-droplet-distribution-for-different-kinds-of-fog_fig1_243483550).
59+
60+
Finally, click "New plot" to generate data.
61+
62+
After some time, the plot should generate and look something like this:
63+
64+
![A line graph representing a phase function in MiePlot](mieplot_graph.png)
65+
66+
To save the results, go to File -> save numerical results as text file.
67+
68+
## Processing the data
69+
70+
Open the file containing numerical results, then locate the raw RGB data in the section following this header:
71+
`Angle Wavelength Perpendicular Parallel`
72+
73+
There should be several thousand lines of data. Copy them them into spreadsheet software like Microsoft Excel or Google Sheets. Average the third and fourth columns (containing perpendicular and parallel polarized light phase functions) into another column (I'm told this is equivalent to unpolarized light, which is what the sun emits).
74+
75+
![Five columns of numerical data in excel](excel.png)
76+
77+
We only care about the new column, so the others can be deleted. With a single column of data remaining, save the workbook as a csv.
78+
79+
## Loading the data
80+
81+
The csv format makes loading our data very easy:
82+
83+
```cpp
84+
std::ifstream file{"mie.csv"};
85+
86+
std::vector<glm::vec3> data;
87+
data.reserve(2000); // Reserve some space to minimize copying
88+
89+
while (file.peek() != EOF)
90+
{
91+
std::string num0, num1, num2;
92+
std::getline(file, num0);
93+
std::getline(file, num1);
94+
std::getline(file, num2);
95+
96+
float red = std::stof(num0);
97+
float green = std::stof(num1);
98+
float blue = std::stof(num2);
99+
100+
data.emplace_back(red, green, blue);
101+
}
102+
```
103+
104+
Unfortunately, we aren't done yet. MiePlot's documentation states that the phase function is normalized to 4pi, so that integrating it as such will give the following result:
105+
$$\frac{1}{4\pi} \int_{0}^{2\pi} \int_{0}^{\pi} f(\theta, \phi) \sin(\theta) \, d\theta \, d\phi = 1$$
106+
However, when we numerically perform this integration ourselves, we get a very tiny result (around 1e-8). I don't know if this is caused by an error on my or MiePlot's part, but I do know a cheeky way to fix it.
107+
108+
![A sign that says "Warning: Engineering disguised as math](engineering_cropped_small.png)
109+
110+
Performing the integration ourselves gives a number with which we can normalize the data:
111+
112+
```cpp
113+
// Lambda: samples the phase function LUT with linear interpolation.
114+
auto SamplePhaseLut = [&](glm::vec3 dir) -> glm::vec3 {
115+
auto cosTheta = clamp(dot(dir, {0, 0, 1}), -1.0f, 1.0f);
116+
auto theta = acos(cosTheta); // [0, pi]
117+
auto uv = theta / M_PI; // [0, 1]
118+
auto tc = uv * (data.size() - 1); // [0, size - 1]
119+
return mix(data[(size_t)floor(tc)], data[(size_t)ceil(tc)], fract(tc));
120+
};
121+
122+
// Integrate surface of unit sphere with uniformly distributed random directions.
123+
constexpr size_t samples = 1'000'000;
124+
glm::dvec3 estimate = {};
125+
for (size_t i = 0; i < samples; i++)
126+
{
127+
const auto xi = glm::vec2(PCG_RandFloat(seed), PCG_RandFloat(seed));
128+
estimate += SamplePhaseLut(MapToUnitSphere(xi)) / UniformSpherePDF();
129+
}
130+
estimate /= samples;
131+
132+
// Normalize the data.
133+
for (auto& c : data)
134+
{
135+
c /= estimate;
136+
}
137+
```
138+
139+
`UniformSpherePDF` and `MapToUnitSphere` look like this:
140+
141+
```cpp
142+
float UniformSpherePDF()
143+
{
144+
return 1.0f / (4.0f * M_PI);
145+
}
146+
147+
// Uniformly maps the unit UV square in [0, 1] to the surface of the unit sphere.
148+
glm::vec3 MapToUnitSphere(glm::vec2 uv)
149+
{
150+
float cosTheta = 2.0f * uv.x - 1.0f;
151+
float phi = 2.0f * M_PI * uv.y;
152+
float sinTheta = cosTheta >= 1 ? 0 : sqrt(1.0f - cosTheta * cosTheta); // Safe sqrt
153+
float sinPhi = sin(phi);
154+
float cosPhi = cos(phi);
155+
156+
return {sinTheta * cosPhi, cosTheta, sinTheta * sinPhi};
157+
}
158+
```
159+
160+
`PCG_RandFloat` is a simple RNG based on [PCG](https://www.pcg-random.org/index.html), but it could be any function that generates a uniformly distributed pseudo-random float in \[0, 1].
161+
162+
I suggest testing your normalization by plugging in a known-good analytical phase function like Henyey-Greenstein or Schlick's. These integrate to 1 and so don't require hacky normalization:
163+
164+
```cpp
165+
float phaseHG(float g, float cosTheta)
166+
{
167+
return (1.0 - g * g) / (4.0 * M_PI * pow(1.0 + g * g - 2.0 * g * cosTheta, 1.5));
168+
}
169+
170+
float phaseSchlick(float k, float cosTheta)
171+
{
172+
float denom = 1.0 - k * cosTheta;
173+
return (1.0 - k * k) / (4.0 * M_PI * denom * denom);
174+
}
175+
```
176+
177+
After normalizing the data, we can put it into a 1D texture.
178+
179+
## Sampling the LUT
180+
181+
We only need to keep in mind that the our LUT-based phase function is parameterized by theta (unlike HG and Schlick above). Here's how you might access it in GLSL:
182+
183+
```c
184+
layout(binding = 0) uniform sampler1D s_phaseFunction;
185+
186+
vec3 phaseLut(float cosTheta)
187+
{
188+
float theta = acos(clamp(cosTheta, -1, 1));
189+
float u = theta / M_PI; // [0, pi] -> [0, 1]
190+
return textureLod(s_phaseFunction, u, 0).rgb;
191+
}
192+
```
193+
194+
If integrated correctly, your volume renderer should produce a result similar to that of [my single-scattering renderer](https://github.com/JuanDiegoMontoya/Fwog/blob/main/example/04_volumetric.cpp):
195+
196+
![Render of a foggy scene with a fogbow and glory visible](render_fog_1.png)
197+
198+
Looking in the other direction reveals the sun. A sun disk is not explicitly rendered in this scene:
199+
200+
![Render of a foggy scene looking towards the sun](render_fog_2.png)
201+
202+
## Premade Phase Function LUTs
203+
204+
Provided are some lightly processed LUTs. The code for loading and normalizing them still applies.
205+
206+
- [Fog phase function from above](mie_fog.csv)
207+
![MiePlot settings for fog](mieplot_settings_fog.png)
208+
- [Cumulus cloud phase function](mie_cumulus.csv)
209+
![MiePlot settings for cumulus clouds](mieplot_settings_cumulus.png)
210+
- [Rain phase function](mie_rain.csv)
211+
![MiePlot settings for rain](mieplot_settings_rain.png)
212+
213+
The rain phase function was used to generate the following image:
214+
215+
![Render of a scene with a rainbow](render_rain_1.png)
216+
217+
Note that the fog and rain parameters were chosen after a brief search for their respective particle size distributions and trying to match the graphs I found. Other parameters can yield perfectly valid results matching those descriptions.
218+
219+
## Issues
220+
221+
A critical assumption made early on is that the data generated by MiePlot uses sRGB primaries. In reality, we only have the phase functions for light of wavelengths 650nm, 510nm, and 440nm.
222+
223+
![Graph of CIE XYZ Color Matching Functions](cie-xyz-cmf.jpg)
224+
225+
We assume that 650nm, 510nm, and 440nm correspond to red, green, and blue sRGB primaries, respectively, missing significant energy from other wavelengths that would have been emitted by the sun.
226+
227+
The incorrect appearance of the rainbow in the previous section is likely caused by this simplification. Despite that, it works well for small droplet media that produce nearly monochromatic scattering.
228+
229+
Radial banding or noise artifacts may appear when there isn't enough angular resolution or particle size samples. Increasing these in MiePlot will make plots take longer to generate (and in the case of angular resolution, increase the size of the data), but improve the quality of the LUT.
230+
231+
## Next Steps
232+
233+
Improving the color rendering of our LUTs will require a round of spectral integration to produce proper tristimulus sRGB values. A second post describing how to do this may be released once I understand the math and figure out how to coax MiePlot into doing my bidding.
234+
235+
## Links
236+
237+
Droplet distributions:
238+
239+
https://www.researchgate.net/figure/Typical-droplet-distribution-for-different-kinds-of-fog_fig1_243483550
240+
241+
https://www.researchgate.net/figure/Raindrop-Size-Distribution-RSD-of-tropical-cyclone-rainfall_fig1_279199640
242+
243+
https://en.wikipedia.org/wiki/Raindrop_size_distribution
244+
245+
Analytical phase functions:
246+
247+
https://cs.dartmouth.edu/~wjarosz/publications/dissertation/chapter4.pdf
248+
249+
https://www.desmos.com/calculator/thqhfqahfp
Loading
Loading

0 commit comments

Comments
 (0)