Glass Shader with Opaque Edge Effect in Unity

Today, I’m going to explain how I created the shader for glass objects in our game. Glass and other translucent objects are often slightly more opaque at the edges, as demonstrated with this beer glass. It gives a feeling of thickness and solid…ness to an otherwise frail object. And it just looks cool! You could probably use it for a bunch of different effects: jellyfish, jello, light bulbs… really anything translucent.

This shader recreates this effect using basic shader concepts. I created it in Unity’s shader language, but the algorithm could apply to any shader.

If you like this post, be sure to follow me on Twitter 🙂

Here’s the final product, applied to a Boston terrier modeled by our artist Kytana Le:

And here’s the final code for the tutorial:

THE HIGH LEVEL

As with the cel outline shader from our last post, this shader was written in Unity’s shader language, which is almost exactly like HLSL, but with some preprocesser tags and formatting that let you access Unity features. You need to understand what “surface normal” means to understand this algorithm, but not Unity :p

This shader has one pass and takes in four properties as input. We apply a texture (which is totally optional and not used in the example pictures), a base color, and an edge color with a tunable dropoff rate. The dropoff rate describes how thick the edge appears, or how much of it bleeds from the edge into the center of the model.

```Properties
{
_MainTex("Texture", 2D) = "white" {}
_Color("Color", Color) = (1, 1, 1, 1)
_EdgeColor("Edge Color", Color) = (1, 1, 1, 1)
_EdgeThickness("Silouette Dropoff Rate", float) = 1.0
}```

The ~brilliant~ algorithm is below. Firstly, we normalize the view direction vector and the surface normal vector. Then, the factor determining how opaque the edge is at this point on the texture is equal to the dot product of the view direction and the surface normal. The closer to being perpendicular that the angle of the view direction and the surface normal are, the lower edgeFactor will be, which we’ll use to make the pixel more opaque. We take the absolute value of the dot product, since the dot product returns a value from -1 to 1, and we don’t want any negative values.

`float edgeFactor = abs( dot(viewDirection, surfaceNormal) );`

Why does this work? In the image below, the arrows represent the surface normals. If you can imagine a vector representing viewDirection going directly into the screen, you’ll see that arrows closer to the edge of the object are also closer to a 90 degree angle to the viewDirection vector.

We apply this edgeFactor to the pixel’s opacity by dividing the input opacity by the edgeFactor, then raising it to the power of the dropoff rate. Since the edgeFactor is a value from 0 to 1, dividing the input opacity by the edgeFactor has the effect of lowering the opacity value, at lower and lower amount until we get to the absolute edge of the surface where the normals are (almost) perpendicular to the view direction and the edgeFactor is (almost) 0. (But never actually 0, I hope, because then I might get a division-by-0-error.) Raising this value to the power of edgeDropoffRate has the very nice effect of making the edge opacity very low for most of the model, then increase dramatically in opacity at the edge.

```float opacity = min( 1.0, color.alpha / edgeFactor );
opacity = pow( opacity, edgeDropoffRate );```

Note that we also clamp the opacity to a 0-1 value, to prevent small errors.

You could apply this same algorithm to the edge color, but I found it prettier to linearly combine the albedo color and edge color based on the edge factor.

```float oneMinusEdge = 1.0 - edgeFactor;
float3 rgb = (_Color.rgb * edgeFactor) + (_EdgeColor * oneMinusEdge);```

Ta-daa, you did it!!

Here’s what the shader can look like with a little tuning to the albedo color, edge color, and edge thickness properties. I think it’s starting to look a little jelly-like, but that’s a post for another time :V

Now I’m getting excited about the possibility of writing a jiggly jello shader with vertex wiggling :p

MISTAKES LEARNED FROM

Mistake #1: Chunks of material missing / inconsistent. You need to make sure ZWrite is Off. We don’t want pixels on the same translucent model to compete for the same spot on the ZBuffer, so don’t allow them to write to it. We do still want them to be able to read, so that other objects can occlude this one. (PS, I may have botched this explanation- kindly write me a correcting comment if I did? :P)

Mistake #2: Parts of model not showing / overlapping. You also need to turn Culling Off. We want to draw both the front and back faces of the model. Normally, for optimization purposes, culling is set to Cull Back by default, which will not draw the back faces of the model. The back faces are usually the inside of the model, so normally the player doesn’t want to see them. However, we do want see the inside surfaces of this model, since it’s completely translucent! :-0

As always, if y’all have any questions, feel free to leave them in the comments or tweet me! 😀 And here’s the link to the final code again:

Good luck,

Lindsey Reid

PS, here’s the Unity graphics settings for this tutorial.