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

Gradient banding issue on mobile

Discussion in 'Shaders' started by prabhukaran, Oct 1, 2020.

  1. prabhukaran

    prabhukaran

    Joined:
    May 5, 2019
    Posts:
    17
    Hey Guys,

    I am trying to achieve a gradient color background for my 2d mobile game, like this -->


    Energy: Anti Stress loops android game


    I tried 1*2 pixel image and set it to bilinear filter mode and stretch it to get the gradient. But I am getting gradient banding issue when playing it in my mobile (1*16 pixel image also tried).

    So I went for gradient shaders and tried gradient with noise shaders too from the below links, but the result looks same.
    https://startupfreakgame.com/2017/01/15/screen-space-gradient-shader-with-dithering-in-unity/
    https://www.reddit.com/r/Unity3D/comments/e5wmbn/smooth_gradient_shader_using_noise_link_in_comment/

    Here is the current shader code I am using:

    Code (CSharp):
    1. Shader "Custom/ScreenGradient"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}
    6.         _NoiseTex("Noise Texture", 2D) = "white" {}
    7.         _BottomColor("Bottom Color", Color) = (1,1,1,1)
    8.         _TopColor("Top Color", Color) = (1,1,1,1)
    9.         _Offset("Offset", float) = 0
    10.     }
    11.     SubShader
    12.     {
    13.         Tags
    14.         {
    15.             "Queue" = "Transparent"
    16.             "IgnoreProjector" = "True"
    17.             "RenderType" = "Transparent"
    18.             "PreviewType" = "Plane"
    19.             "CanUseSpriteAtlas" = "True"
    20.         }
    21.         Cull Off
    22.         Lighting Off
    23.         Blend One OneMinusSrcAlpha
    24.         Pass
    25.         {
    26.         CGPROGRAM
    27.         #pragma vertex vert
    28.         #pragma fragment frag
    29.         #include "UnityCG.cginc"
    30.         struct appdata_t
    31.         {
    32.             float4 position    : POSITION;
    33.             float2 uv        : TEXCOORD0;
    34.         };
    35.         struct v2f
    36.         {
    37.             float4 position    : SV_POSITION;
    38.             float2 uv        : TEXCOORD0;
    39.             half4 color        : COLOR;
    40.         };
    41.         sampler2D _MainTex;
    42.         sampler2D _NoiseTex;
    43.         float4 _MainTex_ST;
    44.         half4 _BottomColor;
    45.         half4 _TopColor;
    46.         float _Offset;
    47.         v2f vert(appdata_t IN)
    48.         {
    49.             v2f OUT;
    50.             OUT.position = UnityObjectToClipPos(IN.position);
    51.             OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
    52.             float factor = mad(OUT.position.y, -0.5, 0.5);
    53.             factor *= 1 + _Offset*2;
    54.             factor -= _Offset;
    55.             factor = clamp(factor, 0, 1);
    56.             //factor = smoothstep(0.0f , 1.0f, factor);
    57.             OUT.color = lerp(_BottomColor, _TopColor, factor);
    58.             //OUT.color = smoothstep(0.0f, 1.0f, OUT.color);
    59.             return OUT;
    60.         }
    61.         float3 getNoise(float2 uv)
    62.         {
    63.             /*float3 noise = tex2D(_NoiseTex, uv * 100 + _Time * 50);
    64.             noise = mad(noise, 2.0, -0.5);
    65.             return noise/255;*/
    66.             float2 scale = 10;
    67.                 return tex2D(_NoiseTex, uv * float4(10.0f, 10.0f, 0.0f, 0.0f)) / 200.0f;
    68.         }
    69.         half4 frag(v2f IN) : SV_Target
    70.         {
    71.             half4 texCol = tex2D(_MainTex, IN.uv);
    72.            
    73.             half4 c;
    74.             c.rgb = IN.color.rgb + getNoise(IN.uv);
    75.             c.rgb *= texCol.a;
    76.             c.a = texCol.a;
    77.             return c;
    78.         }
    79.         ENDCG
    80.         }
    81.     }
    82. }
    Using a blue noise texture from here:
    http://momentsingraphics.de/Media/BlueNoise/FreeBlueNoiseTextures.zip

    settings.PNG

    Result:

    Screenshot_20201001-224102.jpg

    URP gradient shader graph method also tried. I want the background looks exactly same like the Energy loops game. I see there are some noise texture also they have included in the background but the gradient banding is not visible in their game in my mobile. I am a noob in shaders, any help would be much appreciated.

    Thanks
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    It's kind of impossible to tell if the Energy Loops game is using dithering or not from the video & screenshot above since the video & image compression in both are going to intentionally remove any pixel level noise in the image, and in some cases add some noise that possibly wasn't there to begin with. It's entirely possible it's not using any dithering at all, and the lack of apparent banding comes purely from having enough "distance" (in color space) between the colors it's blending between, and the fact it's using additional layers of blurry cloud / Perlin noise to distort and/or blend on top of the gradient is probably masking a lot of the banding that may in fact be there.

    However dithering is still a great way of dealing with this. The problem is both of those linked shaders are doing it the wrong way. For dithering to work properly, at least for it to be able to hide the banding and not be obvious itself, the noise needs to properly match the resolution you're rendering at. Using the current object's UVs and arbitrarily multiplying them is means it's going to be totally luck if it works or not.

    There's also things like you need to make sure the noise texture is uncompressed, otherwise the texture compression can end up blurring the noise, just like web based video / jpeg compression. And you need to make sure the source you're applying dithering to isn't already banded to begin with. Dithering an already banded gradient just means you have noisy banding.

    This tutorial does a good job of showing how to get screen space UVs for sampling a dithering texture.
    https://www.ronja-tutorials.com/2019/05/11/dithering.html

    Then there's the extra gotcha of you need to account for the color space when doing dithering. The easiest solution to this is to make sure your project is set to use Gamma Color Space. Linear Color Space means you need to account for the sRGB color conversion in your dithering, which makes things a lot harder.
     
  3. prabhukaran

    prabhukaran

    Joined:
    May 5, 2019
    Posts:
    17
    Thanks @bgolus for your prompt reply.
    I have applied dithering from the above mentioned tutorial, but the result looks like this:
    Screenshot_20201003-182507.jpg

    I used 1920*1080 pixels black to white color gradient image as source (created in photoshop without gradient banding) and the below same size png noise image as noise texture. Noise texture filter mode is set to Repeat and Bilinear. I tried with low resolution (8*8) noise texture also and that looks distorted gradient. Dithering shader code is same from Ronja tutorial --> https://github.com/ronja-tutorials/...ster/Assets/042_Dithering/BW_Dithering.shader
    I think something I am missing here.

    Noise Image:
    noiseblue1024.png
    settings.PNG

    The project is set to Gamma color space

    Is there any way to achieve enough distance in color space using shader or any other method? I understand that distance in color space means how closer the shades of colors RGB values are. But I am not sure how could I obtain that.

    Thanks
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    If you created it in photoshop, then it's either already dithered, or it has banding and you just can't see it because black and white are colors that are far enough apart.

    Think about it this way. A 24 bit RGB or 32 bit RGBA color image has 8 bits per color channel. That means it has 256 possible values between "0" and "255". For a black and white gradient that means there are only 254 bands of grey between the black and white at either end. If you have that stretched over a 1920 pixel range, that means every 7 pixels are the same value. So the image is already banded, you just can't perceive it. Adding noise to that means you get 7 pixel wide bands of noise, not a smooth gradient.

    If when you're creating your texture in Photoshop you select the "dither" option, which I believe is on by default, then the resulting gradient is dithered to make it appear smoother. Depending on if you created your image as a greyscale image or an RGB color image, that dithering will either also be in greyscale or in RGB. And if the image does already have dithering, it needs to be displayed at exactly 1:1 pixels for the dithering to work.

    It also needs to be set to uncompressed, and not be rescaled to the closest power of 2 resolution, as by default any image imported into Unity will be set to compressed and be rescaled to the closest power of 2 resolution. Compression will add more banding as almost all image compression formats only use 16 bit RGB color palettes (RGB 565, so only 32 color values for red and blue, 64 for green, though it's complicated and they can kind of represent colors a normal RGB 565 bit image can't), and rescaling it will remove any of the noise present in the original image.

    Really, for a linear gradient like above, you should just be using the mesh's UV's directly. Just like the shader was before. Or the screen space UV prior to it being scaled for the dither texture. Though you could switch to doing the lerp in the fragment shader instead of passing the color value from the vertex shader.

    The Ronja example gets away with using a texture because she's intentionally dithering to a low number of colors, lower than the 24 bit color Photoshop is creating. She wants the dithering pattern to be visible. You don't. You also don't want to be multiplying the noise texture UVs by arbitrary values, which is what I suspect you're still doing. But since you didn't post your updated shader I can't say for sure.

    There's no point in using a full screen resolution noise texture, especially since in the real world you won't have any idea what resolution the screen is going to be. Plus it's just a repeating 8x8 Bayer pattern anyway... which you want to have set to repeat and Point, not Bilinear. If you've used the code from Ronja's tutorial properly, it should perfectly display the image at 1:1 pixels. Just make sure the texture is also set to uncompressed! You could also have just kept your original blue noise texture. Honestly I'd recommend that. Bayer patterns have their uses, but blue noise beats it in appearance 99% of the time.

    If you do the dithering properly, it won't really matter. It's also not really something you want to do in the shader. It's probably better to just eyeball it yourself. If you see color banding, make one color darker, make the other brighter, or move their hues further apart until the banding goes away.

    Accurately calculating the perceptual distance between color values in a way that's useful for this situation is complicated.
     
    Last edited: Oct 3, 2020
    prabhukaran likes this.
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Here's an example gradient with banding on the left, blue noise dithering on the right.
    upload_2020-10-3_11-38-8.png

    Here's the same image with the contrast boosted in Photoshop so the banding is more obvious, but notice the dithered side still looks smooth.
    upload_2020-10-3_11-38-46.png

    Btw, your device captured screenshots have worse banding than it does on the actual device due to jpg compression. The above two examples are using png compression so they'll be closer to exactly what you see on device. Both are using a 64x64 LDR RGB blue noise from that same group of blue noise textures you linked to earlier.

    Code (csharp):
    1. Shader "ScreenSpaceGradientDither"
    2. {
    3.     Properties
    4.     {
    5.         _Color1 ("Color 1", Color) = (1,1,1,1)
    6.         _Color2 ("Color 2", Color) = (0,0,0,1)
    7.         [NoScaleOffset] _NoiseTex ("Noise Texture", 2D) = "grey" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque" }
    12.         LOD 100
    13.  
    14.         Pass
    15.         {
    16.             CGPROGRAM
    17.             #pragma vertex vert
    18.             #pragma fragment frag
    19.  
    20.             #include "UnityCG.cginc"
    21.  
    22.  
    23.             struct v2f
    24.             {
    25.                 float4 pos : SV_POSITION;
    26.                 float4 screenPos : TEXCOORD0;
    27.             };
    28.  
    29.             half4 _Color1, _Color2;
    30.             sampler2D _NoiseTex;
    31.             float4 _NoiseTex_TexelSize;
    32.  
    33.             v2f vert (appdata_base v)
    34.             {
    35.                 v2f o;
    36.                 o.pos = UnityObjectToClipPos(v.vertex);
    37.  
    38.                 // calculate and pass screen position to fragment shader
    39.                 o.screenPos = ComputeScreenPos(o.pos);
    40.                 return o;
    41.             }
    42.  
    43.             fixed4 frag (v2f i) : SV_Target
    44.             {
    45.                 // do persepctive divide to get proper 0.0 to 1.0 range screen position
    46.                 float2 screenPos = i.screenPos.xy / i.screenPos.w;
    47.  
    48.                 // if using linear color space, convert the color values back to gamma space
    49.                 #if !defined(UNITY_COLORSPACE_GAMMA)
    50.                 _Color1.rgb = LinearToGammaSpace(_Color1);
    51.                 _Color2.rgb = LinearToGammaSpace(_Color2);
    52.                 #endif
    53.  
    54.                 // lerp between the two color values using the vertical screen position
    55.                 half4 col = lerp(_Color2, _Color1, screenPos.y);
    56.  
    57.                 // calculate pixel accurate UVs for the noise texture
    58.                 // screen pos * screen resolution * (1 / noise texture resolution)
    59.                 float2 noiseUV = screenPos * _ScreenParams.xy * _NoiseTex_TexelSize.xy;
    60.                 half3 noise = tex2D(_NoiseTex, noiseUV);
    61.  
    62.                 // scale noise to be a range of -0.5 to 1.5, and divide by the expected 8 bit per channel color precision
    63.                 half3 dither = (noise * 2.0 - 0.5) / 255.0;
    64.  
    65.                 // add dither to color
    66.                 col.rgb += dither;
    67.  
    68.                 // convert back to linear space
    69.                 #if !defined(UNITY_COLORSPACE_GAMMA)
    70.                 col.rgb = GammaToLinearSpace(col);
    71.                 #endif
    72.  
    73.                 return col;
    74.             }
    75.             ENDCG
    76.         }
    77.     }
    78. }
     
  6. prabhukaran

    prabhukaran

    Joined:
    May 5, 2019
    Posts:
    17
    Thanks a lot @bgolus , that worked.
    I didn't saw your second reply, so trying to figure out the mistake I have done in Ronja's shader for my entire weekend, still I didn't found this awesome solution. If you have a website for shader tutorials I will be the first subscriber.
    Now there is no gradient banding issue in my mobile. I will post here if there is any update on the shader for that blurry cloud effect, if someone needs.
    #LifeSaver

    Here are the links I have referred for shaders, dithering, banding issue. (Just in case if someone wants to know more about gradient shaders for some other requirement)
    https://github.com/jmickle66666666/PSX-Dither-Shader --> works somewhat similar to bgolus code
    https://www.shadertoy.com/view/MslGR8 --> Nice visual representation provided for banding issue
    https://forum.unity.com/threads/visible-color-change-lines.781496/#post-5206160 --> One more bgolus post about dithering
    https://www.ronja-tutorials.com/2019/05/11/dithering.html --> Ronja tutorial about dithering
    https://loopit.dk/banding_in_games.pdf --> pdf about banding in games
    https://github.com/keijiro/KinoBinary --> Kejiro's dithering effect shader
    http://www.shaderslab.com/shaders.html --> some basic shader effects
    http://wiki.unity3d.com/index.php/Dithering_Shader
    https://startupfreakgame.com/2017/01/15/screen-space-gradient-shader-with-dithering-in-unity/
    https://www.reddit.com/r/Unity3D/comments/e5wmbn/smooth_gradient_shader_using_noise_link_in_comment/
    https://assetstore.unity.com/packages/vfx/shaders/minimalist-free-lowpoly-flat-gradient-shader-96148 --> Gradient Shader Asset
     
  7. prabhukaran

    prabhukaran

    Joined:
    May 5, 2019
    Posts:
    17
    Hi, I have added the moving cloud effect using Perlin Noise shader code available here --> https://forum.unity.com/threads/perlin-noise-procedural-shader.33725/
    But I got one problem, in higher end mobile devices I am getting 60 FPS but in lower end devices I am getting around 44 FPS - 46 FPS only. Is there any way to increase the performance? Please help

    floatingnoise.PNG

    Ronja tutorials perlin noise shader and Kejiro shader are giving less than 20 FPS.
    https://www.ronja-tutorials.com/2018/09/15/perlin-noise.html#3d-perlin-noise
    https://github.com/keijiro/NoiseShader

    Added perlin noise and increased the saturation, contrast, brightness of the color in the noise area to look like a cloud effect
    Code (CSharp):
    1. Shader "Custom/BgolusGradient_noise"
    2. {
    3.     Properties
    4.     {
    5.         _Color1 ("Color 1", Color) = (1,1,1,1)
    6.         _Color2 ("Color 2", Color) = (0,0,0,1)
    7.         [NoScaleOffset] _NoiseTex ("Noise Texture", 2D) = "grey" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque" }
    12.         LOD 100
    13.         Pass
    14.         {
    15.             CGPROGRAM
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.             #include "UnityCG.cginc"
    19.             struct v2f
    20.             {
    21.                 float4 pos : SV_POSITION;
    22.                 float4 screenPos : TEXCOORD0;
    23.                 float4 worldPos : TEXCOORD1;
    24.             };
    25.             half4 _Color1, _Color2;
    26.             sampler2D _NoiseTex;
    27.             float4 _NoiseTex_TexelSize;
    28.  
    29.             /*Perlin Noise function*/
    30.  
    31.             float hash( float n )
    32.             {
    33.                 return -0.5 + 1.5 * frac(sin(n)*43758.5453);
    34.             }
    35.             float perlinNoisefunc( float3 x )
    36.             {
    37.                 // The noise function returns a value in the range -1.0f -> 1.0f
    38.                 float3 p = floor(x);
    39.                 float3 f = frac(x);
    40.                 f = f*f*(3.0-2.0*f);
    41.                 float n = p.x + p.y*57.0 + 113.0*p.z;
    42.                 return lerp(lerp(lerp( hash(n+0.0), hash(n+1.0),f.x),
    43.                                lerp( hash(n+57.0), hash(n+58.0),f.x),f.y),
    44.                            lerp(lerp( hash(n+113.0), hash(n+114.0),f.x),
    45.                                lerp( hash(n+170.0), hash(n+171.0),f.x),f.y),f.z);
    46.             }
    47.  
    48.             v2f vert (appdata_base v)
    49.             {
    50.                 v2f o;
    51.                 o.pos = UnityObjectToClipPos(v.vertex);
    52.                 o.screenPos = ComputeScreenPos(o.pos);
    53.  
    54.                 //calculate world position
    55.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    56.                 return o;
    57.             }
    58.             fixed4 frag (v2f i) : SV_Target
    59.             {
    60.  
    61.                 //Set size of the noise cell
    62.                 i.worldPos.x *= -4;
    63.                 float3 value = i.worldPos / 9;
    64.                 //Set Motion speed
    65.                 value.y += _Time.y * -0.5;
    66.                 //get perlin noise
    67.                 float perlinnoise = perlinNoisefunc(value);
    68.  
    69.  
    70.                 float2 screenPos = i.screenPos.xy / i.screenPos.w;
    71.                 #if !defined(UNITY_COLORSPACE_GAMMA)
    72.                 _Color1.rgb = LinearToGammaSpace(_Color1);
    73.                 _Color2.rgb = LinearToGammaSpace(_Color2);
    74.                 #endif
    75.                 half4 col = lerp(_Color2, _Color1, screenPos.y);
    76.  
    77.  
    78.                 //Adding Saturation to the areas where noise is present
    79.                 float3 intensity = dot(col.rgb, float3(0.299, 0.587, 0.114));
    80.                 col.rgb = lerp(intensity, col.rgb, (perlinnoise * 0.2) + 1);
    81.  
    82.                 //Adding Contrast to the areas where noise is present
    83.                 col.rgb = (col.rgb - 0.5) * ((perlinnoise * 0.2) + 1) + 0.5;
    84.  
    85.                 //Adding Brightness to the areas where noise is present
    86.                 col.rgb = col.rgb + (perlinnoise * 0.2);
    87.  
    88.                
    89.                 float2 noiseUV = screenPos * _ScreenParams.xy * _NoiseTex_TexelSize.xy;
    90.                 half3 noise = tex2D(_NoiseTex, noiseUV);
    91.                 half3 dither = (noise * 2.0 - 0.5) / 255.0;
    92.                 col.rgb += dither;
    93.                 #if !defined(UNITY_COLORSPACE_GAMMA)
    94.                 col.rgb = GammaToLinearSpace(col);
    95.                 #endif
    96.                 return col;
    97.             }
    98.             ENDCG
    99.         }
    100.     }
    101. }
    Thanks
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Don't use procedural shader noise on mobile devices. It's going to be way too slow. You'll want to use a texture. The original effect you're aiming for looks like it's a single repeating noise texture that's being panned and sampled twice with the second sample rotated 180 degrees from the first.
    Code (csharp):
    1. float cloudA = tex2D(_CloudTex, cloudUV);
    2. float cloudB = tex2D(_CloudTex, 1.0 - cloudUV);
    3. float cloud = cloudA * cloudB; // probably something a little more fancy than that, but might be good enough
     
    prabhukaran likes this.
  9. prabhukaran

    prabhukaran

    Joined:
    May 5, 2019
    Posts:
    17
    Thanks @bgolus , that gives 60 FPS. I need to work on the cloud texture design and some more tweaks are needed to look like a cloud effect.
     
  10. DmitryAndreevMel

    DmitryAndreevMel

    Joined:
    Mar 7, 2017
    Posts:
    14
    Hi, @bgolus, thanks for providing code sample! have some questions regarding it:

    1. why do you scale noise values to -0.5...1.5 instead of -0.5...0.5? when you scaling to the interval which is not centered at the 0 you offsets the overall color of the resulting image towards the _Color2 value. I guess the length of 2 range is selected to avoid thin strips of solid colors that appear if noise is scaled to -0.5...0.5 interval, but why not -1...1? or probably a smaller interval would be ok?

    2. if noise values are scaled to interval of length 1 and above in scenario of Alpha-To-Coverage and MSAAx4 for example then artifacts are noticeable in areas where alpha is exactly 1 or 0. How do you avoid those incorrect pixels (because of noise value is too large) and at the same time avoid introducing bands of solid alpha/color values when the noise interval is scaled to -0.5...0.5?

    3. why do you mix colors in sRGB space, not linear? I thought the linear color mixing is more correct (the sRGB mixing leads to darkening of overall image). Yes, for more quality it's preferable to use other color spaces, probably YCbCr, but if to chose from sRGB and linear I think the later one is better?

    4. probably tex2Dlod would be more performant for noise sampling than tex2D? no lod calculations required based on uv derivatives...

    5. in some cases you can also apply random noise texture offset each frame.
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    So there are two questions bound up in this one. Why a range of 2, and why centered on 0.5.

    It’s centered on 0.5 because we want to round the value when it’s quantized, not floor it. If you floor it, then it shifts darker, and GPUs always floor a higher precision output value when writing to a lower precision target.

    It’s a range of 2 because it usually looks better. It’s a toss up of reducing how much noise is added, and reducing the appearance of banding & noise. With a range of 1, what you get is the amount of dithering will fade in and out between bands of maximum noise and effectively no noise. This contrast of almost no noise to some noise becomes another kind of banding. Whether or not this is actually an issue that can be seen depends on the use case & person viewing it.

    I have used two solutions.

    Solution 1: If high precision value <= 0.0 or >= 1.0, don’t apply dither. Benefits are that it’s a relatively cheap solution, and prevents the extremes from getting dithered. Cons are there’s a hard cutoff that’s some times visible on the switch to dithered vs. not.

    Solution 2: Blend from dithered to non-dithered high precision value at the extremes within the “step” of the target precision. For example if using MSAA 4x, blend across the top and bottom quarter of the value range, usually with some kind of pow curve. Benefits are no harsh cutoff. Cons are it’s a bit more expensive, and I’ve yet to get it to 100% match the perceptual target.
    Pseudo code example:
    Code (CSharp):
    1. float MSAA = // MSAA x#
    2. float alpha = // original alpha
    3. float ditheredAlpha = // alpha + dithering
    4. float factor = pow(min(
    5.   saturate(alpha * MSAA),
    6.   saturate(MSAA - alpha * MSAA)
    7. ), 2.0);
    8. float outAlpha = lerp(alpha, ditheredAlpha, factor);
    Mixing in gamma sRGB space is not “correct”, no. But it can be a stylistic choice. Using sRGB color space gives more “life” to the color transitions because they are “wrong”. In the early days of the transition from sRGB space rendering to linear space rendering there was a huge push back from VFX artists trying to make fire, because cheap additive red orange particles end up making a convincing transition from reddish to yellow in sRGB color space, but always look muted in linear space. So, again, this was purely a stylistic choice intended to match the reference.

    However, it is important that the dithering is applied in the space it’ll be display in! So even if the original transition is in linear, the resulting image will most likely be displayed in something like sRGB or a gamma of around 2.2, as that’s how most consumer displays work.

    Meh. If the noise texture doesn’t have mip maps, it’s irrelevant. I’ve never been able to measure a difference in performance in that case.


    Oh yeah, for sure. I temporarily dither all the time. For my own stuff I either use a c# script to swap between some number of blue noise textures every frame (there days just reusing the ones included with the post processing stack, with it also swaps every frame), or add some arbitrary x & y offset to the texture or hash sample position based on
    _Time.y
    and my target frame rate or a frame number passed to shaders manually. Hand tweaked until it stops giving the appearance of a panning or crawling texture.
     
    DmitryAndreevMel likes this.
  12. DmitryAndreevMel

    DmitryAndreevMel

    Joined:
    Mar 7, 2017
    Posts:
    14
    @bgolus, thank you a lot again for clarification!

    Didn't know that, it make sense. However for Alpha-To-Coverage seems the hardware performs round, not floor. That's why I was confused (never used dithering for color blending, only as a workaround for imitation of transparency in opaque objects)

    I see, yes, that's what I was feeling wrong about 1-length-range

    I was thinking lately about changing the range based on the 'distance' from 0 or 1. When the value is near 0 or 1 then range is near 1, otherwise - the range is 2. In particular something like this:
    Code (CSharp):
    1. half noise = tex2D(...) - 0.5;
    2. half ditherScale = 1/quantization;
    3. half range = 1 + min((0.5 - abs(value - 0.5)) * 2.0 * quantization, 1);
    4. return value + (noise * range + 0.5) * ditherScale;
    Makes sense! Thank you for clarification!
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Btw, I posted the code I used for dithering A2C for the Oculus Quest version of Falcon Age here:
    https://twitter.com/bgolus/status/1300512202705661953?s=21

    For MSAA 2x a range of 2.0 made the dithering too visible. It also has an arbitrary scale of 1.02 in there as a cheap alternative to clamping both the high and low.
     
    Last edited: Jan 14, 2021