As in comments, go to Output Properties tab, Post Processing section, decrease Dither to 0.0
:
Searching for dither on SE should yield multiple questions where dithering was the solution (instead of the problem). The problem dithering solves is banding. In short, there's only 255 steps from #000000 black to #FF0000 red, so a horizontal gradient with more than 256 pixel width will assign some colors to multiple columns, making those steps possibly perceivable as the bands.
Ideally*, dithering would be implemented as a 16 bit value scaled down to 8 bits by rounding down or up, depending on the pixel's position. This way an integer red component like in your color #FF0000 (255 red) wouldn't be affected by dithering.
However, this is not how Blender does it, it doesn't round up or down, it just adds noise and then rounds:
source/blender/imbuf/intern/divers.c
MINLINE void ushort_to_byte_dither_v4(
uchar b[4], const unsigned short us[4], DitherContext *di, float s, float t)
{
#define USHORTTOFLOAT(val) ((float)val / 65535.0f)
float dither_value = dither_random_value(s, t) * 0.0033f * di->dither;
b[0] = ftochar(dither_value + USHORTTOFLOAT(us[0]));
b[1] = ftochar(dither_value + USHORTTOFLOAT(us[1]));
b[2] = ftochar(dither_value + USHORTTOFLOAT(us[2]));
b[3] = unit_ushort_to_uchar(us[3]);
#undef USHORTTOFLOAT
}
di->dither
is the setting in the interface. It's multiplied by 0.0033
(1/303.(03)
) and then by the result of a function dither_random_value(s, t)
. s
and t
are x and y coordinates. The result of the function is in range [0..1)
- it's implemented slightly differently throughout the code, but always based on "A faster triangle noise"
So it will add noise to every color, even if that color perfectly fits the target color space without rounding.
Custom dithering
First setup the color management as in the question:
Now make a plane with a gradient that has two "flat" areas on either side:
The color ramp is set to two colors: #FF0000 (255 red) and #F50000 (245 red). Keep in mind you can't just input #F50000 as hex, because Blender will assume it's sRGB and convert it to linear space that it uses. So what you have to do is to divide 245/255 in a calculator and copy-paste the result to the red value of the color.
Now the compositor:
I'm only dealing with red here, so that the node setup is as simple as possible. The noise texture I used is Blender Original Distorted Noise with size:0.0
and brightness:1.5
:
I didn't calculate those settings in some clever way, I just tinkered with them until the preview of "Less Than" node was giving me a result with almost completely white one side of a band and almost completely black other side (and smooth transition between them):
So if we blacked every 2nd shade of red, it would look like this:
Comparison
Standard dither 1.0
, compositing turned off
Half dither 0.5
, compositing still off
Zero dither (compositing still off)
Custom dither (dither setting 0.00
but compositing enabled)
The top red bar of each pair is the original output for testing with a color-picker. The other bars have increased contrast in Photoshop by setting the midtone level to 0.03
in RGB Histogram. The idea behind 0.5
dithering is that 0.5*0.0033 < 1/255/2
(0.00165
< 0.00196
) so I thought maybe a value will never be increased by half of the step to the next 8bit value, but I must be missing something, as there's still some noise.
--
*- according to my tiny brain, but I might be missing something; for example, the reasoning of Blender developers could be that you should avoid getting pure black and pure white colors, as those are problems with overexposure or details completely lost in darkness (low/high end clipping). Once you avoid that, the likelihood for any color to be an integer in the chosen color space (that is, before being cast to a lower precision in that format) is very small, and it also doesn't make much sense to treat a set of arbitrary colors differently than the rest. In the case of NPR, you can just disable dithering, export to a format with 16 bits per channel and do some custom dithering if needed.
0.0
. Also please share your .blend file, so we can see if we get the same results. $\endgroup$