Всем привет! Меня зовут Григорий Дядиченко, и я разрабатываю разные проекты на заказ. Сегодня хотелось бы поговорить про дитеринг и бандинг — две стороны одной медали в мире компьютерной графики.

Сталкивались ли вы с ситуацией, когда вроде бы качественная графика в Unity портится противными полосами в тенях или на небе? Эти артефакты — и есть тот самый бандинг, который может испортить впечатление даже от самого продуманного проекта. Сейчас, когда индустрия стремится к фотореализму и бесшовному погружению в VR/AR, подобные мелочи становятся особенно критичными.

В этой статье мы не просто разберемся, откуда берутся эти полосы, а на практике посмотрим, как технология дитеринга в Universal Render Pipeline (URP) помогает их эффективно маскировать. Если вы хотите поднять визуальное качество своего проекта на новый уровень — добро пожаловать под кат!

Что такое бандинг?

картинка из моей статьи про градиенты https://habr.com/ru/articles/537256/
картинка из моей статьи про градиенты https://habr.com/ru/articles/537256/

Бандинг (от англ. banding — образование полос) — это артефакт в компьютерной графике, который проявляется в виде видимых ступенчатых полос или градиентов на участках, которые должны быть гладкими и непрерывными.

Представьте, что вам нужно нарисовать переход от черного цвета к белому, но у вас есть только 5 оттенков серого. Вы не сможете сделать это плавно — переход будет состоять из 5 четких полос. Это и есть бандинг.

Проблема возникает из-за ограниченной глубины цвета (битности). Когда для хранения цвета пикселя выделяется недостаточно бит информации (чаще всего 8 бит на канал, что дает 256 оттенков на цвет), графическому процессу не хватает промежуточных значений, чтобы точно отобразить плавный переход. В результате близкие, но разные оттенки "склеиваются" в один, и мы видим резкий скачок — полосу.

Часто в Unity это происходит в следующих случаях:

  • Градиентное небо (Skybox): Вместо плавного перехода цвета на небосводе видны четкие разноцветные полосы. Это происходит потому, что финальный рендеринг и тональная компрессия «сжимают» цветовую информацию, обнажая ограниченную глубину цвета.

  • Тени (Shadows): В области мягких полутеней (penumbra) вместо плавного затухания появляются концентрические круги или ступенчатые полосы. Причина — в ограниченной точности (битности) карт теней, которой недостаточно для плавных расчетов.

  • Видео и UI (ARGB32): При воспроизведении видео с плавными градиентами или в интерфейсах могут быть видны полосы. Формат в RenderTexture ARGB32 (8 бит на канал) физически не может отобразить достаточно оттенков для абсолютно гладкого перехода.

  • Артефакты компрессии текстур: Сильное сжатие текстур (например, в форматах ASTC или ETC2) может «сломать» плавные градиенты, заменив их на блочные участки однородного цвета, что визуально усиливает эффект бандинга.

Например Safari IOS в WebGL билдах не поддерживает рендер текстуры в форматах с плавающей точкой и высокой битности, поэтому там часто можно столкнуться с этой проблемой.

Что такое дитеринг?

Дитеринг (или размытие ступеней, от англ. dithering) — это техника, используемая в компьютерной графике для обмана человеческого зрения. Представьте, что у вас есть только черная и белая краски, но нужно нарисовать серый цвет. Если вы смешаете их в одну сплошную грязную массу — получится некрасиво. Но если вы нарисуете множество мелких черно-белых точек в шахматном порядке, то с расстояния наш глаз сам смешает их и увидит серый цвет.

Дитеринг делает то же самое: он добавляет в области плавных переходов случайный шум или упорядоченный узор из пикселей разных цветов. Этот шум "разбивает" четкие границы между полосами при бандинге, заставляя наш мозг воспринимать изображение как более плавное и цельное.

Как это работает технически в контексте бандинга?

  1. Есть проблема: У вас есть градиент с бандингом — 3 крупные полосы (например, темно-серый, серый, светло-серый).

  2. Применяется дитеринг: Графический процессор добавляет к каждому пикселю небольшую случайную погрешность (шум). Пиксели на границе полос начинают "перемешиваться".

  3. Результат для мозга: Вместо четкой линии глаз видит шумную, зернистую границу. Мозг интерпретирует эту область не как резкий скачок, а как более плавный переход, потому что шум маскирует идеальную геометрию полос.

Какие алгоритмы дитеринга существуют?

Алгоритмы дитеринга делятся на две большие группы: упорядоченные (Ordered Dithering) и диффузионные (Error-Diffusion Dithering). В реальном времени (игры, Unity) почти всегда используется первая группа из-за своей производительности.

Упорядоченный дитеринг (Ordered Dithering)

Принцип: Используется заранее заданная матрица (паттерн) для определения порога, с которым сравнивается яркость пикселя. Это быстро, детерминировано и не зависит от соседних пикселей, что идеально для параллельных вычислений в шейдерах.

Основные алгоритмы:

a) Dither Patterns (Байеровский дитеринг / Bayer Matrix)

Самый популярный метод в реальном времени.

  • Как работает: Используется матрица NxN (чаще всего 2x2, 4x4, 8x8), которая содержит пороговые значения. Эта матрица тайлится (повторяется) по всему экрану. Текущий пиксель сравнивается с значением в матрице, и на основе этого решается, округлять его яркость вверх или вниз.

  • Паттерны: Чем больше матрица (например, 8x8), тем более качественный и менее заметный узор она создает.

  • Где используется: Практически во всех современных играх и движках. Именно этот метод встроен в шейдеры для борьбы с бандингом, для alpha-clipping (траву, листья) и для дешевой симуляции прозрачности.

Например: Нода Dither в Shader Graph в Unity по умолчанию использует упорядоченный Bayer Matrix (8x8).

b) Blue Noise Dithering (Синий шум)

 картинка для примера взята из https://github.com/Calinou/free-blue-noise-textures
 картинка для примера взята из https://github.com/Calinou/free-blue-noise-textures

Более современный и качественный метод.

  • Как работает: Используется текстура, содержащая "синий шум" — особый тип шума, где спектральная энергия сосредоточена на высоких частотах, а низкочастотные компоненты минимальны. Это делает паттерн дитеринга менее заметным и более приятным глазу, чем байеровская сетка.

  • Преимущество: Узор выглядит как однородное, мелкое "зерно", а не как повторяющаяся структура. Он лучше маскирует артефакты.

  • Недостаток: Требует заранее сгенерированной текстусы (предрасчет), а не простой матрицы.

  • Где используется: В продвинутых рендерерах и в тех случаях, когда качество визуала критически важно. Становится все популярнее в играх.

Диффузионный дитеринг (Error-Diffusion Dithering)

Принцип: При квантовании пикселя (округлении его цвета) возникает ошибка. Эта ошибка "распределяется" (диффундируется) на соседние, еще не обработанные пиксели. Это дает более высокое качество, но является последовательным алгоритмом.

Основные алгоритмы:

a) Floyd–Steinberg Dithering (Флойда-Стейнберга)

Самый известный алгоритм этой группы.

  • Как работает: Ошибка от текущего пикселя распределяется на 4 соседних пикселя с определенными весами (7/16, 3/16, 5/16, 1/16).

  • Преимущество: Дает очень качественный результат, похожий на газетную печать.

  • Недостаток: Алгоритм последовательный, его нельзя эффективно распараллелить для шейдеров, так как обработка каждого следующего пикселя зависит от предыдущего.

  • Где используется: В основном в статичной обработке изображений (Photoshop, при печати, конвертации цветов). В реальном времени не используется из-за неприемлемой производительности.

b) Другие алгоритмы Error-Diffusion

  • Jarvis-Judice-Ninke: Распределяет ошибку на большее количество пикселей, результат еще качественнее, но медленнее.

  • Sierra, Atkinson: Вариации с разными матрицами распределения ошибки.

Примеры и советы для URP

Я больше предпочитаю сейчас собирать шейдер графы для подобного из-за удобства редактирования, но в текст их оформлять неудобно, поэтому приведу пару примеров шейдеров кодом. Для использования матрицы Байера.

Шейдер с матрицей Байера

Shader "Custom/DitherAntiBanding"
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
        _DitherIntensity ("Dither Intensity", Range(0, 0.02)) = 0.005
        _DitherScale ("Dither Scale", Float) = 1.0
    }
    
    SubShader
    {
        Tags 
        { 
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        
        Pass
        {
            Name "DitherPass"
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 screenPos : TEXCOORD1;
            };
            
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            
            CBUFFER_START(UnityPerMaterial)
                float _DitherIntensity;
                float _DitherScale;
            CBUFFER_END
            
            // Та же матрица Байера, что и выше
            static const float BayerMatrix8x8[64] = {
                0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0,
                48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0,
                12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0,
                60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0,
                3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0,
                51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0,
                15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0,
                63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0
            };
            
            float bayer_dither(float2 position, float scale)
            {
                int2 coord = int2(position.x * scale) % 8;
                int index = coord.x + coord.y * 8;
                return BayerMatrix8x8[index];
            }
            
            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = IN.uv;
                OUT.screenPos = ComputeScreenPos(OUT.positionHCS);
                return OUT;
            }
            
            half4 frag(Varyings IN) : SV_Target
            {
                // Исходный цвет
                float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
                
                // Получаем экранные координаты
                float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
                float2 pixelPos = screenUV * _ScreenParams.xy;
                
                // Вычисляем дитеринг и преобразуем в -0.5...0.5
                float dither = bayer_dither(pixelPos, _DitherScale);
                float ditherValue = (dither - 0.5) * _DitherIntensity;
                
                // Добавляем дитеринг и ограничиваем диапазон
                color.rgb += ditherValue;
                color.rgb = saturate(color.rgb);
                
                return color;
            }
            ENDHLSL
        }
    }
}

Подходит для постпроцессинговой обработки кадра.

И не забывайте главное. Если пользуетесь шумами для чего либо, включая тот же VFX, выставляйте им FilterMode: Point. Тогда они будут накладываться корректно, а иначе на них будет влиять билинейная фильтрация текстур выставленная в Unity на текстурах по умолчанию.

Заключение

Я постарался рассказать про дитеринг и бандинг в Unity URP — что это за проблемы, почему они возникают и как их эффективно решать. Мы разобрали разные алгоритмы дитеринга, от классического Байера до модного синего шума, и написали готовые шейдеры для реального использования в проектах. Если вам понравилось и было интересно — ставьте плюсы.

Подписывайтесь на мой блог в телеграм, если вам интересна Unity разработка — там я делюсь не только графическими фишками, но и практическим опытом разработки разных проектов. В современном мире чаще всего основное знать терминологию. Я встречал многих разработчиков, которые сталкивались с бандингом, но не знали что он так называется. И с визуальными артефактами часто нужно просто знать как он называется, чтобы без проблем найти решение своей проблемы.

Комментарии (1)


  1. Jijiki
    16.11.2025 11:54

    в тенях не только это еще может быть, если не ограничить тень то полоски могут появится