Creating a Card Shader with 3D content
This is a breakdown of a couple of shaders I made for Harry Alisavakis’ Technically Speaking Discord Challenge, with the theme of Trading Card Foil. I used Unity (URP) and Amplify Shader Editor to author these shaders, but if you are following this breakdown to replicate some aspects of it, you can perfectly use Shader Graph for most of it except the Stencil Buffer and some tags that you may add later to the generated code that I will also show. As a note, I greatly recommend using Amplify Shader Editor over Shader Graph as it is immensely faster, more complete and just a joy to work with.
You can find a package with the finished version at the end of the post.
Creating a 3D interior with the Stencil Buffer
The most evident effect in this card shader is the 3D interior that can only be seen looking through the card. This effect is achieved using the Stencil Buffer.
The Stencil Buffer is an 8-bit mask you can use to tell shaders whether they should render a pixel or not. A shader can write to the Stencil Buffer and another shader can check that information and choose what to do or render based on that. There is some fancy stuff you can do with the Stencil Buffer but for this effect, we only need to write to and check a single layer.
First, let’s set up a Unity Scene with a Quad and some sample geometry behind it, in my case two cubes. This Quad will act as a window, everything outside it will not be rendered and everything behind it will.
Create a new Unlit shader and a material and assign it to the quad. In Amplify navigate to Pass -> Stencil Buffer and activate the little box on the right.
“Reference” is the Stencil Buffer layer we are gonna use, for this project it doesn't matter what you choose, just make sure you don’t use number 0. We can also ignore Read Mask and Write Mask for the purpose of this breakdown.
“Comparison” compares the reference value to the content of the Stencil Buffer. What we want from this Shader is to write to the Stencil Buffer our reference value, doesn’t matter what the previous value was. Because of that, we use “Always” so we write to it no matter what.
“Pass Front” is the action we are gonna take for the pixels in the front face of the quad. “Replace” will write our reference value into the Stencil buffer, which is exactly what we want.
If you are doing this in code, you just have to add this piece of code to the start of the Forward pass. As you see, the setting mirror the code one-to-one.
Now let’s create a new Lit Shader for the contents of our card, create a material, assign it to the cubes and navigate to the Stencil Buffer.
In this case, we want to mask every pixel that is in the mask and discard the rest. First, we need to read from the appropriate layer, so we set up the reference to the same number as the other shader and set “Comparison” to “Equal” so we only act based on the information of that layer. “Pass Front” is set to Keep because we want to keep those pixels if we are inside the mask.
Once you save, it will seem as if it stopped working, but that is not the case! It is in fact discarding every pixel outside the quad and rendering only those behind it, but because the engine knows the quad is in front, it will not render what is behind it.
This is achieved with the Depth Buffer, every opaque object writes to the Depth Buffer and Unity uses that to know whether an object is in front of another, but we can tweak the behavior of a specific shader.
Setting ZWrite to Off will make Unity render that object behind everything else, even if it is in front of the scene view.
We’ve done it!
Let’s now add the rest of the card. Add a new quad with a new Lit Shader and navigate to the Stencil Buffer Options.
In this case, we want it to render only where the stencil buffer mask is not present, so we set “Comparison” to “Not Equal”.
If you orbit around the scene you may notice the effect doesn’t work at certain angles.
The Stencil and Depth Buffers are working, but we need the window shader to write to the Stencil Buffer BEFORE everything else renders, for that, we need to adjust the Render Queue.
In the Foil Shader navigate to SubShader -> Tags -> Queue and increase the index by one so it always renders after the window. Likewise, increase the queue index of the objects inside the card, in my case they are transparent, which by default is rendered after the opaque geometry.
Now that we have our scene set up and everything working, we can dive into the shaders.
Most effects in this card change depending on the view angle, whether it is to fake depth or just to shift colors or parameters.
Let’s start with the sparkly glitter that only shows up at certain angles. First, we create a vertical band that moves from left to right depending on the view direction to fake reflection.
I learned this effect from Cyan (@Cyanilux on Twitter), in which we just offset our UV coordinates by the View Direction. I’ve also added some more parameters to control precisely the location and potency of the effect.
Because we want a vertical band, we can use the U channel from the UVs, which is a linear gradient in the horizontal coordinate, and feed it to a triangle wave node which will create a nice faded vertical band. Saturate the output to clamp it between 0 and 1 and connect it to a power node to control size and intensity. The saturate node is extremely important because the black part in the triangle wave preview can contain negative values that will play badly later on. I connected the result of this graph to the emission node so it acts as unlit color.
Now we want to color this band. For a normal foil shader, you would choose to reflect different colors of the rainbow depending on the angle, I recommend you to check Alan Zucconi’s post about rainbow representation if you choose to do so (https://www.alanzucconi.com/2017/07/15/improving-the-rainbow/). In my case, I settled on coloring the band based on a two-color gradient that moves along with the band.
As you can see, I use the same structure to move the UVs around and feed the U channel to the Time input of a Gradient Sample Node. You may be confused on why it says time but that is just indicating the usual purpose of the node. In reality, lower values, in our case darker colors, will be mapped to the left side of the gradient and lighter colors to the right side. We then multiply this graph with the vertical band and we have our colored band.
To make it look like glitter we will then multiply it by another graph.
To make perfectly rounded specks we use a Voronoi noise and step through it to generate a dotted mask. In addition to that, I also used a simplex noise to add some more specks with different form factors. We then multiply it by our previous result and…
Let’s disconnect our result from the master and focus now on the surface pattern.
Just as before, we start with offsetting the UV coordinates by the View Direction, but this time around I multiply it by a Gradient Noise, this will offset the UVs locally, deforming the UVs and creating interesting patterns. By multiplying it by the view direction we make the distortion dependent on it.
By using the pattern to lerp between two colors we achieve the exact same behavior as sampling it with a two-color gradient like before.
Just as before, disconnect the previous graph from the master node, and let’s dive into the next part.
I refer to glare to the periodical diagonal shine I added because there is never too much shiny stuff.
Because I want to make it periodical, I use a sawtooth wave connected to the time node, that way we have a linear gradient that periodically resets. We then offset the UVs by this value and rotate them to make them diagonal. Feed the UVs to a rectangle node with a very high height (or width) value and we are done.
Borders and pentagons
The graphs we created are then modulated by some procedural borders. They all follow the same structure.
Just keep in mind that if you subtract a rectangle from another you may create negative values, so be sure to saturate if needed if you see unwanted effects.
Let’s mix everything
These operations are self-explanatory. The albedo part of the card is very faint, as most of the work is done by emission and smoothness/metallic.
Starting from the left, we lerp the glitter out of the text area, in this case, I settled for very faint glitter, but we could have just multiplied the glitter by the mask to completely get rid of the stars in that area. We multiply the combined colored glitter by an emission float so we can use the excess brightness for post-processing effects like bloom. We mask the stars out of the outer edges and add the glare.
The surface pattern is also decreased in the text area thanks to a lerp, which we add to the previous result.
Finally, we increase the emission of the combined effect in the border areas, which are the sum of the outer edge, window border, pentagons, and text area border.
Metallic and Smoothness
Thanks to two lerps, we can adjust smoothness and metallic values over the text area, the pattern, and the borders.
We made it! If you’ve been following, the resulting graph will look somewhat like this.
To achieve the final result, I added a pulsing glow in a third quad behind the card, and a fourth quad behind the cubes, as well as a white directional light and a very bright yellow point light. The text is just Text Mesh Pro with some HDR glow. To top it off, add some post-processing bloom and you will get something like this.
You can grab the finished version here (Dependencies: Amplify Shader Editor, URP, TextMeshPro, the rotating script uses DoTween too)
If you want more shader and game dev content be sure to follow me on Twitter: @ValerioMarty
You can also check my itch.io page for finished projects.