Focus and Depth of Field
For my CSE 168 final project, I implemented two techniques into my ray caster, depth of field with focus and volumetric rendering. The former can be seen in the above image, where the focal point is in between the two Laughing Buddha models. Below, you can see the gradual effect of increasing the f/stop, i.e. reducing the aperture diameter.
f/Stop values of 20, 80, 160, 500, 1000 from top to bottom
Secondly, I implemented volumetric rendering. My algorithm consists of a ray marching approach, albeit one different from the standard forward evaluation kind. For each point on a particular path, after the ray traversal returns to that depth level, the algorithm samples the ray at various points in the reverse direction. At each sample point, I evaluate the 4 parameters of the volume and modify the radiance at that point accordingly. This also applies for rays that miss geometry; they are evaluated at infinity.
The 4 parameters of the volume are:
In order to simplify my algorithm, I chose to make 2 core assumptions about the nature of the volume, that is is infinite in all dimensions and homogenous in its density. By making these two assumptions I was able to simplify my logic routines. If a volume is infinite, then all rays will pass through it. If a volume is homogenous, then absorption and emission can be treated as constant and generalized into a single extinction factor.
Volume Rendering Quality
The quality of the rendered output when rendering a scene with a volume is directly proportional to the granularity with which the back sampling is done on the ray. If multiple samples are allowed, then the noise present in the volume will greatly decrease and the overall image will be much smoother. However, this comes at significant computational cost, as the running time becomes a double exponential.
In order to increase performance, I parallelized my ray caster. The image is subdivided into tiles, where each tile consists of a block of pixels. Depending on the supported hardware concurrency of the machine, a number of threads are created. Each thread is assigned a tile. As threads finish processing tiles, they request new ones from the master thread. In this way, all processors can be utilized fully.
The example on the left was rendered with far greater granularity and path length than the above image. Despite that the above image has 16 times the number of pixels than the one on the left, the left image took longer to render.
Other Miscellaneous Features.
I also added some additional features for programmer productivity. When starting an image rendering function, the engine has an optional parameter for previewing. The tiles are organized into blocks, and I typically demarcate each image as having 5 blocks (20% of the image). Whenever a block finishes processing, the image is displayed on screen. This allows me to monitor the rendering and save my own time by quickly exiting the program if my parameters are incorrect and/or the image is not rendering as desired. This is particularly useful, as high resolution images and volumes can take many hours to render, even with optimum parallelization.