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 4.5+ 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


Don't miss the next tutorial!

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


Ways to Support

In the past months 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

Write a Comment

Comment

26 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!

  9. 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

Webmentions

  • Arrays & Shaders in Unity 5.4+ - Alan Zucconi November 18, 2016

    Thanks for respond. Yes, I read new tutorial but still failed. Mind to share you solution updated to 5.4? Thanks.

  • Blogs e Canais sobre Unity 3D que você deve seguir | November 18, 2016

    Thanks for respond. Yes, I read new tutorial but still failed. Mind to share you solution updated to 5.4? Thanks.