Search Unity

Jaggy white pixel artifacts on edges with specular and antialiasing

Discussion in 'Shaders' started by Lex-DRL, Apr 23, 2015.

  1. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    Here's a problem.
    You've got a mesh. You apply specular shader. You enable antialiasing in quality settings.
    And suddenly a white pixel dots appear on some random edges when you zoom out from object.
    upload_2015-4-23_17-10-8.png upload_2015-4-23_17-10-46.png
    But this is not the problem itself. This is just a prehistory.

    And now, here's the actual problem: this is a known issue. And it's known since Unity 3!

    http://forum.unity3d.com/threads/un...ular-materials-causing-white-artefacts.68120/
    http://answers.unity3d.com/questions/677140/why-do-specular-shaders-bug-out-when-viewed-from-a.html
    http://answers.unity3d.com/questions/38099/pixelartifacts-on-edges-when-activating-antialiasi.html
    http://forum.unity3d.com/threads/white-pixels-from-aa-and-specular-shader.130498/
    http://forum.unity3d.com/threads/fireflies-anti-aliasing.82887/

    Many people confirmed it. And it happens regardless of using normal maps or even specular maps at all! You can see it in my screenshots above.

    It. Just. Happens. With no relation to mipmaps, normal-mapping or filtering.

    In previous Unity versions (3 and 4) there was a dirty hack which fixed it. This hack is more related to some voodoo magic rather than predictable behavior. It's about adding this line in the end of your surface function:
    Code (CSharp):
    1. o.Normal = fixed3(0,0,1);
    i have no idea why it worked (and looks like nobody knows) but it worked.
    Until now.

    Since Unity 5 was released this fix stoped working.

    So... I faced this issue. Again. With the only possible way to fix it: to find where it comes from.

    You may think this is just another topic with some random guy raging about some bug, but it's not the case.
    I'm here to share the solution.

    After several hours of trial and error, I finally found where it comes from. These white dots are caused by normal, which becomes non-normalized after transforming from tangential space (in which it's described in surface function) to the space in which lighting function works (I don't remember if it's object space or world space).
    So, in short, even if your normal is normalized in surface function, it may and will become non-normalized when it's passed to lighting function.

    So the only "nice and clean" solution is writing your own lighting function, in which you normalize incoming normal first.
    For those of you who's not so familiar with writing your own lighting functions... here it is:

    Code (CSharp):
    1. #pragma surface surf NormalizedBlinnPhong halfasview
    2. fixed4 LightingNormalizedBlinnPhong (SurfaceOutput s, fixed3 lightDir, fixed3 halfDir, fixed atten)
    3. {
    4.     // TODO: conditional normalization using ifdef
    5.     fixed3 nN = normalize(s.Normal);
    6.  
    7.     fixed diff = max( 0, dot(nN, lightDir) );
    8.     fixed nh = max( 0, dot(nN, halfDir) );
    9.     fixed spec = pow(nh, s.Specular*128) * s.Gloss;
    10.  
    11.     fixed4 c;
    12.     c.rgb = _LightColor0.rgb * (s.Albedo * diff + spec) * atten;
    13.     UNITY_OPAQUE_ALPHA(c.a);
    14.     return c;
    15. }
    Yes, you're welcome. ;)

    It's based on MobileBlinnPhong from "Mobile/Bumped Specular" and, obviously, this is old-style (pre-U5) surface function (with no realtime GI support).

    Feel free to comment if this helped and if I found the right place where subj comes from.

    The big question is still there, however. It's:
    Why do normals become un-normalized when AA is enabled in Quality Settings?

    Hoping to get answer from UT in the comments below.
     
    Pia and hopeful like this.
  2. Zicandar

    Zicandar

    Joined:
    Feb 10, 2014
    Posts:
    388
  3. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    I guessed that.
    The only reason why loss of normalization can occur after any transformation is if transformation matrix is composed of non-normalized vectors. So in our case we may have all of them non-normalized (tangent, normal, binormal) due to interpolation from vertex program.

    But it doesn't explain why these white pixel artifacts appear only when AA is enabled.

    When there's no AA, the same vertex and fragment programs are used, with the same "loss of normalization due to interpolation" scenario. But they don't cause these artifacts.
     
  4. Zicandar

    Zicandar

    Joined:
    Feb 10, 2014
    Posts:
    388
    Perhaps they do cause these artifacts, but without the MSAA the nr of samples for those pixels is only 1, so chances of us getting a (even stronger) white pixel is a lot less.
    It could also be that the parts getting so high pixelation would always be < 1 pixel otherwise, and thus blended away/discarded completly at those angles.
    Oh and I have managed to get these pixely edges even without MSAA (deferred mode).
     
  5. echo4papa

    echo4papa

    Joined:
    Mar 26, 2015
    Posts:
    158
    I've seen artifacts like this when building to mobile(iOS specifically) when I have split edges in my models. Interestingly not on UV seams, but on unwelded edges, which always struck me as odd since Unity will split your edges for you for smoothing group changes. I wonder if this has been the real cause.
     
  6. Zicandar

    Zicandar

    Joined:
    Feb 10, 2014
    Posts:
    388
    Umm, generally due to mipmapping I thought it was best to HAVE a UV seam where you have a hard (unwelded?) edge. As it allows the painted area to be stretched out around it to prevent mip-mapping artifacts.
     
  7. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    Yes, these artifacts could be caused by lack of UV padding.
    But as you can see from the screenshots, they're also present when no textures are used.
     
  8. echo4papa

    echo4papa

    Joined:
    Mar 26, 2015
    Posts:
    158
    It's not a hard edge thing, if the normals are still averaged(smoothed) the problem can still be seen. Even still, without a UV seam you should see no mipping artifacts, for the same reason why dilation helps mipping artifacts.
     
  9. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
    Hi, I've come across this same issue (checked and is not related to UVs in my case), and currently I'm using the standard shader, so I've spent some time trying to figure out where is the normal that I've to normalize.
    I've checked the following files, and normalized every normal I could find (I don't know if that's the proper way to do it:p):
    UnityGlobalIllumination.cginc
    UnityPBSLighting.cginc
    UnityStandardCore.cginc
    UnityStandardCoreForward.cginc

    But doesn't seem to be working, so I wanted to ask if someone had to solve this same problem that I'm having with the standard shader.
    Cheers.

    (also here is a screenshot of my problem)
    Screenshot_3.png
     
  10. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    @geroppo Are you sure it's about the same issue discussed here?
    From your screenshots I see the issue appearing only in the shadow area. And seems like it's not getting white, but is getting the main brownish color instead. As if in those artifact areas there were no shadow for some reson.

    Try to disable shadows at all. Do you still see the problem?
     
  11. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
    you were right, seems to be another issue. Disabling shadows (turning of the "receive shadow" of each mesh) makes the artifacts dissapear. Any guess what could it be then ? because it seems to be related to anti aliasing, if I turn it off then it gets fixed. I've tried using the post processing effect anti aliasing that unity provides, but doesn't solve the original issue (attached below)
    Screenshot_2.png .
    THAT^ get's fixed only by using the um..."built in" anti aliasing (instead of the post processing one). But after turning on that anti aliasing, the white/shadow artifacts comes up.

    EDIT: Also, in the UnityGlobalIllumination.cginc at the end of the function DecodeDirectionalSpecularLightmap I found a comment saying something (hopefully) related to these jagged lines. Code below
    Code (CSharp):
    1.  
    2. inline half3 DecodeDirectionalSpecularLightmap (half3 color, fixed4 dirTex, half3 normalWorld, bool isRealtimeLightmap, fixed4 realtimeNormalTex, out UnityLight o_light)
    3. {
    4.     o_light.color = color;
    5.     o_light.dir = dirTex.xyz * 2 - 1;
    6.  
    7.     // The length of the direction vector is the light's "directionality", i.e. 1 for all light coming from this direction,
    8.     // lower values for more spread out, ambient light.
    9.     half directionality = max(0.001, length(o_light.dir));
    10.     o_light.dir /= directionality;
    11.  
    12.     #ifdef DYNAMICLIGHTMAP_ON
    13.     if (isRealtimeLightmap)
    14.     {
    15.         // Realtime directional lightmaps' intensity needs to be divided by N.L
    16.         // to get the incoming light intensity. Baked directional lightmaps are already
    17.         // output like that (including the max() to prevent div by zero).
    18.         half3 realtimeNormal = realtimeNormalTex.zyx * 2 - 1;
    19.         o_light.color /= max(0.125, dot(normalize(realtimeNormal), normalize(o_light.dir)));
    20.     }
    21.     #endif
    22.  
    23.     o_light.ndotl = LambertTerm(normalize(normalWorld), normalize(o_light.dir));
    24.  
    25.     // Split light into the directional and ambient parts, according to the directionality factor.
    26.     half3 ambient = o_light.color * (1 - directionality);
    27.     o_light.color = o_light.color * directionality;
    28.  
    29.     // Technically this is incorrect, but helps hide jagged light edge at the object silhouettes and
    30.     // makes normalmaps show up.
    31.     ambient *= o_light.ndotl;
    32.     return ambient;
    33. }
     
    Last edited: Jan 8, 2017
  12. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    Sorry man. I don't know where it comes from. But, obviously, it's another issue. Which could be (and probably is) completely unrelated to the one discussed here.

    So the best thing I can tell is to try to find the answer yourself. It's Unity, get used to it! ;) It's all about constantly fixing some bugs after UT that appear in unexpected places and got fixed even more unexpected way. Especially when it comes to graphics/shaders.

    You could just aplly trial and error aproach here, top-to bottom.
    Modify 31st line replacing o_light.ndotl with some non-critical-point constant (i.e., not perfect 0 or 1 or 0.5 but something in-between, like 0.738).
    And then, if the problem is in ndotl value, check what you've got there and dig deeper to find where it comes from, step-by-step
     
  13. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
    Alright, thanks!
     
  14. Lex-DRL

    Lex-DRL

    Joined:
    Oct 10, 2011
    Posts:
    137
    And send a bugreport with a simplified-isolated-case scene, of course. Don't expect it to be fixed soon, though.
    But there's a really high chance that your bugreport today will become the actual reason why this specific bug will be fixed two years later.
     
  15. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
  16. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    8,138
    This will be "fixed" likely in either 5.6 or 5.7 when they add a new Forward+ render path that doesn't use the screen space shadows.

    Until then I use this:
    https://forum.unity3d.com/threads/fixing-screen-space-directional-shadows-and-anti-aliasing.379902/
     
    TeRainty and Lex-DRL like this.
  17. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
    Oh wow thank you very much!
     
  18. 4s4

    4s4

    Joined:
    May 15, 2012
    Posts:
    71
    Is there any fix for this problem? I encountered on 2018.11.f1.
    I'm using standard shader with no textures in the example. I'm not using shadows in the scene.
     
  19. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    8,138
    Looks like what happens with either bad geometry or otherwise extremely thin bits of geometry with vertex normals pointing in significantly different directions.

    It's a little complicated to explain, but the way MSAA works it's possible to end up drawing a triangle using values that aren't on the triangle.

    To explain that, you need to understand rasterization and barycentric interpolation.

    A triangle is made up of three vertices. When drawing a triangle on screen, the values used are an interpolation of the three vertex values based on their distance from each traingle within the triangle.
    https://www.scratchapixel.com/lesso...-rendering-a-triangle/barycentric-coordinates

    The GPU draws triangles using rasterization. The short version is the GPU calculates if a triangle covers the center of each pixel or not, aka a coverage sample, and if it does, it draws the triangle using the interpolated values for that position.

    Where MSAA causes problems is it does multiple coverage samples within a pixel to see if it hits a triangle (none at the center of the pixel), but it still uses the interpolated values from the center of the pixel. This means the MSAA coverage samples may determine a triangle is visible to a pixel, but the triangle may not actually cover the center of the pixel. This means it'll interpolated values outside of the triangle! This is a problem for triangles with a decent amount of normal variation as that over interpolated value may be pointing in a direction that doesn't exist in the vertex data.

    One solution is to not have super thin bits of geometry, likely using mesh LODs to remove as many of these as possible. This isn't perfect as you can get thin geometry just from rotating a mesh so that some faces are being viewed edge on from the camera view.

    The other solution is to use centroid sampling on the vertex to fragment interpolators. This requires a custom shader, and can make visually smooth continuous surfaces have seams. It's also been broken in Unity for the last 5 years such that it flickers on and off for reasons unknown.
     
    Last edited: Nov 24, 2018
  20. AndrewAyre

    AndrewAyre

    Joined:
    Sep 21, 2017
    Posts:
    1
    Curious what you mean here by centroid sampling potentially causing seams, could you explain/elaborate?
     
  21. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    8,138
    With 4x MSAA, almost every GPU uses a rotated grid sampling pattern like this:
    upload_2018-11-28_9-26-6.png
    This is a single pixel. The red dot is the center of the pixel, and the green dots are the coverage sample locations. As described above, normally if a triangle covers only one of the coverage samples, the fragment shader is still calculated with interpolated values from the center of the pixel which the triangle may, or may not actually cover.
    upload_2018-11-28_9-30-10.png
    This can lead to over interpolated values, which causes things like normal directions to start pointing directions they shouldn't. In the case above, using centroid sampling would instead use the one coverage sample's position as the interpolation position ensuring the values exist on the triangle. If the triangle covered two or three coverage samples, the interpolated position would be the mid point between them.

    This all sounds good, but when you have two triangles that share the same edge, things can get funny. The most obvious way to show this is with texture UVs. Here are the edges of two triangles that are side by side with a texture mapped across them. The blue circle is the interpolation point for the triangles.
    upload_2018-11-28_9-56-52.png
    With pixel center sampling, they match and the texture is seamless across the edge. With centroid sampling the texture is being sampled at different positions. With textures this usually isn't really that noticeable, but it can lead to some extra aliasing that wasn't there before. It also messes with the screen space pixel derivatives used to calculate the mip level causing potentially causing even more aliasing.

    For normals on smooth geometry without extreme normal variance in the triangles' vertices, the over interpolated pixel center normal of the triangle that doesn't cover the pixel center will likely be fairly close to the geometry that does cover it. This means a mostly flat plane will look properly smooth. With centroid sampling the difference can be enough that slight creases can appear in the normals, especially when using specular.

    For normals on geometry with extreme variance the opposite is true as the over interpolated normal produces the issues @4s4 shows, and centroid sampling would "fix" the problem by preventing over interpolated normals.

    So, both pixel center and centroid sampling have problems in different scenarios. Ideally you'd want both if you could detect when one is going bad.

    There's a Valve talk related to VR where they tackle this problem. The pass both the centroid and center sampled normal to the fragment shader and try to detect when the center normal is over interpolated. If it is, they fall back to the centroid sample.
    Page 44: http://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf

    I implemented Valve's centroid sampling technique in a shader I posted here:
    https://forum.unity.com/threads/try...metric-specular-aliasing.494588/#post-3217303
     
    Last edited: Nov 28, 2018
unityunity