Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Official Global Variables & Properties Scope

Discussion in 'Shader Graph' started by FredMoreau, Jul 4, 2023.

  1. FredMoreau

    FredMoreau

    Unity Technologies

    Joined:
    May 27, 2019
    Posts:
    96
    Hey fellow Shader Artists!

    I've read here and there about how confusing and frustrating it can be to deal with global properties in Shader Graph and decided to gather some information here to the best of my knowledge and understanding.

    I might be wrong and will correct and/or add more as I learn new things.
    Please reply to this thread if you have any questions or information you want to share on the topic.

    Global VS static
    Static variables can be used in both ShaderLab and Shader Graph (using a Custom Function node), but they cannot be read nor assigned to from outside shader code.
    They are, by definition, shared between all material instances.

    Unlike static variables, globals are variables handled in C#, that will be assigned to all materials with a matching definition (name & type).

    Global VS Local (Per Material)
    In Shader Graph, a property is made local when set as exposed.
    If you check Override Property Declaration and set the Shader Declaration dropdown to Global, it will still be a Local (Per Material) property if exposed is checked.

    In ShaderLab, a global property is a variable that is declared within the HLSLPROGRAM body.
    float _MyCustomProperty;

    The variable is made local when it is associated with a Property:
    _MyCustomProperty("MyCustomProperty", Float) = 0


    Global
    A Global variable can be read and assigned to from C# using Shader.GetGlobalFloat("_MyFloat") and Shader.SetGlobalFloat("_MyFloat", value).
    This will change the value in all material instances, except those where the value was overridden using a Property Block.
    It cannot be set at material instance level with Material.SetFloat().

    Since HLSL doesn't allow for variable initialization, global variables have no default value, and must be initialized from C#, or else they will always default to zero/black.

    Shader Graph allows changing the default value of global variables.
    While it allows previewing the effect of a global in Shader Graph, this only gives a preview of what will happen when the variable is assigned to.

    Local (Per Material)
    A Local variable can be read and assigned to from C# at material instance level, using material.GetFloat("_MyFloat") and material.SetFloat("_MyFloat", value).

    This will change the material's property value, unless overridden using a Property Block.
    It cannot be set at global level with Shader.SetGlobalFloat().

    Default Values
    Global

    Global properties have no default value. They are not serialized and require explicit initialization with Shader.SetGlobalFloat("_MyFloat", value).

    Local (Per Material)
    Changing the default value of an exposed (local) property will not propagate to existing material instances.
    That is similar to changing the default value of a MonoBehaviour field, which neither propagates to existing instances of that MonoBehaviour.
    Making Material Instances as Material Variants of the Shader Graph (nested) Material allows them to inherit the properties they don't override, just like Prefabs allows the same for MonoBehaviours.

    Reading from Shader
    Although a variable can be modified from within shader code, it is worth noting that a variable value read from C# will always returned the last value it was assigned from C#.

    In short: what happens on the GPU stays on the GPU.
    Say you have a shader global (not exposed) float "_MyFloat".
    Calling Shader.GetGlobalFloat("_MyFloat") will only return the last value it was assigned from C#, but not any value that it was assigned from within Shader code.

    DOTS Instancing
    DOTS allows for another type of property declaration : Hybrid Per Instance.

    If you check Override Property Declaration and set the Shader Declaration dropdown to Hybrid Per Instance, the effect of the exposed checkbox differs. Since those variables are by definition per instance, they cannot be global.
    Unchecking the exposed checkbox will simply just add a [HideInInpector] attribute to the property, hiding it from the material inspector.

    Whether it is visible or not, the property can be read and assigned to using material.GetFloat("_MyFloat") and material.SetFloat("_MyFloat", value).
    This will change the material's property value, unless overridden using a Property Block.

    For more information, see:
    Entities Graphics | 1.0.11
    Manual: DOTS Instancing shaders

    Array Types
    Although ShaderLab allows for arrays (Floats, Vectors and Matrices) that can be set with Shader.SetGlobalFloatArray(), Shader.SetGlobalVectorArray() and Shader.SetGlobalMatrixArray(), Shader Graph doesn't feature typed arrays properties.
    In general, arrays become useful with iterators and are otherwise less performant than Vectors or Matrices.
    In other words, it's good practice to pack floats and vectors in vectors or matrices.

    UX Improvements
    Shader Graph Property Scope & Visibility settings

    We are planning to rework the UX to make the scope and visibility settings easier to work with and enable hiding local properties.

    If this is important to you, or want to add comment, please let us know on the Product Board:
    https://portal.productboard.com/uni...sual-effects/c/2239-property-scope-visibility

    Shader Globals
    Having to write a C# component for the sole purpose of initializing shader globals isn't ideal.
    We are considering adding Shader Globals to Project Settings so that users can easily initialize them.

    Please let us know what you think about this here on the Product Board.
    https://portal.productboard.com/uni...endering-visual-effects/c/2256-shader-globals
     
    Last edited: Sep 27, 2023
    BOXOPHOBIC, DevDunk and florianBrn like this.
  2. Ne0mega

    Ne0mega

    Joined:
    Feb 18, 2018
    Posts:
    699
    Shadergraph:
    i uncheck exposed to get global declarations to work. And they do. They also update continuously (ie from update) and globally if I tell them to via script, across all shaders and materials using that global parameter.

    Except the texture2Ds. The texture2Ds, (generated) i found only work once if applied globally. If I generated a new one, it would be black... and this killed me for days trying to figure out why, until i gave up and made them local. I can get render textures to apply globally, though.

    But this, (it has been a while since i gave up on the idea of generated global texture2Ds) might be related to how I defined them as static in C#... ..although they are still accessible as static through script)

    I have not played with "override property declaration" yet... ..mayhaps I will again and tackle this again.
     
    Last edited: Jul 6, 2023
  3. FredMoreau

    FredMoreau

    Unity Technologies

    Joined:
    May 27, 2019
    Posts:
    96
    Thanks @Ne0mega,

    I'll look into this Texture2D exception to learn if this is a limitation or a bug.
     
  4. FredMoreau

    FredMoreau

    Unity Technologies

    Joined:
    May 27, 2019
    Posts:
    96
    Hi @Ne0mega,

    I took some time to experiment with Shader.SetGlobalTexture and Texture2D and could not reproduce what you mentioned above.

    You'd usually create one texture and use SetGlobalTexture once, then write to that same texture.
    Here's a test script that generates an 8x8 texture and sets it to a Global upon Start, then applies some random colors to its pixels every frame.

    But if for some reason you need to create a new texture, it also works, as you can test with the component's context menu "Create New Texture".

    Please let me know if that works out for you, or if the issue you mentioned is different.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class GlobalTexGen : MonoBehaviour
    4. {
    5.     [SerializeField] string _referenceName = "_Texture";
    6.     [SerializeField] Color32 _color1 = Color.red;
    7.     [SerializeField] Color32 _color2 = Color.blue;
    8.  
    9.     Texture2D texture;
    10.     Color32[] colors = new Color32[64];
    11.  
    12.     void Start()
    13.     {
    14.         CreateNewTexture();
    15.     }
    16.  
    17.     void Update()
    18.     {
    19.         for (int i = 0; i < 64; i++)
    20.             colors[i] = Color32.Lerp(_color1, _color2, Random.Range(0f, 1f));
    21.  
    22.         texture.SetPixels32(colors);
    23.         texture.Apply();
    24.     }
    25.  
    26.     [ContextMenu("Create New Texture")]
    27.     void CreateNewTexture()
    28.     {
    29.         texture = new Texture2D(8, 8);
    30.         Shader.SetGlobalTexture(_referenceName, texture);
    31.     }
    32.  
    33. #if DEBUG
    34.     Rect drawRect = new Rect(0, 0, 64, 64);
    35.     private void OnGUI()
    36.     {
    37.         GUI.DrawTexture(drawRect, texture);
    38.     }
    39. #endif
    40. }
     
    Ne0mega likes this.
  5. Ne0mega

    Ne0mega

    Joined:
    Feb 18, 2018
    Posts:
    699
    I appreciate it, and ill try later.

    I didnt really expect you to try to reproduce though, since there are alot of other things in my project that could be causing the blackout.

    But thank you.

    Edit:. I dont know what context menu is, but if it is an editor thing i am not using editor. This texture2D is generated once, at beginning of scene. Creating a new texture, then Apply(true, true).

    I tried just now with not exposed in shadergraph, not exposed with global override, as well as exposed with override global, and the global texture did not work, but like I said global colors and floats work, all defined at the same time, but not updated, so for some reason there is some kind of disconnect. I will maybe try a coroutine to wait a frame then apply the texture, as perhaps global textures ate sent to GPU earlier than floats and colors.

    Edit 2:. The waitforendofframe then apply global texture2d coroutine worked. So textures must be defined globally in a different and earlier stage of a frame than colors and floats. This is all occuring in the first frame of the scene.
     
    Last edited: Jul 18, 2023
  6. FredMoreau

    FredMoreau

    Unity Technologies

    Joined:
    May 27, 2019
    Posts:
    96
    Hi @Ne0mega,

    I was referring to the
    [ContextMenu("Create New Texture")]
    I added on the
    void CreateNewTexture()
    method I put in the test MonoBehaviour above.

    So it is indeed in the Editor, but I tested at Runtime in a Build also.
    Although, I did not test instantiating renderers after the texture was created, everything happens in the same scene that already contains renderers with the material applied.
    But
    Shader.SetGlobalTexture()
    is called before
    texture.Apply()


    Anyway, thanks for sharing your learnings. I want to know if this is a limitation we should document or a bug we should fix.
     
  7. AndreasWang

    AndreasWang

    Joined:
    Jul 31, 2019
    Posts:
    17
    In my experience, this seems to be the opposite of how it works? When using functions in C# like
    Shader.SetGlobalVector()
    , the ShaderGraph will only pick up on the variable when exposed is unchecked like @Ne0mega mentions. Is it possible to get a confirmation on what the intended use is here, and if it is a bug otherwise?
     
  8. FredMoreau

    FredMoreau

    Unity Technologies

    Joined:
    May 27, 2019
    Posts:
    96
    Thanks for the heads up @AndreasWang. You can never have too many proofreaders :)
     
    AndreasWang likes this.