Search Unity

Snap objects to pixel perfect grid in shader results in per-case-jaggedness

Discussion in 'Shaders' started by Isomania, Jul 1, 2022.

  1. Isomania

    Isomania

    Joined:
    Jan 28, 2021
    Posts:
    16
    Hi!

    I'am trying to achieve a pixel perfect look, in unity 3D engine.

    For my desired effect to take place, the camera has to be snapped to a grid. Which I've already achieved. Next, each object that moves has to be snapped to the same camera grid.

    To achieve this, I have specific vertex shaders for all materials that should be assigned to objects which are non-static. The way the vertex shader works is described by the following code/comments:

    Code (CSharp):
    1. // create v2f
    2. v2f o;
    3.  
    4. // get object center and vertex pos in world space
    5. float4 objectOriginWorld = float4(unity_ObjectToWorld._m03_m13_m23, 1.0);
    6. float4 vertexWorld = mul(unity_ObjectToWorld, v.vertex);
    7.  
    8. // go from world to view
    9. float4 objectOriginView = mul(UNITY_MATRIX_V, objectOriginWorld);
    10. float4 vertexView = mul(UNITY_MATRIX_V, vertexWorld);
    11.  
    12. // then view to projection
    13. float4 objectOriginProjection = mul(UNITY_MATRIX_P, objectOriginView);
    14. float4 vertexProjection = mul(UNITY_MATRIX_P, vertexView);
    15.  
    16. // then snap object origin
    17. float4 objectOriginProjectionSnapped = ClipSnap(objectOriginProjection);
    18. // and use snapped object origin to get snapped vertexProjection
    19. float4 vertexProjectionSnappedDiff = float4(objectOriginProjectionSnapped.xyz - objectOriginProjection.xyz, 1.0);
    20. float4 vertexProjectionSnapped = float4(vertexProjection.xyz + vertexProjectionSnappedDiff, 1.0);
    21.  
    22. // we only care about vertex so all transformations will be applied to it
    23. // first clip to view
    24. float4 vertexViewSnapped = mul(inverse(UNITY_MATRIX_P), vertexProjectionSnapped);
    25. // then to world
    26. float4 vertexWorldSnapped = mul(inverse(UNITY_MATRIX_V), vertexViewSnapped);
    27.  
    28. // and output to v.vertex, o.pos and o.worldPosition respectively
    29. o.worldPosition = vertexWorldSnapped;
    30. o.pos = vertexProjectionSnapped;
    31.  
    32. // independent from vertex snap
    33. o.worldNormal = UnityObjectToWorldNormal(v.normal);
    34. o.screenPosition = ComputeScreenPos(o.pos);
    35. o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    36.  
    37. // Defined in Autolight.cginc. Assigns the above shadow coordinate
    38. // by transforming the vertex from world space to shadow-map space.
    39. TRANSFER_SHADOW(o)
    40.  
    41. return o;
    42.  
    The ClipSnap function:

    Code (CSharp):
    1.  
    2. // snaps given clip pos to grid
    3.  
    4. float3 ClipSnap(float3 clipPos)
    5.  
    6. {
    7.  
    8.     // note that clipPos goes from -1 to 1 so we transfer it to go from 01
    9.  
    10.     float2 clipPos01 = (clipPos.xy + 1.0) * 0.5;
    11.  
    12.     // then, we have a fault in that when they are smacked down EXACTLY at rounding edge
    13.  
    14.     // because of floating point precision, we add a tiny value to make it go away
    15.  
    16.     //clipPos01 += 1e-5; // commented out because this only changes the edge cases, not a fix!
    17.  
    18.     // get the rounded clipXY (to snap to the camera grid)
    19.  
    20.     float2 rounded = round(renderResolutionExtended * clipPos01) / renderResolutionExtended;
    21.  
    22.  
    23.     // offset by half a pixel
    24.  
    25.     float2 offset = 0.5 / renderResolutionExtended;
    26.  
    27.  
    28.     // get the new clippos and remap to -1 to 1
    29.  
    30.     float2 newClipPos = (rounded + offset) * 2.0 - 1.0;
    31.  
    32.  
    33.     // create float4 clippos and return it
    34.  
    35.     return float3(
    36.  
    37.         newClipPos.xy,
    38.  
    39.         clipPos.z
    40.  
    41.     );
    42.  
    43. }
    44.  
    45. float4 ClipSnap(float4 clipPos)
    46.  
    47. {
    48.  
    49.     return float4(
    50.  
    51.         ClipSnap(clipPos.xyz),
    52.  
    53.         clipPos.w
    54.  
    55.     );
    56.  
    57. }

    Tldr:
    I go to clip/projection space to snap the rendered object's midpoint to the closest camera ray. And then back to object space.

    However as seen here this results in edge cases where the objects snap to different positions on camera movement. I think this is a floating-point precision issue, since it only happens on very specific object positions and with slight change of position the problem goes away. However, I ofcourse do not want it to have to worry about these edgecases. My thought is that it might be the result of too many matrix multiplications (floating point does not have infinite precision, and multiplying multiple numbers multiple times, might degrade precision). In which case how do i reduce the amount of matrix multiplications? Changing UNITY_MATRIX_V and UNITY_MATRIX_P to use UNITY_MATRIX_VP, as well as inverse(UNITY_MATRIX_V) and inverse(UNITY_MATRIX_P) to inverse(UNITY_MATRIX_VP) does not help! :( I also use multiple cameras so i do not know how to remove the inverse(UNITY_MATRIX_P) and inverse(UNITY_MATRIX_V).

    Any help would be nice, even if it is a complely different system to achive the "pixelperfectness"!
     
    henners999 likes this.
  2. fajerbujt

    fajerbujt

    Joined:
    Feb 29, 2020
    Posts:
    14
    Did you ever solve this? Trying to achieve something similar but have encountered the same issue