blur: Dither output using triangular RGB blue noise
Blurring images usually creates a lot of gradual color transitions, especially at higher radii. When the output is quantized to 8-bit RGB (8 bpc / 24 bpp) for display, the lost information (quantization error) results in visible banding. This is most apparent in scenes that are predominantly grayscale because of the reduced color fidelity, but it can still be an issue in scenes with many colors. To fix the banding, this commit dithers the blur output during quantization. This is done in the final output mixing step because dithering works best when it operates on each individual pixel; upscaling a dithered image will not help much. Error diffusion dithering is ideal, but it is not practical for GPU fragment shaders because it requires processing each pixel sequentually. Instead, we use ordered dithering, which tiles a pattern across the entire image to influence rounding during quantization. The most visually-appealing pattern is used for ordered dithering: blue noise [1]. Other patterns considered: - Bayer matrix: visible pattern in output - White (random) noise: high-frequency components are distracting and make tiling repetitions apparent - Interleaved gradient noise, generated in shader [2]: aliasing artifacts, somewhat visible pattern, and expensive ALU operations - Oculus Dither17 pattern, generated in shader [3]: same issues as interleaved gradient noise Blue noise contains very few low-frequency components, making it ideal for dithering because it tiles seamlessly and does not distract from the actual image. When dithering, the blue noise is reshaped from uniform distribution to a triangular PDF distribution. This makes the noise appear more uniform when used for dithering: instead of some areas having less visible noise than others, the noise appears to be evenly spread across the entire image [4] [5]. A naïve implementation of triangular reshaping is relatively expensive (+60 µs), but using an optimized implementation of sign() reduces the cost to ~30 µs [6]. Finally, to avoid adding noise to pure black (#000000) images or affecting saturation, the dithering implementation needs to perform gamma correction [5]. The real sRGB transfer function is relatively expensive because it's a piecewise function with linear and exponential (gamma 2.4) parts that average to gamma 2.2, so we approximate it instead with gamma 2. This makes the performance cost of gamma correction negligible while still producing acceptable results. Most applications of dithering add up to 1x LSB (least significant bit - i.e. 1/256 for 8-bpc output) of noise, but this implementation adds up to 4x LSB (i.e. 1/64 for 8-bpc) of noise. While this adds more noise to the output, it was empirically determined to be more effective for reducing banding in color gradients than 1x LSB. There are still nearly no visible noise artifacts when using blue noise and gamma correction. Note that dithering requires the blurred image to be rendered at 10-bit HDR (10 bpc / 30 bpp) internally; otherwise, it would just add noise without fixing banding. This increases rendering time by ~300 µs on an Adreno 640 GPU at 1440x3040 resolution, which is a worthwhile tradeoff for the significant improvement in quality. [1] http://momentsingraphics.de/BlueNoise.html [2] http://www.iryoku.com/downloads/Next-Generation-Post-Processing-in-Call-of-Duty-Advanced-Warfare-v18.pptx [3] https://developer.oculus.com/blog/tech-note-shader-snippets-for-efficient-2d-dithering/ [4] https://loopit.dk/banding_in_games.pdf [5] https://loopit.dk/rendering_inside.pdf [6] https://twitter.com/SebAaltonen/status/878250919879639040 Change-Id: I80559654a19c6cc6f2f53c94b64963d0bb888af5 Signed-off-by:Mohammad Hasan Keramat J <ikeramat@protonmail.com> Signed-off-by:
Simão Gomes Viana <devel@superboring.dev>
Showing
- libs/renderengine/gl/GLESRenderEngine.cpp 4 additions, 1 deletionlibs/renderengine/gl/GLESRenderEngine.cpp
- libs/renderengine/gl/GLFramebuffer.cpp 6 additions, 6 deletionslibs/renderengine/gl/GLFramebuffer.cpp
- libs/renderengine/gl/GLFramebuffer.h 4 additions, 1 deletionlibs/renderengine/gl/GLFramebuffer.h
- libs/renderengine/gl/filters/BlurFilter.cpp 110 additions, 15 deletionslibs/renderengine/gl/filters/BlurFilter.cpp
- libs/renderengine/gl/filters/BlurFilter.h 14 additions, 1 deletionlibs/renderengine/gl/filters/BlurFilter.h
- libs/renderengine/gl/filters/BlurNoise.h 87 additions, 0 deletionslibs/renderengine/gl/filters/BlurNoise.h
Loading
Please register or sign in to comment