Making a Glass Shader, Part 2: Highlights & Reflections

This lesson is a continuation of a tutorial on how to make a very shiny glass shader. Make sure you've finished that part before continuing on with this one!

When we last left off, we had a shiny reflective shader with transparency and distortion, and we're basically 90% of the way to the look we want, but we're probably only halfway through the work. Ain't it always that way?

Remaining todos:

We want a rim that's stronger on one side of the object
We wanna boost those shiny highlights
We wanna have toon lighting!

So here's the deal about our pending todo list: we can't do any of them from inside a surface function. Yeah. Not a one. We have to be able to control the lighting for those, and we don't have access to lighting data from inside the surface function. We're gonna have to roll our own custom lighting function again, like we did in Part 10 when we made toon lighting. Time to make light of the situation (ha...)

### Nonstandard Standard

The lighting we do this time will be different from in Part 10 because we want to continue to use the Standard light model. We want that because if we stay within the Standard model, there's a lot of initialization and stuff being moved behind the scenes to improve our light and shadows and such that we can take advantage of. It's quite unintuitive, so I had to piece together how to do it by staring at the internal Unity shaders for a few hours until they started making sense. Fortunately it's not actually very hard, it's just that there isn't any documentation.

First, let's get that toon lighting going. Add these properties:

Then update your surface pragma to

#pragma surface surf StandardGlass

#include "UnityPBSLighting.cginc"

then declare the new variables

When we use the standard lighting model, we have to declare TWO lighting functions. An actual lighting function and a GI function. The GI stands for Global Illumination, and it's going to deal with the actual light and shiny stuff, and then the lighting function is basically the final stop.

Add these functions -- the names are important, they have to follow the format established in the surface directive.

Let's get our GI function going first. This isn't so different from what we did in Part 10, just a bit out of order.

in the non-GI lighting function, if you just return the gi.indirect.diffuse color, you'll get this

Note that if you play with the Occlusion slider, you'll see the shadow color change, because the Occlusion value is being used to determine the strength of the ambient light. (Stuff like that being handled for us is one of the advantages of staying in the Standard model!)

If you play with the metallic and smoothness sliders though, you'll find they don't do anything anymore. That's because nothing's being done with our metallic and smoothness values right now.

What about the rim? Well that's written to the Emission value, and the whole point of Emission is to ignore lighting, so we're bypassing messing with the emission stuff entirely.

### Reflections

Incidentally, here are some of the values we have access to in the GI function from the UnityGIInput getting passed in

Pretty darn handy! That's more data than we had access to when making our first toon lighting function!

unity_SpecCube0 is a special variable that's set automatically behind the scenes, and it contains the most important reflection probe to the object in question, which exists here as a texcube data type.

Tex cubes are basically enclosed seamless textures, like 360 panoramas. We can't just sample them straight up like normal textures though because they have HDR data that has to be decoded into the color space for our shaders. The decode data for these texcubes gets autofilled into special variables that follow the format [name]_HDR, which is a lot like how we've used the autofilled [name]_ST in the past.

What I think is really cool here though, is that the number that gets put in the 3rd argument of UNITY_SAMPLE_TEXCUBE_LOD determines how blurry the result is. UNITY_SPECCUBE_LOD_STEPS is defined internally as 6. So specifying a range between 0 and 6 determines blurriness, with 0 being the original texcube color, and 6 being super blurry. We should keep using  UNITY_SPECCUBE_LOD_STEPS  instead of substituting 6 because that'll help us remember that 6 isn't an arbitrary number we came up with and so we shouldn't change it. Also I think it's possible to set number of LOD levels in quality settings? Need to mess with that more. Anyway.

This is a matter of taste but I added the last line,

gi.indirect.specular *= max(0.25, gi.indirect.diffuse);

because I think the reflections would look more natural if they were a bit darker in the shadows, but not too much darker, so this multiplies them by the larger of either the final light color or 0.25, so they'll never get darker than a quarter of their original brightness.

We can test all this out by updating our LightingStandardGlass function to this:

woohoo we've got reflections again. Check out how when you slide the smoothness levels down and up the reflection goes from blurry to sharp. You'll also notice that even though the alpha value of the color here is at about 0.5, there's no transparency. That's because we're not using alpha transparency, so we'll need to bring back the Albedo color we set in the surface function.

Let's make this update

We're multiplying s.Albedo by 1 - Metallic so that the more metallic the object is, the less of its original color comes through. We're then using s.Albedo * _ShadowColor as the floor for how dark this color can possibly get, because there's not much point in having a stylized custom shadow color if we're just go and multiply everything by black anyway.

So here's what we've got so far

I don't think anyone has ever implemented toon lighting quite like this before. It's the wild west out here! I invented this method just for this tutorial but it's pretty cool, I might do it this way from now on when I'm in the Forward rendering path.

It's not immediately obvious but we're missing something important. Can you tell what it is?

Highlights! We lost our highlights! Time to add them back.

### Highlights For Kids & People Who Don't Know How To Shader

Let's add some properties for our highlights. We'll also add our property for the side rim we're gonna add.

Declare these new ones above the lighting functions. Also move _RimColor up here. (Make sure you delete it down below, you'll get compiler errors if it's declared twice!)

Now we can add this at the bottom of our GI function. Note that the more metallic the object is, the more the gloss color comes from the Albedo color!

This however, introduces a new problem. When the object is very metallic and uses the albedo color for the highlight, it just makes the object look more transparent within the highlights because the grabtex color is coming through.

We want the object to become LESS see-through when there's a highlight, not more! So we'll have to fix this! We can't sample the original texture again from the light function, we don't have the UV coordinates anymore. So we'll need to rearrange something else.

declare a half3 grabColor above your lighting functions. Since it doesn't start with an underscore, this will help us remember that this doesn't come from a material property.

We're gonna change our surface function to this:

So we're not even writing the grab texture to albedo here anymore, we're just chucking it into that variable for later!

We'll now update our LightingStandardGloss function to put the blend right before we add our reflections and highlights.

Since the UnityGI stuff is initialized before this function runs, when we mix s.Albedo for our highlight, it hasn't been mixed with the grab texture yet, so we will definitely get the original albedo color.

There we go.

I still think that looks a little strange. These highlights should be brighter.

We're multiplying the final gloss mix by unity_ColorSpaceDouble, which is defined in UnityCG.cginc and makes the color twice as bright correctly based on whether or not you're in Gamma or Linear color space. We're then also multiplying the _GlossSmoothness / glossSize * 2 to account for this, so that our gloss smoothness slider still does something.

Oh yeah, that looks great. But we can do better. Shinier.

This GGXTerm thing is a function I found buried deep in the builtin CGincludes (specifically, UnityStandardBDRF.) It adds a really bright specular, which is usually not cartoony enough for my taste, but I think it works great when combined with a toony specular.

Positively blinding!

Alright! And for our final touch, the side rim!

Basically, this is a rim like a typical rim, except we keep doing dot products with the light direction, to have the rim's placement be more heavily determined by light direction than view direction.

Save and have a look!

oh yeah! I guess the difference is suble but I think it goes a long way towards giving the illusion of thickness. I think she finally looks properly glassy now.

Whew, we did it! For extra fun, try setting the shadow color to a blue/green color to get some nice coke bottle vibes.

It also wouldn't be too hard to modify this shader to use a samplerCube passed in as a material property for reflections rather than the scene reflection probe, but I'll leave that to ya'll as extra credit.

The shader we made in today's tutorial is attached to this post as XibGlass.shader.  You can also check out the code online here. If you have any questions or want to share what you come up with, let me know in the comments here, on Twitter, or in Discord. And if this tutorial helped you out, please consider becoming a patron!