Tested on:
macOS | iOS |
---|---|
Pollux is a Monte Carlo Path Tracer built completely in Metal. It can run on both macOS and iOS.
[Select the appropriate scheme](/Debug\ Views/scheme.png) and run by clicking on the play button.
If you are running on iOS you must run on an actual device as Metal is not supported on the simulator.
Scenes are described in a json
file format. Where each file must contain:
A camera
dictionary with entries for:
fov
: the camera's field of viewdepth
: the depth of the rays castpos
: an array containing 3 floats EXACTLY, detailing the position of the camera in the scenelookAt
: an array containing 3 floats EXACTLY, detailing the target that the camera is looking atup
: an array containing 3 floats EXACTLY, detailing the direction of "up" for the cameraA materials
array of dictionaries with each material having entries for:
bsdf
: the type of bsdf it is. BSDF types are defined as -1 for light, 0 for diffues, and TODO: ADD MORE BSDF TYPES
name
: the name for a material. This is not used in code, but helpful for reading the scene file.color
: the albedo color of the material. TODO: Add Texture Map?
A geometry
array of dictionaries with each geometry having entries for:
name
: "Sphere" the name of the geometry, useless in code, but helpful for reading the scene file.type
: the geometry type. The current types are 0 = SPHERE
, 1 = CUBE
, TODO: Add meshes?
material
: the index of the material that this geometry has. Not to be confused with bsdf
. This is just the index of a material from the materials array.translate
: an array containing 3 floats EXACTLY, indicating the translation of the geometry in the x, y, and z axes respectively.rotate
: an array containing 3 floats EXACTLY, indicating the rotation of the geometry around the x, y, and z axes respectively.scale
: an array containing 3 floats EXACTLY, indicating the scale of the geometry in the x, y, and z axes respectively.An optional environment
dictionary with entries for:
filepath
: the name of the image file to be usedemittance
: an array containing 3 floats EXACTLY, indicating the illumination of the environment mapExample scenes can be found in the Scenes/
folder.
In order to make the code as portable as possible to different platforms, the UI is as simple as it gets: One ViewController with its view being cast as an MTKView
to potentially take advantage of Direct-To-Display capabilities in Tier 2 devices.
As soon as the view controller loads, a PolluxRenderer
is instantiated that handles everything. PolluxRenderer
conforms to the MTKViewDelegate
protocol and renders everything. In the initializer, it sets up the entire compute pipeline, and then later at each step, it calls pathtrace()
which then sets the appropriate pipeline stages:
GENERATE_RAYS
: Generates rays from the camera. This happens once per compute iteration.
COMPUTE_INTERSECTIONS
: Computes the intersections of the rays with the objects in the scene. This happens depth
number of times per compute iteration.
SHADE
: Accumulates color at each bounce based on the intersection with the ray and scene. It then reflects the ray off the object. This also happens depth
number of times per compute iteration.
FINAL_GATHER
: After the ray has been bounced around, it adds the ray's accumulated color to the frame buffer and displays it.
PlatformXXXXXXXX
:
In order for this project to work with both macOS and iOS, I had to consolidate the different types of View elements for different platforms. In order to do that, I use a process Apple calls "Shimming", which basically defines types based on what OS that this is being built on. This is done using preprocessor flags. A very helpful description of this can be found in this WWDC talk from 2014. Because there are different names for different data types between macOS and iOS, I just define the appropriate data type using a preprocessor #if
and a typealias
(which is exactly like typedef
in c) to make sure that the right type is being used. An example of this is how in macOS view controlles are called NSViewController
, but in iOS they are called UIViewController
. I just set the appropriate name to a unified PlatformViewController
that I can use throughout the code without having to worry about platform compatability. The full list of defined types is available in PlatformTypes.swift
.
SharedBuffer<T>
:
This project required the development of lots of support data structures to handle data management between the CPU and GPU. Luckily, I was able to encapsulate most large buffers in a SharedBuffer<T>
class that represents a buffer that is accessible to both the GPU and CPU, with its storage mode being .storageModeShared
. This allowed me to very cleanly use a buffer across the CPU and GPU with as much code as I would had it been an array on the CPU. The code is also extendable to all data types supported on the GPU, so feel free to use the code for any other Metal project you may need. TODO: Port this file to a framework/separate repo
.
simd_la
:
All of my linear algebra calculation is done using simd
types, which runs operations using simd
processing to optimize linear algebra calculations. There is already a very comprehensive linear algebra toolkit using these types, but I found that there wasn't anything that did 3D space manipulations using them. So I created a mini-linear algebra library for translate
, rotate
, and scale
operations for 3D vectors. This is used when I create the transformation matrices during scene parsing.
Loki
:
By far my favorite name for anything of made, this is basically a pseudo-random number generator built completely in Metal. TODO: bundle this up into a framework/separate repo
. This took around 2 days to get to work, because random number generators are very important with Monte Carlo based calculations because any periodic behavior is instantly visible to the viewer. I realized very quickly that variance was not as important as the rng's period when I first used this code as a framework and found very noticeable artifacts in the image. The final implementation is based on this research paper from 2012 that uses a combination of which a hybrid approach combining LFSR with a Linear Congruential Generator (LCG) in order to produce random numbers with periods of well over 2^121. This works very well in my code. I also extended the algorithm to combine up to three seeds, which in my case are the threadid
, iteration
, and depth
of each ray.
[ ] Scene file loading [ ] Naive Shading [ ] Multiple Importance Sampling [ ] Stream Compaction [ ] Acceleration Structure / GLTF Loading? [ ] AR Kit??
Better CPU/GPU Synchronization
Can't figure out how to work with semaphores for some reason. If any prospective reader wants to take a crack at it, look for MARK: SEMAPHORE CODE
in PolluxRenderer.swift
.
Implement a multi-buffer model to speed up write to display times There's a way to do this, just can't figure it out.
Play around with different memory storage modes for the Shared Buffers
Look into faster linear algebra computations in Metal (maybe using MPS?)
Different memory address spaces in my shaders for local variables
Illustrative scenes for reflection, refraction, subsurface scatter, and environment maps:
Ray Direction Debug View:
Broken Stream Compaction
Broken Partition
Code based on CUDA Path tracer
Mariano Merchante’s KD-Tree (with permission)
Volumes Subsurface Scattering and StackExchange Post for Subsurface Scattering
Swift OBJ Loader
NVidia’s Stream Compaction for Reference
FlexMonkey and Metal By Example
Metalkit.org