Художники нарисовали яркую графику, программисты встроили ее в игру, аниматоры добавили движения — казалось бы, все, готово. Но нет, менеджерам не нравится:

«Надо убрать серые пятна и белые линии. Тут пульсирующая кнопка дергается, там прогресс-бар лесенкой идет».

Смотришь игровые ресурсы — нет в них ничего такого, все спрайты обрезаны. Читаешь код — формулы правильные, точности шейдера хватает. Но результат все равно получился неважный. Где ошибка?



Небольшой опрос для тех, кто уже знает откуда берутся артефакты. Что делать в такой ситуации?

  1. Нужна мощная видеокарта и свежие драйверы;
  2. Стоит сделать скачиваемые наборы графики для всех возможных разрешений экрана;
  3. У квадратных текстур с размерами степени двойки нет таких проблем;
  4. Это все из-за сжатия графики (PVRTC/DXT5/ETC1/...);
  5. В графическом редакторе придется слегка размазать края;
  6. Так и должно было получиться, ведь мы не подготовили графические данные;
  7. Поможет только антиалиасинг;
  8. Нужны текстуры и таргеты в режиме premultiplied alpha.

Какой вариант ответа правильный, почему именно он и как побороть артефакты графики читайте под катом.

Билинейная фильтрация текстур


На мобильных платформах используется билинейная фильтрация текстур, чтобы спрайты могли двигаться плавно и на разных разрешениях не расплываться на пиксели. В терминах OpenGL ES это параметр GL_LINEAR:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


Пример увеличения в 4 раза (по центру GL_NEAREST, справа GL_LINEAR).


Пример уменьшения в 2 раза (по центру GL_NEAREST, справа GL_LINEAR).


В игре видны белые артефакты у веток деревьев, хотя в редакторе ничего такого нет. По описанию все как здесь

Когда текстура нарисована с масштабом отличным от 1.0 или с дробными координатами, на границе с прозрачностью возникают цветовые артефакты. Дело в том, что OpenGL ES для обычных RGBA-текстур сначала независимо интерполирует каналы R,G,B,A, и лишь потом этот результат смешивается с экраном. Если для канала с альфой (A) интерполирование выглядит естественным, то для цветовых каналов R,G,B ранее невидимые пиксели начинают влиять на видимых соседей на границе объекта.

Как одно из решений проблемы предлагается использовать текстуры с предумноженной альфой (premultiplied alpha), тогда интерполирование будет происходит без артефактов. Именно режим premultiplied alpha полезен при отрисовке графики в таргет, в частности при самостоятельной реализации сглаживания 3D-моделей средствами OpenGL ES 2.0.

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


Пример 2-х пиксельной обводки в прозрачной области (крайняя справа)

Кроме того, можно собрать графические наборы под все разрешения и рисовать спрайты только в целых координатах. На retina-экранах движение будет достаточно плавным. Только вот графики будет очень много.

Пиксельные шейдеры


Стоит лишь задуматься, как происходит рисование текстуры в нецелых координатах в единичном масштабе, как сразу возникает проблема алиасинга границы. Дело в том, что железо рассчитывает в пиксельном шейдере физические пиксели, и для нашей дробной границы происходит смещение текстурных координат s,t за пределы [0..1]. В режиме GL_CLAMP_TO_EDGE при выходе за границы текстуры происходит дублирование рамки. Для текстур, которые стыкуются, это нормально, а вот для различных спрайтов возникают утолщения.


Чтобы не рябили края спрайта достаточно сделать однопиксельную (для масштаба 1) или 2-х пиксельную (для масштаба 0.5) прозрачную рамку и рисовать ее как часть спрайта. Иными словами, команда trim в графическом редакторе не только удаляет прозрачность, но и добавляет проблему алиасинга. Поэтому правильнее будет делать trim, а вслед за ним увеличивать полотно относительно центра на 2 пикселя (обычная графика) или 4 пикселя (retina графика с возможностью downscale).




Пример алиасинга границы (сверху) и плавного движения (снизу)

Для 3D, кстати, идея overdraw тоже работает, но реализация требует предварительных расчетов и формирования дополнительных треугольников с прозрачностью.

Атласы


При помещении текстуры в атлас теряется свойство режима GL_CLAMP_TO_EDGE, ведь теперь он работает на границах всего атласа. А раз нет дублирования границ у элементов атласа, то и стыки могут сломаться.


Просветы в меде при помещении текстур в атлас

Тут на помощь приходит дублирование внешней границы стыкующихся элементов в самом атласе. Размер рамки зависит от масштаба отрисовки, обычно хватает 1-2 пикселей. При масштабе >= 1 достаточно однопиксельной рамки. Лишь в случае уменьшения требуется 2 и более пикселей.

Сжатые атласы


Чтобы игра могла работать на устройствах с небольшим размером оперативной памяти приходится сжимать атласы в один из доступных форматов: PVRTC, ETC1, DXT1, DXT5, ETC2 и т.д.

Особенность этих аппаратных форматов — разделение всей текстуры на блоки размером 4x4 и дальнейшее сжатие с потерями. Помимо блочности, для каждого блока доступно как правило не более 2 базовых цветов. В итоге все 16 пикселей блока получаются выбором из 4 возможных значений, рассчитанных каким-то образом на основе базовых цветов.

Для форматов с независимыми блоками (ETC1/2, DXT1/5) достаточно расширить каждый элемент, который помещается в обычный атлас, до прямоугольника с размером кратным 4 пикселям. В таком случае соседние элементы не будут влиять друг на друга.


Пример обособленности элементов атласа в блоках 4x4

Для формата PVRTC, где почти на каждый пиксель оказывают влияние цвета 4 смежных блоков, приходится разводить элементы атласа на расстояние в 4 пикселя. Это достигается за счет формирования 2-х пиксельной защитной области по контуру каждого элемента атласа, причем полезная область в этом случае центрируется.



Для полигональных атласов помогает соблюдение дистанции между соседними элементами.

Итоги


Рассмотренные методы объединяет специальная обработка графических данных: как отдельных текстур, так и элементов атласа. Такая обработка хорошо автоматизируется и, как правило, не отвлекает сотрудников от творческого процесса.

Поэтому в опросе наиболее общий правильный ответ — 6, «Так и должно было получиться, ведь мы не подготовили графические данные». Замечу, что это не умаляет полезности других способов.

Теперь самое время задуматься и спросить: «что умеет делать используемый сборщик ресурсов и предусмотрена ли подготовка графических данных процессами?»

P.S. Приветствуются замечания к рассмотренным методам, а также информация о других эффективных способах борьбы с артефактами в 2D-графике.

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


  1. Jedi_Knight
    24.08.2017 16:08

    Обводки иногда называются extrude. Мне сейчас приходится делать библиотеку для рантайм-атласов, потому что всё готовить заранее не всегда удобно.


    1. Andrew2016 Автор
      24.08.2017 16:22

      У нас при создании атласов (статических) препроцессинг делается самостоятельным шагом. Для динамических атласов хорошим подспорьем будут заранее подготовленные блоки. Также рекомендую решение с premultiplied alpha для несжатой графики.


  1. SBKarr
    25.08.2017 00:37
    +2

    Довольно редкий метод для устранения артефактов при рисовании текста (и подобных тексту изображений) в 2D: использовать матрицу проекции, основанную на наибольшем общем делителе и рисование по целочисленным координатам.

    const int32_t gcd = sp_gcd(size.width, size.height);
    const int32_t dw = (int32_t)size.width / gcd;
    const int32_t dh = (int32_t)size.height / gcd;
    const int32_t dwh = gcd * dw * dh;
    
    // если gcd слишком маленький, уменьшаем параметры в матрице
    // иначе вычисления будут вызывать артефакты уже другого рода
    float mod = 1.0f; 
    while (dwh * mod > 16384) {
    	mod /= 2.0f;
    }
    
    Mat4 proj;
    proj.scale(dh * mod, dw * mod, -1.0);
    
    // offset manually
    proj.m[12] = -dwh * mod / 2.0f;
    proj.m[13] = -dwh * mod / 2.0f;
    proj.m[14] = dwh * mod / 2.0f - 1;
    proj.m[15] = dwh * mod / 2.0f + 1;
    
    Идея метода в том, чтобы проецировать целочисленные координаты как можно точнее к центру пикселя (который, как известно, имеет смещение (0.5, 0.5)). Работает идеально для экранов со стандартным соотношением сторон (3:4, 9:16), работает тем лучше, чем больше GCD. Позволяет экономить на антиальясинге и получать pixel-perfect шрифты в большинстве случаев.

    А ещё имеет смысл по возможности использовать векторные исходники, которые на конечном устройстве рисуются точно по нужным параметрам (не стоит забывать, что если у iOS есть только x2 и x3, то на андроиде можно встретить и x1.5, x1.25). Недостатки: необходимость преобразовать векторные исходники в растр на устройстве пользователя, и невозможность использовать сжатие текстур. Время на создание растра может компенсироваться тем. что загружать по сети оптимизированные для пользовательского устройства ресурсы всё равно выйдет медленнее. Главный недостаток — низкое поголовье векторных художников в игрострое (да и вообще).


    1. Andrew2016 Автор
      25.08.2017 08:38

      Успешно применяем бесплатные векторные шрифты и библиотеку. Размер шрифта делаем такой, чтобы глиф получился ровно по физическим пикселям. Получаются четкие тексты.
      Когда на сцене не слишком много надписей хорошее ускорение дает кэширование глифов: растеризация символов делается один раз, а рисование готовых символов происходит достаточно быстро в каждом кадре.


      1. SBKarr
        25.08.2017 10:50

        С шрифтовыми атласами есть другая проблема: сложно анимировать размер глифов, ибо сложно вычислить все требуемые комбинации глифа и размера, и дорого перерисовывать атлас на ходу. У нас под каждую анимируемую надпись во время самой анимации используется отдельный мини-атлас, который при необходимости дополняется на ходу.


        1. Andrew2016 Автор
          25.08.2017 11:02

          Про анимации как раз есть интересный момент: мы рассчитываем финальное положение и размер надписи (prescale) и на этих данных запускаем анимацию. Таким образом при пульсации глифы не перестраиваются.


          1. BathPirate
            25.08.2017 11:28

            Отличная статья, как и другие! Вопрос у меня по аутлайнам для шрифтов — насколько заметил вы используете врисованные уже в атлас с глифами, верно? Или отдельный атлас с аутлайнами и отдельный с глифами? Или все же это шэйдерные аутлайны?


            1. Andrew2016 Автор
              25.08.2017 11:33

              На сам текст могут накладываться эффекты. По большому счету это быстрые аналоги эффектов из программ Adobe Photoshop/Flash. В движке есть опция рисовать ли эффекты отдельно (чтобы буквы читались поверх тени) или можно применять сразу для глифа. Особо сложные надписи выполнены художниками в растре. Но это уже скорее не про артефакты…


  1. vics001
    25.08.2017 01:14

    Вопрос по артефактам. Кто знает, почему все VR игры полны этих артефактов (лесенок, почти нечитаемых надписей), это как-то связано с проекцией, что в VR она не фиксирована? Или это связана с ресурсами, потому что надо поддерживать 60 FPS? Или разрешением в VR?
    Какая технология должна улучшиться, чтобы VR выглядел на уровне 2D (даже не последних лет)?


    1. Dzen1
      25.08.2017 06:09

      Мне кажется это связанно с ресурсами, «потому что надо поддерживать 60 FPS». Для VR игр необходима постоянная частота кадров, постоянная нагрузка на глаза, без смены FPS.


      1. ZimM
        25.08.2017 13:13

        Для VR — даже не 60, а 90 FPS. Еще и в очень высоком разрешении, чтобы пиксели в глаза не бросались


        1. vics001
          25.08.2017 13:26

          Sony симулирует 120 FPS, но игра рисует только 60 FPS. Там рисуется чуть больше картинка, чем видна, и 1/120 движения просто пересчитывает вычислительный блок.


          1. ZimM
            25.08.2017 13:46

            Насколько я помню, у Sony минимум 60 FPS, но рекомендуется все же 90 FPS. Oculus и Vive так точно требуют 90 FPS.


            1. vics001
              25.08.2017 14:30

              Да похоже, это выбирается разработчиком, использовать reprojection или нет Reddit Sony reprojection algorithm.