Unity Shader Properties

August 31, 2023

Introduction

A Shader Property enables you to configure a material’s settings without having to hardcode values in the shader. If you’re familiar with C#, a Shader Property is like a C# field.

By the end of this article, you will know everything you need to know about Shader Properties.

What is a Shader Property?

A Shader Property exposes a Shader Variable in the Unity material inspector window. You know how C# fields only show up in the Inspector if you make them public or use SerializeField? A Shader Property is like that.

What is the format for a Shader Property?

Shader properties follow a standard format and expect specific values. In general, you need to provide a property name, inspector name, type, and default value for the property.

[Optional Attributes] _PropertyName (“Inspector Name”, Type) = default value

Do I need to set up a matching variable in the shader pass?

Yes. If you don’t, that variable won’t exist.

I recommend that you think of this the other way around. You always need to set up a variable in the shader pass. Optionally, you might want to expose that variable in the Inspector. To add an Inspector control for it, then you need to add a Property to match your variable.

What is the syntax for the CBUFFER block?

You start the block with a CBUFFER_START(UnityPerMaterial) command. Then, you end the block with a CBUFFER_END command. You define any variables in the CBUFFER the same way you would outside the CBUFFER.

CBUFFER_START(UnityPerMaterial)
  int _MyInteger;
CBUFFER_END

Do I need to wrap my matching variables in the CBUFFER?

In short, yes. If you don’t, then Unity won’t be able to use the SRP Batcher with that material.

What shader properties can I use?

Unity supports the following property types:

  • Integer
  • Float
  • Color
  • Vector
  • Texture2D
  • Texture2D Array
  • Texture3D
  • Cubemap
  • Cubemap Array

What attributes can I use for the Shader Properties?

Unity supports some basic attributes that affect how the property works.

In my opinion, there are three important attributes:

  • HDR: Makes the texture or color use HDR values
  • HideInInspector: Hides the property in the editor
  • Normal: Makes Unity pop a warning if the Texture is not set to Normal Map format.

How do I declare and use each of the shader properties?

I put together example usage for each of these basic types.

// Basic Types

_MyInteger ("Integer", Integer) = 0
_MyFloat ("Float", Float) = 2.0
_MyFloatClamped ("Float with Clamped Range", Range(0.0, 1.0)) = 0.5
_MyVector ("Vector", Vector) = (0, 1, 0, 0)
_MyColor ("Color", Color) = (1, 0, 0, 1)


// Textures

_MyTexture ("Texture2D", 2D) = "white" {}
[HDR] _MyTextureArray ("Texture2DArray", 2DArray) = "black" {}
_MyTexture3D ("Texture3D", 3D) = "red" {}
_MyCubemap ("Cubemap", Cube) = "" {}
_MyCubemapArray ("CubemapArray", CubeArray) = "" {}


// Attributes

[HDR] _MyHDRColor ("Color (HDR)", Color) = (0, 2, 0, 1)
[HideInInspector] _MyHiddenFloat ("Float (Hidden)", Float) = 99.0
[Normal] _MyNormalMap ("Normal Map", 2D) = "bump" {}

How do I declare these shader properties in the CBUFFER?

I put together example usage for this set of properties in the CBUFFER.

CBUFFER_START(UnityPerMaterial)
    int _MyInteger;
    float _MyFloat;
    float _MyFloatClamped;
    float4 _MyVector;
    float4 _MyColor;

    TEXTURE2D(_MyTexture);
    SAMPLER(sampler_MyTexture);
    float4 _MyTexture_ST;

    TEXTURE2D_ARRAY(_MyTextureArray);
    SAMPLER(sampler_MyTextureArray);
    float4 _MyTextureArray_ST;

    TEXTURE3D(_MyTexture3D);
    SAMPLER(sampler_MyTexture3D);
    float4 _MyTexture3D_ST;


    TEXTURECUBE(_MyCubemap);
    SAMPLER(sampler_MyCubemap);
    float4 _MyCubemap_ST;

    TEXTURECUBE_ARRAY(_MyCubemapArray);
    SAMPLER(sampler_MyCubemapArray);
    float4 _MyCubemapArray_ST;

    float4 _MyHDRColor;
    float _MyHiddenFloat;

    TEXTURE2D(_MyNormalMap);
    SAMPLER(sampler_MyNormalMap);
    float4 _MyNormalMap_ST;
CBUFFER_END

As you can see, it’s a hassle to set up Texture if you also need to set up the Sampler and _ST variables.

What is the difference between Float and Range?

Range is like a wrapper for the Float type. It probably should have been an attribute instead of a type. There is no fundamental difference between a Float and Range type, but the Range type changes how it appears in the Inspector.

Do I need to use the Curly Braces after the Texture properties?

Yes. If you don’t use the curly braces, Unity will throw an error if the subsequent property is an integer, float, vector, or color property.

Can I set up the matching variable in the Shader or SubShader?

No. You must set up the matching variable in the Pass. You can’t set it up in the Shader or SubShader sections. If you try to set it up in the Shader or SubShader, Unity will throw a Shader error: '’Parse error: syntax error, unexpected TVAL_ID”.

Do you have a full shader example showing proper usage of Shader Properties?

Yes. I wrote a shader that demonstrates how to use every shader property and all the (important) attributes. This shader only works in URP.

When you apply the shader to a material, it automatically renders a lovely inspector.

Here’s the underlying shader code. The Fragment stage is a stub. It uses the _MyColor and _MyTexture properties, but that’s it.

Shader "Example/OccaSoftware/ShaderPropertiesDemo"
{
    Properties
    {
        // Basic Types
        _MyInteger ("Integer", Integer) = 0
        _MyFloat ("Float", Float) = 2.0
        _MyFloatClamped ("Float with Clamped Range", Range(0.0, 1.0)) = 0.5
        _MyVector ("Vector", Vector) = (0, 1, 0, 0)
        _MyColor ("Color", Color) = (1, 0, 0, 1)


        // Textures
        _MyTexture ("Texture2D", 2D) = "white" {}
        [HDR] _MyTextureArray ("Texture2DArray", 2DArray) = "black" {}
        _MyTexture3D ("Texture3D", 3D) = "red" {}
        _MyCubemap ("Cubemap", Cube) = "" {}
        _MyCubemapArray ("CubemapArray", CubeArray) = "" {}


        // Attributes
        [HDR] _MyHDRColor ("Color (HDR)", Color) = (0, 2, 0, 1)
        [HideInInspector] _MyHiddenFloat ("Float (Hidden)", Float) = 99.0
        [Normal] _MyNormalMap ("Normal Map", 2D) = "bump" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
            };

            CBUFFER_START(UnityPerMaterial)
                int _MyInteger;
                float _MyFloat;
                float _MyFloatClamped;
                float4 _MyVector;
                float4 _MyColor;

                TEXTURE2D(_MyTexture);
                SAMPLER(sampler_MyTexture);
                float4 _MyTexture_ST;

                TEXTURE2D_ARRAY(_MyTextureArray);
                SAMPLER(sampler_MyTextureArray);
                float4 _MyTextureArray_ST;

                TEXTURE3D(_MyTexture3D);
                SAMPLER(sampler_MyTexture3D);
                float4 _MyTexture3D_ST;

                TEXTURECUBE(_MyCubemap);
                SAMPLER(sampler_MyCubemap);
                float4 _MyCubemap_ST;

                TEXTURECUBE_ARRAY(_MyCubemapArray);
                SAMPLER(sampler_MyCubemapArray);
                float4 _MyCubemapArray_ST;

                float4 _MyHDRColor;
                float _MyHiddenFloat;

                TEXTURE2D(_MyNormalMap);
                SAMPLER(sampler_MyNormalMap);
                float4 _MyNormalMap_ST;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _MyTexture);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_MyTexture, sampler_MyTexture, IN.uv);
                return color * _MyColor;
            }
            ENDHLSL
        }
    }
}

Conclusion

As a game developer, it is crucial that you master the use of Shader Properties. You need to be able to create flexible shaders that designers can easily edit in the Inspector. Shader Properties are the fundamental tool for powering this functionality. In this guide, you learned everything that you need to know about Shader Properties.

To continue to level up your Unity shader skills, I recommend that you read my comprehensive guide on HLSL’s step function.

Want to build better games?

© 2024