Visible banding in UI Toolkit CSS gradients

Hi Ui Team

I’m seeing noticeable color banding in wide, dark UI Toolkit linear-gradient and radial-gradient backgrounds, both editor and on mobile.

The project uses Linear color space and URP

Please advise

This is most likely because UI Toolkit uses a 16bit render texture per default (to save on mobile bandwidth) but applies that to all platforms.
You can create a custom panel configuration + create a custom render texture and set it to a 32 bit format

There could be many things at play (graphics api, render target format, urp settings, alpha gradient, etc). Could you submit a case in the bug reporter (Help>Report a bug), we’ll investigate.

Following causes banding, due to 0% i think?

    background-color: #081217;

    background-image: linear-gradient(
            160deg,
            #1e3a42 0%,
            #081217 100%
    );

Meanwhile im submitting the project as bug report.

Update: CASE IN-151280

Possibly. Even if the shader performs its computations in half precision, blending can take place in the RT precision. So if it’s R8G8B8A8_SRGB it could be 8 bits, which can easily produce bands like that. For example, with Vulkan the minimum guaranteed is 8 bits in this case:

basic blend operations are performed with a precision and dynamic range no lower than that used to represent destination components.

Also if the render target is SRGB (likely for overlay UI in linear project unless you enabled HDR output and it’s actually being used), the blending would occur before the SRGB-encoding, so in linear. This can’t yield a perceptually smooth gradient. Not saying it is the case, but it’s something that we’ve seen this in the past.

Sorry for not fully understanding the technical details, but this game is built purely with UI Toolkit using a Screen Space - Overlay setup. I’d like to use a modern Unity version along with modern workflows (like the Linear color space), and I’d love to resolve this without compromising on visual quality or look and feel.

Since I’ve already submitted the bug report along with the project files, I’m assuming it will be tested and hopefully a fix or workaround can be provided?

Yes, the case will be investigated, no need to submit again.

@jGate99

This is 8-bit quantization. The gradient is written to an 8-bit target, so it only has as many distinct colors as the biggest difference between your stops, which is 43 here. Those 44 values get spread over the whole fill, so each one covers a wide flat strip. The lower the contrast, the fewer values you get and the wider the strips, so dark low-contrast gradients are the worst case. This is not specific to UI Toolkit: CSS gradients in a browser band the same way, for the same reason.

You can reduce the banding appearance with a small custom shader that offsets each pixel by half a color level, so the two neighboring levels interleave instead of meeting at a hard edge. On your gradient the widest band goes from 48px to 8px.

  1. Create > Shader Graph > UI.
  2. Wire Default Gradient into a Custom Function node, and that into the Gradient input of Render Type Branch. Feed a Screen Position node into the Custom Function’s second input. Leave the other Render Type Branch inputs empty, they fall back to the built-in fills so solids, textures and text still render normally.
  3. On the Custom Function node add two Vector4 inputs named In and ScreenPosition, one Vector4 output named Out, then paste the body below.
  4. Make a material from the graph and assign it with -unity-material.

Keep the element with the gradient a leaf if you can. -unity-material is inherited, so its children end up rendering with the custom material too, and every material change breaks the batch and costs extra draw calls. If it needs children, set -unity-material: none; on them.

Custom Function body:

float2 pixelPos = ScreenPosition.xy * _ScreenParams.xy;
float noise = frac(52.9829189 * frac(dot(pixelPos, float2(0.06711056, 0.00583715))));
float offset = (noise - 0.5) / 255.0;
Out = float4(saturate(In.rgb + offset), In.a);
// In Linear use:
// Out = float4(saturate(In.rgb + (offset * 2.0) * max(sqrt(max(In.rgb, 0.0)), 0.039)), In.a);

Shader Graph

USS

.my-gradient {
    -unity-material: url("project://database/Assets/DitherGradient.mat");
}

Shouldn’t this just be a setting in panel settings? I could see this workaround could be helpfull on low end devices but on pc would be good to have ability to choose output format/bit depth.