in programming, shader, tutorial, Unity3D

Arrays & shaders: heatmaps in Unity

Share Button

This tutorial explains how to pass arrays to shaders in Unity. This feature has been present for a long time, but is mostly undocumented. Unity 5.4.0 Beta 1 will introduce a proper API to pass arrays to shaders; this technique however will work with any previous version.

If you are using Unity 5.4+, please refer to the Arrays & Shaders in Unity 5.4+ tutorial.

Introduction

One of the characteristic which makes shaders hard to master is the lack of a proper documentation. Most developers learn shaders by messing up with the code, without having a deep knowledge of what’s going on. The problem is amplified by the fact that Cg / HLSL makes lot of assumptions, some of which are not properly advertised. Unity3D allows C# scripts to communicate to shaders using methods such as SetFloat, SetInt, SetVector and so on. Unfortunately, Unity3D doesn’t have a SetArray property method, which led many developers to believe Cg / HLSL doesn’t support arrays either. Which is not true. This post will show how is possible to pass arrays to shaders. Just remember that GPUs are highly optimised for parallel computations, and that using for loops within a shader will dramatically drops its performance.

heatmap4

Step 1: The arrays

If you are familiar with heatmaps, you’ll know that they visualise the density of a certain phenomenon using a colour gradient. They are usually generated from a set of points, each one with its radius and intensity. There is no easy way to implement a heatmap in a shader, without using arrays. What we are going to do is to pass a list of points to the material, and iterate on each one to calculate its colour contribution for every pixel of the image. There are then three informations needed for each points: its position, its radius and its intensity. Since Unity3D doesn’t provide APIs to set arrays, they won’t be mentioned in the Properties section of the shader. Instead, they’ll be declared as the follow:

Cg / HLSL doesn’t support arrays with variable size, so they need to be initialised with the maximum number of points (100, in this example). We also have to signal to the shader that these variables will be modified from outside, hence the uniform qualifier. As it happens in C, there is an extra variable which indicates how many points are actually used.

It’s possible to notice that instead of having three variables per each point, we only have two. This is due to a nasty bug feature of Cg which doesn’t allow arrays such as float _Intensities [100] to be accessed from outside the shader. All the arrays we want to access from C# must be packed arrays, such as  float2, float3, and so on. For this reason, the radius and intensity of points will be packed, respectively, in the x and y fields of _Properties.

Step 2: The shader

The variables _Points and _Properties are actual arrays, so their elements can be accessed simply using the square bracket notation.

For every pixel of the geometry, lines 41-47 calculate the heat contribution given from each point. The final heat, h, (ranging from 0 to 1) is then used to sample a texture which will determine the actual colour and opacity. Lines 6-7 are necessary if we want the geometry to support alpha transparency.

Step 3: The C# code

The only thing which is missing, is the initialisation of the arrays. On compilation, something magical happens: every cell of the array _Points[i] will be accessible from C# as _Pointsi. Armed with this knowledge, we can pass an array very easily to the shader:

All the public fields can be initialised directly from the inspector. The overall look of the heatmap can change dramatically just by playing a little bit with its heat texture. Using toom ramps generally yields visually pleasant results.

map_02

Step 4: A more general approach

To overcome the huge limitation Unity3D has when it comes to pass arrays to shaders, we can create a more general class.

In order to work, it needs a material with a shader which contains an array. Its name must be specified in the string name. The shader must also have a variable with the same name of the array, followed by _Length.

Conclusion & download

Using arrays in Shader is possible with any recent version of Unity, due to a poorly documented feature. Official APIs are planned from Unity 5.4.0 Beta 1. The technique introduced in this tutorial is compatible with earlier versions.

Arrays can be used for a variety of reasons. They can be used to initiate hundreds of properties via scripting, without the need to expose each one of them individually.

You can download the complete Unity package for this project here.

Other resources

Support this blog! ♥

In the past two years I've been dedicating more and more of my time to the creation of quality tutorials, mainly about game development and machine learning. If you think these posts have either helped or inspired you, please consider supporting this blog.

PatreonBecome a Patron Oo4th_patreon_name
PaypalDonate on PayPal
Twitter_logoFollow on Twitter

Don't miss the next tutorial!

There's a new post every Wednesday: leave your email to be notified!


Write a Comment

Comment

36 Comments

  1. Hi!

    Didn’t have the time to read the whole article yet, but something catched my eye and just wanted to comment that the next Unity version will support setting array properties on shaders:

    “Shader uniform array support. Uniform arrays can be set by new array APIs on MaterialPropertyBlock. Maximum array size is 1023. The old way of setting array elements by using number-suffixed names is deprecated.”

    Source: https://unity3d.com/es/unity/beta/unity5.4.0b1

    Thanks for your amazing articles!

  2. > Just remember that GPUs are highly optimised for parallel computations, and that using for loops within a shader will dramatically drops its performance.

    Does this still hold true if we’re replacing 100 instanced shaders with one shader using an array of 100 points as in your example?

    • Each shader comes with its own overhead. I believe that 100 different shaders (each on in its own material) will be MUCH worse than a single shader with an array. This is also because different materials might require different draw calls. If you can use a single material to draw 100 things, go with that. :p (within reasons)

  3. Quick question: I’ve noticed in a few tutorials that you make use of the Hidden/SHADER_NAME naming feature, but don’t understand how you are applying these shaders to materials. Would you mind enlightening me? Thanks!

    • Hey! I use that for a couple of reasons. First of all, I don’t want the list of my shaders to be too long. The second (and more important) reason is that some shaders are designed for a specific use or effect only. Hence, they should not be used elsewhere.

      You can attach a Shader to a material by simply dragging it from the project window and releasing it onto the material you want. If you select a material, in the inspector, you’ll also see “Shader” and the dropdown menu. You can also drop the shader there! 😀

  4. Hi there, pretty great tutorial, as usual! I have two quick questions, though:

    1) what exactly is the main input of that shader? I mean, what are the vertices being inputed? I ask that because it wasn’t clear that o.worldPos means, considering that we are passing the position of the points of interest directly via C#;

    2) out of curiosity, how many ms does this image effect shader takes to run in your machine (excluding, of course, the Camera.AAResolve command), according to your profile? And in which screen resolution? Thanks!

  5. Thanks for writing this tutorial. I’m using this project as a launching off point for something else, but I’m immediately running into an issue with Unity and Android as the shader from your project fails to compile (pink error) and reports:
    Invalid const register num: 201. Max allowed is 31.

    I’ve pinned it down to the for loop, most likely, however I’m still pretty novice when it comes to programming shaders. Googling suggests that this error is a response from too many constances being declared, though the code doesn’t outright seem to declare any?

    Anyways, do you think, is there possibly a way around this?

  6. Hey Alan, thank you very much for this great tutorial!
    I was wondering, would it be possible to use lists instead of an array to determine the number of points of the shader.
    Thank you!

  7. It’s not working in WebGL : WARNING: Shader Unsupported: ‘Hidden/Heatmap’ – Pass ” has no vertex shader

    Do you know why ?
    Thnaks !

  8. Thanks for the updated post for 5.4.

    I’m missing something regarding the position part of the shader.
    In your heatmap example, if I move the quad which has the heatmap, the heatmap seems to be in world space, not staying on top of the quad but stays in same position as I move the quad.

    Any help with that would be appreciated

    • Hey!
      Yes, this was the intended behaviour.
      I am passing the world position of the points to the shader!
      If you want something different, then you’ll have to add (or subtract?!) the position of the quad from the points’ position!

      • Can you please point me to such subject? I have a 3d object I want to draw a heatmap onto, I have the coordinates of the 3d mesh (in local space and not world space). and I want the material / shader to work wherever I place the 3d object.

        Thanks !

        • You can just pass the position of the quad in world space to the Shader, using material.SetVector4(“_NameOfProperty”, yourQuad.transform.position);
          Then, sum (or subtract, depending on the code) this value from the position of the 3D points of the heatmap.

          You can also do this directly in the C# code, if you don’t want to pass an extra parameter to the shader.

          • This is such a great tutorial but we are struggling with the same problem as bergos.. do the modifications have to happen in the vert function? And if so, to o.pos or after it has been converted to worldPos? Any help would be much appreciated!

          • Figured it out working off of Alan’s recommendation!

            –MODS TO SHADER FILE–
            1. Add a _QuadTransform Vector Property to the Properties block:
            Properties{
            _HeatTex(“Texture”, 2D) = “white” {}
            _QuadTransform(“Quad Transform Vector”,Vector) = (0,0,0,0)
            }

            2. Add a _QuadTransform declaration BEFORE you utilize it in the vert function:
            #pragma vertex vert
            #pragma fragment frag
            float4 _QuadTransform; //Added This

            3. Modify your vert function to subtract the Quad float3 xyz from o.worldPos.xyz:
            vertOutput vert(vertInput input) {
            vertOutput o;
            o.pos = UnityObjectToClipPos(input.pos);
            o.worldPos = mul(unity_ObjectToWorld, input.pos).xyz;
            o.worldPos.xyz -= _QuadTransform.xyz; //Added This
            return o;
            }

            –MODS TO HEAT MAP SCRIPT–
            4. Add a public quad GameObject Property to script:
            public GameObject quad; //Added This
            public GameObject parent;
            public Vector4[] positions;
            public Vector4[] properties;

            5. Add setVector call to Start() method
            material.SetVector(“_QuadTransform”, quad.transform.position); //This
            heatmap.SetInt(“_Points_Length”, count);
            heatmap.SetVectorArray(“_Points”, positions);
            heatmap.SetVectorArray(“_Properties”, properties);

            6. In Update() method call to SetVector to ensure if Quad moves then the shader will be aware and update the heatmap accordingly:
            void Update()
            {
            if (material != null && quad != null)
            {
            material.SetVector(“_QuadTransform”, quad.transform.position);
            }
            }

            Done!

          • Now that I have the heatmap moving with the quad.transform.xyz I was curious if you had any thoughts on how to add to the solution to ensure that the heat map follows the quad after a rotation is applied. My use case would be… rotating the Quad about Y as to emulate the Quad spinning like a record player.

            I have managed to spin the heatmap but it breaks the logic in the frag() function..

            0. Include this:
            #include “UnityCG.cginc”

            1. Updated Properties, Structs, and variable declarations:
            Properties{
            _HeatTex(“Texture”, 2D) = “white” {}
            _QuadTransform(“Quad Transform Vector”,Vector) = (0,0,0,0)
            _RotationDegrees(“Rotation Degrees”, Float) = 0.0
            }

            float4 _QuadTransform;
            float _RotationDegrees;

            struct vertInput {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
            };
            struct vertOutput {
            float4 pos : SV_POSITION;
            fixed3 worldPos : TEXCOORD1;
            float2 uv : TEXCOORD0;
            };

            2. Added this to the vert() function:
            vertOutput o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.worldPos = mul(unity_ObjectToWorld, v.vertex);
            o.worldPos.xyz -= _QuadTransform.xyz;

            // Rotating UV
            const float Deg2Rad = (3.14159265359 * 2.0) / 360.0;
            // Pivot Pt
            float2 pivot = float2(0.5, 0.5);
            // Rotation Matrix
            float cosAngle = cos(radians(_RotationDegrees));
            float sinAngle = sin((_RotationDegrees));
            float2x2 rot = float2x2(cosAngle, -sinAngle, sinAngle, cosAngle);
            // Rotation with pivot
            float2 uv = v.texcoord.xy – pivot;
            o.uv = mul(rot, uv);
            return o;

            3. And return this as a fixed4 from the frag() function instead of the color.
            return tex2D(_HeatTex, output.uv);

            I can’t figure out how to keep your additional return from the frag() while updating the output.uvs…any thoughts or wisdom for this shader noob would be much appreciated!

  9. Great article! This is the only document i can find about using array in shaders.

    I have a question as a beginner: Is “for loop” only supported in vertex shader, not in fragment shader?

    I’ve written a fragment shader to show heat map, it works well without loop, but failed with loop.

    • Hey! I don’t think there’s any issue with for loops in the fragment function!
      I have used them for volumetric shaders, if I remember correctly. There are some limitations through. Like texture sampling in for loops.

  10. Hi Alan,

    Tried to make it working on unity 5.4.1f1 but this doesn’t work, no message error appear and texture not draw. How to resolve this ? Thanks a lot

  11. Hi Alan,

    Sorry but after tested on unity 5.4 the new tutorial, nothing appear on map.
    Tried:

    void Awake ()
    {
    //Renderer renderer = GetComponent();
    //Material material = renderer.sharedMaterial;
    //material.SetFloatArray(array);

    material.SetInt(“_Points_Length”, 3);
    material.SetFloatArray(“_Points”, new float[50]);
    material.SetFloatArray(“_Properties”, new float[12]);
    }

    Any chance to make a working example ? maybe for update unity version too ?

    Thanks
    Ilan

Webmentions

  • Arrays & Shaders in Unity 5.4+ - Alan Zucconi May 9, 2017

    Hey!

    Did you implement the function in Awake to initialise the array to its full size at the start of the scene?

  • Blogs e Canais sobre Unity 3D que você deve seguir | May 9, 2017

    Hey!

    Did you implement the function in Awake to initialise the array to its full size at the start of the scene?