Transparent and Crystal Clear: Writing Unity URP Shaders with Code, Part 3

24 min readSep 27, 2022

Hi, I’m Ned, and I make games! Have you ever wondered how shaders work in Unity? Or, do you want to write your own shaders for the Universal Render Pipeline, but without Shader Graph? Either because you need some special feature or just prefer writing code, this tutorial has you covered.

This is the third tutorial in a series on HLSL shaders for URP. If you’re starting here, I would recommend at least downloading the starting scripts here, so you have something to work from.

  1. Introduction to shaders: simple unlit shaders with textures.
  2. Simple lighting and shadows: directional lights and cast shadows.
  3. Transparency: blended and cut out transparency.
  4. Physically based rendering: normal maps, metallic and specular workflows, and additional blend modes.
  5. Advanced lighting: spot, point, and baked lights and shadows.
  6. Advanced URP features: depth, depth-normals, screen space ambient occlusion, single pass VR rendering, batching and more.
  7. Custom lighting models: accessing and using light data to create your own lighting algorithms.
  8. Vertex animation: animating meshes in a shader.
  9. Gathering data from C#: additional vertex data, global variables and procedural colors.

If you prefer video tutorials, here’s a link to a video version of this article.

If you talk to shader devs, few words strike more fear into their hearts than “transparency!” Well, in this video, I want to demystify transparent shaders a bit. In the process, we’ll learn about render queues, custom inspectors, blend modes, alpha clipping, winding culling, and double sided normals.

Before I move on, I want to thank all my patrons for helping make this series possible, and give a big shout out to my “next-gen” patron: Crubidoobidoo! Thank you all so much.

Let’s delay no further and get to programming!

Alpha Blending. So far, our shaders have ignored the alpha channel of the main texture, staying completely opaque. I say it’s time we changed that! Transparency is a complicated topic for many reasons, but it’s pretty easy to get started with it — just add one line to your pass block.

This Blend command determines how the rasterizer combines fragment function outputs with colors already present on the screen (or render target). These were drawn by shaders that have run before! The color returned by the fragment function is called the source color, while the color stored on the render target is called the destination color.

The source color is returned by the Fragment function.

The rasterizer multiplies each color by some number and adds the products together, storing the result in the render target and overwriting what was already there. You can specify these multipliers using the Blend command: first the source color multiplier, then the destination color multiplier.

Blend One Zero results in fully opaque materials — the default. For transparency, we need to linearly interpolate between the source and destination colors based on the source color’s alpha. Thankfully, ShaderLab has a “source alpha” and “one minus source alpha” multiplier, perfect for our goals. Add this to your forward lit pass block.

The red ball is supposed to be transparent, but does not blend correctly.

In the scene, test out blending by lowering the alpha on your material’s color tint or slotting in a texture with an alpha channel. Unfortunately, it doesn’t take long to notice some issues. First, transparent objects don’t always blend with objects behind them.

The depth buffer stores surface depths of each drawn pixel.

ZWrite Modes. Remember the depth buffer from our discussion of shadow mapping? Currently, the rasterizer stores transparent objects’ positions in the depth buffer, preventing fragments behind it from ever running. We can’t blend colors with a material that was never drawn! We need a way to prevent transparent surfaces from being stored in the depth buffer.

Thankfully, this is also easy to do. Add this ZWrite Off command to your forward lit pass block. This prevents the rasterizer from storing any of this pass’s data in the depth buffer.

Now, surfaces behind this shader will always draw.

Render Queues. Hmm, but there’s still some weirdness. The skybox completely overwrites all transparent objects — due to render order.

The fully opaque green and red pixels result in different final colors depending on draw order (of course, only if the depth buffer is turned off).

The blend operation depends on the order objects draw to the render target. Objects behind a transparent object must draw first in order to blend with them! Thankfully, we can control draw order using render queues.

When preparing to render a scene, URP takes a look at all renderable objects and sorts them by render queue. There are a few queues you can place shaders into using a Queue tag, set in the SubShader tags block. The default setting is “Geometry”, used for opaque materials. The “Transparentqueue tag comes after Geometry. By placing MyLit into this queue, we can ensure opaque objects draw first.

There’s also a Skybox queue, which runs in-between Geometry and Transparent. Previously, the skybox rendered after MyLit, and since MyLit had ZWrite Off, the rasterizer allowed the skybox shader to overwrite it. Using the transparent queue, this is not an issue.

For debugging and a few other more advanced systems, Unity also has a RenderType tag. This should be set to either “Opaque” or “Transparent.” RenderType doesn’t affect render order, but let’s go ahead and set it now.

Now, both transparent spheres should show up in front of the skybox. Notice that the spheres even draw over each other correctly. Knowing what we do about render order, you might guess — correctly — that URP sorts objects within the same queue by distance from the camera, back to front. This is crucial for transparent shaders.

The left model writes to the depth buffer, the right model does not.

Unfortunately, this sorting does not extend to triangles within a single mesh. If you have a mesh with many pieces that overlap, they might overwrite one another. The only reliable way to fix this is to break your mesh into many pieces. Just one of the headaches you must endure while working with transparent materials…

Before moving on, take a look at the frame debugger. You can see render order and verify that each object is in the correct queue. Look under “DrawTransparentObjects!”

If for some reason you need to tweak the draw order on a material basis, every material has a queue field at the bottom of its inspector. You can change queues and even give priority to certain materials. The “Geometry+1” queue runs after all other objects in the geometry queue, but still before the skybox queue.

The white cube uses a transparent Lit material and casts no shadows!

There’s one more bug you might have noticed: transparent objects cast fully opaque shadows. Transparent, or partial, shadows are very complicated, and I will not be covering them in this series. For better or worse, the Lit shader does not support transparent shadows either. The best we can do is disable shadows for transparent objects. But, to do that, we need to set up some C# infrastructure…

Custom Material Inspectors. If you wanted to use this shader for opaque materials like before, you could use an opaque texture and tint. However, that’s hardly an optimal solution. Since we turned z-writing off, there would be no protection from overdraw. The queue and render type would also be incorrect.

Is there a way to change these using material properties? Well, yes, but we would have to remember to set them all correctly in unison. Let’s create a custom material inspector script to easily switch between opaque and transparent modes using a single dropdown menu.

Unity editor scripts are placed in folders named “Editor,” so create that first. Inside, create a C# script called “MyLitCustomInspector.”

Open it, and delete the Start and Update functions. Change MyLitCustomInspector to inherit from ShaderGUI, which is located in the UnityEditor namespace.

There’s a lot to editor scripts, but we only need to use a couple key features. First, override the OnGUI method, which Unity calls whenever it needs to draw a material inspector. We can get the currently viewed material using the target field of MaterialEditor.

Before going any further, set up some properties in the shader file. Properties don’t only store values used in HLSL code — they also store metadata about a material. Create a Float _SurfaceType property to remember if this material is opaque or transparent. Add a HideInInspector attribute to ensure this property won’t show up in the user-facing inspector.

Next, create three Float properties for the source blend, destination blend, and z-write modes. To direct ShaderLab to use a property value in the Blend and ZWrite commands, surround the name with brackets. Do this for the ForwardLit pass. The shadow caster can use the default values of Blend One Zero and ZWrite On.

Now we need to change the Queue and RenderType tags! Unfortunately, pure ShaderLab doesn’t support variable tags - take care of this in the C# custom inspector! For now, reset the “RenderType” to “Opaque” and remove the “Queue” tag — it will default to “Geometry.”

Return to the custom inspector. Create an enum containing all “surface types” we’d like to support: opaque and transparent. Then, to add a dropdown containing all values in this enum, call EditorGUILayout.EnumPopup in OnGUI. The first argument is the UI label, while the second is the current value.

Unity stored the current value in the material’s “_SurfaceType” property. However we can’t just read it directly if we want to support serialization, undo, shader variants, and other editor features.

First, Unity encapsulates a property inside a MaterialProperty class. It passes a list to OnGUI containing a MaterialProperty instance for each property of the shader, and provides a BaseShaderGUI.FindProperty method to easily grab the one corresponding to “_SurfaceType.”

MaterialProperty stores property values as floats, but its easy to cast them to SurfaceType. Pass that as the second argument to EnumPopup — the current value. EnumPopup returns the value displayed in the popup— either what was passed or a new value the user selects. Either way, cast it back to a float and reset the value in MaterialProperty.

Surround the EnumPopup call with Begin- and EndProperty functions. These enable material variants and all their cool features — only in Unity 2022. Unfortunately, these functions don’t exist before then. Be sure to either surround them in #if blocks or just omit them if you’re working in Unity 2021 or earlier.

Now, to listen for user input and set material properties appropriately, surround EnumPopup with these Begin- and EndChangeCheck functions. EndChangeCheck returns true if the user picks a new value from the dropdown.

Create an UpdateSurfaceType function, taking the material as an argument. Call it inside the EndChangeCheck if block. UpdateSurfaceType will refresh the ZWrite mode, Blend modes, tags and shadow caster based on the _SurfaceType. Since the function runs post-input, its safe to read properties directly from the material.

Use a switch statement to refresh the material based on the SurfaceType enum. Set render queue using Material.renderQueue. Override the “RenderType” tag using SetOverrideTag. Set ZWrite and Blend properties using SetInt. There’s a handy enumeration from Unity for blend modes- include the Unity.Rendering namespace to access it. As for ZWrite, 1 corresponds to On and 0 to Off.

Lastly, to turn off shadows, we can disable the shadow caster pass. There’s SetShaderPassEnabled to handle this.

Finally, back in OnGUI, ensure a base.OnGUI call comes at the end of the method. This draws the default inspector, which includes all properties of your material without the HideInInspector attribute.

All that’s left to do is register this inspector class to our shader. In the .shader file, use the CustomEditor command inside the Shader block.

Check it out in the scene! You can now easily switch between opaque and transparent modes. Verify that everything looks good in the frame debugger too.

Switching to Lit and back to MyLit breaks the synchronization between properties and the dropdown menu!

You might notice one issue. If you switch shaders, the material might not initialize correctly until you mess with the surface type dropdown.

The ShaderGUI class has a callback when a new shader is assigned to a material: AssignNewShaderToMaterial. Override it and leave the base call at the top. Check if the new shader is the “MyLit” shader using its name field. If true, call UpdateSurfaceType.

In addition, in Unity 2022, material variants will not update correctly unless we add one more function: ValidateMaterial. Unity calls this whenever a material property changes, either when the user edits it or a parent variant changes. Have it call UpdateSurfaceType.

Another bug squashed!

Alpha Cutouts. Is there a way to combine the flexibility of a transparent material with the performance of an opaque one? Sort of! Another common transparency strategy is called alpha testing, alpha clipping or alpha cutouts. In this mode, we use a texture like a cookie cutter to render only certain parts of a mesh.

The leaves here are rendered using alpha cutouts.

It’s a hybrid mode! Each fragment is either fully opaque or fully transparent. No blending is required, making it safe to write to the depth buffer and cast shadows.

Supporting cutouts requires small adjustments to just about all code we’ve written so far, but let’s start with the custom inspector. Add a TransparentCutout member to the SurfaceType enum. While we’re here, rename the Transparent mode to TransparentBlend, to be more clear.

In UpdateSurfaceType, we need to set everything up for cutouts. They have the same Blend and ZWrite modes as opaque surfaces, but a different queue and render type tag. They also do cast shadows. Reorganize the switch statement to take all this into account.

AlphaTest Queue. Notably, cutouts should run in a new queue, called “AlphaTest.” This queue runs after geometry but before skybox. But, since cutouts don’t need blending, why not just use geometry? It’s a question of optimization.

The cheaper white sphere prevents the expensive rainbow sphere from rendering behind it.

Beyond enabling transparency, draw order is a powerful optimization tool! Imagine this situation: two objects have opaque shaders, but one is much more resource intensive. We’d like to minimize overdraw to prevent shading the expensive material when it’s not visible. Unity tries to achieve this with depth sorting, but it’s not always reliable.

By placing the expensive shader into a later queue, we ensure it’s drawn when more of the depth buffer is filled. Alpha cutouts are more expensive than opaque shaders, so Unity orders them later to save time!

Clip and Discard. But back to the code. In MyLitForwardLitPass.hlsl, utilize this clip HLSL function to do the work. If you pass it a number less than or equal to zero, it will discard the currently running fragment. What does that mean?

discard is a command to the rasterizer, causing it to pretend like it never invoked a particular fragment. It will short circuit the fragment function, returning immediately after clip, and throw out all data pertaining to the fragment — not writing to the depth buffer or render target. It’s as if the fragment function was never called!

We want to clip this fragment if the alpha value is less than a threshold — let’s use one-half for now. Subtract the alpha component by 0.5 and pass that to clip. To apply the color tint’s alpha, multiply it with the texture sample.

Go ahead and try it out! Grab a texture with an alpha channel and change your material to “Transparent Cutout” mode. Pretty cool, but there are several things to fix.

Alpha Cutoff. Let’s start with the easiest. Right now, we always clip pixels below 50% alpha, but that might not be appropriate. Let’s add a property to make this adjustable.

Define a “_Cutoff” property in the .shader file. It should always range between zero and one, so use the special Range property type to create a slider.

By the way, this property has a “magic” name, meaning Unity always expects the alpha cutout threshold in a property named “_Cutoff.” This is important to some advanced features, like baked lighting. For now, just make sure your property is named “_Cutoff.”

Then in MyLitForwardLitPass.hlsl, define _Cutoff near the other properties and subtract _Cutoff from alpha, instead of 0.5.

The left sphere has _Cutoff = 0.001, the right sphere has _Cutoff = 1.

Now, you can edit your material to better match the alpha values in the texture. Next problem: we’re clipping even in blended materials! This is not only incorrect, simply having a clip function in your shader can drastically decrease performance. We should use a keyword to remove the clip line when in opaque or blended modes.

Shader Features. This will be the first time we’ve actually used a keyword to change our own code. These keywords have no value, not even true or false, so an #if block cannot parse them. Instead, test if the keyword is defined, using the defined function. A keyword is defined when it is enabled, and it is not defined if its disabled.

There’s a shortcut for this: #ifdef. Keep in mind that there is not an #elifdef for whatever reason, so I use the entire defined syntax for clarity sometimes. Anyway, use an _ALPHA_CUTOUT keyword to include or omit the clip function. Wrap the clip call in a #ifdef block and we’re good to go.

It’s possible to enable and disable keywords from C#, so take care of this in the custom inspector. In UpdateSurfaceType, enable or disable _ALPHA_CUTOUT based on the surface type.

Now, we need shader variants based on _ALPHA_CUTOUT. Moving to the .shader file, add a #pragma to generate them. Instead of multi_compile, use a shader_feature_local command. Shader features are just like multi compiles, in that they generate shader variants based on a list of keywords. The difference is in game builds.

When you build your game, or create a distributable, Unity must determine which compiled shader variants to include in the build. It gathers all shaders used by your game and starts filtering shader variants.

Unity includes all variants generated by multi_compile commands, but before including a shader_feature variant, it checks to make sure some material has the required keywords enabled. For instance, Unity only includes MyLit variants with _ALPHA_CUTOUT enabled if a material has a cutout surface type (and thus enables the _ALPHA_CUTOUT keyword).

Since this check happens at build-time, keywords which change dynamically at runtime (like the URP lighting keywords) should use multi_compile. Otherwise, use shader_feature.

Why bother with this? Well, shader variants are not cheap to compile! Shader features help optimize build time.

Shader features always have an implicit “_” in their keyword list; in other words, they always trigger a variant with none of the listed keywords enabled. Of course, that variant may or may not be used by your game, but Unity is ready for the possibility!

Finally, the local suffix indicates that the keyword is unique to this shader and will not be set globally. We set _ALPHA_CUTOUT on a material-by-material basis, so it can use local variants. Keywords set globally by URP, like _MAIN_LIGHT_SHADOWS, cannot be local. Unity has a hard limit on the number of global keywords it can support, so it’s good practice to use local variants when possible.

Your shader should look the same as before, but your FPS will thank you for this optimization!

Common HLSL Files. The next bug to tackle is shadows: objects no longer cast shadows that match their cutout shape! To fix that, clip fragments in the shadow caster pass.

First, let’s take the alpha clipping logic in MyLitForwardLitPass.hlsl and move it to a function, called TestAlphaClip. Pass the color texture sample as an argument. To make this available in MyLitShadowCasterPass.hlsl, we should add it to a separate file and #include it in both files.

Create a new “MyLitCommon.hlsl” file. Remove TestAlphaClip from MyLitForwardLitPass.hlsl and paste it here. TestAlphaClip requires a couple of properties, _Tint and _Cutoff. Properties are defined shader-wide, so it makes sense to move property definitions to the common file. Include the URP library to have access to the TEXTURE2D macros.

In MyLitForwardPass.hlsl, trim duplicated code and #include the new common code file.

Let’s take a moment and think about this. HLSL is very C++-like, and #include causes the compiler to literally copy and paste the contents of the common file over the #include line. What happens if I accidentally #include a file twice? Unity will throw an error saying a variable has already been declared.

OK, but duplicating #include lines is not common. Well, what if MyLitCommon and MyLitForwardPass also #include a file called MyMath.hlsl? Then MyMath would be duplicated!

To simplify things and prevent errors, it’s customary to wrap all code in an HLSL file inside something called a guard keyword block. First, check if a keyword is not defined (that’s what #ifndef does, it’s shorthand for #if !defined()). On the very next line, #define the guard keyword. Don’t forget the #endif at the end of the file!

Now, the first time MyMath is included, the compiler defines the guard keyword. The second time, the guard keyword is enabled, and it skips the code inside. Pretty smart! Go ahead and add guard keywords to MyLitCommon. I also add them to all HLSL files to be safe.

Clip in the Shadow Caster. After all this cleanup, your shader should still work the same. Let’s finally add clipping to the shadow caster.

First, in the .shader file, add the _ALPHA_CUTOUT shader feature to the shadow caster pass.

In MyLitShadowCasterPass.hlsl, add #include “MyLitCommon.hlsl”. At the moment, the shadow caster has no UVs to sample the main texture. We will need to pass them all the way to the fragment stage.

To do that, add a uv field to Interpolators. Each field here is another that the rasterizer has to interpolate, and it’s best to keep this struct as small as possible. Surround the uv field with an #if block, ensuring it’s only interpolated when needed. It’s not as important to do this in the Attributes struct, but it can’t hurt.

In the vertex function, transfer the UV to the output struct, again only if _ALPHA_CUTOUT is defined.

Then, in the fragment function, sample the color texture and call TestAlphaClip. We can wrap all this in an #if block too. Remember, clip discards fragments if passed a value below zero, causing the rasterizer to throw them out and not write to the depth buffer. Since the depth buffer becomes the shadow map, clipped fragments won’t show up there either.

With that, your shadows correctly match the clipped shape. Make sure you test out all modes on your material, to make sure all the #if blocks are set up correctly.

There’s one last problem I want to fix, but it requires a little more explanation…

Double Sided Rendering. Often, games use alpha clipping on flat planes, in which case everything looks fine. But if you turn on alpha clipping on a sphere, you might notice the inside becomes invisible!

A half sphere with culling enabled and disabled.

Face Culling. This is another optimization technique called face culling, winding culling, or simply “culling.” The particulars aren’t important, but basically, the rasterizer determines whether it’s rendering the front or back of a triangle on the mesh. The back face is usually on the inside of a model, so the rasterizer “culls” it, or decides not to render it. If you’ve ever had weird issues importing from Blender and had to reverse faces, culling is the culprit.

In our case, we’d like to render the inside of the sphere. Luckily, it’s easy to turn off culling.

In MyLit.shader, add a new float property called _Cull. We’ll use this to set another ShaderLab command, Cull. Cull can take three different values: Off, Front and Back. Off completely turns off culling, while the other two cull one side of a triangle.

Unity has a C# enum for these options, and we can instruct the default material inspector to create a dropdown using it. In the enum, Back has an int value of two, so let’s set that as the default.

Then, add a Cull command to your forward lit and shadow caster passes, with the _Cull property in brackets.

Return to the scene and try it out. Turning off culling reveals both sides of the sphere!

Double Sided Normals. But, a new bug! The lighting is incorrect. Notice that both sides of a triangle receive the same amount of light, as if the sphere is made of paper. This is because both sides have the same normal vector — it is not automatically flipped for the back face. We need to do that ourselves.

In MyLitForwardLitPass.hlsl, flip the normal vector given to InputData if rendering the back of a triangle. To flip a vector, simply multiply it by negative one. But, how do we know which side of the triangle is rendering? The rasterizer has this information and makes it available through a special, fragment-stage-only semantic.

To grab it, simply add another argument to the Fragment function. Both the exact semantic and the argument type depend on the current platform. Thankfully, Unity provides macros to sidestep the issue. Regardless, frontFace’s type is essentially a boolean, true if the triangle front face is visible.

Unity also provides another macro to choose a value based on frontFace, like the ternary operator in C#. Use that to either multiply the normal vector by 1 or -1 before setting it in lightingInput.

So, that fixes lighting, but triangle back faces now have shadow acne. Since shadow biases also depend on normals, we need to flip them too. Unfortunately, rasterizer front face data is not available in the vertex stage — it runs before the rasterizer!

In the first image, the red normal vector is within ninety degrees of the blue view direction. In the second image, it’s outside the range — flip it!

Luckily there’s another way. If we assume the normal vector should always point roughly towards the camera, we can flip it if it does not. Let’s try to keep the angle between the normal vector and the view direction less than ninety degrees. If the angle is greater, flip the normal. This brings it back within the acceptable range!

There’s a very simple function to find the angle between two vectors: the dot product. It returns the cosine of that angle. Cosine of ninety degrees is zero, and if the dot product is less than zero, the angle between the vectors is greater than ninety degrees.

Let’s implement this In MyLitShadowCasterPass.hlsl using a new FlipNormalBasedOnViewDir function. Pass it position and normal. Calculate the view direction using a built in URP function (which we also used in the forward lit fragment function). Then, only if the dot product of the normal and view direction is less than zero, multiply the normal by negative one. Return the normal.

Modify the clip space calculation to call FlipNormalBasedOnViewDir.

Back in the scene editor, it looks like there’s no more shadow acne! Mission accomplished!

This technique isn’t perfect — sometimes it creates shadows that kind of flicker as you rotate around the object. But, that’s probably preferable to shadow acne.

Face Rendering Mode. Double sided normals are not free — both the flipping operations and the front face semantic have a bit of overhead. It’s a good idea to use a keyword to turn this feature on and off. Thinking about it, there’s also no reason to ever flip normals if culling is on. This leaves only three useful configurations: back culling, no culling, and no culling with normal flipping.

Let’s address these concerns by updating the custom inspector. Create a new enum, FaceRenderingMode, to encapsulate the aforementioned modes. Similarly to the surface mode, use another hidden property to keep track of this setting. In OnGUI, add another enum dropdown controlling this new property: _FaceRenderingMode (and the appropriate surrounding functions for Unity 2022).

In UpdateSurfaceType, update the _Cull property and either enable or disable a keyword, _DOUBLE_SIDED_NORMALS. If the face rendering mode is FrontOnly, set _Cull to Back using Unity’s CullMode enum. Otherwise, turn culling off. Then, enable or disable our keyword appropriately.

Moving on to MyLit.shader, first hide the _Cull property in the inspector and remove the Enum attribute (it’s handled by code now). Second, add another hidden property for _FaceRenderingMode. Third, add a shader feature for _DOUBLE_SIDED_NORMALS to both passes.

In MyLitForwardLitPass, wrap all the normal flipping code in #if blocks. There’s a bit of trickery to hide the front face semantic when it’s not needed. It’s ugly, but it works!

In MyLitShadowCasterPass, similarly wrap the call to FlipNormalBasedOnViewDir within a #if block.

Each sphere has a different face rendering mode setting.

In the scene editor, try out different face rendering modes to make sure everything works as expected!

In the future, think carefully whether an object actually needs any of these options. Turning off culling can dramatically increase performance cost, and double sided normals aren’t exactly cheap either. These options are very useful for things like foliage — just don’t blindly enable them for all materials!

With that, I would consider this a fully functional shader! It supports all the basic needs: textures, lighting, shadows, transparency, and even goes above and beyond the Lit shader with double sided normals! But, of course, we’re far from done.

In the next tutorial, I will focus on more surface options! We’ll implement a new lighting model called PBR — physically based rendering — which gives more customization options to make lighting more realistic. Rough, metallic, glassy, smooth, shiny, and glowing materials are in our future!

For reference, here are the final versions of the shader files.

If you enjoyed this tutorial, consider following me here to receive an email when the next part goes live. Part 4 is available here!

If you want to see this tutorial from another angle, I created a video version you can watch here.

I want to thank Crubidoobidoo for all their support, as well as all my patrons during the development of this tutorial: Adam R. Vierra, Amin, autumnboy, Ben Luker, Ben Wander, bgbg, Bohemian Grape, Boscayolo, Brannon Northington, Brooke Waddington, Cameron Horst, Charlie Jiao, Christopher Ellis, CongDT7, Connor Wendt, Crubidoobidoo, Dan Pearce, Daniel Sim, Davide, Derek Arndt, Dongsik Gang, Elmar Moelzer, Eren Aydin, far few giants, Henry Chung, Howard Day, Isobel Shasha, Jack Phelps, John Lism Fishman, John Luna, Joseph Hirst, JP Lee, jpzz kim, JY, Kat, Kyle Harrison, Lasserino, Leafenzo (Seclusion Tower), lexie Dostal, Lhong Lhi, Lien Dinh, Lukas Schneider, Mad Science, Marcin Krzeszowiec, Mattai, Minh Triết Đỗ, Oliver Davies, P W, Patrick, Patrik Bergsten, rafael ludescher, Richard Pieterse, Robin Benzinger, Sam CD-ROM, Samuel Ang, Sandro Traettino, santhosh, SHELL SHELL, Simon Jackson, starbi, Steph, Stephan Maier, Steve DeBusschere, Syll art-design, Taavi Varm, Team 21 Studio, thearperson, Thomas Terkildsen, Tim Hart, Tomasz Patek, ultraklei, Vincent Thémereau, Voids Adrift, Wei Suo, Wojciech Marek, Xavier Larrosa Rogel

If you would like to download all the shaders showcased in this tutorial inside a Unity project, consider joining my Patreon. You will also get early access to tutorials, voting power in topic polls, and more. Thank you!

If you have any questions, feel free to leave a comment or contact me at any of my social media links:

🔗 Tutorial list website ▶️ YouTube 🔴 Twitch 🐦 Twitter 🎮 Discord 📸 Instagram 👽 Reddit 🎶 TikTok 👑 PatreonKo-fi 📧 E-mail: nedmakesgames gmail

Thanks so much for reading, and make games!

Change log:

  • May 29th 2023: Material inspector changes, required for material variants in Unity 2022.

©️ Timothy Ned Atton 2022. All rights reserved.

All code appearing in GitHub Gists is distributed under the MIT license.

Timothy Ned Atton is a game developer and graphics engineer with ten years experience working with Unity. He is currently employed at Golf+ working on the VR golf game, Golf+. This tutorial is not affiliated with nor endorsed by Golf+, Unity Technologies, or any of the people and organizations listed above. Thanks for reading!




I'm a game developer and tutorial creator! If you prefer video tutorials, check out my YouTube channel NedMakesGames!