Making a Glass Shader, Part 1: GrabPass & Refraction

Last time we learned to make a chromatic aberration image effect. In this lesson we're going to learn how to use Grabpass to create a shader that makes objects look like they are made of glass!

Setup

Open a scene in the Unity Editor. Make sure you have at least one light (there will be a directional light by default in a new scene!) Within the Hierarchy, right click and go to Light > Reflection Probe to add a reflection probe to the scene.


Click the reflection probe so that you can see its component in the Inspector tab. Change the Type dropdown to Custom


If you got the Mokapot asset we used in Lesson 18, go to the BJ MokaPot folder in your assets and drag the Demo_ReflectionProbe into the cubemap slot in the component. If you don't have it, I've attached it to this post as Demo_ReflectionProbe.exr. Your component should look like this:


In the lighting tab, put your favorite skybox into the Skybox Material slot. Set the environment lighting source to Color and set the ambient color to a light gray.


Add a plane, put its transform position at 0, 0, 0.

Download these textures into your assets and extract the files from the zip. https://freepbr.com/materials/planet-surface-2/ 

Make a new material called PlanetSurface and put it on the plane. (You can use the Standard shader or any other shader we have made to this point!) We won't do anything else to this material, but since we're going to make a glass shader we'll want things that we can see though our glass objects so that we know our shader is working!

Finally, drag Claire (or any other model you like) into the scene and also put her transform position at origin. (origin is another way of saying 0,0,0!)

If you have Gizmos turned on, your scene should look something like this:


If you want to see the gizmos but you can't, press the button circled here:


When it's on, it looks like this:


If you still can't see them, click the arrow that looks like an upside down triangle next to the word Gizmos and make sure that 3D Icons is turned on. You can toggle the icons next to the Camera and Reflection Probe types to turn their icons on and off individually.


Transparency

Glass is commonly found in day to day life, so it's also something we commonly want to have show up in our video games. So what does glass actually look like? Well, it's see-through. By that I mean, you can see stuff that's behind it. But it's not invisible. It's shiny. What does shiny mean? Well, it has highlights from light hitting it, and often you can see reflections on it. 

Let's see if we can get something like that using the built in Standard shader. (This part is optional, you don't have to follow along unless you want to!)

I added a sphere to the scene and put a new material on it I called Standard Transparent.


If I turn down the alpha value of the main color, the sphere doesn't look any different.


It does if I set the rendering mode to Fade though, but that doesn't look like glass. Looks like a thin plastic film imo.


Turned up smoothness all the way, but it seems more like a milky bubble.


I turned up Metallic halfway. I guess that's more glass-like, but I'm not really feelin' it.


Turning up metallic all the way is just worse.


(Incidentally getting these reflections is why we've added a reflection probe to our scene. This is what happens if I disable the component without altering any of the properties I just set in the material)


Even worse, if we lower the alpha value by a whole lot, the whole thing is barely visible, which is not how glass works at all.


Setting Rendering Mode to Transparent is an improvement, as now the shiny highlights stay visible even when the alpha is at zero.


 But there's still just...something missing. Cripes, it's enough to drive one to drink!


speaking of which, check out this shot glass. Ooh and look at what happened to the silhouette of my fingers!



the stuff you can see though the glass is a bit distorted. Some kinda science related to this about light bending, I don't really know much about it, but I know it's called "refraction" and we should try to make our shader do something like that.

There's something else I noticed. That sphere in the game looks paper thin, but glass is heavy and has thickness.


If you look closely, there are highlights on the inside if the silhouette of the shot glass that kinda trace its form and give it solidity. It seems like a good opportunity to use a rim like we learned how to make in lesson 9! But the rim on this glass doesn't have a uniform thickness, it's definitely easier to spot in some areas than others.

Alright, so we have our task cut out for us. We need to make a shader that is reflective, has shiny highlights that stay visible even when transparency is low, we need to have a rim that's heavier in some places than others, and we need to be able to see objects behind the shader, but distorted. Let's get started!

The Basics

Duplicate the Claire model -- let's have at least two in the scene so that we can have stuff to see through the transparency. Move the duplicate a bit back and to the side so it's not in the way. Manually or using the cool tool I made, make a new Surface shader with a file name XibGlass and a shader path "Xibanya/Special/XibGlass". Make a new material called ClaireGlass. Put the XibGlass shader on the material. Put the material on Girl_Body_Geo in the hierarchy. Put Claire's main albedo texture into the texture slot. (It is called Girl01_diffuse in your assets.) You should have this:


Time to go into the shader and add some essentials. Like these properties!


I declared the new properties in the SubShader. I've added float2 uv_BumpMap and float3 viewDir to the input struct and deleted some template cruft.


You don't have to indent-align your variables like I do, I just like doing that.

Down in the surface function, I'm unpacking the normal like we learned how to do in Lesson 5, and I'm adding a rim like we learned how to do in Lesson 9.


Save and go back to Unity. Now we can drag in the normal map (called Girl01_normal in your assets) and our sliders to make her all shiny.


However, if you set the alpha value of the main color to anything less than 100%, nothing happens, even if the render queue is transparent.


Behind the scenes, unless you tell a surface shader otherwise, no matter what you do in the surface function, it will always set the alpha value to 1 behind your back!

Alpha Transparency & Z Ordering 

We can start by updating our tags to this:

Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }

But that won't quite be enough. The line #pragma surface surf Standard tells Unity how to read this shader, and the stuff you add after that are basically options for tweaking how this shader gets interpreted. One option we can add is "alpha," which tells Unity that we actually want to keep our alpha value. If we do that though, we'll have another problem!


Now all the parts of the mesh are getting drawn all at once. I know we asked for transparency, but this isn't what we had in mind. The issue here is with the Z order. When we talk about Z, we usually mean depth (Y is up and down, X is side to side.) We want things shown in the right Z order -- we want things to be shown in the right back to front order.

There's something called a Depth Buffer, this is temporary storage Unity reserves for keeping information about how to draw things back to front. If an object is behind something, usually it's hidden, so we don't need to worry about its Z order anymore, so it's not kept in the Depth Buffer, but we can tell Unity to keep certain things written in specifically, which is helpful when we're dealing with transparency. So the fix for this is to add this after your tags but above the CGPROGRAM block.


ZWrite On tells Unity we actually do want to write this to the Depth Buffer, and ColorMask A means to only write the transparency, leaving the actual color for our next pass, which is our surface shader.

This fix works perfectly!


Cool, okay well, you can delete it now. Why? 'Cuz we're actually not gonna use alpha transparency at all! I just showed you that because it's a very important fix to learn and you'll probably use it a lot later. No, we're not gonna use alpha transparency today -- we're gonna do something fancier!

GrabPass

Where we put that ZWrite pass, let's instead put a different kind of pass -- the GrabPass!


This is a special pass that captures a texture of the stuff behind the object before Unity starts to draw it. If we used that texture to color our model, the result would basically be transparent. You may be wondering how this is different from alpha transparency. The key difference is that with alpha transparency, the shader has no idea what anything around the model looks like. You can do things like have a semi transparent sprite multiply its own color by the colors of whatever is behind it, but you can't do anything conditional like a true color dodge. With GrabPass, the colors behind the object are inside a texture you can access as you like, which opens up opportunities for lots of very cool effects. 

If GrabPass gives you more options than regular transparency, why not just use it all the time? Keeping a texture of all the stuff behind an object takes up memory. We don't want to eat up precious computer resources unless we have to, and most transparent effects are perfectly lovely without needing to actually access the colors of whatever is behind the object. But we DO need to do that to imitate refraction because we're not just laying the colors of our shader down on top of what's behind it, we're going to distort what we can see through our shader too.

When we add GrabPass with just the { } brackets the way we've done here, Unity will write the captured texture to a sampler2D called _GrabTexture, so let's declare that now.


We can also give the grab texture a custom name. Be aware that doing that will result in slightly different behavior. If you don't give it a custom name, Unity will take a new capture every time you call on the _GrabTexture. If you don't, Unity will just take the capture once per unique name. This becomes more important if you have a bunch of different shaders in the scene using GrabPass. It's not very relevant to us today because even in this shader we will only use _GrabTexture once.

We can't extract the texture color exactly the way we would with a normal texture because _GrabTexture isn't just a square, it's like a slice of the screen. We'll need the screen coordinates to correctly unpack it. Fortunately those are easy to come by in a surface shader. Add float4 screenPos; to your Input struct.


Then in the surface function we can turn the screenpos into UV coordinates for the grab texture like this

float2 grabUV = IN.screenPos.xy / IN.screenPos.w;

If you do this, Unity may complain about division by zero, because sometimes IN.screenPos.w is zero, and as you know, you shouldn't divide by zero. Paste this under your other #pragma directives

#define EPSILON     1.192092896e-07


I found this while rooting around in a bunch of the built in CGIncludes. It's the smallest possible number such that 1 + EPSILON != 1. So it's basically as small as you can get without actually being 0.

We can then divide by max(EPSILON, IN.screenPos.w) and never have to worry about dividing by zero! So let's go ahead and blend our grab texture with our main texture based on our main color alpha value.



Not bad, eh? So about that refraction. Well I don't know anything about science or math, but we don't need realistic refraction at all, we just need to distort the seethrough image a bit. If we figure out something that looks good, only someone really annoying would complain about it being unrealistic.

Easiest way to distort a texture is to just do something funny to its UV coordinates. Remember how we used _Time.y to scroll UVs? Imagine if instead of offsetting UV coordinates in a uniform way, we could offset them individually? This seems like the perfect use case for using a texture as a distortion map. And we already have the perfect map to use! I'm talking about our normal map! Let's use that to modify our UV coordinates.

above float2 grabUV, add this line:

half2 distortion = exp2(o.Normal).xy;

exp2 is a function we haven't used before, but it's pretty darn simple. It's the same as writing

half2 distortion = o.Normal.xy * o.Normal.xy;

or 

half2 distortion = pow(o.Normal.xy, 2);

It raises something to the power of 2. (exp2 means exponent 2!)

Then multiply grabUV by distortion like this.


Not bad! 


 And the best part is, using the normals means that the distortion is greater in the bumpier parts of the object, like Claire's hair. This has involved absolutely zero research or science, but it seems pretty convincing to me! 


(I'm sure somewhere out there a physicist is reading this and frothing with rage.)

It's possible that we want some distortion but not that much distortion though, so let's add a property called _Distortion.



Then we can update our half2 distortion line to

half2 distortion = lerp(float2(1, 1), exp2(o.Normal).xy, _Distortion);

That'll let us fine tune how must "refraction" we actually have.


Here's what we've got so far. This is already lookin way glassier than the sphere I was messing with earlier!


We'll do more to make it even fancier, but I will split the lesson here, since we will not do anything else with GrabPass and this post is already getting pretty long. Next up: we're going to make a custom lighting function to have fine control over our specular highlights and sample the reflection probe ourselves!

The code for the shader up to this point is attached to this post as XibGlass.shader. Note that this shader will not look exactly like the preview image because we are only halfway done. It will look like the last image in this post though!

On to part 2, custom lighting and super shiny highlights! 

This tutorial is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. The code shared with this tutorial is licensed under a Creative Commons Attribution 4.0 International License. 


Tier Benefits
Recent Posts