**Matthew Ellison & Leah Ryu CS87 Final Project** **by Leah Ryu (F003MC1) and Matthew Ellison (F004SPK)**

Motivational images

We were interested in building a night scene of a cave with crystals. We wanted to have a fish in the cave, which, to match the theme, would cast the shadow of it's inner spirit, a dragon. Here are a number of motivational images:

Translucent crystals (subsurface scattering) https://cen.acs.org/physical-chemistry/geochemistry/Naicas-crystal-cave-captivates-chemists/97/i6

crystals

Fish shadow https://pixels.com/featured/fish-casting-shadows-jeff-swan.html)

fish_shadow

Light streaming into cave https://blog.investmentpropertiesmexico.com/lifestyle/4-must-visit-underwater-caves-in-mexicos-yucatan-peninsula

cave

Theme

The connection to the theme (it’s what’s on the inside that counts) is illustrated by the relationship of the fish to the dragon shadow--it’s a common theme across several Asian mythologies that a koi can, through hard work, transcend and become a dragon. Additionally, we are hoping to evoke the mood of a hidden grotto within the earth, which more obliquely references the theme of interiority.

Features

Volumetric & Subsurface Scattering Path Tracing

Homogeneous Henyey-Greenstein medium

Code:`src/media/homogeneous_henyey-greenstein.cpp`; `include/medium.h`. We first implemented a basic homogeneous volumetric path tracer using the Henyey-Greenstein phase function. The Henyey-Greenstein phase function takes in three parameters: By adjusting these three parameters we can simulate a variety of media. Implementing this required a fair amount of additional plumbing, as I'm sure the grading reader knows. This included making the Li call take current media as a parameter, and adjusting the json file so that the camera had a medium it was in, and each surface had two media associated to it, one for each side. With no volumes: sigma_t = sigma_s = g = 0 Volumes with various parameters:
sigma_t=1, sigma_s=0.7, g=0 sigma_t=1, sigma_s=0.7, g=0.5 sigma_t=1, sigma_s=0.7, g=-0.5 sigma_t=0.5, sigma_s=0.3, g=0
According to http://misclab.umeoce.maine.edu/boss/classes/RT_Weizmann/Chapter3.pdf, a reasonable approximation for the inside of water is a medium with g = 0.924, sigma_t = 2, sigma_s = 1.5. Jensen water medium

Nee and MIS

Code: `src/integrators/path_tracer_volumetric.cpp` Our basic volumetric path tracer integrator yielded very noisy results, so we decided to add next event estimation and mixed importance sampling to our implementation. Basic materials integration with volumetrics:
Mats integrator, no fog Mats integrator, with fog
Code: `src/integrators/path_tracer_volumetric_nee.cpp` Adding only next event estimation caused the characteristic sparkly appearance in both volumetrics and regular materials:
NEE integrator, no fog NEE integrator, with fog
Code: `src/integrators/path_tracer_volumetric_mis.cpp` In our final MIS implementation, we importance sampled in the usual way for regular materials. For volumetrics, we took one random scatter sample and one sample in the direction of the light source. This results in a nice, fine-grained fog:
NEE MIS integrator, no fog NEE MIS integrator, with fog

Non-exponential bleed (subsurface) medium

Code: `src/media/nonexponential_bleed.cpp` One way to simulate subsurface scattering is to place a volume with a very high scattering coefficient inside a surface boundary--while this is less efficient than a non-empirical diffusion approximation, which “provide a closed-form and accurate solution where the surface is flat, areas with high curvature (for example, the nose or ears of a human face) present a problem in that the semi-infinite slab approximation fails. If we instead consider the medium embedded in the surface to be just another volume, we can use the same path tracing technique for subsurface scattering as for general volume rendering.” (source https://graphics.pixar.com/library/PathTracedSubsurface/paper.pdf ) This paper also suggests a more physical, “non-exponential” approach to such a subsurface scattering volume; “As light moves through a participating medium, the proportion of unscattered to scattered light is a function of the distance traveled” (see section 7, Non-Exponential Free Flight). We implemented a non-exponential free-flight media wherein, as opposed to the homogeneous Henyey-Greenstein medium in which “the probability of scattering is proportional only to the scattering cross section of the medium,” the “probability . . . becomes a function of both the scattering cross section as well as the distance traveled in the medium.” Built into the paper’s nonexponential medium was also a “bleed” parameter, which we implemented to allow the user to control the degree to which light penetrates the SSS medium. The higher the bleed number, the greater the penetration. Results:
g = 0.9, sigma_t = 50, sigma_s = 45, bleed = 0.5 g = -0.9, sigma_t = 50, sigma_s = 45, bleed = 0.5 g = 0.9, sigma_t = 50, sigma_s = 45, bleed = 3
As you can see, the `g` parameter, which determines forward or backward scattering in relation to the light source, greatly changes the look of the medium. With lower bleed parameter:
Backscatter: g = -0.9, bleed = 0.1 Front scatter: g = 0.9, bleed = 0.1

Straight Line Hack

To improve rendering of SDS paths for underwater objects, we decided to introduce a non-physical hack where shadow rays, when refracting through a dialectric, continue in their original direction instead of the true refraction direction. This makes it so that the traced shadow rays actually reach the light source --- especially if the light source is small or far away. Below is a test of the straight line hack to improve visability of a checkboard texture below a dialectric plane:
Without hack With hack
In the above example, we see that the ground became a good bit lighter --- which we think is due to more shadow rays actually reaching the light source --- while the walls became a bit but less lighter, due to extra indirect lighting. This work is implemented in the integrators at

`src/integrators/path_tracer_volumetric_nee_hack.cpp` and `src/integrators/path_tracer_volumetric_nee_mis.cpp`.

Anisotropic BSDF approximation

One feature of selenite, or more correctly "satin spar", is that it is chatoyant.
satin spar chatoyant https://66.media.tumblr.com/0d9ab0ea7ff630fbb5326eb5bc53403d/tumblr_prgjrk2n2h1st8h25_1280.jpg We found an anisotropic bsdf described in the Filament renderer documentation which seemed able to approximate this effect -- here are two of their reference images: https://google.github.io/filament/Materials.md.html#materialmodels/litmodel/anisotropy https://google.github.io/filament/Filament.html#lighting/imagebasedlights/anisotropy aniso1 aniso2
Their approach uses tangent and bitangent vectors to form a "bent normal" vector which the incoming ray is then reflected about. As the anisotropy parameter varies, the specular highlights aligns more towards either the tangent or bitangent vectors. We adapted their approach to construct a anisotropic dialectric, which, on reflection, behaves like the Filament anisotropic material, and behaves like a usual dialectric on refraction. To make the tangent and bitangent vectors make sense for a crystal, we made it so that the tangent direction was as "upward" as posible in the tangent plane to the shading normal, where "upward" is the direction the object or crystal is pointing, and which must be entered into the json file. See `src/materials/anisotropic_dialectric.cpp` for our implementation. Here are some examples of our anisotropic dialectric:
aniso1 aniso-1 aniso-0
Because we were not able to replicate the brushed metal reflections above, we are not confident that this anisotropic material is working properly, but it does seem to generally warp reflections in the appropriate direction as anisotropy is varied.

Rough Dielectric Material

We had a few motivations for making a rough dialectric material. One was to potentially use it for our water surface to avoid SDS paths, and another was that it might be a good model for our crystal surfaces. We implemented this material by layering the cosine lobe idea from the Phong BRDF on top of the reflection and refraction directions from the dialectric. In more detail, the rough dialectric sample decided whether to reflect or refract in the same way as the standard dialectric, then used our orthonormal basis structure to sample a cosine lobe around the usual reflection/refraction direciton. The pdf for the sample was then scaled by the reflection/refraction probability. See `src/materials/rough_dialectric.cpp` for the impelmentation. Here are some examples of the `04_refr` scene with different cosine exponents:
exponent 1 exponent 50 exponent 1000
As the cosine exponent tends to infinity, we expected the rough dialectric to approach the usual dialectric:
exponent 1M exponent 100M glass
It takes a surprisingly high exponent to get rid of noticeable blur! We also modified the rough dialectric (and all our specular materials) to take a color scatter parameter. Our reference for this was https://google.github.io/filament/Materials.md.html#materialmodels/litmodel/absorption. Here are some examples tinting the rough dialectric different colors:
exponent 1M exponent 100M glass

“Invisible” and “Substitute” Materials

Substitute fish and invisible occluding dragon
To render a fish casting a dragon shadow, we had to figure out how to create an “invisible” surface that also occluded (the dragon), as well as a “substitute” material (the fish) which would be visible but not occlude. To do this, we added the boolean fields `is_invisible` and `is_substitute` to our surface base class, as well as a `hit_invisible` parameter to our `Li_helper`. Shadow rays were traced with "hit_invisible" set to false, and all other rays with it set to true. When a `hit_invisble = false` ray hit an invisible material or a `hit invisible = true` ray hit a substitute material, the ray continues as if the surface wasn't there by a recursive call to `Li_helper`. Any other surface intersections are treated as usual. You can see this process in `src/integrators/path_tracer_volumetric_nee_mis.cpp`. Here is an example showing invisible spheres in the Jensen box:
visible non-matte spheres invisible non-matte spheres visible matte spheres invisible matte spheres
Note in the above that the shadows cast by invisible surfaces do not depend on the surface material, and that effects such as the increased shadowing near touch points (due to occluding of non-shadow rays) do not appear when the surfaces are made invisible. Our straightforward approach using shadow rays, though, gives the dragon shadow effect we wanted.
Another neat thing is that, for an invisible surface in media, one can decide whether or not the invisible surface casts a volume or just a surface shadow by changing the "hit_invisible" parameter for volumetric shadow rays.

Other Timesinks

K-D Tree

We were hoping to implement photon mapping to give better crystal caustics and avoid the straight-line hack. We unfortunately didn't get this running, but we did code (from scratch) and test the key data structures: the k-d tree with k nearest neighbors and the max heap to speed up nearest neighbor search. The implementation is located in `include/darts/kdtree` and `src/kdtree.cpp`, and the testing file is at `kdtree_test.cpp`. Here is an test run where we build a photon map from 1M randomly generated photon positions, note that the the other photon parameters are set to 0. To make this work, the stack limit must be expanded (`ulimit -s 32000` works), and I had to work in terminal (not vscode) to effect this stack change. The data structures are quite fast, and the following test with 1M photons just takes a second or two:
vpn-investment-office-231-129-169:Release matthewellison$ ./kdtree_test

CREATING 1000000 RANDOM PHOTONS... DONE
BUILDING K-D TREE FROM PHOTONS... DONE

FINDING PHOTON CLOSEST TO (0.602656, 0.647544, 0.709846)
 It is:
photon position: (0.606516, 0.643150, 0.709278)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000

FINDING 5 PHOTONS CLOSEST TO (0.602656, 0.647544, 0.709846)
 They are:
photon_ptr : 0x7ffee9251630
photon position: (0.602962, 0.639063, 0.704450)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000
square distance : 0.000101

photon_ptr : 0x7ffee9250890
photon position: (0.593963, 0.650356, 0.713013)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000
square distance : 0.000094

photon_ptr : 0x7ffee9251650
photon position: (0.606516, 0.643150, 0.709278)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000
square distance : 0.000035

photon_ptr : 0x7ffee9252050
photon position: (0.600354, 0.655034, 0.715481)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000
square distance : 0.000093

photon_ptr : 0x7ffee9251cd0
photon position: (0.601870, 0.654557, 0.707129)
photon power (rgb): (0.000000, 0.000000, 0.000000)
photon pi, theta: 0.000000, 0.000000
square distance : 0.000057

Division of Labor

We mostly paired-programmed our features, though a few were either implemented or designed/prepared independantly prior to a joint programming session. Items besides the following were programmed together. Matt Leah

Final image

Scene rendered in Blender (example shot): a Blender render of the scene We weren't able to use the Discovery cluster to render our images because the EXR outputs from the cluster looked very different from those rendered on our local machines. Despite spending a couple of days on these bugs, which in no small part were being caused by uncaught NaNs (when dividing by zero PDFs) and by Discovery's disparate `abs()` function, we were unable to get the EXR Discovery images to match our local references. Having rendered our final scene on several different machines with different seeding and averaging the results, this is the final image: final render