Code Review, Best Practices and ShaderLab vs Shader Graph

Hi, I’m completely new to ShaderLab (some HLSL experience) and attempted to recreate a Shader Graph tutorial with ShaderLab (https://game-developers.org/unity-ribbon-spiral-shader-graph-tutorial/).

It pretty much works except for the last part (gradient color), but I have no idea about best practices and have commented questions I could not find a definitive answer to in the code below.
Thoughts?

Shader "Particles/Ribbon"
{
    Properties
    {
        [Header(Color)]
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        // Declare vector properties without 4 values in editor?
        [Header(Constants)]
        _Up ("Up", Vector) = (0,1,0)
        _Left ("Left", Vector) = (-1,0,0)
        [Header(Streaks)]
        _Width ("Width", Float) = 0.5
        _Gap ("Gap", Float) = 0.5
        // Can't specify integer?
        _Count ("Count Multiplier", Int) = 3
        [Header(Animation)]
        _Speed ("Speed", Float) = 1
        _Offset ("Offset", Float) = 0
        // No easy way to define gradients in ShaderLab?
        // _Gradient ("Gradient", Gradient) = ???
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent"
            // Any tag to enable _Time updates in preview?
        }

        // For transparency to work (?)
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            // Use CGPROGRAM or HLSLPROGRAM?
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            /** What we get to work with */
            struct appdata_t
            {
                float4 vertex : POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
            };

            /** Information from vertecies */
            struct v2f
            {
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
                /** Object Space */
                float4 os : POSITION1;
                float2 uv : TEXCOORD0;
            };

            // Redeclare properties
            sampler2D _MainTex;
            fixed4 _Color;
            float4 _MainTex_ST;
            float3 _Up;
            float3 _Left;
            float _Width;
            int _Count;
            float _Speed;
            float _Offset;
            float _Gap;

            /** Process vertecies */
            v2f vert(appdata_t v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color * _Color;
                o.os = v.vertex;
                return o;
            }

            /** Process pixels */
            fixed4 frag(const v2f i) : SV_Target
            {
                half4 col = i.color;

                const float3 val1 = normalize(_Left);
                // Radial mask
                const float val2 = acos(dot(val1, normalize(cross(i.os, _Up))));
                // Normalize (?) spirals
                const float val2_5 = val2 / (UNITY_PI * 2);
                // Number of spirals
                const float val3 = val2_5 * sign(dot(val1, i.os)) * _Count;
                // Animate spirals
                const float val4 = _Offset + _Time.y * _Speed + val3;
                // Transparency gradient - fading out towards the top (positive Y)
                col.a = 1 - step(_Width, frac(dot(_Up, i.os) / _Gap + val4));

                return col;
            }
            ENDHLSL
        }
    }
}

Also, a question that comes to mind after doing this is, do people still write shaders with just ShaderLab or have people moved to Shader Graph? The documentation on ShaderLab is very lackluster, unlike what I have seen from Shader Graph, which actually has a complete library of all nodes.

As of now I feel like the way to go is to just use Shader Graph with the Custom Function Node (writing code is quicker, cleaner and more convenient than building a network of nodes in my opinion).

Thanks ^^

Most people will probably move to Shader Graph at this point. Though anyone who can write shader code will find them to be cumbersome and slow to use. Both Shader Lab and Shader Graph will continue to be used going into the future. Almost all of Unity’s own shaders for both the URP and HDRP are written directly in HLSL and in Shader Lab files, and Shader Graph itself internally just outputs a Shader Lab file behind the scenes!

I would maybe be slightly pedantic and say that Shader Lab itself is entirely documented at this point. But I view all the stuff outside of the HLSLPROGRAM & ENDHLSL block as being ShaderLab, and everything inside as being HLSL, though that’s not entirely true as the #pragma lines are all ShaderLab specific as well, and there’s a lot of Unity specific code that most shaders are going to be reusing via #include lines.

Some random comments on the comments in your code, starting with the one you called out in the body of your post:

Nope. This is a Shader Graph specific “thing” right now. But it can be replicated in Shader Lab… since again, Shader Graph is effectively (though not actually) just producing a ShaderLab file with HLSL inside it.

It’s important to understand a gradient in Shader Graph is hard coded in the shader, and is not editable via material properties. It is always the gradient you setup in the Shader Graph’s blackboard (the floating window used mainly for setting up material properties). The output HLSL just literally has a struct with two arrays of 8 color and 8 alpha value keys, each holding the color or alpha and the “time” between 0.0 and 1.0. This is effectively a mirror of the c# Gradient class.

https://docs.unity3d.com/Packages/com.unity.shadergraph@12.0/manual/Gradient-Node.html

  • This documentation is actually a bit off, as the color entries will usually be float4(r, g, b, time) and the alpha entries would be float2(alpha, time), not lone floats. Though technically there’s nothing wrong with that HLSL.

The arrays are then “sampled” assuming they’re correctly in order using the code shown here:
http://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Sample-Gradient-Node.html

You can copy paste that code, along with the Gradient struct itself and have it work in your code.
https://github.com/Unity-Technologies/Graphics/blob/master/com.unity.shadergraph/ShaderGraphLibrary/Functions.hlsl#L28

Of course you having to set the color values manually as there’s no nice UI for it when not using Shader Graph.

Nope. It’ll always show as a 4 component vector. You can write custom material property drawers or a custom material editor to handle this if you want to. ShaderLab itself doesn’t handle it on its own. Shader Graph extended this to include Vector2, Vector3, and Vector4 types, but they don’t seem to be supported by ShaderLab itself sadly.

Though it should be noted this is purely a visual thing for the inspector! Even with ShaderLab it is internally a full Vector4 for the material property as that’s the only kind of vector materials support!

When the property is assigned to the shader uniform it is turned into whatever dimension or number type is specified by the shader code itself.

Same here as with vectors. Materials only support float value properties, and will only display float values even for an “Int” property. I don’t really know why they included it as an option if they don’t draw them as integers in the inspector. There is a built in material property drawer that can help here: [IntRange]

Not that I know of. You just need to click on the little play button in the material preview.

Yes. The blend mode is telling the GPU how to blend the output of the shader with what’s already in the buffer. In your example that’s the blend mode for traditional alpha blending.

Either work. CGPROGRAM is technically cruft from a decade ago when Unity did use Nvidia’s Cg shader language instead of Direct3D HLSL. The only difference between using CGPROGRAM and HLSLPROGRAM is when using CGPROGRAM Unity automatically adds #inlcude "UnityCG.cginc" to the file at compile time, and HLSLPROGRAM does not. Otherwise they both assume all code inside is HLSL.

AFAIK that info is still undocumented.

And I wanted to touch on this real quick. The Properties {} in ShaderLab are for telling the Unity Material class what values to serialize. I.E.: what values to save in the material asset. They also happen to be used to display them in the inspector by default, but that can be modified or turned off. The properties in the Material class do not need to exist in the shader code itself! You can have dummy ones used entirely for c# purposes. And similarly you can have “properties” (uniforms in HLSL parlance) defined in the HLSL code that do not exist in the properties with the assumption these will be set manually from c# either on the material or as shader globals. Things like matrices or arrays can’t be serialized at all for materials, so they have to be set from c#.

5 Likes

Thank you so much for the extensive reply answering pretty much all my questions.

Normally though I am able to find information like this eventually, but with ShaderLab it was exceedingly difficult.
I don’t know if Unity docs’ SEO is bad or what it is, but as an example I can’t find any information on [IntRange] and it is not mentioned in Unity - Manual: ShaderLab: defining material properties.

I do like the ability to write shaders with text only without having to go too low-level, coming from Unreal Engine where using HLSL in their shader graph is discouraged because it prevents constant folding, something you can do yourself in Unity.

I will keep trying ^^

Also, have you verified that hitting the little play button in the preview window enables _Time updates? because for me it doesn’t.

They might have broken it at some point. It used to work!

1 Like

Because for whatever reason it’s here:
https://docs.unity3d.com/ScriptReference/MaterialPropertyDrawer.html

1 Like