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

Blending multiple textures results in white fringes

Discussion in 'Shaders' started by _Kizu_, Sep 11, 2013.

  1. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Hi !

    I'm trying to write a splatting terrain shader able to perform a non-linear blend of multiple textures, as described here on Frictional Games' blog. In short, three RGBA textures are displayed on the surface, and their blending is controlled by a fourth RGB texture (R controlling the opacity of the first texture, G the second and B the third). Finally, this blending is modulated by each texture's own alpha channel.

    $07_blend_dissolve_alpha.jpg

    [HR][/HR]
    The first step is the linear blending of the 3 textures, and I found several ways to do it : "Additive blending", "lerp-blending", and "alpha-like blending". The visual result of these 3 methods are not the same.

    $texture_blending.png

    For this screenshot, the 3 splat-textures are plain R, G and B so the final result should be the same as the blend control texture on the right. Notice that the "alpha-like blending" and the "lerp blending" both create fringes right at the middle of the gradients, white being the background color.

    The "additive blending" is more accurate, but modulating the blend is very complicated with this method (I've struggled for hours to achieve a poor result), and I'm willing to try with the other methods.

    So I have 2 questions :
    • Why are these fringes here ? I can't understand myself how the blend formulas can leave some "empty" surface that let us see the background color...
    • Is it possible to get rid of them and if yes, how ?

    Thank you !
     
    Last edited: Sep 11, 2013
  2. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Here is the code for the three blending methods :
    "Additive blending" (just like in Unity's Terrain shader) :
    Code (csharp):
    1.  
    2. half3 output;
    3. output = _MainCol.rgb;
    4. output += ctrl.r * tex1.rgb;
    5. output += ctrl.g * tex2.rgb;
    6. output += ctrl.b * tex3.rgb;
    7. o.Albedo.rgb = output;
    "Alpha blending", using the alpha formula :
    Code (csharp):
    1.  
    2. half3 output;
    3. output = ctrl.r*tex1.rgb + oneMinusCtrl1*_MainCol.rgb;
    4. output = ctrl.g*tex2.rgb + oneMinusCtrl2*output.rgb;
    5. output = ctrl.b*tex2.rgb + oneMinusCtrl3*output.rgb;
    6. o.Albedo.rgb = output;
    And the third one using the CG lerp method :
    Code (csharp):
    1. half3 output;
    2. output = _MainCol.rgb;
    3. output = lerp(output, tex1.rgb, ctrl.r);
    4. output = lerp(output, tex2.rgb, ctrl.g);
    5. output = lerp(output, tex3.rgb, ctrl.b);
    6. o.Albedo.rgb = output;
     
  3. mouurusai

    mouurusai

    Joined:
    Dec 2, 2011
    Posts:
    349
    The mask texture is normalized?
     
  4. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Hi mouurusai, thank you for the answer. I'm not sure what you mean by normalized (sorry, I'm a total shader noob), but if it means that for every pixel, R+G+B = 1 yes, it is. I made the texture with Photoshop using the gradient tool, I saved it in PNG lossless and imported it in Unity as Truecolor RGB 24.

    $blendMask_import.jpg

    [EDIT] : I just double-checked and some pixels in the gradients have R+G+B = 254 (the majority has a sum of 255). Could it be the source of the fringes ? If yes, I'm not sure how to proceed in photoshop to avoid this.
     
    Last edited: Sep 11, 2013
  5. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    I assume the _MainCol is white.. that seems to be the cause. It makes sense you can still see it with alpha blending. Let's look at a pixel in your texture that has a value of (0.5, 0.5, 0). Now, understand that alpha blending is supposed to simulate transparency. If you put two transparent objects that let half of the light through behind each other, you can still see what's behind them. Actually, no matter how many transparent objects you have, you will always be able to see a bit of light going through them all, as long as none of them are completely opaque. Even normalizing the texture (so that the euclidean distance of the color is 1) won't help you much. (0.707, 0.707, 0) are just two a bit less transparent objects.

    So, you really don't want to use alpha blending for this. I assume you want each channel to represent the actual percentage of how much the texture contributes to the final image. That can be easily done with additive blending! It just requires some tricks. You probably don't want to waste a channel by storing the contribution of the main texture. That should be 1 - sum(others), so let's make it so!

    Code (csharp):
    1. half3 output;
    2. half bg_ctrl = 1.0 - saturate(dot(ctrl, 1.0)); // A fancy 1 - sum(ctrl)
    3. output = bg_ctrl * _MainCol.rgb;
    4. output += ctrl.r * tex1.rgb;
    5. output += ctrl.g * tex2.rgb;
    6. output += ctrl.b * tex3.rgb;
    7. o.Albedo.rgb = output;
    You said this kind of blending is difficult to handle... how so? You just need to make sure the sum doesn't get much over 1. I guess you could try the same thing with alpha blending, but it might still produce incorrect results further on: (1, 0.5, 0.5) would make the red leak through.
     
  6. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Hi Dolkar, thank you for the answer. You're right, _MainCol is white and when I tweak it the surface's color changes.

    Awesome explaination, it perfectly makes sense now that you say it. I assume the same reasoning applies to the "lerp blending".
    Thank you for the background trick, very helpful !

    Additive blending is nice for linear gradients, but as you said it requires to make sure the sum of the 3 control channels is always 1. It becomes tricky when trying to modulate the gradients using each texture's own alpha channel : each decrease of any control value has to be balanced by an equal increase of the others, and vice-versa. It becomes more and more complex with each new texture the shader must blend : I had to forget about SM 2.0 support (not enough temp registeries), and to use conditions in the shader which I've read is to be avoided.

    Code (csharp):
    1. half  ctrl1 = getBlend(ctrl.r, tex1.a); // getBlend modulates the control value so it's not a linear gradient anymore
    2. half  ctrl2 = getBlend(ctrl.g, tex2.a);
    3. half  ctrl3 = getBlend(ctrl.b, tex3.a);
    4.  
    5. half miss1 = ctrl.r - ctrl1;
    6. half miss2 = ctrl.g - ctrl2;
    7. half miss3 = ctrl.b - ctrl3;
    8.  
    9. half3 output;
    10. output  = _MainCol.rgb;
    11. if(ctrl.r > 0)
    12.     output += (ctrl1+miss2+miss3)*tex1.rgb;   // Balance each control texture so the sum of the three is equals to 1
    13. if(ctrl.g > 0)
    14.     output += (ctrl2+miss1+miss3)*tex2.rgb;
    15. if(ctrl.b > 0)
    16.     output += (ctrl3+miss1+miss2)*tex3.rgb;
    17. o.Albedo.rgb = output;
    With this code, I'm having a hard time finding which formula should be used in the getBlend method. Maybe it's the right way to go and I should try again, but I've read several articles that let me think the lerp blending was the most used in splatting shaders (see this post on Max Mc Guire's blog, and this one on Steam Squad's blog).

    Should I put in more efforts in finding the getBlend formula, or rather look towards lerp blending according to you ?
     
    Last edited: Sep 12, 2013
  7. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    I'd say a simple multiplication would do for the blend... Though that breaks the "let's make stuff sum up to 1" premise. So let's ensure it to be that way in the shader even after the alpha is applied. I think it would be reasonable to do this: If the sum after blending with the texture alphas is below 1, whats left will be occupied by the background texture. If it's above one, it gets "normalized" to make the sum equal to 1 again, which means the background will be hidden. Now to translate it to code:

    Code (csharp):
    1. half3 ctrl = source_ctrl * half3(tex1.a, tex2.a, tex3.a);
    2. half ctrl_sum = dot(ctrl, 1.0);
    3. half bg_ctrl = saturate(1.0 - ctrl_sum); // Will be 0 if ctrl_sum >= 1
    4. if (ctrl_sum > 1.0)
    5.     ctrl /= ctrl_sum;
    6.  
    7. half3 output = _MainCol.rgb * bg_ctrl;
    8. output += tex1.rgb * ctrl.r;
    9. output += tex2.rgb * ctrl.g;
    10. output += tex3.rgb * ctrl.b;
    Conditions are actually cheap as long as all you do in them is a simple assignment.

    I'm not getting one thing though... why are you limiting yourself to just three textures? Won't you ever need more? You could easily have up to five this way if you use the alpha of the control texture as well and use an actual background texture instead of a color. The rest is just a slight modification of the code:

    Code (csharp):
    1. half4 ctrl = source_ctrl * half4(tex1.a, tex2.a, tex3.a, tex4.a);
    2. half ctrl_sum = dot(ctrl, 1.0);
    3. half bg_ctrl = saturate(1.0 - ctrl_sum); // Will be 0 if ctrl_sum >= 1
    4. if (ctrl_sum > 1.0)
    5.     ctrl /= ctrl_sum;
    6.  
    7. half3 output = tex0.rgb * bg_ctrl;
    8. output += tex1.rgb * ctrl.r;
    9. output += tex2.rgb * ctrl.g;
    10. output += tex3.rgb * ctrl.b;
    11. output += tex4.rgb * ctrl.a;
     
    Last edited: Sep 12, 2013
  8. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    I first tried a splatting shader with only 3 textures to keep things as simple as possible for now, but it's true that using your method it's just as simple to have 5 textures than 3. I sure need to practise and learn how to use CG's functions as efficiently as you do !

    Sounds nice, only thing is when "normalizing" the sum of the 3 control values to 1 again, I think we would need to take in account how much each value must be affected, depending on the texture's alpha. Otherwise the blending is likely to be too linear.

    I made a try with the code you posted and it is pretty linear (it is mostly due to the fact that ctrl is simply obtained multiplying source_ctrl and tex.a, imho)

    $blend1.jpg

    One of my most successful tries looked liked this, and it's pretty close to what I am looking for - except for the fact that the blending is done the wrong way (could not get it the right way !)

    $blend2.jpg

    I'm not sure how to "normalize" the control vector by non-linearly modifying the 3 control values, though...
     
  9. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    What are the alpha values of the textures in this case? It seems to be just a flat value.. can't get any less linear than that.
    The second image is just wrong. You're not modifying the stone texture at all. Even if you reversed the blending, you'd still have a seam between pure sand and sand + stone.

    I guess you're looking for something like this: http://www.gamasutra.com/blogs/Andr...196339/Advanced_Terrain_Texture_Splatting.php
     
  10. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Thank you for the article, it's exactely what i'm trying to achieve (also seen on Steam Squad's blog, although no code was given there).

    The alpha value of the sand texture is plain white, and the alpha value of the pavement texture is nearly the same as its desaturated rgb. The same textures were used for both screenshots.

    You're right, if the second screenshot's blending was not reversed, a seam would be visible. To get rid of it I'd try to multiply tex.a by something like min(1.0, 4.0*(-ctrl+1.0) (so we still have a gradient but only at the very end of the blending).

    However in any cases it seems like a simple multiply is not enough to achieve the effect I'm looking for, and I'm not sure if the "normalization" of the ctrl would allow such an effect even if a nice blend formula was found, because it is likely to cancel it by smoothing the control values. I'll try to tweak your code tonight but I'm not sure I'll get interesting results because of that.

    Thank you for your time !
     
    Last edited: Sep 12, 2013
  11. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    Well.. that's a completely different kind of blending. You're controlling the height offset instead the actual of contributions of the textures. That changes just about everything. It no longer makes sense to ensure the sum equals to 1. I don't like the way they are doing it though... I'd prefer to blend it all on one go. This looks quite well on paper:

    The actual heights will be computed by height_offset (ctrl) + base_height (tex.a) for every channel. Then, the maximum will be computed and the texture will be blended based on the distance from it's own height to the maximum. That will allow for smooth blending instead of a binary decision. Where to put the background texture though? You can't control it's height offset. I think the obvious approach is to give it a flat height of 0 and then scale the height offsets of other textures in such a way that 0 -> -1 and 1 -> 1. That way, any layer can be both completely hidden and completely hide the background layer. It might still bleed through where you don't want it to if the background texture's base height is high enough, so you should stick to low values there. Anyways, code! (untested again)

    Code (csharp):
    1. half blend_factor = 5; // Tweakable. Higher values make the blending sharper
    2.  
    3. half4 height_offsets = ctrl * 2 - 1;
    4. half4 base_heights = half4(tex1.a, tex2.a, tex3.a, tex4.a);
    5. half4 heights = base_heights + height_offsets;
    6. half max_height = max(0.0, max(heights.r, max(heights.g, max(heights.b, heights.a)))); // Madness!!
    7.  
    8. half bg_blend = saturate((tex0.a - max_height) * blend_factor + 1);
    9. half4 blends = saturate((heights - max_height) * blend_factor + 1);
    10.  
    11. half3 output = tex0.rgb * bg_blend;
    12. output += tex1.rgb * blends.r;
    13. output += tex2.rgb * blends.g;
    14. output += tex3.rgb * blends.b;
    15. output += tex4.rgb * blends.a;
    So there.. I hope it works :) The blending might feel a bit awkward. You have to realize though that you're not controlling the transparency now, you are physically moving the layers up and down. The actual height scale here moves from -1 to 2, where the background layer's height can move from 0 to 1, based on its alpha texture. You should keep that low otherwise only layers with very high height offsets will be able to completely cover it. Just experiment with it and you'll see.... as long as it works, haha :)
     
    Last edited: Sep 12, 2013
  12. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    I just tried your algorithm, it does not seem to work exactly as expected for the moment, but I need to understand and tweak it more before being able to tell what is wrong. Visually the control texture seems to be ignored, so some of the textures are blended everywhere (i.e. plain red and plain red results in a yellow surface), while some others are not seen at all. On the other side when trying with the pavement texture, the cracks between the stones are filled with other textures, that's good !

    I'll give you more feedback tomorrow when I've tweaked it. Thanks a lot for your help !
     
  13. Bretwalda-Games

    Bretwalda-Games

    Joined:
    Sep 13, 2013
    Posts:
    1
    We made terrain splatting shaders with non-linear blend of textures. It uses our know-how algorithms described on GameDev.net.
    You can find it in Unity Asset Store and here is a demo.

    $6cf9fd03c46bbe439fdc38eeedfc5335.jpg
     
  14. _Kizu_

    _Kizu_

    Joined:
    Jul 7, 2013
    Posts:
    10
    Hi, thank you for the links. Your shader is awesome !


    @Dolkar : I tried to play around with the algorithm you made, and although I did not manage to get the right result yet, I'm not too far from it - I hope. It still requires some work, and I also need to learn more from CG and shaders in general to be more efficient :)


    Wow, we drifted a lot from the original question !
    So the answer is : these fringes are here because blending a 0.5 transparent surface with another 0.5 transparent surface does not result in a opaque surface, just like in real life ! Thank you Dolkar for pointing this out. I found a nice video explaining a similar problem that can be encountered when alpha-blending a texture.
    It's not easily possible to avoid this using either the alpha formula or a lerp, the way I did. Maybe with some additional work on the blend-control texture it could be possible but I did not try. So additive blending seems to be the way to go.