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

Strange render artifact: dotted white lines along quad borders

Discussion in 'Shaders' started by a436t4ataf, Dec 18, 2019.

  1. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    I'm just interested to hear if this rings a bell for anyone - is this some typical n00b error? It feels like something I already know but I can't think of anything specific that would cause this behaviour (my obvious candidates were all dismissed by trying different texture-import settings and different textures).

    I recently noticed that I'm getting an artifact along quad edges.

    • Almost-planar mesh, with every quad using identical corner coords to its neighbours
    • Artifact disappears if I remove the normal map from the mesh (so the "white dots" are presumably just the sun-reflection, i.e. some error in the normal sampling)
    • Artifact ALMOST disappears if I disable mipmaps on the normal-texture
    • Artifact FULLY disappears if I disable mimaps AND change sampling-mode to point
    • Clamp vs repeat has no positive effect
    • The problem happens on almost all normal maps
    • The problem disappears for almost all non-normal maps used in the normal-map slot
    • Normal maps are seamless - verified in photoshop, zoomed in 50x or more and can't see the boundary



    You can see the dotted-lines running top-left to bottom-right - they lie on exact quad boundaries, but not every quad, and they change a little frame to frame. Seems very much like its a sampling error: the wrong normal is being generated along those lines only.
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    So, does each quad have a unique normal map texture that’s mapped across the entire surface?

    What shader are you using?
     
  3. Gufetto

    Gufetto

    Joined:
    Oct 19, 2015
    Posts:
    29
    Maybe you already did it, but since you didn't write about it: have you tried using the Border mipmap setting in the import settings?

    Does your texture works fine if you use it with another shader?

    Are your textures packed(like multiple textures packed inside a single one)? I don't think it's the case, but if they are, it might be bleeding one texture into another when generating the mipmaps.

    Have you already tried scaling the uv in the vertex shader to exclude the borders? If nothing else works this might be a quick solution. Something like
    uv = uv*0.98 + 0.01;
     
  4. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Sorry, forgot to mention that: yes, tried it; no, no effect.

    Yes, and I tried dozens of different textures (randomly picked from the project) - most of the normal maps have the problem, but only with this shader. So it's something wrong with the shader or mesh, not the textures themselves, I think.

    That's a good thought, that would be one of my suspicions too - but it's just a plain simple texture. 512x512.

    I'm scrolling off the edges, and tiling across the whole mesh, so I don't think that would work, it would just generate a different artifact?
     
  5. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    3 textures tiled differently across the mesh.

    Mesh split (in screenshot) into approximately 10 quads across, 100 quads down ( you can see a patch about 7 across, and about 6 down in the screenshot).

    UV's tiled 3x across (0..1..2..3), and approximately 50x down (0..1..2... ... ...50).

    Each quad gets the UV for the mesh, and there is nothing that's specifically querying the quad-local coords or using them. Hmm. This gives me an idea...

    The line of code that single-handedly creates/removes the artifacts is:

    Code (CSharp):
    1. UnpackNormal(tex2D(_NormalMap, uv - 1.1*float2( 1.1 * t * _Speed, 0.9 * t * _Speed)));
    2. // uv = 0..1..2..3 across, 0..1......50 down
    3. // t = _Time.x
    4. // _Speed = shader-property: float (0..1)
    5.  
    HOWEVER (idea from above) the UVs across the quads themselves are being bilinearly interpolated by hand (to ensure consistent lateral tiling from quad-to-quad in UV space, even as the mesh deforms), and that's the only piece of shader code that has a direct dependence on the quad coords themselves, so maybe there's an error in my bilinear code.

    (or, possibly, in my mesh generation when it adds the vertex-attributes that are needed by the bilinear interpolation in shader).

    Code (CSharp):
    1.  
    2.                 float2 corner1 = IN.corners0001.xy;
    3.                 float2 corner2 = IN.corners0001.zw;
    4.                 float2 corner3 = IN.corners1110.xy;
    5.                 float2 corner4 = IN.corners1110.zw;
    6.              
    7.                 float2 uvBiliQuad = invBilinear( IN.pointInQuad, corner1, corner2, corner3, corner4 );
    8.                 float2 uvCorner1 = IN.uvCorners0001.xy;
    9.                 float2 uvCorner2 = IN.uvCorners0001.zw;
    10.                 float2 uvCorner3 = IN.uvCorners1110.xy;
    11.                 float2 uvCorner4 = IN.uvCorners1110.zw;
    12.              
    13.                 float uAlongV0 = lerp( uvCorner1.x, uvCorner4.x, frac( uvBiliQuad.x ));
    14.                 float uAlongV1 = lerp( uvCorner2.x, uvCorner3.x, frac( uvBiliQuad.x ));
    15.                 float u = lerp( uAlongV0, uAlongV1, frac(uvBiliQuad.y) );
    16.              
    17.                 float vAlongU0 = lerp( uvCorner1.y, uvCorner2.y, frac( uvBiliQuad.y ));
    18.                 float vAlongU1 = lerp( uvCorner4.y, uvCorner3.y, frac( uvBiliQuad.y ));
    19.                 float v = lerp( vAlongU0, vAlongU1, uvBiliQuad.x );
    20.                 float2 uv = float2( u, v );
    21.  
    Where invBilinear is Inigo Quilez's bilinear implementation, converted from OpenGL to HLSL (only change: different winding order for triangle verts).
     
  6. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    NB: when I tested the bilinear implementation, I used a colourful UV texture with numbers and letters (like the many you get from googling "uv debug texture") -- I don't think I'd have been able to notice the subtle quad-border errors with that texture, they're too hard to see until you get to an extreme case like dark-brown background with white-hilight.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Whenever you see
    frac()
    used anywhere near UVs, that's the problem.

    GPUs determine the mip level to use for a texture by how much the UVs change from one pixel to another. These are called screen space partial derivatives. If you use frac() it means between two pixels the UV coordinate might be going from 0.99 to 0.01, which is a huge jump and will cause the GPU to think it needs to use to the smallest mip map.

    "But wait" I hear you thinking, "the vertex values are all within 0.0 and 1.0, and this is at the edge of the quad, there are no other pixels outside of the mesh!" And you'd be correct, but also wrong. GPUs always run the fragment shader on batches of 2x2 pixels at a time, these are known as pixel quads. If a mesh only hits a single pixel of that 2x2 pixel quad, all 4 pixels run the fragment shader, even if some of those pixels are outside of the polygon! For those pixels it's still using interpolated values, but they're over interpolated to a position outside of the polygon's bounds. That means even if your vertex attributes are within a 0.0 to 1.0 range, at the edges the fragment shader might be calculating values just below 0.0 or above 1.0. Normally this would allow the GPU to properly calculate the derivatives for those pixels, but since you're using
    frac()
    you're inducing discontinuities in the UVs.

    There are three main solutions to this problem in general.
    First: Don't use frac(). Obvious enough, but not always an option.

    Second: Don't use tex2D(). You can instead compute the derivatives manually using
    ddx(uv)
    and
    ddy(uv)
    with UVs calculated without
    frac()
    , and then use
    tex2Dgrad()
    instead. This lets you use derivatives that don't have discontinuities but still use UVs that do.
    Code (csharp):
    1. float2 dx = ddx(i.uv);
    2. float2 dy = ddy(i.uv);
    3. float2 uv = frac(i.uv);
    4. fixed4 col = tex2Dgrad(tex, uv, dx, dy);
    Note, the UVs used for the derivatives don't need to even match those used to sample the texture, they just need to be the same orientation and scale, the position is irrelevant since we're only using the derivatives (ie: the amount of change between pixels).
    https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-tex2dgrad

    Third: Use this technique http://vcg.isti.cnr.it/~tarini/no-seams/
    The short version is calculate two sets of UVs, one using
    frac()
    going from 0.0 to 1.0, the other offset also using
    frac()
    but going from -0.5 to 0.5. Use
    fwidth()
    (another screen space partial derivative function) to pick the one with the smallest derivatives.
    Code (csharp):
    1. float2 uv1 = frac(i.uv);
    2. float2 uv2 = frac(i.uv + 0.5) - 0.5;
    3. float2 uv = fwidth(uv1) < fwidth(uv2) ? uv1 : uv2;
    Works great on things like seamless UVs on a cylinder, sphere, or arc, which is what the original paper was for. But requires textures that are seamless, using repeat wrapping, and don't need to have their UVs limited to a strict range (like 0.0 to 1.0).


    I suspect just not using frac() is the answer for you. However...


    But looking at your code, I have one more question. Why are you using Inigo's bilinear quad code at all? That's used for determining the UVs for the surface of an analytical quad. If you're rendering geometry you don't need any of that since you already get UVs from the mesh vertices. Plus iq's original function has code to return -1 for positions outside of the quad because he wants to be able to detect when it's outside of the quad, which you definitely don't want to do here. The geometry is already limiting the area being rendered, and you need the over interpolated values for proper UV derivatives. In fact if you look at the shader toy example for the technique and comment out the "quad borders" (the outlines he's drawing around the quad) you'll see the exact same edge artifacts you have!
    upload_2019-12-18_10-40-18.png

    If you're using the bilinear quad code to try to get around triangle affine texture mapping and want quad affine texture mapping, there are cheaper ways to do it.
    https://forum.unity.com/threads/hel...-the-mesh-in-a-weird-way.545413/#post-3605017
     
  8. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Argh, yes! frac + ddx/ddy + mipmaps. That's the answer to my: "is this some typical n00b error?" :).
     
  9. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    It was a drop-in starting point that I already knew worked, so I didn't have to worry about debugging it. No reason to keep it.
     
  10. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Sure, I implemented projective interpolation too. But it's not enough in my cases - I needed the coords to match exactly at seams, and projective interpolation won't do that. There may be other things that do, but bilinear is the only one I knew of that does.
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    My example is not perspective corrected texture mapping, which is what I think you’re referring to. It’s simplified affine quad mapping, which should produce exactly the same result as what your code is doing, but with less complexity.

    Also if all you care about is the seams mapping, bog standard UV mapping does that too.
     
  12. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    The thread you referenced talks about doing (x, y/z, z), which to me looks like interpolation in projective space, and then de-projecting in fragment shader. e.g. first hit I get for googling "affine quad mapping" is this: http://reedbeta.com/blog/quadrilateral-interpolation-part-1/ - which seems to be the same technique.

    And the final two images on that webpage show the problem for me: seam mismatch.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    The technique I'm showing off is a little different than perspective corrected texture mapping, or projective mapping as it's called in the link you posted, and does actually keep the seems matching. However I realize when generalized to a quad with more distortion than just one axis it may not be any better than normal UVs, which as mentioned previously, also don't suffer from the edge seams you're talking about since the UVs on all triangle edges will be linear.

    They look like this (from the link you posted).
     
  14. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Yep, so ... characteristics I need:

    1. uniform U and V across the surface of the quad (no jump in d(U|V) at the point where one tri switches to other tri)
    2. U values along bottom and top edges align perfectly with neighbouring quads
    3. V values along left and right edges align perfectly with neighbouring quads
    For which:
    • UV mapping fails on 1 (interpolation happens linearly across the tri, so cannot capture the context of a quad)
    • Perspective corrected mapping fails on 2 and 3 (interpolation happens projectively across the tri, so edge coords depend on the rest of the quad's shape, which neighbour quads have no access to and so cannot align with)
    • Bilinear interpolation succeeds on all 3
     
  15. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Depressingly, even though I believe you're correct about frac + mips being the cause of these artifacts ... after correcting for this, there's no change in the output.

    (for quickness, I went with the Tarini paper's approach - I like the elegance of it, and it was only a few lines change to replace "o.uv" (I don't use UV directly at the moment, only the frac'd versions) with "o.uvFrac and o.uvFracOffset" in the vertex shader, and then have the fragment shader pick one based on fwidth)

    IDE full-project search reveals I'm not using frac() anywhere else, so ... more investigation required. As you pointed out, the jump in derivatives would perfectly explain what we're seeing visually, so ... I'll try tex2Dgrad next (maybe I made some typo in implementing the Tarini approach - although it's so little code it's hard to believe I could screw it up :D) and see what happens.
     
  16. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    tex2Dgrad greatly reduces the severity of the artifacts, but they're still there. It's now subtle enough to be tolerable (as opposed to previously, where it was glaringly obvious), but I'd like to find what bug I have left and squish it.

    ...and now I have to figure out why my attempt at Tarini approach failed too.
     
  17. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    Update1: tex2Dbias with a -999 bias (to get rid of the mipmapping on those textures) gets rid of the artifacts entirely.

    Update2: I've found some situations where my modification of Inigo's code just ... fails ... for no apparent reason, declaring that points definitely inside the quad are outside the quad.

    The incorrect values lie in a Moiré pattern across the surface, which is itself quite special to look at :). It's heavily dependent upon position in world space, so I'm suspicious of a floating point inaccuracy / rounding error somewhere. But also bemused that it works correclty on 95+% of quads with no visible errors, and then fails spectacularly on 5% :).
     
  18. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Are you still using iq's original invBilinear function unmodified? I made a comment about that you may have missed.
    Basically, iq's version of this is guaranteed to have mip issues on the edges. Remove the line that sets the u and v to -1. That was only needed because the ShaderToy example is using the bilinear UV to also determine if it intersected with the quad. When you're rendering geometry the mesh is doing that for you.

    After that, along with removing the frac() calls in your example, it should work flawlessly, even with using tex2D().*

    * The one big caveat is iq's (and Nathan Reed's) examples are specifically for 2D quads. If you're doing this math on true 3D positions, I don't think it works.
     
    Last edited: Jan 2, 2020
  19. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    I tried some other implementations - they work fine, it's only iq's that has issues deep inside the quad itself (along with issues on the extreme edges). In particular, thin quads (e.g 5-10 times longer in one dimension than the other) seem to mess it up (But other implemetnations have no issue there).
     
    bgolus likes this.
  20. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    ...so I've moved on, I'm now using more detailed implementations, BUT: quick look into "why?" iq's is suffering suggests its a floating-point accuracy issue. The main difference for other implementations is that everyone else chooses their denominators for division selectively halfway through the algorithm, based on which floats are going to give the best division result.

    (you can choose which line to divide by - pick the one that has the longer length, so that floating-point inaccuracies don't bite you)
     
  21. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,924
    And (debugging some other code and rethinking about this for separate reasons), I noticed the side-comment on the reedbeta webpage:

    "(There’s still a C1 seam between the quads, where the mapping derivatives jump; but that’s unavoidable as long as we insist that the texture completely fill the quad.)"

    ...which I believe is the explanation for remaining (subtle) jumps in normal map sampling. And I suspect there's no way to get rid of them: you would need to have all the data for ALL adjacent quads when texturing one quad, so you could calculate the ddx/y across the borders (ouch!). So my current hack of disabling mipmapping entirely might be the only practical solution!