I'll spread this over several posts so I can add bits and pieces as I find the time to work on them. I have a bunch of code lying around that deals with glass, but since I'm sharing, I thought I'd take the opportunity to rethink the whole thing from scratch, and post as I build it. That way we can also discuss each step separately and critique the choices made along the way.
Requirements:
A complete glass shader should support the following optical properties: reflection, refraction, transmission, absorption, and dispersion (did I leave anything "visible" out?). All these effects are wavelength-dependent, so there's a big choice to be made along the way regarding the potential need for a spectral color model. This hypothetical "complete" model would be relatively expensive to compute (in *any* renderer) and clearly "overkill" in many situations (e.g: glass windows), so we'll need the ability to turn features on/off as needed (or end up with several specialized shaders if we can't keep the full version both flexible *and* efficient).
Space:
It is customary to do all reflectance calculations in tangent space, where the x and y axes are two orthonormal vectors lying on the plane tangent to the shade point "P", and z is the unit normal to that plane. This simplifies some of the math and can therefore lead to more efficient code. However, since both VEX and RSL provide globals and runtime functions in some space other than tangent ("camera" space for those two), working in tangent space will inevitably mean some transformations. Whether working in tangent space is still advantageous after that, remains to be seen. As a result, we'll need to look at the costs involved in writing our functions in either space, and base our final choice on what we find.
Naming Conventions:
I'll adopt the following naming conventions for the main variables:
vector n - unit normal to the surface
vector wi - unit incidence vector, points *toward* the source
vector wo - unit exitance vector, points *toward* the observer
vector wt - unit transmission direction
[float|spectrum] eta_i - index of refraction for the incident medium
[float|spectrum] eta_t - index of refraction for the transmissive medium (glass)
[float|spectrum] kr - fraction of incident light/wavelength that gets reflected
[float|spectrum] kt - fraction of incident light/wavelength that gets transmitted
All angles, unless otherwise stated, are in *radians*!
All vector parameters, unless otherwise stated, are expected to be normalized!
Fresnel:
This is the workhorse for glass, it alone is responsible for 90% of the visual cues that say "glass". The Fresnel functions determine the fraction of light that reflects off a surface (and also the fraction that gets transmitted, after refraction, *into* the surface). Glass is a "dielectric" material (does not conduct electricity), so we'll use that form of the function. We'll also ignore light polarization (we're doing glass, not gems... a full model for gem stones would need to take polarization into account).
But wait! both RSL and VEX already *have* this kind of fresnel function, so why re-invent the wheel?!?
Implementations are all slightly different among shading languages. Having our own will hopefully provide a homogeneous (actually, we're shooting for "identical") look and API across renderers -- if we find the renderer's native fresnel is identical to ours we could always choose to switch to the native version (which is usually faster).
The following is, to the best of my knowledge, an accurate Fresnel implementation in VEX for dielectrics (unpolarized). In case we find it useful at some point, I give it for both "current" space (camera space for VEX and RSL), and tangent space. Here's the fragment for world space:
// Full Fresnel for dielectrics (unpolarized)
//-------------------------------------------------------------------------------
// world space
void wsFresnelDiel(vector wo,n; float eta_i,eta_t;
export vector wr,wt; export float kr,kt;
export int entering)
{
if(eta_i==eta_t) {
kr = 0.0;
wr = 0.0;
kt = 1.0;
wt = -wo;
entering = -1;
} else {
float ei,et;
// determine which eta is incident and which transmitted
float cosi = wsCosTheta(wo,n);
if(cosi>0.0) {entering=1; ei=eta_i; et=eta_t; }
else {entering=0; ei=eta_t; et=eta_i; }
// compute sine of the transmitted angle
float sini2 = sin2FromCos(cosi);
float eta = ei / et;
float sint2 = eta * eta * sini2;
// handle total internal reflection
if(sint2 > 1.0) {
kr = 1.0;
wr = 2.0*cosi*n - wo;
kt = 0.0;
wt = -wo; // TODO: this should be zero, but...
} else {
float cost = cosFromSin2(sint2);
float acosi = abs(cosi);
// reflection
float etci=et*acosi, etct=et*cost,
eici=ei*acosi, eict=ei*cost;
vector para = (etci - eict) / (etci + eict);
vector perp = (eici - etct) / (eici + etct);
wr = 2.0*cosi*n - wo;
kr = (para*para + perp*perp) / 2.0;
// transmission
if(entering!=0) cost = -cost;
kt = ((ei*ei)/(et*et)) * (1.0 - kr);
wt = (eta*cosi + cost)*n - eta*wo;
}
}
}
The support functions like cosFromSin() and so on, are there just for convenience and to help with readability. These are included in the header.
After some testing, it looks like VEX's current version of fresnel() (when used as it is in the v_glass shader) and the custom one given above, are identical. Here's an initial test (no illumination, no shadows... this is just a test of the function).
Yes, you'd expect the ground to be inverted in a solid glass sphere. The one on the right is a thin shell. I ran a lot of tests beyond that image, and I'm fairly confident it's working correctly.
The first thing that jumps out from that image though, is the crappy antialiasing of the ground's procedural texture function for the secondary rays. In micro-polygon mode, you'd need to raise the shading quality to 4 or more to start getting a decent result... at a huge speed hit. It looks like there's no area estimation for secondary rays in micropolygon mode. In ray-tracing mode however, things are good -- whether it uses ray differentials or some other method, the shader gets called with a valid estimate and can AA itself properly. The micropolygon problem needs to get looked into though.
You would expect the built-in version to run faster than the custom one, and it does (~20% faster)... as long as you keep the reflection bounces low (1 or 2). As the number of bounces increases, our custom version starts out-performing the built-in one. Yes, this is weird and I very much suspect a bug. By the time you get to around 10 bounces, the custom code runs around 7 times faster (!!!) -- something's busted in there.
OK. That's it for now. It's getting late here, so I'll post a test hipfile and the code sometime soon (Monday-ish).
Next up: Absorption.
(glass cubes with a sphere of "air" inside and increasing absorption -- no illumination, no shadows, no caustics, etc)












