Basics, part 6

What every line of a Unity shader file is for

A small Unity shader file read line by line, from the material's settings to both eyes of a headset, plus the tags, render state and Blend lines other shaders use.

  • Part 68 min dose
  • Beginner

The first time I opened a .shader file, it looked like a wall of curly brackets and SHOUTY WORDS. It’s really a few boxes inside each other, and most of them say the same thing in every shader. Let’s take apart the small file behind this little sticker, top to bottom. It has one control of each kind.

A sticker to drag and tweak
Drag the sticker, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

How big the sticker is.0.40
How many thin rings echo the edge.2
The sticker's color.
A soft light around the sticker.
Which shape the sticker has.
sticker.hlsl draws the demo above
Fig. 1

Drag the sticker, then try every control. Each one becomes a line of the Unity file below.

Boxes inside boxes

Copy for Unity, the cube button on a demo’s code window, turns the demo into this file. Point at or tap any dotted word for its meaning.

What Copy for Unity gives you, 188 lines
Shader "DocMe/unity-shader-file/fig_01"
{
    Properties
    {
        _Size ("Size", Range(0.2, 0.6)) = 0.4
        [IntRange] _Rings ("Rings", Range(0, 4)) = 2
        _Tint ("Tint", Color) = (1, 0.4, 0.8, 1)
        [ToggleUI] _Glow ("Glow", Float) = 1
        [Enum(Circle, 0, Square, 1, Diamond, 2)] _Shape ("Shape", Float) = 0
        _Pointer ("Pointer", Vector) = (0.5, 0.5, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #include "UnityCG.cginc"

            float4 _Pointer;
// The site's shared helpers, inlined so the file stands alone (97 lines). Open to read them.
            // Library: common
            // Always included. Constants, coordinates, anti-aliasing, hashes and the stage colors.
            // The same code runs on this site (DOCME_WEB) and in Unity; only where the picture's size comes from differs.

            #define PI 3.14159265359
            #define TAU 6.28318530718

            float remap(float x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }
            float remap01(float x, float a, float b) { return saturate((x - a) / (b - a)); }

            // Width over height of the picture: the canvas on the site, the quad's scale in Unity.
            float docmeAspect()
            {
            #ifdef DOCME_WEB
                return _ScreenParams.x / _ScreenParams.y;
            #else
                return length(unity_ObjectToWorld._m00_m10_m20) / length(unity_ObjectToWorld._m01_m11_m21);
            #endif
            }

            // Square coordinates with the origin in the middle: the short side of the picture spans -1..1.
            float2 centered(float2 uv)
            {
                float a = docmeAspect();
                return (uv * 2.0 - 1.0) * float2(a, 1.0) / min(a, 1.0);
            }

            // The pointer (or the _Pointer material setting in Unity) in the same coordinates as centered().
            float2 pointerCentered()
            {
                float a = docmeAspect();
                return (_Pointer.xy * 2.0 - 1.0) * float2(a, 1.0) / min(a, 1.0);
            }

            // Width of one pixel in centered() units. Measured once per pixel before shade() runs, outside any branch.
            static float docmePixel;
            float pixelSize() { return docmePixel; }

            // Anti-aliased coverage of a signed distance: 1 inside, 0 outside, a one pixel ramp at the edge.
            float fillAA(float d)
            {
                float w = max(fwidth(d), 1e-5);
                return saturate(0.5 - d / w);
            }
            // Anti-aliased line of a given width (same units as d) around the zero line of d.
            float strokeAA(float d, float width) { return fillAA(abs(d) - 0.5 * width); }

            // Cheap hashes: one random number per input. Dave Hoskins, Hash without Sine (MIT License).
            float hash11(float p)
            {
                p = frac(p * 0.1031);
                p *= p + 33.33;
                p *= p + p;
                return frac(p);
            }
            float hash12(float2 p)
            {
                float3 p3 = frac(p.xyx * 0.1031);
                p3 += dot(p3, p3.yzx + 33.33);
                return frac((p3.x + p3.y) * p3.z);
            }
            float2 hash22(float2 p)
            {
                float3 p3 = frac(p.xyx * float3(0.1031, 0.1030, 0.0973));
                p3 += dot(p3, p3.yzx + 33.33);
                return frac((p3.xx + p3.yz) * p3.zy);
            }

            // Turns a point by a radians, counterclockwise: mul(rot2(a), p).
            float2x2 rot2(float a)
            {
                float c = cos(a), s = sin(a);
                return float2x2(c, -s, s, c);
            }

            // Color. Shaders work in linear light, as Unity does in a linear project; the screen gets sRGB at the end.
            float3 srgbToLinear(float3 c) { return lerp(c / 12.92, pow((c + 0.055) / 1.055, float3(2.4, 2.4, 2.4)), step(0.04045, c)); }
            float3 linearToSrgb(float3 c)
            {
                c = max(c, 0.0);
                return lerp(c * 12.92, 1.055 * pow(c, float3(0.41666667, 0.41666667, 0.41666667)) - 0.055, step(0.0031308, c));
            }
            // Blends two colors in steps that look even to the eye (in sRGB), for value ramps such as distance bands.
            // Edges and coverage still blend with lerp, in linear light, as light really adds up.
            float3 tint(float3 a, float3 b, float t) { return srgbToLinear(lerp(linearToSrgb(a), linearToSrgb(b), t)); }
            // Rolls HDR values above 1 off softly instead of clipping them.
            float3 tonemapACES(float3 x) { return saturate((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14)); }
            float luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }

            // The dusk stage colors, in linear light. Series colors are used in this order: mint, pink, butter.
            float3 themeSurface() { return float3(0.0080, 0.0048, 0.0243); }
            float3 themeInk() { return float3(0.8794, 0.8154, 1.0000); }
            float3 themeMuted() { return float3(0.3971, 0.3280, 0.6383); }
            float3 themeGrid() { return float3(0.0243, 0.0138, 0.0646); }
            float3 series1() { return float3(0.2085, 0.8710, 0.5771); }
            float3 series2() { return float3(1.0000, 0.2749, 0.5771); }
            float3 series3() { return float3(1.0000, 0.6577, 0.2540); }
            // A glowing sticker with one control of each kind, to read in its Unity file.
            uniform float _Size;   // @range 0.2 0.6 0.01 = 0.4 | Size | How big the sticker is.
            uniform float _Rings;  // @range 0 4 1 = 2 | Rings | How many thin rings echo the edge.
            uniform float3 _Tint;  // @color = #ff66cc | Tint | The sticker's color.
            uniform float _Glow;   // @toggle = 1 | Glow | A soft light around the sticker.
            uniform float _Shape;  // @select = 0 | Shape | Circle=0, Square=1, Diamond=2 | Which shape the sticker has.

            float4 shade(float2 uv)
            {
                float2 p = centered(uv) - pointerCentered();

                // The distance from this pixel to the sticker's edge: negative inside.
                int shape = int(_Shape + 0.5);
                float d = length(p) - _Size;
                float2 r = shape == 2 ? mul(rot2(PI / 4.0), p) : p;
                float2 q = abs(r) - 0.7 * _Size;
                if (shape > 0) d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.2 * _Size;

                float3 col = float3(0.012, 0.012, 0.025);
                col += _Tint * 0.6 * exp(-8.0 * max(d, 0.0)) * _Glow;
                float gap = 0.09;
                float ring = abs(d - gap * clamp(round(d / gap), 1.0, max(_Rings, 1.0)));
                col += _Tint * 0.5 * strokeAA(ring, 0.012) * saturate(_Rings);
                col = tonemapACES(col);
                col = lerp(col, _Tint, fillAA(d));
                col = lerp(col, float3(1.0, 1.0, 1.0), strokeAA(d, 0.016));
                return float4(col, 1.0);
            }


            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert(appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
                docmePixel = length(fwidth(centered(i.uv))) * 0.70710678;
                return float4(shade(i.uv).rgb, 1.0);
            }
            ENDCG
        }
    }
}

The outer boxes are ShaderLab, Unity’s language for describing a shader: Shader holds everything, Properties the settings, SubShader the drawing and Pass the code. The code itself, between CGPROGRAM and ENDCG, is HLSL (more in a later lesson).

The first line, Shader "DocMe/unity-shader-file/fig_01", is the name you pick in a material’s Shader menu (each slash opens a submenu). It needn’t match the file’s name.

The material’s settings in Properties

Each line in Properties becomes one setting on the material:

_Size ("Size", Range(0.2, 0.6)) = 0.4
PartWhat it is
_SizeThe variable name the code reads.
"Size"The label the material shows.
Range(0.2, 0.6)The type, here a slider from 0.2 to 0.6.
= 0.4Where a new material starts.

The other lines bring more types. Tint is a Color (a color picker), Glow and Shape are a Float (one number) and Pointer a Vector (four numbers). A word in square brackets is a drawer, which only changes how the setting looks. IntRange makes Rings stop on whole numbers, ToggleUI turns Glow into a checkbox and Enum turns Shape into a dropdown of named numbers.

Further down, a variable has the same name:

uniform float _Size;   // @range 0.2 0.6 0.01 = 0.4 | Size | How big the sticker is.

Before each draw, Unity copies the material’s value into it. Uniform means one value for all pixels, and the comment only builds this site’s slider. Misspell the name and the variable stays at 0, however much you move the setting.

SubShader, Tags and Pass

Tags { "RenderType"="Opaque" "Queue"="Geometry" }

A shader can hold several SubShaders, say a simpler one for older graphics cards, and Unity uses the first one the card can run. Its tags say when and how it draws. Queue sets its turn, and the lowest number goes first. Solid things draw in Geometry, then see-through things in Transparent, back to front so each blends over what’s behind. RenderType is a label for when Unity redraws the scene with stand-in shaders.

A Pass is one drawing of the object, and toon shaders often add a second for an outline. Passes have tags too. LightMode names the pass’s lighting job, such as the main light or shadows, and without it the pass draws unlit.

Where the code begins

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"

#pragma lines are notes for the compiler, which turns this text into graphics card instructions. The first two name the functions: vert, the vertex shader, runs for every corner of the mesh, and frag, the fragment shader, for every pixel it covers. target 3.0 asks for shader model 3.0, a feature set every graphics card has today.

UnityCG.cginc holds Unity’s ready made helpers. After it come the site’s helpers (folded away above) and the demo, word for word.

Two little structs

A struct is a bundle of values with one name. The first, appdata, carries the two things the mesh hands over for each corner, its position and its UV. The second, v2f (vertex to fragment), is what vert passes on to frag:

struct v2f
{
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD0;
    UNITY_VERTEX_OUTPUT_STEREO
};

The words after the colons are semantics, labels that tell the graphics card what each value is. SV stands for system value, one the graphics card itself acts on.

SemanticMeans
POSITIONThe corner’s position on the mesh.
TEXCOORD0The mesh’s first UV set, or in v2f a slot to carry a value along.
SV_POSITIONWhere the corner lands on screen.
SV_TargetThe color frag writes.

vert places corners, frag colors pixels

v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(v2f, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;

UnityObjectToClipPos moves the corner from the mesh’s own space onto the screen, and the UV comes along (the UNITY_ lines come later). The graphics card then blends the corners’ values across each triangle, giving every pixel its own UV, like in the first lesson.

float4 frag(v2f i) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
    docmePixel = length(fwidth(centered(i.uv))) * 0.70710678;
    return float4(shade(i.uv).rgb, 1.0);
}

frag hands each pixel to shade, the demo’s own function. The docmePixel line first measures how big a pixel is, for the soft edges.

Both eyes at once

Headsets need one picture per eye. In Unity’s single pass instanced mode, the graphics card draws each object twice in one go, one copy (instance) per eye, and the UNITY_ lines tell each copy its eye. Without them, the shader shows in one eye only. VRChat’s SDK compiles every shader for this mode on upload, so keep them. The odd one out, UNITY_INITIALIZE_OUTPUT, fills v2f with zeros first.

Taking it to Unity

  1. Press Copy for Unity on the demo, or Copy on the file above.
  2. In Unity’s Project window, right-click and choose Create > Shader > Unlit Shader. Open it, replace everything with what you copied, and save.
  3. Choose Create > Material, and pick DocMe > unity-shader-file > fig_01 in its Shader menu.
  4. Add a quad with GameObject > 3D Object > Quad, and drag the material onto it.

Scale the quad to 1.78 in X for the demo’s wide frame, then play with the settings.

The render state around your code

Render state lines set up the graphics card’s own steps around your code. Cull throws away triangles facing the wrong way before frag sees them. The depth buffer remembers how far away the nearest thing on each pixel is: ZTest checks new pixels against it, ZWrite says whether they store their distance there, and Offset nudges their distance a little, so a decal stops flickering on its wall.

ColorMask picks the channels written, and AlphaToMask smooths cutout edges under MSAA. Stencil bends brains a little. It keeps a hidden number on each pixel, for portals and mirrors. Every value waits in the fold-out at the end, where I look them up too.

Mixing with the screen using Blend

Blend mixes the color frag returns, the source (Src), into the color on screen, the destination (Dst). Each is multiplied by one of Blend’s two words, then they’re added:

c=s S+d Dc = s\,S + d\,D

Here ss is the source, dd the destination, SS and DD are Blend’s two words, and cc is the result. OneMinus means 1 minus.

Try each Blend line

Turn on JavaScript to run this demo.

Which Blend line draws the pink bar.
The alpha frag returns with the pink color.0.60
blend.hlsl draws the demo above
Fig. 2

Pick each line in the Blend menu and slide Alpha. The pink bar is what frag returns. Here frag mixes by hand, with the ramp and the mint disc standing in for the screen, so Copy for Unity has no Blend line. Premultiplied matches Traditional because frag multiplies its color by Alpha first, as that line expects. Additive, Soft additive and Multiplicative ignore the slider.

LineOption and look
Blend SrcAlpha OneMinusSrcAlphaTraditional, like tinted glass.
Blend One OneMinusSrcAlphaPremultiplied, the same look for a color already multiplied by its alpha.
Blend One OneAdditive. It only brightens, which suits glows.
Blend OneMinusDstColor OneSoft additive, brightening gently but never past white.
Blend DstColor ZeroMultiplicative. It only darkens, for shadows and tints.

A see-through material puts it all together:

Tags
{
    "RenderType"="Transparent"
    "Queue"="Transparent"
}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

Its queue draws it after solid things, so they show through, and ZWrite Off keeps it from hiding things drawn later.

Every value, for looking things up

Queue

All SubShader tags go in one Tags block, on one line like Tags { "Queue"="Transparent" "RenderType"="Transparent" } or over several like the see-through example above. The tags in the Queue, RenderType and More SubShader tags tables all go in that same block, even where a row shows one alone or in a Tags line of its own.

Tag, its turn, and what it’s for
"Queue"="Background" (1000)
Drawn before all the others, for things behind everything.
"Queue"="Geometry" (2000)
Solid things. The default.
"Queue"="Geometry+1" (2001)
A name plus a number is a turn of its own, here right after Geometry.
"Queue"="AlphaTest" (2450)
Cutouts. They’re solid surfaces with holes where alpha is low, drawn after the fully solid ones.
"Queue"="AlphaTest+50" (2500)
The last turn Unity counts as solid. From 2501 up, it counts as see-through.
"Queue"="Transparent-1" (2999)
A minus works too, here right before Transparent.
"Queue"="Transparent" (3000)
See-through things, drawn back to front.
"Queue"="Overlay" (4000)
Effects on top of everything, like lens flares.

RenderType

Any word works. Unity’s built-in shaders use these:

Tag and the shaders that use it
"RenderType"="Opaque"
Most solid shaders.
"RenderType"="Transparent"
Most see-through shaders, such as glass-like ones, particles and text.
"RenderType"="TransparentCutout"
Cutout shaders, and vegetation drawn in two passes.
"RenderType"="Background"
Skybox shaders.
"RenderType"="Overlay"
Halo and lens flare shaders.
"RenderType"="TreeOpaque"
The bark of terrain trees.
"RenderType"="TreeTransparentCutout"
The leaves of terrain trees.
"RenderType"="TreeBillboard"
Far terrain trees, drawn as flat pictures that face you.
"RenderType"="Grass"
Terrain grass.
"RenderType"="GrassBillboard"
Terrain grass drawn as flat pictures that face you.

More SubShader tags

Line and what it does
Tags { "DisableBatching"="True" }
Unity never merges this mesh with others to draw them in one go (dynamic batching). Merging moves the corners into world space, the whole scene’s shared coordinates, which breaks shaders that work in the mesh’s own space.
Tags { "DisableBatching"="LODFading" }
The same, but only for meshes in an LOD Group (simpler copies swapped in with distance) whose Fade Mode isn’t None.
Tags { "DisableBatching"="False" }
Merging is allowed. The default.
Tags { "ForceNoShadowCasting"="True" }
Casts no shadows. With forward rendering, Unity’s default way of lighting, and the old Vertex Lit path, it receives none either. False is the default.
Tags { "IgnoreProjector"="True" }
Projectors, which throw a picture onto whatever is under them, skip it. Meant for see-through things, which projectors can’t handle. Only in the built-in render pipeline, Unity’s older way of drawing a scene, which VRChat uses. False is the default.
Tags { "PreviewType"="Plane" }
The material preview shows a flat square. Skybox shows it as a sky, and Sphere, a ball, is the default.
Tags { "CanUseSpriteAtlas"="False" }
Warns that the shader needs each sprite’s own UVs, so the Legacy Sprite Packer shouldn’t pack the sprites into a shared texture. If they are packed, Unity shows an error in the Inspector. True is the default.
Tags { "RenderPipeline"="UniversalPipeline" }
The SubShader is only for URP. HDRenderPipeline means only for HDRP. URP and HDRP are Unity’s newer ways of drawing a scene (VRChat uses the older built-in one). Any other value, or no tag, means not for URP or HDRP.
Tags { "VRCFallback"="Toon" }
VRChat’s own tag, naming the shader VRChat shows instead when someone blocks yours in their Safety settings. Hidden hides the mesh. All values.

Pass tags

In the Pass’s own line, like Tags { "LightMode"="ShadowCaster" }:

Tag and what the pass does
"LightMode"="Always"
Draws in every case, with no lighting. The default.
"LightMode"="ForwardBase"
Forward rendering’s main pass. It brings in ambient light, the main directional light and lightmaps (light baked into textures). The less important lights come in too, done cheaply per corner or as soft light from all around.
"LightMode"="ForwardAdd"
Forward rendering again, once for each extra light worked out per pixel, added on top.
"LightMode"="Deferred"
For deferred shading, the other way to light a scene. The pass only stores each pixel’s color, which way it faces and how shiny it is in screen sized textures (the G-buffer), and Unity lights them all afterwards.
"LightMode"="ShadowCaster"
Draws only how far away the object is, into a shadow map or the camera’s depth texture.
"LightMode"="MotionVectors"
Works out how far each pixel of the object moved on screen since the last frame.
"LightMode"="Vertex"
The old Vertex Lit path for objects without lightmaps, with every light worked out per corner.
"LightMode"="VertexLMRGBM"
The old Vertex Lit path, for objects with lightmaps on PC and consoles, which pack light brighter than white into the lightmap (RGBM).
"LightMode"="VertexLM"
The same on mobile, where lightmaps hold light up to twice white (double LDR).
"LightMode"="Meta"
Never drawn on screen. Unity uses it only while baking lightmaps, or for Enlighten’s realtime light bouncing.
Tags { "LightMode"="ForwardBase" "PassFlags"="OnlyDirectional" }
Used in a ForwardBase pass with forward rendering, it hands the pass only the main directional light, ambient light and light probes. The less important lights are left out.
Tags { "RequireOptions"="SoftVegetation" }
Draws the pass only when Soft Vegetation is on in the Quality settings.

Render state

Each line goes in a Pass, or in a SubShader to set it for all of its passes.

LineWhat it does
Cull BackThrows away triangles facing away from the camera, before frag runs. The default.
Cull FrontThrows away triangles facing the camera, so you see the mesh’s inside, as outlines need.
Cull OffKeeps both sides, for double sided cloth or see-through things.
ZWrite OnEach pixel stores its distance in the depth buffer, so things behind it get hidden. The default.
ZWrite OffLeaves the depth buffer alone, as see-through things usually want.
ZClip TrueParts nearer than the camera’s near limit, or past its far limit, get cut off. The default.
ZClip FalseThose parts are squashed onto the limit instead.
Offset -1, -1Pulls the depth closer, so a decal wins against the wall under it. The first number is scaled by how steeply the triangle slopes away from you, the second is a small fixed step. Positive numbers push away.
ColorMask RGBWrites only these channels, here all but alpha. Any mix of R, G, B and A works. Without the line, all four.
ColorMask 0Writes no color at all, for a pass that only fills the depth or stencil buffer.
ColorMask RGB 1A number after the channels sets them for only that render target, 0 to 7, for shaders that draw into several textures at once.
AlphaToMask OnWith MSAA, the anti-aliasing that checks several spots in each pixel, turns alpha into smooth cutout edges. Without MSAA, results differ between graphics cards. Off is the default.
Conservative TrueDraws every pixel a triangle touches at all, not only those whose middle it covers. Graphics cards that can’t do this ignore the line. False is the default.
ZWrite [_ZWrite]Any value can come from a material setting instead, by writing its name in square brackets. The setting holds the value as a number. For ZWrite, Off is 0 and On is 1. For Cull, Off is 0, Front 1 and Back 2. The Enum drawer [Enum(UnityEngine.Rendering.CullMode)] turns such a setting into a dropdown of those names.

ZTest

LineDraws the pixel when it is
ZTest LessNearer.
ZTest LEqualNearer, or as near. The default.
ZTest EqualExactly as near.
ZTest GEqualFarther, or as far.
ZTest GreaterFarther. It shows only where something else covers it.
ZTest NotEqualAnything but exactly as near.
ZTest AlwaysAnywhere, and nothing hides it.
ZTest NeverNowhere, so it never draws.
ZTest DisabledAnywhere, with the depth test switched off.

Blend

LineLooks like
Blend OffNothing is mixed, and the color replaces what’s on screen. The default.
Blend SrcAlpha OneMinusSrcAlphaTraditional, like tinted glass.
Blend One OneMinusSrcAlphaPremultiplied, the same look for a color already multiplied by its alpha.
Blend One OneAdditive, which only brightens.
Blend OneMinusDstColor OneSoft additive, brightening gently but never past white.
Blend DstColor ZeroMultiplicative, which only darkens.
Blend DstColor SrcColor2x multiplicative. Where the shader’s color is below 0.5, half of white, the screen darkens, and above it the screen brightens.
Blend One Zero, Zero OneAfter the comma come separate words for alpha. Here the color replaces the screen’s, and the screen keeps its alpha.
Blend 1 One OneA number first sets only that render target, 0 to 7, for shaders that draw into several textures at once.

Blend words

WordMultiplies by
One1, which keeps the color as it is.
Zero0, so the color drops out.
SrcColorThe color frag returns, channel by channel.
SrcAlphaThe alpha frag returns.
DstColorThe color already on screen, channel by channel.
DstAlphaThe alpha already on screen.
OneMinusSrcColor1 minus the color frag returns.
OneMinusSrcAlpha1 minus the alpha frag returns.
OneMinusDstColor1 minus the color on screen.
OneMinusDstAlpha1 minus the alpha on screen.
SrcAlphaSaturateThe smaller of the alpha frag returns and 1 minus the alpha on screen. For the alpha channel it is 1.

BlendOp

LineThe result
BlendOp Adds S+d Ds\,S + d\,D. The default.
BlendOp Subs S−d Ds\,S - d\,D.
BlendOp RevSubd D−s Sd\,D - s\,S.
BlendOp MinThe smaller of source and destination; Min and Max ignore the factors.
BlendOp MaxThe larger of source and destination.
BlendOp LogicalXor and fifteen moreLogic on the stored numbers, which are kept as rows of on/off switches (bits), switch by switch. They need DirectX 11.1 or Vulkan. All sixteen: LogicalClear, LogicalSet, LogicalCopy, LogicalCopyInverted, LogicalNoop, LogicalInvert, LogicalAnd, LogicalNand, LogicalOr, LogicalNor, LogicalXor, LogicalEquiv, LogicalAndReverse, LogicalAndInverted, LogicalOrReverse, LogicalOrInverted.
BlendOp Multiply and fourteen moreBlend modes like a paint program’s. They only work with OpenGL’s advanced blending. All fifteen: Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, HSLHue, HSLSaturation, HSLColor, HSLLuminosity.

Stencil

LineWhat it does
Stencil { Ref 2 Comp Always Pass Replace }This is the window, writing 2 wherever it draws. Give it an earlier queue than what’s seen through it.
Stencil { Ref 2 Comp Equal }The world behind the window, drawn only where 2 was written.
Ref 2The pass’s own number, 0 to 255. The test compares it with the stored one, and Replace writes it. 0 by default.
ReadMask 255The number is stored as 8 on/off switches (bits). This says which of them the test looks at. 255, all of them, is the default.
WriteMask 255Which bits a write may change. 255 is the default, and 0 changes none (it doesn’t write a 0).
Comp EqualThe test that compares Ref with the stored number. Always by default.
Pass ReplaceWhat to write when the stencil test and the depth test both pass. Keep by default.
Fail ZeroWhat to write when the stencil test fails. Keep by default.
ZFail IncrSatWhat to write when the stencil test passes but the depth test fails. Keep by default.
CompBack, PassBack, FailBack, ZFailBackThe same, only for triangles facing away. CompFront and the rest work only for triangles facing you. The plain ones win when both are set.

Stencil tests

CompDraws the pixel when Ref is
NeverNo value of Ref. Nothing draws.
LessSmaller than the stored number.
EqualThe same as the stored number.
LEqualSmaller or the same.
GreaterBigger.
NotEqualDifferent.
GEqualBigger or the same.
AlwaysAnything, with no test at all. The default.

Stencil writes

Pass, Fail or ZFailWrites
KeepNothing, and the stored number stays. The default.
Zero0.
ReplaceRef.
IncrSatThe number plus 1, stopping at 255.
DecrSatThe number minus 1, stopping at 0.
InvertThe number with all 8 of its on/off switches (bits) flipped, so 5 becomes 250.
IncrWrapThe number plus 1, and 255 wraps round to 0.
DecrWrapThe number minus 1, and 0 wraps round to 255.

Picking a SubShader, and other lines

LineWhat it does
LOD 300Goes in a SubShader and says how heavy it is. A script can set a limit, and Unity then skips SubShaders above it. There’s no limit by default. Put heavier SubShaders first. Unity’s Unlit shaders are 100, Standard is 300.
Fallback "Unlit/Color"At the end of the Shader block, it names the shader to use when none of the SubShaders can run. It matters for shadows too, since a shader casts them when any of its SubShaders, or any fallback, has a ShadowCaster pass.
Fallback OffNo stand-in, so Unity shows its magenta error shader instead.
CustomEditor "MyShaderGUI"Names a C# class that draws the material’s settings its own way, like the tabbed panels of big toon shaders. It goes in the Shader block, after the SubShaders.
Name "Outline"The Pass’s name, written inside it and shown in the Frame Debugger (the Unity window that steps through a frame one drawing at a time). Unity stores it in capitals, so other shaders ask for OUTLINE.
UsePass "MyToon/OUTLINE"Copies a named pass from another shader into this SubShader. You write the shader’s name, a slash and the pass’s name in capitals.
GrabPass { "_BackgroundTexture" }Sits in a SubShader and copies what’s on screen so far into a texture later passes can read, for glass that bends what’s behind it. With a name, Unity grabs once per frame. GrabPass { } grabs again for each batch that uses it, as _GrabTexture. It’s slow, so grab sparingly.

Takeaways

  • A Unity shader is ShaderLab boxes around HLSL code, and its tags and render state say when and how it draws.
  • Each Properties line is a name, a label, a type and a start value, read by the variable with the same name.
  • vert places the corners, frag colors the pixels, and the UNITY_ lines keep both eyes of a headset right.
Search the lessons

Type any word from the lessons, like normalize or smoothstep. The start of a word is enough.