Search Unity

Precision issues when subtracting values of adjacent texels

Discussion in 'Shaders' started by trzy, Feb 24, 2021.

  1. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    I'm attempting to learn about URP shaders by converting Splatoonity to URP. It's essentially working but I've encountered a strange artifact that I suspect is caused by a subtraction. Interestingly, this is not present in the original non-URP code base.

    The splat decal ought to be smooth but looks like this instead:

    Splat1.jpg

    This is caused by the bump mapping calculation that attempts to create a bump at the edge of the splat by computing per-fragment normals from the intensity map that forms the splat. The relevant code is:

    Code (csharp):
    1.  
    2. // Sample splat map texture (the world texture with all splats, indexed by the lightmap UVs) with offsets
    3. float4 splatSDF = SAMPLE_TEXTURE2D(_SplatTex, sampler_SplatTex, uv_SplatTex);
    4. float4 splatSDFx = SAMPLE_TEXTURE2D(_SplatTex, sampler_SplatTex, uv_SplatTex + float2(_SplatTex_TexelSize.x, 0));
    5. float4 splatSDFy = SAMPLE_TEXTURE2D(_SplatTex, sampler_SplatTex, uv_SplatTex + float2(0, _SplatTex_TexelSize.y));
    6.  
    7. ...
    8.  
    9. // Create normal offset for each splat channel, in the plane of the surface
    10. float4 offsetSplatX = splatSDF - splatSDFx;
    11. float4 offsetSplatY = splatSDF - splatSDFy;
    12.  
    The problem appears to be caused by those last two subtractions. If we draw offsetSplatX.x, we see:

    Splat2.jpg

    But in the original this quantity looks more like:

    Splat3.jpg

    I'm completely stumped by what could be causing this. My code is using HLSL and the original is older Cg ShaderLab code. Same version of Unity running both (2020.2). Graphics settings are essentially the same.

    Because the offsets are normalized later anyway, I've tried computing the sign() as well as "splatSDF/splatSDFx-1" to no avail.

    Shader debugging in MSVC and RenderDoc isn't working for me (I can't get either to show the shader source or even a disassembly). But I'm pretty confident the subtraction is where the problem is. Every other quantity appears to be the same between the Cg and HLSL/URP versions of the shaders.

    Any pointers on how to debug or resolve this would be much appreciated!
     
  2. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,023
    What happens if you visualise splatSDF, splatSDF.x and splatSDF.y? Do they differ from what was produced by the old shader?
     
  3. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    I don’t think so. Note that we only care about the .x component here (xyzw correspond to four possible splat colors). Visually, the SDFs look smooth with no artifacts in both cases. SDFx and SDFy (which are the SDF texture offset by one texel in x and y) look about the same.

    I can’t rule out an issue with texture sampling. The SplatTex is an 8192x8192 map created at run time (same exact code in both versions) that corresponds exactly to a lightmap texture — it contains all static geometry unwrapped the same way.

    But 1.0/8192.0 should not cause any precision issues. Replacing _SplatTex_TexelSize.x with a literal 1/8192 has no effect.

    Lastly, as a test, I tried sampling out 10 or even more texels out. This only slightly reduced the artifacts but it is hard to draw conclusions. It could be that the difference in the SDF values is still quite small. Although, on the other hand, the gradient should be largest near the edge of the splats so if anything I would expect that precision issues are less likely there... Hmmm...
     
  4. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,023
    How do you declare the textures and what texture format do you use?
     
  5. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    @aleksandrk The code is the exact same C# code in both cases:

    Code (csharp):
    1.  
    2. splatTex = new RenderTexture (sizeX, sizeY, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
    3. splatTex.Create ();
    4. splatTexAlt = new RenderTexture (sizeX, sizeY, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
    5. splatTexAlt.Create ();
    6.  
    sizeX and sizeY are both set to 8192.

    At run-time, I can see that bilinear sampling is being used (changing it to point from Inspector, by the way, makes the problem even worse).
     
  6. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,023
    I suggest to try and compare preprocessed shader source between the two versions.
     
  7. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    You mean by viewing the generated code for each? Any ideas on what to look for? The old code is written as a surface shader and I’ve converted the new code to follow a URP lit shader template.

    The problematic lines are identical between shaders (except for the change from tex2d() to SAMPLE_TEXTURE2D()) but I can have a look later today. Might be beyond my skill level to pick up on any subtleties.
     
  8. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,023
    No, since you're on 2020.2 there's a checkbox "Preprocess only" right above the "Show compiled code" button. If you toggle it, it will show preprocessed source, which you can then compare. I'd look at the places where the texture you're sampling is declared and where it gets sampled.
     
  9. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    Thanks for helping guide me here. Gave that a try. Here is pretty much the complete preprocessed code (lines 39-41, 52-53 are the relevant ones):

    Code (csharp):
    1.  
    2. Texture2D _SplatTileNormalTex;
    3. SamplerState sampler_SplatTileNormalTex;
    4. Texture2D _SplatTex;
    5. SamplerState sampler_SplatTex;
    6. Texture2D _WorldTangentTex;
    7. SamplerState sampler_WorldTangentTex;
    8. Texture2D _WorldBinormalTex;
    9. SamplerState sampler_WorldBinormalTex;
    10. cbuffer SplatShaderLitBuffer {
    11.     float4 _SplatTileNormalTex_ST;
    12.     float _SplatTileBump;
    13.     float _SplatEdgeBump;
    14.     float _SplatEdgeBumpWidth;
    15.     float4 _SplatTex_ST;
    16.     float4 _SplatTex_TexelSize;
    17. };
    18. struct Attributes
    19. {
    20.     float4 positionOS : POSITION;
    21.     float3 normalOS : NORMAL;
    22.     float4 tangentOS : TANGENT;
    23.     float2 uv : TEXCOORD0;
    24.     float2 uvLM : TEXCOORD1;
    25. };
    26. struct Varyings
    27. {
    28.     float2 uv : TEXCOORD0;
    29.     float2 uvLM : TEXCOORD1;
    30.     float4 positionWSAndFogFactor : TEXCOORD2;
    31.     half3 normalWS : TEXCOORD3;
    32.     half3 tangentWS : TEXCOORD4;
    33.     half3 bitangentWS : TEXCOORD5;
    34.     float2 uv_SplatTex : TEXCOORD7;
    35.     float4 positionCS : SV_POSITION;
    36. };
    37. void ComputeSplat (out float4 splatMask, out half3 splatNormal, float2 uv_SplatTex, half3 tangentWS, half3 binormalWS)
    38. {
    39.     float4 splatSDF = _SplatTex . Sample (sampler_SplatTex, uv_SplatTex);
    40.     float4 splatSDFx = _SplatTex . Sample (sampler_SplatTex, uv_SplatTex + float2 (_SplatTex_TexelSize . x, 0));
    41.     float4 splatSDFy = _SplatTex . Sample (sampler_SplatTex, uv_SplatTex + float2 (0, _SplatTex_TexelSize . y));
    42.     half splatDDX = length (ddx (uv_SplatTex * _SplatTex_TexelSize . zw));
    43.     half splatDDY = length (ddy (uv_SplatTex * _SplatTex_TexelSize . zw));
    44.     half clipDist = sqrt (splatDDX * splatDDX + splatDDY * splatDDY);
    45.     half clipDistHard = max (clipDist * 0.01, 0.01);
    46.     half clipDistSoft = 0.01 * _SplatEdgeBumpWidth;
    47.     static const float _Clip = 0.5;
    48.     splatMask = smoothstep ((_Clip - 0.01) - clipDistHard, (_Clip - 0.01) + clipDistHard, splatSDF);
    49.     float splatMaskTotal = max (max (splatMask . x, splatMask . y), max (splatMask . z, splatMask . w));
    50.     float4 splatMaskInside = smoothstep (_Clip - clipDistSoft, _Clip + clipDistSoft, splatSDF);
    51.     splatMaskInside = max (max (splatMaskInside . x, splatMaskInside . y), max (splatMaskInside . z, splatMaskInside . w));
    52.     float4 offsetSplatX = splatSDF - splatSDFx;
    53.     float4 offsetSplatY = splatSDF - splatSDFy;
    54.     float2 offsetSplat = lerp (float2 (offsetSplatX . x, offsetSplatY . x), float2 (offsetSplatX . y, offsetSplatY . y), splatMask . y);
    55.     offsetSplat = lerp (offsetSplat, float2 (offsetSplatX . z, offsetSplatY . z), splatMask . z);
    56.     offsetSplat = lerp (offsetSplat, float2 (offsetSplatX . w, offsetSplatY . w), splatMask . w);
    57.     offsetSplat = normalize (float3 (offsetSplat, 0.0001)) . xy;
    58.     offsetSplat = offsetSplat * (1.0 - splatMaskInside) * _SplatEdgeBump;
    59.     float2 splatTileNormalTex = _SplatTileNormalTex . Sample (sampler_SplatTileNormalTex, uv_SplatTex * 10.0) . xy;
    60.     offsetSplat += (splatTileNormalTex . xy - 0.5) * _SplatTileBump * 0.2;
    61.     float3 worldTangentTex = _WorldTangentTex . Sample (sampler_WorldTangentTex, uv_SplatTex) . xyz * 2.0 - 1.0;
    62.     float3 worldBinormalTex = _WorldBinormalTex . Sample (sampler_WorldBinormalTex, uv_SplatTex) . xyz * 2.0 - 1.0;
    63.     float3 offsetSplatWorld = offsetSplat . x * worldTangentTex + offsetSplat . y * worldBinormalTex;
    64.     float2 offsetSplatLocal = 0;
    65.     offsetSplatLocal . x = dot (tangentWS, offsetSplatWorld);
    66.     offsetSplatLocal . y = dot (binormalWS, offsetSplatWorld);
    67.     splatNormal = offsetSplatWorld * splatMaskTotal;
    68. }
    69. Varyings LitPassVertex (Attributes input)
    70. {
    71.     Varyings output;
    72.     VertexPositionInputs vertexInput = GetVertexPositionInputs (input . positionOS . xyz);
    73.     VertexNormalInputs vertexNormalInput = GetVertexNormalInputs (input . normalOS, input . tangentOS);
    74.     float fogFactor = ComputeFogFactor (vertexInput . positionCS . z);
    75.     output . uv = ((input . uv . xy) * _BaseMap_ST . xy + _BaseMap_ST . zw);
    76.     output . uvLM = input . uvLM . xy * unity_LightmapST . xy + unity_LightmapST . zw;
    77.     output . uv_SplatTex = ((input . uvLM . xy) * _SplatTex_ST . xy + _SplatTex_ST . zw);
    78.     output . positionWSAndFogFactor = float4 (vertexInput . positionWS, fogFactor);
    79.     output . normalWS = vertexNormalInput . normalWS;
    80.     output . tangentWS = vertexNormalInput . tangentWS;
    81.     output . bitangentWS = vertexNormalInput . bitangentWS;
    82.     output . positionCS = vertexInput . positionCS;
    83.     return output;
    84. }
    85. half4 LitPassFragment (Varyings input) : SV_Target
    86. {
    87.     SurfaceData surfaceData;
    88.     InitializeStandardLitSurfaceData (input . uv, surfaceData);
    89.     float4 splatMask;
    90.     half3 splatNormal;
    91.     ComputeSplat (splatMask, splatNormal, input . uv_SplatTex, input . tangentWS, input . bitangentWS);
    92.     float3 splatAlbedo = surfaceData . albedo;
    93.     splatAlbedo = lerp (splatAlbedo, float3 (1.0, 0.5, 0.0), splatMask . x);
    94.     splatAlbedo = lerp (splatAlbedo, float3 (1.0, 0.0, 0.0), splatMask . y);
    95.     splatAlbedo = lerp (splatAlbedo, float3 (0.0, 1.0, 0.0), splatMask . z);
    96.     splatAlbedo = lerp (splatAlbedo, float3 (0.0, 0.0, 1.0), splatMask . w);
    97.     half3 normalWS = TransformTangentToWorld (surfaceData . normalTS,
    98.     half3x3 (input . tangentWS, input . bitangentWS, input . normalWS));
    99.     normalWS = normalize (normalWS + splatNormal);
    100.     half3 bakedGI = SampleSH (normalWS);
    101.     float3 positionWS = input . positionWSAndFogFactor . xyz;
    102.     half3 viewDirectionWS = SafeNormalize (GetCameraPositionWS () - positionWS);
    103.     BRDFData brdfData;
    104.     InitializeBRDFData (splatAlbedo, surfaceData . metallic, surfaceData . specular, surfaceData . smoothness, surfaceData . alpha, brdfData);
    105.     Light mainLight = GetMainLight ();
    106.     half3 color = GlobalIllumination (brdfData, bakedGI, surfaceData . occlusion, normalWS, viewDirectionWS);
    107.     color += LightingPhysicallyBased (brdfData, mainLight, normalWS, viewDirectionWS);
    108.     color += surfaceData . emission;
    109.     float fogFactor = input . positionWSAndFogFactor . w;
    110.     color = MixFog (color, fogFactor);
    111.     return half4 (color, surfaceData . alpha);
    112. }
    113.  
    ComputeSplat() gets called from the fragment shader and right at the beginning the texture is sampled. Looks pretty straightforward to me.

    The original shader once preprocessed looks like this:

    Code (csharp):
    1.  
    2. void surf (Input IN, inout SurfaceOutputStandard o) {
    3.     float4 splatSDF = tex2D (_SplatTex, IN . uv2_SplatTex);
    4.     float4 splatSDFx = tex2D (_SplatTex, IN . uv2_SplatTex + float2 (_SplatTex_TexelSize . x, 0));
    5.     float4 splatSDFy = tex2D (_SplatTex, IN . uv2_SplatTex + float2 (0, _SplatTex_TexelSize . y));
    6.     half splatDDX = length (ddx (IN . uv2_SplatTex * _SplatTex_TexelSize . zw));
    7.     half splatDDY = length (ddy (IN . uv2_SplatTex * _SplatTex_TexelSize . zw));
    8.     half clipDist = sqrt (splatDDX * splatDDX + splatDDY * splatDDY);
    9.     half clipDistHard = max (clipDist * 0.01, 0.01);
    10.     half clipDistSoft = 0.01 * _SplatEdgeBumpWidth;
    11.     float4 splatMask = smoothstep ((_Clip - 0.01) - clipDistHard, (_Clip - 0.01) + clipDistHard, splatSDF);
    12.     float splatMaskTotal = max (max (splatMask . x, splatMask . y), max (splatMask . z, splatMask . w));
    13.     float4 splatMaskInside = smoothstep (_Clip - clipDistSoft, _Clip + clipDistSoft, splatSDF);
    14.     splatMaskInside = max (max (splatMaskInside . x, splatMaskInside . y), max (splatMaskInside . z, splatMaskInside . w));
    15.     float4 offsetSplatX = splatSDF - splatSDFx;
    16.     float4 offsetSplatY = splatSDF - splatSDFy;
    17.     float2 offsetSplat = lerp (float2 (offsetSplatX . x, offsetSplatY . x), float2 (offsetSplatX . y, offsetSplatY . y), splatMask . y);
    18.     offsetSplat = lerp (offsetSplat, float2 (offsetSplatX . z, offsetSplatY . z), splatMask . z);
    19.     offsetSplat = lerp (offsetSplat, float2 (offsetSplatX . w, offsetSplatY . w), splatMask . w);
    20.     offsetSplat = normalize (float3 (offsetSplat, 0.0001)) . xy;
    21.     offsetSplat = offsetSplat * (1.0 - splatMaskInside) * _SplatEdgeBump;
    22.     float3 worldTangentTex = tex2D (_WorldTangentTex, IN . uv2_SplatTex) . xyz * 2.0 - 1.0;
    23.     float3 worldBinormalTex = tex2D (_WorldBinormalTex, IN . uv2_SplatTex) . xyz * 2.0 - 1.0;
    24.     float3 offsetSplatWorld = offsetSplat . x * worldTangentTex + offsetSplat . y * worldBinormalTex;
    25.     float3 worldTangent = float3 (1, 0, 0);
    26.     float3 worldBinormal = float3 (0, 1, 0);
    27.     float2 offsetSplatLocal = 0;
    28.     offsetSplatLocal . x = dot (worldTangent, offsetSplatWorld);
    29.     offsetSplatLocal . y = dot (worldBinormal, offsetSplatWorld);
    30.     float4 normalMap = tex2D (_BumpTex, IN . uv_MainTex);
    31.     normalMap . xyz = UnpackNormal (normalMap);
    32.     float3 tanNormal = normalMap . xyz;
    33.     tanNormal . xy += offsetSplatLocal * splatMaskTotal;
    34.     tanNormal = normalize (tanNormal);
    35.     float4 MainTex = tex2D (_MainTex, IN . uv_MainTex);
    36.     half4 c = MainTex * _Color;
    37.     c . xyz = lerp (c . xyz, float3 (1.0, 0.5, 0.0), splatMask . x);
    38.     c . xyz = lerp (c . xyz, float3 (1.0, 0.0, 0.0), splatMask . y);
    39.     c . xyz = lerp (c . xyz, float3 (0.0, 1.0, 0.0), splatMask . z);
    40.     c . xyz = lerp (c . xyz, float3 (0.0, 0.0, 1.0), splatMask . w);
    41.     o . Albedo = c . rgb;
    42.     o . Albedo = offsetSplatX . xyz * 20;
    43.     o . Metallic = _Metallic;
    44.     o . Smoothness = lerp (_Glossiness, 0.7, splatMaskTotal);
    45.     o . Alpha = c . a;
    46. }
    Seems effectively identical?
     
  10. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    I discovered that changing the texture type from RenderTextureFormat.ARGB32 to RenderTextureFormat.ARGBFloat improves things a little (see below) but it is still far from perfect and the original shader does not need this modification.

    Splat4.jpg

    The splats are blitted into SplatTex using a different shader, which I've also translated to URP. As far as I can tell (I've examined the RenderTexture by dumping it to disk), there's nothing wrong there and the aliasing is caused by the shader I posted earlier.

    Edit: ARGBFloat should be completely unnecessary because the splat source texture atlas is a tga file in RGB8 format. There should not be any substantial change in precision.
     
    Last edited: Feb 25, 2021
  11. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    I think I know what's going on. My render texture is way too large for the tiny scene I have. I only have a cube and a few spheres in my scene whereas the original codebase has a substantial amount of geometry. As a result, the splats are rendered in very high fidelity to the 8192x8192 render texture and the delta between any two neighboring texels is 0 or very near to it. I think that is the primary source of the noise.

    I tried creating a project with using the built-in render pipeline rather than URP and I do still see some difference in the characteristic of the noise for similar sized scenes but it's difficult to say conclusively. By reducing the render texture from 8192x8192 to 2048x2048 in my scene, I have:

    Splat5.jpg

    Now to figure out how to add gloss, squash some warnings about keywords being declared both globally and locally, and move on :)
     
    aleksandrk likes this.