Search Unity

Issues with shaderProperty and for-loop

Discussion in 'Shaders' started by Noktai, Jul 30, 2015.

  1. Noktai

    Noktai

    Joined:
    Feb 11, 2013
    Posts:
    40
    Hello,

    I'm faily new to shader coding, so this might just be something silly haha. Somehow one of my shader properties is causing some problem with an endlessly looping for-loop.

    Code (CSharp):
    1. unable to unroll loop, loop does not appear to terminate in a timely manner (994 iterations) or unrolled loop is too large, use the [unroll(n)] attribute to force an exact higher number
    Code (CSharp):
    1. for( int i = 0; i < _TextureWidth; i += 2 ) //_TextureWidth is 8, but it causes "unable to unroll loop" somehow
    2.                 {
    3.                     float4 curColor = tex2D( _ColorMap, float2(i / 8.0, 0.5  ));
    4.                     float4 difference =  c - curColor;
    5.                     if(  any(difference) != 0)
    6.                     {
    7.                         //c = fixed4( 0, 1, 0 , 1 );                      
    8.                     }else
    9.                     {
    10.                         float4 replacementColor = tex2D( _ColorMap, float2( (i+1.0) / 8.0, 0.5 ) );
    11.                         mappedColor = replacementColor;
    12.                         //c.rgb = mappedColor.rgb;
    13.                         c = mappedColor;
    14.                         c.rgb *= c.a;
    15.                         return c;
    16.                     }
    17.                    
    18.                    
    19.                 }
    (sorry for the formatting, this always happends when I copy from monodevelop :( )

    Here are the settings on the material;
    upload_2015-7-30_4-20-30.png

    And here is the C# script just incase;
    Code (CSharp):
    1. public void BuildTexture()
    2.     {
    3.         colorMap = new Texture2D( colorSwaps.Count * 2, 1, TextureFormat.ARGB32, false);
    4.         colorMap.filterMode = FilterMode.Point;
    5.         colorMap.mipMapBias = 0;
    6.         colorMap.anisoLevel = 0;
    7.         colorMap.wrapMode = TextureWrapMode.Clamp;
    8.        
    9.  
    10.         for( int i = 0; i < colorSwaps.Count; i++ )
    11.         {
    12.             colorMap.SetPixel( i * 2, 0, colorSwaps[i].originalColor );
    13.             colorMap.SetPixel( (i*2) + 1,0, colorSwaps[i].replaceMentColor);
    14.         }
    15.         colorMap.Apply();
    16.  
    17.  
    18.         if(spriteRenderer == null ) this.spriteRenderer = GetComponent<SpriteRenderer>();
    19.         if( materialProperties == null ) materialProperties = new MaterialPropertyBlock();
    20.         spriteRenderer.GetPropertyBlock( materialProperties );
    21.         materialProperties.SetTexture( "_ColorMap", colorMap );
    22.         materialProperties.SetFloat("_TextureWidth", colorMap.width);
    23.         spriteRenderer.SetPropertyBlock( materialProperties );
    24.  
    25.         Debug.Log("BuildTexture");
    26.     }
    27.  
    when I change _TextureWidth to 8 the error resolves, but that makes me lose a lot of flexibility.
    Any idea's?
     
  2. FuzzyQuills

    FuzzyQuills

    Joined:
    Jun 8, 2013
    Posts:
    2,871
    Are you trying to animate a sprite-sheet, by any chance? I wouldn't do it this way at all... :/

    Anyway, this tells me that either the compiled for-loop is terminating wrong (huh?!) or the for-loop compiled is too large. I would say it's most likely door number two. It could possibly mean also that your adding on condition's a bit too weird for the compiler. (I don't see the need to use i += 2, I just use i++, but anyway... :D explanation?)

    That's all I can get from this... :D Good luck! Also, which shader target?
     
  3. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    How can it unroll a variable length loop?
     
  4. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    First off, I believe there's a warning missing, where because you're attempting to use "tex2D" in a loop, it cannot be dynamic and has to be unrolled. That, however, means the number of iterations needs to be known at the time the shader is compiled... which it isn't, because _TextureWidth is a shader property set at runtime.

    You have several options, mostly depending on what values of _TextureWidth you can expect:
    • The computationally fastest way, but hardest to implement, is to make _TextureWidth a constant. If you know it can, for example, only be 8, 16 or 32, you would make three shader variants, where each variant assumes _TextureWidth is a fixed value. You would then be able to choose between them in your script. This is the fastest, because the final shader code contains no branching.
    • Limit the maximum number of iterations of your dynamic loop. If you know, for example, that _TextureWidth is always less than 16 or other small-ish number, you can figure out that the number of iterations of the loop in that case is 8, so you would add the "[unroll(8)]" attribute in front of the loop. That causes the compiler to assume there will never be more than eight iterations and the loop can be unrolled. Interestingly, the compiler still adds a branch for each unrolled iteration in sm3.0+, but avoids it in sm2.0, so it can still be compiled to that target.
    • Replace "tex2D(_ColorMap, float2(i /8.0, 0.5))" with "tex2Dlod(_ColorMap, float4(1/8.0, 0.5, 0, 0))". Since tex2Dlod does not require derivatives to figure out the mipmap level sampled (instead you pass it directly as the 4th argument), it can be used in a dynamic loop. Because this version requires branching, it needs at least shader model 3.0 to work.
    Even though the last two options (well, on sm3.0+) use branching, the conditions are dependent on a static variable and do not vary between pixel shader invocations. Simply put, they don't slow the shader down too much. Still, I'd go for the first option, if the possible _TextureWidth values are limited.

    EDIT: After looking through the code again, you do have pixel dependent branching in there, which tends to be quite slow. You could optimize it a bit by, instead of sampling and returning the replacement color inside of the loop, you just figure out the coords of the replacement color and then sample and return it once at the end of the shader. You could also get rid of the branching inside of the loop by not using "break" when you find the correct value, but whether that makes the shader faster depends on many factors and you'll have to profile it yourself.
     
    Last edited: Jul 30, 2015
    Farage likes this.
  5. Noktai

    Noktai

    Joined:
    Feb 11, 2013
    Posts:
    40
    I am swapping one color by another. Colors are set in a texture in a pair of 2; the first color is the color to be replaced, the second color is the one that replaced the first color. Since I'm using pairs I have to increment by 2.

    I'm just using the default shader target right now. I'm new to to shaders so I don't really have a clue what all the targets do, and what their usecases are :p

    Ahh you are correct, I indeed have the following warning;
    shader warnings never come up in my console for some reason.

    I don't like the limitation of [unroll(x)] but for my usecase right now it's alright. I've tried it and it works fine now, thanks!
    I wasn't aware of all these limitations.

    Even though it works I get the following warning now though.
    I'm not familiar with shader jargon yet, so I have no clue what it is trying to say :p

    Your text2Dlod seems like a better solution, but I have to research what it does exactly, since I don't feel comfortabel using it if I don't understand it yet. But thanks for suggesting that one too, will give it a look later :)

    I am not sure what "branching" means in this context, another case of me not knowing the jargon yet :(

    After looking at the error message in my first post again, I can see that they litterally suggested the unroll tag, doh!

    Thanks for taking the time to write that all up!
     
  6. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    Branching happens when the processor decides between executing two groups of instructions. In the case of a dynamic for loop, it happens at every iteration, because the loop can either terminate, running the instructions after the loop, or continue on, running the body.

    Gradient-based operations are operations that require data about its current pixel neighborhood, like how much a value in the middle of the shader somewhere changes compared to other pixel shader invocations around it... an easy operation, considering nearby pixels are running in parallel the same instructions at the same time. This is used mainly for mipmapping: The faster the coordinates change from pixel to pixel, the more area needs to be covered and the smaller mip level is sampled. The reason they cannot be used in branches is simple: What if the neighboring pixel takes a different branch or breaks out of a loop sooner? In that case the value you want to read is not even calculated!

    In your case, however, you don't care about mipmapping. You want to sample the first, largest, mip level no matter what, since it's a lookup texture and averaging the values wouldn't make much sense. That's exactly what tex2Dlod is for: Instead of leaving it up to the hardware what mip level to sample, you specify it outright as tex2Dlod(sampler, float4(x, y, 0, mip)), with mip = 0 being the first level. In general, you should use tex2Dlod instead of tex2D every time you don't need mipmapping or when you want to always sample from a specific level.

    I hope that helped :)
     
    eDmitriy likes this.
  7. Noktai

    Noktai

    Joined:
    Feb 11, 2013
    Posts:
    40
    Oh that makes sense. That's actually really handy to know since my games are mostly pixel art, and I never use mipmapping on that. Thanks a bunch :)

    I've tried testing it on android for fun and giggles and unfortunately it doesn't seem to be working there.
    upload_2015-8-4_2-18-45.png

    This is how it is supposed to look, but on android it looks like this;
    upload_2015-8-4_2-26-54.png

    removing [unroll] creates a different result, but it is still incorrect.

    In case you're wondering what it's for, I'm using it for this transition; (the terrain in the mainmenu is recolored by the shader)

    And I also use it re-color some enemies in the game.

    Any idea what it could be
     
  8. Plutoman

    Plutoman

    Joined:
    May 24, 2013
    Posts:
    257
    I do feel like there's quite easier ways to do what you're looking to do - ie, just swapping textures is probably a lot faster and easier. You can also do a lot easier solutions like rendering them as separate objects, and just assigning colors. Since you don't seem to be changing geometry, that would be as simple as setting a color from a script.

    That would be rendering one plane behind (or skybox) the second plane for the mountain, the next level of the ground, the next level of the bottom dirt.. etc. And then you just have a color assigned to them.

    I love shaders, but this does seem overly complex for what you're doing (clever, though, I will admit!).
     
  9. Noktai

    Noktai

    Joined:
    Feb 11, 2013
    Posts:
    40
    I don't like the idea of having to make 10 different textures for different enemies just because I want to swap a color :p

    Since the color change on the environment is very smooth, it would be crazy to do that with texture swapping.

    Still very curious why android deals with the shader this way, it seems to just recolor the entire texture, that would suggest something is going wrong whilst sampling?
     
  10. Plutoman

    Plutoman

    Joined:
    May 24, 2013
    Posts:
    257
    That's why I suggested rendering the colored areas as separate objects. This doesn't seem like a case where the performance of any overdraw is going to kill the device.. I would suggest drawing them as separate planes, using an alpha mask and an assigned diffuse color in the shader. Then, you just change the color from a script and you get all colors of the spectrum, based on the assumption that you have a mask per body part on the enemy (which, should be simple to create based off the existing enemies you have).

    Other options: render as one object, but use a texture as a mask. Where if the red channel is > .5f, use color 1 (assigned from script). If blue is > .5f, use color 2. This limits to 4 colors, but you just have to build one mask per enemy/background.
     
  11. Noktai

    Noktai

    Joined:
    Feb 11, 2013
    Posts:
    40
    I'm actually already adapting a "proper" palette system, where I save the indice of the palette colours into a texture. This will eliminate the need for a for loop altogether. But I'd still like to know why this current shader is breaking, just for learning purposes.
     
  12. Plutoman

    Plutoman

    Joined:
    May 24, 2013
    Posts:
    257
    That works. Works easily if you use an integer based texture, but I'm not sure how mobile support would be for that, either. The whole unroll issue was already covered, but I honestly can't help you with Android, since I only work with desktop/xbox one/ps4 compatibility at this point, and primarily desktop still at that. Mobile is quite limited in terms of support, it's never even specifically speed I was concerned with (I had a build on an S3 at one point pushing 700k polys to the screen at 20fps), but just compatibility which was all over the place.

    I would suggest using tex2dlod instead, if you have not switched to it already, and try doing it with various combinations of unrolling and hardcoding it. Someone with more experience on mobile should comment in besides me, though! Good luck!
     
  13. fromjava

    fromjava

    Joined:
    Sep 10, 2015
    Posts:
    2
    your answer is very nice, soved my problem, thanks~!
     
  14. fromjava

    fromjava

    Joined:
    Sep 10, 2015
    Posts:
    2
    你的回答太给力了!我暂时采用第二种办法