Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Is it possible to pass a Texture2DArray to a shader function as a property?

Discussion in 'Shaders' started by brn, Aug 9, 2017.

  1. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    To save on a whole heap of duplicate code Id like to be able to pass a Texture2DArray as a property to a function within my shader. However I cant quite decipher the correct way to do this from the HLSL_support.cginc and its helper macro's

    Using the Macro's works just fine but will result in a lot of duplication.
    I'm trying to do something like this but with a Texture2DArray sampler.

    Code (CSharp):
    1. //Sudo code
    2.  
    3. sampler2D _Texture1, _Texture2,  _Texture3;
    4.  
    5. struct PackedPBR {
    6.     float4 Albedo;
    7.     float3 Normal;
    8.     float4 Packed;
    9. };
    10.  
    11. void DuplicateCode( sampler2D texture, out PackedPBR pPBR )
    12. {
    13.      // do stuff and sample texture modify pPBR
    14. }
    15.  
    16. void surf(Input IN, inout SurfaceOutputCustom o) {
    17.      PackedPBR  pPBR;
    18.      DuplicateCode(_Texture1, pPBR);
    19.      DuplicateCode(_Texture2, pPBR);
    20.      DuplicateCode(_Texture3, pPBR);
    21.  
    22.      o.Normal= pPBR.Normal;
    23.      o.Albedo = pPBR.Albedo;
    24.      o.Metallic = pPBR.Packed.r;
    25.      o.Occlusion = pPBR.Packed.b;
    26.      o.Smoothness =  pPBR.Packed.a;
    27. }
    Theres a good chance I'm going to have to revisit this shader a lot over the project. I'd love to keep things tidy.
     
    Last edited: Aug 9, 2017
  2. JonathanPearl

    JonathanPearl

    Joined:
    Jan 25, 2015
    Posts:
    15
    I've struggled with this too.

    While I have no solution for passing it as a property, you could possibly define a macro that is your functions that could expand into calling the Texture2DArray macros. That should at least alleviate the duplicate code problem. I don't think the shader compiler cares that it's technically using 3 different functions from expanded macros vs 1 function called 3 different times with different parameters.
     
    brn likes this.
  3. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    Thanks Jonathan. I ended up making the "DuplicateCode" function its own Macro.
    Looks terrible but does the job. If I find a cleaner option I'll be sure to post it here.
     
    Last edited: Aug 13, 2017
  4. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Hey, would you mind giving an example of your solution? I've been looking to solve this problem as well and I'm sure others will come across this thread looking for a solution too.
     
    Last edited: Sep 16, 2017
  5. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    Hi Invertex

    I cant show you the whole shader becuase it was for a client, however I define the custom macro like this.
    Its going to look like a mess but thats to get around how macros work.

    Code (CSharp):
    1.  
    2.         #define GETPACKEDPBRWORLDSPACE(texAlbedo,textNorm,texMetallic,uv,pbr,wsPos,wsNorm, arrayIndex) \
    3.         { \
    4.             float3 uvSlice = float3(uv,arrayIndex);\
    5.             pbr.Albedo =  UNITY_SAMPLE_TEX2DARRAY(texAlbedo, uvSlice);\
    6.             pbr.Packed = UNITY_SAMPLE_TEX2DARRAY(texMetallic, uvSlice);\
    7.             float3 nMap = UnpackNormal(UNITY_SAMPLE_TEX2DARRAY(textNorm, uvSlice));\
    8.             float3 wsViewDir = wsPos - _WorldSpaceCameraPos.xyz;\
    9.             pbr.Normal  = PerturbNormal(wsNorm,wsViewDir, uvSlice.xy, nMap);\
    10.         }                            
    And use the macro like this in the body of the shader
    Code (CSharp):
    1. GETPACKEDPBRWORLDSPACE(_Overlay,_OverlayNorm ,_OverlayPacked, IN.texCoOrds1.xy, overlay, IN.worldPos, wsNormal, IN.indexValue.g);
     
  6. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Thanks for showing the code snippet! Though I'm a bit confused, I don't see how the compiler would be told to turn for example "_Overlay" into what UNITY_DECLARE_TEX2DARRAY() does. How were you trying to define them before this? Were you just writing "Texture2DArray _Overlay;" ?

    Also, you could get a little performance boost if you use UNITY_SAMPLE_TEX2DARRAY_SAMPLER to just reuse the first _Overlay texture's sampler, and if you don't need mipmaps, UNITY_SAMPLE_TEX2DARRAY_SAMPLER_LOD to avoid mip check by manual selection of mip level.

    UNITY_SAMPLE_TEX2DARRAY_SAMPLER(texMetallic, texAlbedo, uvSlive);
    or
    UNITY_SAMPLE_TEX2DARRAY_SAMPLER_LOD(texMetallic, texAlbedo, uvSlice, 0);
     
  7. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    Set up your 2DArrays as a shader property like this.

    Properties {
    _Overlay("Albedo (rgb), Alpha (A)", 2DArray) = "" {} // Top
    _OverlayNorm ("Normal", 2DArray) = "" {}
    _OverlayPacked ("Metal,Height,Occlusion,Smoothness (RGBA)", 2DArray) = "" {}
    }

    and then declare them like so

    UNITY_DECLARE_TEX2DARRAY(_Overlay);
    UNITY_DECLARE_TEX2DARRAY(_OverlayNorm);
    UNITY_DECLARE_TEX2DARRAY(_OverlayPacked);
     
    Jason-Michael likes this.
  8. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Yeah that's how I was using them too, but I was kind of confused by how you're using your function, but I guess "pbr" is a struct you're processing the values into. That is definitely a bit awkward for my needs, so I think I've come up with a sort of simpler solution.

    Normally, UNITY_SAMPLE_TEX2DWHATEVER will infer the name of the sampler by tacking on "sampler_" to the input texture's variable name, and this is normally fine when done from within the actual shader program because that variable name is exposed to that area of the code. But in a custom function, your texture name would be whatever the local input parameter name definition for it is, and thus sampler_arrayParameterName ends up pointing to a variable that doesn't exist anywhere and the compiler throws an error.

    So instead, I define my own version of their macro that doesn't tack on the "sampler" part.
    Code (CSharp):
    1. #define T3X_SAMPLE_TEX2DARRAY_SAMPLER(tex,samplertex,coord) tex.Sample (samplertex,coord)
    2. #define T3X_SAMPLE_TEX2DARRAY_SAMPLER_LOD(tex,samplertex,coord,lod) tex.SampleLevel (samplertex,coord,lod)
    Now I can create and chain functions to feed my textures and arrays into, passing them around without the need for spaghetti code. I'll use part of your code as an example:


    Code (CSharp):
    1. UNITY_DECLARE_TEX2DARRAY(_Overlay);
    2. UNITY_DECLARE_TEX2DARRAY_NOSAMPLER(_OverlayNorm);
    3. UNITY_DECLARE_TEX2DARRAY_NOSAMPLER(_OverlayPacked);
    4.  
    5. void FillPBR(PBR pbr, Texture2DArray albedo, Texture2DArray norm, Texture2DArray metal, SamplerState samp, float2 uv, int slice)
    6. {
    7.     float3 slice = float3(uv, slice);
    8.     pbr.Albedo = T3X_SAMPLE_TEX2DARRAY_SAMPLER(albedo, samp, slice);
    9.     pbr.Packed = T3X_SAMPLE_TEX2DARRAY_SAMPLER(metal, samp, slice);
    10.     pbr.Normal = SampleArrayNormalUnpacked(norm, samp, slice);
    11. }
    12.  
    13. float3 SampleArrayNormalUnpacked(Texture2DArray normArray, SamplerState samp, float3 uvSlice)
    14. {
    15.     return UnpackNormal(T3X_SAMPLE_TEX2DARRAY_SAMPLER(normArray, samp, uvSlice));
    16. }
    17.  
    18. void surf (Input IN, inout SurfaceOutputStandard o) {
    19. {
    20.     FillPBR(pbr, _Overlay, _OverlayNorm, _OverlayPacked, sampler_Overlay, IN.uv, someSliceValue);
    21. }
    These functions and defines could be placed into a CGINC file for global usage by any of your shaders that implement the CGINC file, so all sorts of utility functions could be built around the processing of texture arrays like this, only requiring an extra input parameter or two for manually referencing the sampler.

    I have to do a lot of iterating and blending between various sets of dynamically sized texture arrays, so being able to re-use an array blending and processing function like this across different shaders will be quite helpful.
     
    Last edited: Sep 16, 2017
  9. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    Out of curiosity Invertex, which version of unity are you using? Your example is very similar to my first attempts in unity 5.6.1. In 5.6.1 however it wasn't possible to declare the type Texture2DArray as a function property. Which resulted in resorting to the macro madness.
     
  10. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    It should work in 5.6, but I am on 2017.1. I guess I left out a somewhat important part, in shader model 3.0 and below, Samplers and Textures aren't separate and proper texture arrays don't exist, the UNITY_DECLARE_TEX2D and similar hides this detail from you, if you wrote UNITY_DECLARE_TEX2D_NOSAMPLER in a `#pragma target 3.0` or lower shader, it would actually still make a texture with sampler, a "sampler2D", no different. And for DECLARE_TEX2DARRAY, it is declaring a samplerCUBE. So when it tries to compile, it's going to compile for all targets, and those lower targets will error because Texture2DArray and SamplerState don't exist.

    So what needs to be done, is that the defines for this stuff should be wrapped up like so:
    Code (CSharp):
    1. #if SHADER_TARGET > 30
    2. #define T3X_SAMPLE_TEX2DARRAY_SAMPLER(tex,samplertex,coord) tex.Sample (samplertex,coord)
    3. #define T3X_SAMPLE_TEX2DARRAY_SAMPLER_LOD(tex,samplertex,coord,lod) tex.SampleLevel (samplertex,coord,lod)
    4.  
    5. float3 SomeArrayBlendExample(Texture2DArray array, SamplerState samp, float3 uvSlice)
    6. {
    7.     return T3X_SAMPLE_TEX2DARRAY_SAMPLER(array, samp, uvSlice);
    8. }
    9. #endif
    And your shader should be set to `#pragma target 3.5` or higher. Otherwise, if you really need that lower of a shader target compatibility as well, you can do an #else and create some secondary macros and functions that take in the sampler2D/sampler2Dcube instead when lower targets are being compiled for.
     
    Last edited: Sep 16, 2017
  11. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    Great to know it works in 2017. When the project upgrades I'll be sure to rewrite things :)
     
  12. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Just gave it a test in 5.5.4 and it works there too! So any time you want to it should be fine :)
     
  13. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    One issue, which I hope is just a bug and not "intended" because it makes no sense, but I reproduced it in both 5.5 and 2017.1, is that #pragma target 3.5 and #pragma only_renderers d3d11 vulkan metal, is not preventing the d3d9 path from being compiled, and thus will throw a compilation error if you're defining any of this inside the shader instead of in a cginc, to define it within the shader, you also have to wrap any parts in the shader that use these features with the #if SHADER TARGET > 30, quite annoying though thankfully most of my stuff is globally defined for now. Hopefully they'll be able to fix that.
     
  14. brn

    brn

    Joined:
    Feb 8, 2011
    Posts:
    320
    That makes more sense to what my experiences have been so far. Before I went down the custom macro path I didn't wrap parts of the shader with #if SHADER TARGET > 30. Thinking #pragma target 3.5 and excluding unwanted renderers would be enough. While its wasted a considerable amount of time to work around,The shader is only in a pre alpha like stage anyhow. All high level criteria have been hit. When it comes time to optimise things based on significant realworld application I'll go over it in detail properly. Hopefully by then the compiler will have had a chance to mature.