Heterogeneous Volumetric Participating Media

The feature "30.1 - Heterogeneous Volumetric Participating Media" allows us to render participating media such as smoke and fog. I provide three different types of media, homogeneous, exponential density and heterogeneous using openvdb files.

preview


Implementation

For the implementation of this feature, I tried to follow the description from the PBR book and its implementation as well as the implementation of Mitsuba.

To implement this feature, we need: a Medium class which can sample a new free path inside the medium and calculate the transmittance along a ray, a Phase Function class which samples a new direction for the scattering inside of the function, a new Volumetric Path Integrator to be able to handle media in the scene. Additionally, the scene class has been extended to enable the registration of multiple media in the scene, and to return a random media and the closest medium to the ray.

Phase Function

For the PhaseFunction I implemented an isotropic phase function, which simply samples uniformly in all directions and I also implemented the Henyey-Greenstein phase function which has a parameter g, and for g=0 it behaves like the isotropic phase function, for g > 0 it mostly scatters forward, and for g < 0 it mostly scatters backward.

Medium

For the Medium class, there are two main methods Tr to calculate the transmittance along the ray and sample to sample a free path. In the beginning I had a simplified implementation for the homogeneous medium, however, the result deviated greatly from Mitsuba, so I decided to the delta tracking algorithm for all media types. The idea here is to start at the beginning of the ray, and progress along the ray with random step size to avoid artifacts. For the sample method we continue along the ray until we either exit the medium, or if density / max_density is bigger than a random number. In the latter case, we have found a medium interaction, which will then be handled by the integrator. In the Tr method, we continue along the ray until we exit the medium (or reach the end of the ray) to calculate the transmittance. PBRT also includes Russian roulette in case the transmission gets low. Therefore, I also added the Russian roulette termination criteria. The parameters describing our media are the absorption and scattering coefficients, sigma_a and sigma_s, and the maximum density. The attenuation coefficient simga_t can be calculated by adding sigma_a and simga_s. The albedo is calculated by dividing sigma_s by sigma_t and is therefore always between 0 and 1.

The main difference between Heterogeneous, ExponentialDensity and Homogeneous medium is the density function. For the homogeneous case, we simply use the max_density parameter which can be defined in the xml. For the exponential density case, we use an exponential function max_density * exp(-b * height) to slowly decrease the density in the up-direction. Both the up direction and the b in the exponent can be defined in the xml. This creates a nice foggy scene. Lastly, for the heterogeneous case, I wanted to be able to read cloud data from openvdb files and render these media. The files I have looked at generally only contain density grid information. Therefore, in my implementation, we read the corresponding density from the file in the getDenstiy method. All three media types use a bounding box to decide where the participating media should be rendered. For the heterogeneous medium we can define a bounding box where the medium should be placed, and then we map this bounding box to the bounding box from the openvdb file. I do this by first mapping the bounding box to a unit cube and then mapping the unit cube to the VDB bounding box. To be able to control how dense the VDB medium is rendered, I added the density_scale parameter.

Volumetric Path Integrator

Lastly, we need to implement a volumetric path tracer to enable the medium to be rendered. I start by extending the path_mis implementation from previous assignments. For each ray, I start by looping over all media. For each medium I sample a new path inside of it. If we detect a medium interaction, we randomly sample an emitter to check whether it is visible from the medium interaction point. If so, we add the contribution of the light together with the medium transmittance along the shadow ray. The next ray is sampled by using the phase function, where we check whether the newly intersected point is an emitter and recalculate the w_mat weight similar to the surface interaction case in the path_mis integrator. In case of no medium interactions, we check for a surface interaction. This case is basically the same as the implementation in path_mis, with the difference that we always multiply with the calculated transmittance of the medium along the ray. In case the scene contains more than one medium, I loop over all media and multiply transmittance together.

Initially, I implemented this integrator without looping over all media. However, then as soon as I added more than one medium to the scene, the different media started to fade. Always choosing the closest medium also did not work properly for the shadows the media were casting. Additionally, when we have intersecting media, choosing the closest one also does not work properly.

As my project partner implemented subsurface scattering, and also had to adapt the path_mis implementation to work properly for his feature, I have created a second volumetric path integrator called volpath_sss. I managed to somewhat merge our two implementations together. However, as I did not have time to test and evaluate this integrator properly.

Usage:

<medium type="heterogeneous|exponential_density|homogeneous">
 <phase type="henyey|iso"><float name="g" value="0"/></phase>
 <color name="sigma_a" value="1 1 1"/>
 <color name="sigma_s" value="2 2 2"/>
 <float name="max_density" value="1"/>
 <vector name="origin" value="0.0 0.0 0.0"/>
 <vector name="size" value = "2 2 2"/>
...
</bsdf>

Additional Tags for exponential density:
 <vector name="up" value="0 1 0"/>
 <float name="expon_b" value="2"/>

Additional Tags for heterogeneous:
 <string name="vdb" value="meshes/file.vdb"/>
 <float name="density_scale" value="1"/>

The following files have been modified or added to implement this feature:

  • include/medium.h
  • src/medium.cpp
  • src/homogeneous.cpp
  • src/exponentialDensity.cpp
  • src/heterogeneous.cpp
  • src/volpath.cpp
  • src/volpath_sss.cpp
  • include/nori/scene.h
  • src/scene.cpp
  • include/nori/common.h

Validation

For the validation of the homogeneous participating media, I used Mitsuba and could achieve almost identical results. For the exponential density, I simply compare it with the homogeneous version, as the basic idea is to have thicker fog below and slowly fading fog in the up direction.

I was planning on validating my heterogeneous participating media result with Mitsuba as well, however, Mitsuba uses their own .vol file type and not openvdb. They provide a mitsuba2 vdb converter to be able to convert openvdb to .vol files. However, neither me nor my project partner managed to get this converter running. Therefore, I ended up validating my version by checking whether the expected shape is rendered and that the parameters act as expected.

As for the volumetric path tracer, I validated that it produces the same results as path_mis when there are no media in the scene. The following two sections describe the validations for media and phase function.

Validation - Media

Comparison to Mitsuba - Homogeneous Medium

Here we can see that my homogeneous medium is almost identical to the result of Mitsuba.

  • Medium 1:
    • sigma_a: 1,1,1
    • sigma_s: 2,2,2
    • max_density: 1
  • Medium 2:
    • sigma_a: 1,1,1
    • sigma_s: 1,1,1
    • max_density: 1
Mine - Medium 1 Mitsuba - Medium 1 Mine - Medium 2 Mitsuba - Medium 2

As the albedo is sigma_s / (simga_a + sigma_s), choosing sigma_s to be smaller than sigma_a will result in darker a medium.

  • sigma_a: 2,2,2
  • sigma_s: 0.5,0.5,0.5
  • max_density: 1
Mine Mitsuba

I would have liked to compare colored homogeneous media to Mitsubas implementation. However, Mitsuba only provides access to simga_t and albedo, and in case sigma_t should have 3 different values, we are restricted to values between 0 and 1 because of the datatype. Therefore, I was not able to produce meaningful results that could be compared with my implementation.



Additional Results

Exponential Density

In the following comparison, you can see the effects that can be achieved when using exponential density instead of homogeneous. I show how the b-parameter can control the density fall off and how the "up"-direction controls the fall-off direction.

  • For all media, we use the same simga_a, simga_s and max_density:
    • sigma_a: 2,2,2
    • sigma_s: 1,1,1
    • max_density: 1
  • Exponential Right:
    • up: 1,0,0
    • expon_b: 2
  • Exponential Up 1:
    • up: 0,1,0
    • expon_b: 2
  • Exponential Up 2:
    • up: 0,1,0
    • expon_b: 5
Homogeneous Exponential Right Exponential Up 1 Exponential Up 2

Foggy scenes can be simulated by expanding the bounding box size to encompass the whole cornell box.

  • sigma_a: 2 2 2
  • sigma_s: 1 1 1
  • max_density: 1
  • up: 0,1,0
  • expon_b: 10
No Media Exponential Density

Heterogeneous Media

My implementation can successfully render the expected bunny cloud shape. I additionally show a comparison of the rendering with and without fog.

  • Bunny:
    • sigma_a: 2.5,2.5,2.5
    • sigma_s: 0.5,0.5,0.5
    • density_scale: 15
Without fog With fog

Here are some additional renderings of smoke, with different parameters.

  • Light:
    • sigma_a: 1,1,1
    • sigma_s: 2,2,2
    • density_scale: 10
  • Dark:
    • sigma_a: 2.5,2.5,2.5
    • sigma_s: 0.5,0.5,0.5
    • density_scale: 10
  • Green:
    • sigma_a: 1,1,1
    • sigma_s: 0.5 2 0.5
    • density_scale: 10
  • Blue:
    • sigma_a: 3,3,3
    • sigma_s: 0.5 0.5 2
    • density_scale: 100
Smoke - Light Smoke - Dark Smoke - Green Smoke - Blue

Multiple mediums

Here I show that adding multiple mediums produces the same medium rendering as if only one medium was added, and that we can have overlapping mediums as well.

  • Green cube (left):
    • Homogeneous
    • sigma_a: 1,1,1
    • sigma_s: 1,2,1
    • max_density: 0.5
  • Red cube (right):
    • Homogeneous
    • sigma_a: 1,1,1
    • sigma_s: 2,1,1
    • max_density: 1
  • Exponential Density:
    • sigma_a: 2,2,2
    • sigma_s: 1,1,1
    • max_density: 1
    • up: 0 1 0
    • expon_b: 10
1 homogeneous medium 2 homogeneous media Exponential density All 3 media


Validation - Phase Function

I have implemented the isotropic and Henyey-Greenstein phase function. Both of these have also been implemented in Mitsuba. Therefore, I compare the homogeneous medium with different parameters for these functions.

Isotropic and Henyey-Greenstein with g = 0

Isotropic and Henyey-Greenstein with g = 0 should behave the same, as both versions just scatter the light isotropically. This can be observed in the following 4-way comparison.

Isotropic - Mine Isotropic - Mitsuba Henyey (g=0) - Mine Henyey (g=0) - Mitsuba

Henyey-Greenstein - Forward and Backward scattering

Here I have a bug in my code and I was not able to figure out where. As far as I can tell, the implementation is basically identical to PBRT's implementation. However, both for g > 0 and g < 0 the medium gets brighter in my implementation. When I now use the slightly different formulas from Mitsuba my medium gets darker for |g| > 0. And even then, both results don't match Mitsuba's result. I included two different Henyey-Greenstein implementations, one can be included in the xml with the keyword 'henyey' which matches the implementation of PBRT and the other can be included with the keyword 'henyey_m' which uses Mitsuba's version. I assume that the actual error is not in the phase function implementation, however I did not figure it out in time. Below I show the results for the two different wrong versions.

g = 0.7
henyey - Mine henyey_m - Mine Mitsuba
g = -0.7
henyey - Mine henyey_m - Mine Mitsuba