Jump to content

Glass


Mario Marengo

Recommended Posts

Stu already posted the ultimate glass shader, but I thought it might be fun to try it the old fashion way, just for giggles :P

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).

post-148-1149926813_thumb.jpg

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.

post-148-1149926350_thumb.jpg

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)

post-148-1149926738_thumb.jpg

  • Like 4
Link to comment
Share on other sites

Hey, thanks all :D I'm having fun dissecting this one (or re-dissecting), and I fully expect all of you to slap me upside the head if I start doing something stupid!

Jim (Wolfwood) has already contributed a lot by looking into some of the Mantra-side issues (some false positives, and some possibly real) and bouncing around approaches. So this is already very much *not* a one man show.

A fun trip ahead I think... though it'll take some time to get to the end I'm sure.

P.S: I re-read what I wrote and realized that one shouldn't attempt to copy-paste at 2AM... that code fragment wouldn't have compiled... way to go: screw up the bit that's preceded by "The following is, to the best of my knowledge, an accurate Fresnel implementation in VEX..." :lol: (I went back and fixed it... I think).

Link to comment
Share on other sites

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.

Hi Mario,

For those at home who are not so familiar with Mantra and want to learn some of the gory details, turning on raytracing (with the -r switch; found in the Command field's [+] button) affects only the primary rays. When enabled, it makes Mantra fire out the primary rays from camera (through each pixel) into the scene and shades any surface(s) they strike. When left alone in micropolygon mode, Mantra will "dice" up all the surfaces in the view to small micropolygons and shades all of these. However, if an object's shader wants to, it can shoot rays into the scene itself.

So, knowing that the secondary rays are always raytraced in either micropolygon mode or raytracing, this must be related to the sampling of the primary rays only.

So Mario et al, do you think this problem is due to the distribution of the micropolygons? i.e in the dicing method? Have you experimented with different surfaces topologies to test that out? What are you using currently? A Primitive sphere? I've long been confused about the algorithm Mantra uses to dice geometry. Trying to battle shader aliasing for rendering ocean surfaces makes me wish we knew the exact method so we could fight with our eyes open.

Or, less likely, do you think that somehow the calculation of the attributes that you're using eg. the surface Normal, differ in micropolygon mode?

Finally; have you tried to render this with 8.1? Are there any improvements you've noticed over 8.0 in this type of scene?

Thanks for all the wonderful info!

Jason

Link to comment
Share on other sites

I believe there is an error in the default VEX Glass SHOP...

The test to see whether you are entering or leaving the surface in the fresnel() function material reads:

dot(nn, N) < 0 ? 1/eta : eta

where nn is the frontfaced normals. This is being tested against N. The default render results are definitely not like Mario's above and explains why I never got results comparable to other examples.

Shouldn't the test be from the viewer to the surface normal? Like this:

fresnel(-V, nn, dot(-V, N) < 0 ? 1/eta : eta, kr, kt, R, T);

The Advanced RenderMan Book uses this form to test for the enter/leaving test (Listing 17.3, glass.sl, p491).

After doing this change to the default VEX Glass shader, the render looks pretty much like Mario's above.

post-371-1150077191_thumb.jpg

Now to get the top reflection in the thin wall glass sphere object.

If there is an error in the default VEX Glass Shader, then this tutorial needs ammending:

http://odforce.net/tips/shaderwriting4.php

and all my shaders derived from the default VEX Glass shader.

You really have to watch the surface normals on your geometry as well. In the thin wall sphere the inner sphere requires a Primitive SOP to reverse the inner primitive only so that the surface normals point inward.

Anxiously awaiting more Mario. Bravo! Fantastic!

Link to comment
Share on other sites

Disclaimer: what follows is complete, 100% speculation based on the results I'm seeing. I haven't spoken with anyone from SESI (yet) in order to confirm or deny any of it. I will try to get more reliable information as things progress, but right now, it's all just that: speculation... so please take it with a few large cubes of refractive salt :P

So Mario et al, do you think this problem is due to the distribution of the micropolygons? i.e in the dicing method? Have you experimented with different surfaces topologies to test that out? What are you using currently? A Primitive sphere? I've long been confused about the algorithm Mantra uses to dice geometry. Trying to battle shader aliasing for rendering ocean surfaces makes me wish we knew the exact method so we could fight with our eyes open.

I've tried different surface types and they all show slightly different versions of secondary-ray aliasing (at quality=1). I don't think it has to do so much with the distribution of the mp's as much as what it does with the derivative information (gathered at the mp) when shooting the secondary rays. A texture can only antialias itself when given a somewhat accurate estimate of the area being shaded. A pure ray tracer doesn't have the luxury of knowing the area of the surface each hit represents, so it usually approximates it using ray differentials, or by shooting lots of rays and filtering, or both. A micropolygon renderer knows the area of the micropolygon of the primary surface, but then would have to use some other method for the secondary rays. My gut tells me that when Mantra is in micropolygon mode, all secondary rays have an area estimate of zero (point sampling), whereas in raytracing mode, something like what I mentioned above is going on and the area estimates exist and are valid. In MP-mode you need to raise the quality, which shoots more point-sampled rays and so gets closer to the real result, at a price.

Now, I'm using the reflectlight() and refractlight() calls (1-sample with zero angular spread) in the current shader. It is possible that a different call, like trace(), might carry differentials with it (I haven't tried it). But even if it did, I don't think we would be much better off because then we'd have to handle the trace recursion ourselves, which would be a horrible mess inside a shader (assuming it's possible at all). Ditto with shooting 4 rays, one from each corner of the mp, since I believe derivatives might be non-existent by the time we get to the third surface and beyond.

One possibility that occurs to me would be to determine the angular differential at P (dNdu,dNdv) and shoot multiple uniform (not cosine-weighed) rays over the solid angle. But then we'd be back at square one if the derivatives at higher bounce levels are invalid...

We really need more information before we can make an intelligent choice for MP-mode. :(

Or, less likely, do you think that somehow the calculation of the attributes that you're using eg. the surface Normal, differ in micropolygon mode?

I sincerely hope not! :)

Nah, I really don't think N or any other global is different between the two modes. Derivatives on the other hand... and specifically how they get carried around during recursion... *that's* where I suspect a difference.

Finally; have you tried to render this with 8.1? Are there any improvements you've noticed over 8.0 in this type of scene?

I haven't tried any of this in 8.1 yet. I'll post the test bundle soon (I just need to clean up a couple things), then we can all do some testing of this stuff.

Cheers!

Link to comment
Share on other sites

Hey Jeff,

Shouldn't the test be from the viewer to the surface normal? Like this:

    fresnel(-V, nn, dot(-V, N) < 0 ? 1/eta : eta, kr, kt, R, T);

Hmmm... not so sure about that. At one point, I did a comparison between my custom Fresnel and the v_glass shader just to see the difference, and they were pretty much identical, provided that:

1. Your objects all have correctly oriented normals. (really, no glass shader will have a hope in hell of "getting it right" unless this is the case).

2. The shader doesn't force shading normals to face front (or manipulate them in any other way before computing Fresnel)... ever.

3. And one other somewhat mysterious thing... something that seemed puzzling at the time (and which I sort of "shelved" for further testing), was the result I got after removing the sidedness logic from the fresnel() call itself (moved it outside the function call)... it started giving me all sorts of unexpected results. For example, when, instead of calling it in the way that the v_glass code does:

fresnel(-V, nn, dot(nn, N) < 0 ? 1/eta : eta, kr, kt, R, T);

you called it something like this:

vector n = normalize(N);

vector wo = normalize(-I);

float myeta = dot(n,wo)>0.0 ? 1.0/eta : eta;

fresnel(-wo, n, myeta, kr, kt, R, T);

then all kinds of craziness ensued... maybe the optimizer was chopping off something important... or maybe I was doing something wrong. I didn't persue it at the time because I was concentrating on the custom version, but it's worth checking out... even if it's just to confirm that I'm just full of poo :)

You really have to watch the surface normals on your geometry as well. In the thin wall sphere the inner sphere requires a Primitive SOP to reverse the inner primitive only so that the surface normals point inward.

Yup. This is *very* important. And it will be equally as important with the shader I'm developping here. There's no way for the shader to handle arbitrary normal directions predictably over multiple bounces.

P.S: the top reflection in the spherical shell is in the region of total internal reflection of the inner surface, and I *believe* the built in fresnel() handles TIR propperly now, so it should show up if you raise the number of reflection bounces high enough... :unsure:

Cheers!

Link to comment
Share on other sites

My gut tells me that when Mantra is in micropolygon mode, all secondary rays have an area estimate of zero (point sampling), whereas in raytracing mode, something like what I mentioned above is going on and the area estimates exist and are valid. In MP-mode you need to raise the quality, which shoots more point-sampled rays and so gets closer to the real result, at a price.

28503[/snapback]

Would this mean that if your groundplane shader (your griddy thing) was not antialiased that you should get more equal results? In other words, if you're concerned that in one mode you're getting a sample area approximate carried into the derivatives and in the other mode they're set to zero (point sample), then maybe you can make sure your groundplane shader is heeding the derivatives passed to it.

Perhaps if you have your groundplane shader printf out the derivatives when your raylevel() is greater than 1?

Cheers,

Jason

Link to comment
Share on other sites

Would this mean that if your groundplane shader (your griddy thing) was not antialiased that you should get more equal results?  In other words, if you're concerned that in one mode you're getting a sample area approximate carried into the derivatives and in the other mode they're set to zero (point sample), then maybe you can make sure your groundplane shader is heeding the derivatives passed to it.

Perhaps if you have your groundplane shader printf out the derivatives when your raylevel() is greater than 1?

Yes, the grid shader uses the stripes VOP (pulse train) which does take the reported area into account to AA itself.

As far as printing the area reported by secondary rays in MP-mode, well... I did, and no, I'm not getting zero, so there goes that theory :P

And now that I look at it more closely, even the geometric edges are aliased (in the reflections/refractions), so something else is going on here.

Hmmm... time to ask the "big people" about this, as it is hard to construct a meaningful test for secondary rays in the shader(s).

Link to comment
Share on other sites

Absorption...

The Model:

I'm going to start with the simplest possible model: Beer's Law (there are a billion entries if you Google for it). This is just an exponential decay: exp(-k*d), where k is, in this case, an absorption coefficient, and d is the distance travelled within the absorbing medium (glass). This model is like the 0th-order approximation to scattering, and given that we have some of the machinery for ray-traced single scattering from the SSS shader, we could always bring that stuff over here and attempt a more sophisticated model. But this simple model will do for now -- we can always return and enhance it later.

Determining Distance:

We need to determine how far the ray has travelled inside the glass in order to calculate the loss in intensity due to absorption (we're looking for the d in Beer's Law). One way to do this would be to, upon a ray entering the glass, call rayhittest() in the transmitted ray's direction, and see how far the hit happened, if at all. But there's a way to do this without any ray casting. When we call reflectlight() or refractlight() from the shader, what really hapens is a recursive sequence: our shader casts one or more rays, some of which will hit our glass again, calling our shader, which may cast more rays... etc, up to the "bounce level" limit set by the user. We can use this to our advantage to determine d, like this: if the ray is secondary (raylevel>0) and it is exiting the glass (entering==0), then attenuate the result according to the distance length(P-Eye). The reason for using length(P-Eye) instead of length(I) is that I is not guaranteed to have the right length for secondary rays (in our tests for Mantra it seems to always be unit length for secondary rays).

Here's a pyramid viewed from above through an ortho camera and with eta=1, showing extinction due to distance travelled:

post-148-1150260525_thumb.jpg

Attenuation by Absorption:

Colored glass gets its color from absorbing/scattering some wavelengths and letting others pass through, like a filter. This means there are different absorption coefficients (the k in Beer's Law) for each wavelength. We'll treat the RGB components of our color model as three separate wavelengths (this is *not* the same as working with a spectral color model, but it's the simplest, and might just be good enough for our needs).

From the artistic point of view though, we'd like to be able to say "I want my glass to be *this* color", instead of thinking in terms of absorption coefficients, which would require some weighted version of the *complement* of that color -- e.g: you'd need to set absorption to cyan in order to get red, which isn't be very intuitive at all. To further complicate matters, absorption is tied to distance -- there's more attenuation the farther the ray travels. So, say we're thinking in terms of positive colors (instead of complementary ones), and we set absorption (or "glass color" in our case) to the color {1,0.5,0.1} (expecting the red component to travel the farthest), then there will be a point after a certain distance, at which the *only* visible color will be pure red (because the green and blue components will have died out).

Some big choices to be made here.

I've opted for simplicity of use over physically based parameters. Basically, I'm providing a "Glass Color" parameter and then internally reinterpreting it as absorption coefficients such that the results are hopefully not too far from the given color. But even then, there are at least two distinct ways to go about it: 1) we attenuate "white light" and then tint the result (a lazy hack that results in a somewhat "painted" look), or 2) do the exponential decay with different weights for each RGB channel (a little closer to the "real thing"). For now, I've left the two approaches in the shader; they can be selected with the "Absorption Type" parameter, which defaults to the RGB method.

Here's a comparison, with the "tint" method (on the left) and the "RGB" method (on the right).

post-148-1150260537_thumb.jpg

To control the overall amount of absorption (divorced from color) I've added the parameter "Absorption Strength" which is a scalar multiplier of distance. But sometimes we may also want to alter the *rate* at which the falloff happens (the shape of the curve itself), so I've added an "Absorption Exponent", which is the exponent to which distance is raised before plugging it into the function. Numbers lower than one make absorption happen at a faster rate, whereas numbers larger than 1 make it more gradual.

Here are a few images to illustrate what I mean. From left to right, the exponents are 0.5, 1, and 2:

post-148-1150260549_thumb.jpg

It also occurred to me that, even though both transmitted and internally reflected rays undergo absorption, it would be useful to add a couple of controls to tweak the extent to which either type of ray is attenuated. So I added two parameters: "Transmitted Weight" and "Reflected Weight". These are both [0,1] weights and are used to control how much absorption influences each type of ray... for some interesting results.

Left: trans=0, refl=0, Middle: trans=1, refl=0, Right: trans=0, refl=1

post-148-1150260570_thumb.jpg

Color Correction:

I was never a big fan of the standard "Tint" and "Amp" parameters that are usually given for reflection, transmission, etc. These are really primitive color correction controls -- a surface has a color (albedo) and glass has a color (transmission with absorption), the rest is color correction of the various reflectance/scattering models. So I've separated those controls in their own color correction tab. Right now there's a set for reflection and transmission (I guess I should add one for global correction). And for now, each set has HSV, gamma, contrast, and tint. this is the standard set we use over here, but I can always add others if someone can think of a good reason.

OK. That's it for basic absorption. Next up is dispersion.

Here be the bundle. Enjoy! :)

Glass_Part2.zip

Link to comment
Share on other sites

You should really send that shader to SideFx so that they have a proper VexGlass for the next release of Houdini
Really your shader must be included in Houdini installation

But, but... wait! it's not finished yet! :huh::P

Thanks for the compliment guys, but I'm sure SESI would have absolutely no problem writing a better version of anything I do here, and I'm sure they eventually will. For the moment, I think they're concentrating more on improving Mantra itself -- and that's music to my ears! :)

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...