Rendering Diamonds

by Steve Olivieri

This project focuses on rendering realistic diamonds. It was inspired by two projects from the Stanford Rendering Content, listed as [1] and [2] in the references. This website serves as documentation for the project.

A real diamond.

A real diamond.

Stanford #1

Stanford Rendering Contest Project #1 [1].

Stanford #2

Stanford Rendering Contest Project #2 [2].

3D Modeling

Because I have no experience with 3D modeling, I decided to use diamond meshes from 3D Lapidary. For this project, I chose the "Round Brilliant" mesh [3].

Unfortunately, the meshes from 3D Lapidary are in Auto-CAD's DXF format. Since wxRayTracer only reads PLY files, I needed to convert them. I used meshconv, a conversion tool developed at Princeton. This tool runs in both Linux and Windows, and converts many popular 3D file formats [4]. Use meshconv with the following parameters to convert a DXF file into a PLY file that wxRayTracer can read.

meshconv -c ply -tri -plyold diamond.dxf

The above commands instructs meshconv to ouput a PLY file (-c ply), to triangulate the mesh (-tri), and to use the old PLY standard that wxRayTracer supports (-plyold).


I focused on two methods for rendering realistic diamonds. The first, Fresenel Reflection, is covered in Ray Tracing from the Ground Up [15], so I will cover it only briefly here. The second, spectral dispersion, is the heart of this project.

Fresnel Reflection

The Fresnel equations provide us with a physically realistic model for rendering transmissive objects. When light is reflected on the surface of a transmissive object, we must consider not only the incidence angle but also the polarization of the light wave. These two equations specify the reflection coefficient for light parallel and perpendicular to the incident plane.

Fresnel Equations

If we ignore the polarization of the light, we can simplify the reflection coefficient to the following equation.

Reflection Coefficient

Of course, the transmission coefficient is simply 1-R. For more details on Fresnel reflection, consult Chapter 28 of [15].

Spectral Dispersion

Transmissive materials have an index of refration that specifies the distortion of light entering them from a vacuum. In a traditional ray tracer, all light is refracted at the same angle. In this project, I attempt to implement wavelength-dependent refraction in transmissive materials. According to Snell's Law, the angle of refraction depends on the wavelength of the light and the angle of incidence.

Snell's Law

In order to model spectral dispersion in wxRayTracer, we need a way to model the dispersive properties of materials and a way to represent monochromatic light.

Modeling Dispersive Materials

One common way to describe the dispersive power of a material is to determine its refractive index at the B and G Fraunhofer Lines and take the difference.


The DISP for diamond is 0.044 [7]. In wxRayTracer, we must create a new BTDF that holds this information. I created a copy of the FresnelTransmitter BTDF, presented in Chapter 28 of the class text, called DispersiveTransmitter. This new BTDF adds a member called DISP and features a revised sample_f() routine, shown below.

RGBColor DispersiveTransmitter::sample_f(const ShadeRec& sr, const Vector3D& wo, Vector3D& wt) const { Normal n(sr.normal); float cost_thetai = n * wo; float eta; float e_out = eta_out; static double nVS = (sr.w.vp.vs_max - sr.w.vp.vs_min + 1); // Adjust the IOR for monochrome wavelength, using DISP. if(sr.ray.isMonoChrome) { double wl = sr.w.vp.get_color_wl(sr.ray.wlIndex); e_out += (0.5f * disp * ((wl - sr.w.vp.vs_min) / nVS)); } // Invert IOR and normal if transmitted ray is outside. if(cos_thetai < 0.0 { cos_thetai = -cos_thetai; n = -n; eta = e_out / eta_in; } else { eta = eta_in / e_out; } float temp = 1.0 - (1.0 - cos_thetai * cos_thetai) / (eta * eta); float cos_theta2 = sqrt(temp); wt = -wo / eta - (cos_theta2 - cos_thetai / eta) * n; return (fresnel(sr) * white / (eta * eta) / fabs(sr.normal * wt)); }

The new material that uses the DispersiveTransmitter BTDF is called Diamond. Diamond is based on the Dielectric material presented in Chapter 28 of the class text. In place of the FresnelTransmitter is the new DispersiveTransmitted BTDF. In addition, Diamond has a new shade() function. This new shade() function splits non-monochrome rays into monochrome rays and then traces them. Only some of the new monochrome rays will rearch light sources and contribute to the final shade value.

Diamond::shade(ShadeRec& sr) { RGBColor L(Phong::shade(sr)); Vector3D wi; Vector3D wo(-sr.ray.d); RGBColor fr = fresnel_brdf->sample_f(sr, wo, wi); Ray reflected_ray(sr.hit_point, wi); float t; RGBColor Lr(0), Lt(0); float ndotwi = sr.normal * wi; if(fresnel_btdf->tir(sr)) { if(ndotwi < 0.0) { Lr = sr.w.tracer_ptr->trace_ray(reflected_ray, t, sr.depth + 1); L += cf_in.powc(t) * Lr; } else { Lr = sr.w.tracer_ptr-<trace_ray(reflected_ray, t, sr.depth + 1); L += cf_out.powc(t) * Lr; } } else { // Reflection Lr = fr * sr.w.tracer_ptr->trace_ray(reflected_ray, t, sr.depth + 1) * fabs(ndotwi); // Transmission if(sr.ray.isMonochrome) { Vector3D wt; RGBColor ft = fresnel_btdf->sample_f(sr, wo, wt); Ray transmitted_ray(sr.hit_point, wt, sr.ray.depth, true, sr.ray.wlIndex); float ndotwt = sr.normal * wt; Lt = (sr.w.vp.get_color_rgb(sr.ray.wlIndex) * (ft * sr.w.tracer_ptr->trace_ray(transmitted_ray, t, sr.depth + 1) * fabs(ndotwt))); } else { Ray tmp(sr.ray); for(int i = 1; i < sr.w.vp.color_samples + 1; i++) { Vector3D wt; sr.ray.isMonochrome = true; sr.ray.wlIndex = i; RGBColor ft = fresnel_btdf->sample_f(sr, wo, wt); Ray transmitted_ray(sr.hit_point, wt, sr.ray.depth, true, i); float ndotwt = sr.normal * wt; Lt += ((ft * sr.w.tracer_ptr->trace_ray(transmitted_ray, t, sr.depth + 1) * fabs(ndotwt))); sr.ray = tmp; } } if(ndotwi < 0.0) { L += cf_in.powc(t) * Lr; L += cf_out.powc(t) * Lt; } else { L += cf_out.powc(t) * Lt; L += cf_in.powc(t) * Lt; } } return L; }

To support these changes, I had to modify the Ray class and the ViewPlane class. The Ray class declaration now contains two new members.

class Ray { Point3D o; // origin Vector3D d; // direction int depth; // reflection depth bool isMonochrome; // used for dispersive transmissions int wlIndex; // index to spectrum colors for monochrome light // constructors, etc. }

The changes to the ViewPlane are more extensive. First, I added three new members to the ViewPlane.

class ViewPlane { public: // ... int color_samples; // number of color samples in spectrum float vs_min; // minimum spectrum value float vs_max; // maximum spectrum value protected: vector<double> wlSamples; // spectrum values vector<RGBColor> wlColors; // RGB equivalents for wlSamples private: void generateSpectrum(void); RGBColor wl_to_rgb(const double wl); }

Then, I added primitive spectrum support to the ViewPlane. I based my implementation on the PBRT ray tracer [12] and [11]. Unfortunately, wxRayTracer uses RGB colors throughout and I did not have time to change everything to XYZ CIE spectrum. This is a significant limitation in my implementation. The two most interesting new methods in ViewPlane are wl_to_rgb():

static float rgb_adjust(double c, double factor, float gamma) { if(isZero(c)) return 0; else return pow(c * factor, gamma); } RGBColor ViewPlane::wl_to_rgb(const double wl) { float r, g, b; float factor; if(wl < vs_min || wl > vs_max) { r = g = b = 0; } else if(wl < 439) { r = -(wl - 440) / 60); g = 0.; b = 1.; } else if(wl < 489) { r = 0.; g = (wl - 440) / 50; b = 1.; } else if(wl < 509) { r = 0.; g = 1.; b = -(wl - 510) / 20; } else if(wl < 579) { r = (wl - 510) / 70; g = 1.; b = 0.; } else if(wl < 644) { r = 1.; g = -(wl - 645) / 65; b = 0.; } else { r = 1.; g = 0.; b = 0.; } if(wl < vs_min || wl > vs_max) factor = 0; else if(wl < 419) factor = 0.3 + 0.7 * (wl - 380) / 40; else if(wl < 700) factor = 1.; else factor = 0.3 + 0.7 * (780 - wl) / 80; r = rgb_adjust(r, factor, gamma); g = rgb_adjust(g, factor, gamma); b = rgb_adjust(b, factor, gamma); return RGBColor(r, g, b); }

and generateSpectrum(), which is called whenever one of the new ViewPlane members is changed:

void ViewPlane::generateSpectrum(void) { wlSamples.erase(wlSamples.begin(), wlSamples.end()); wlColors.erase(wlColors.begin(), wlColors.end()); // Generate new spectrum values. float d = (vs_max - 20) - (vs_min + 20); float step = d / (color_samples - 1); // Use IEC values for 3-color spectrum. if(color_samples == 3) { wlSamples.pusb_back(vs_min); // non-mono, cancels in calculations wlSamples.push_back(435.8); // blue wlSamples.push_back(546.1); // green wlSamples.push_back(700.0); // red wlColors.push_back(RGBColor(1.0)); wlColors.push_back(RGBColor(0., 0., 1.); wlColors.push_back(RGBColor(0., 1., 0.); wlColors.push_back(RGBColor(1., 0., 0.); } else { // Non-monochrome cancelling color wlSamples.push_back(vs_min); // Spectrum samples wlSamples.push_back(vs_min + 20); for(int i = 2; i < color_samples; i++) wlSamples.push_back(wlSamples[i - 1] + step); wlSamples.push_back(vs_max - 20); // Generate the RGB equivalents for the new spectrum. wlColors.push_back(RGBColor(1.0)); for(unsigned int i = 1; i < wlSamples.size(); i++) wlColors.push_back(wl_to_rgb(wlSamples[i])); } }

Finally, I had to make a small change to the hit_objects() method in the World class. If the ray hits an object, that ray is now stored in the ShadeRec object returned by hit_objects().


Here are some results from the project. While these images show that the concept works, none of them is very realistic looking. I simply ran out of time to make a better image. I believe that the camera and light positioning is responsible for many of the strange defects in these images. A better ray tracer would have area lights and caustics working properly.

Diamond with total internal 

This diamond has almost no transmitted rays, and no color. The model was designed for an IOR of ~1.4, but diamonds have an IOR of ~2.417. The pure white face is reflecting all colors of light at the same IOR.

Diamond with yellow color, IOR 

This diamond has an IOR of only 1.1. The yellow and white highlights are the result of my spectral dispersion implementation. This scene was rendered with only three color samples.

Diamond with yellow color, 
IOR 1.4.

The same scene again, this time with an IOR or 1.4. The colors look better, but again we use only three samples.

Sidways diamond with 
yellow color.

This scene turns the diamond on its side and uses an IOR of 1.1 so that we can better see what's happening. There is color banding at the top due to the limited number of color samples (again, 3). Everything is too yellow!

Diamond with 9-color 

The diamond in this scene has an IOR of 1.4 and the spectrum now contains nine samples. There are artifacts on the band separating the head from the bottom where the IOR is not correct. Despite adding more colors, everything is still too yellow. This final image took exactly 11 minutes to render (16 samples, depth of 20).

Diamond with 9-color 
spectrum and DISP = 0.1.

The previous scene again, this time with DISP = 0.1. This shows the effect of changing the dispersion power of a material. Still too yellow, though!


[1] Standford Project #1:

[2] Standford Project #2:

[3] Gem Models:

[4] Mesh Converter:

[5] Graphics Gems Revisited:

[6] CIE Color Space:

[7] Rendering Diamonds:

[8] Fresnel Equations:

[9] Beer's Law:

[10] Rendering the Phenomena of Volume Absorption in Homogeneous Transparent Materials:

[11] Spectrum to RGB:

[12] PBRT Documentation:

[13] Rendering Light Dispersion With A Composite Spectral Model:

[14] Diamond Image:

[15] Ray Tracing from the Gound Up:

Valid XHTML 1.0 
Transitional Valid CSS!