В этой статье мы расскажем об использовании чисел с плавающей точкой при отладке шейдеров для мобильных устройств на ПК. Мы уверены, что опыт краснодарской студии Plarium будет полезен для вас вне зависимости от того, какой движок вы используете.
Как мы выявили проблему
В процессе создания эффектов мы постоянно сталкивались с тем, что некоторые из них со временем рассыпаются на пиксели.
Мы исследовали эту проблему и определили причины ее возникновения: оказалось, далеко не все гаджеты подвержены артефактам. В основном это касается старых мобильных устройств или моделей со слабыми характеристиками. Причем некоторые с виду одинаковые эффекты то работали как следует, то теряли плавность движений и приобретали пикселизацию.
В ходе анализа технической документации видеопроцессоров устройств, находящихся в группе риска, мы выяснили, что не все гаджеты имеют полную поддержку 32-битных чисел с плавающей точкой. Именно это вызывает проблемы с артефактами и изображением.
А как там в двоичной системе?
Вспомним, как в двоичной системе выглядят числа с плавающей точкой.
Можно представить такое число как контейнер для хранения фиксированного количества значащих цифр и запятой в любом месте этого числа. Очевидно, что большие числа имеют меньшую точность.
Например, у нас есть значащее число — 123456789. Если поставим запятую после первого знака (1,23456789), то сможем изменять это число с точностью до 0,00000001. Но если запятую поставить перед последним знаком (12345678,9), то точность будет уже 0,1.
Если мы используем 32-битные числа, то любое такое число точно передает 7 знаков. Это позволяет приложению работать около 70 часов без видимых проблем. Речь идет о времени, когда пользователь видит именно запущенную игру, а не на паузе или в свернутом состоянии. Достаточно большой запас, не правда ли?
Но дело в том, что многие мобильные устройства поддерживают только 16-битные числа с плавающей точкой в пиксельном (фрагментном) шейдере. В таких числах всего 4 точных знака, что приводит к заметной потере точности при анимации текстур уже через 10–15 минут.
В поисках решения:
- Мы разделили причины потери точности на три группы:
- При передаче — значение конвертируется из 32 бит в 16 бит.
- При вычислении — попытка операций с числами разного порядка.
- При переполнении — любое число с плавающей точкой имеет свое максимальное и минимальное значение.
Решить проблему на каждом конкретном устройстве нереально, поэтому мы сосредоточились на том, чтобы визуализировать ошибку на ПК при создании эффекта и заранее принять меры по ее устранению.
На ПК нет возможности напрямую эмулировать режим работы с числами 16 бит. Драйвер видеокарты автоматически преобразует все 16-битные инструкции в 32 бита, и визуально отследить потерю точности можно только спустя продолжительное время. Поэтому для визуализации артефактов от потери точности мы написали собственный менеджер времени и расширения для шейдеров.
Менеджер времени
Наше «отладочное» время стартует с опережением на несколько часов. Таким образом художник сразу может увидеть проблемы с эффектом. Также менеджер автоматически сбрасывает время каждый раз, когда приложение находится на паузе или свернуто. Именно в этот момент скачок эффектов наименее заметен игроку.
С появлением в новой версии Unity системы управляемого процесса рендеринга кадра (Scriptable Rendering Pipeline) мы смогли отказаться от отдельного скрипта управления временем и делать все непосредственно при формировании кадра.
Расширения для шейдеров
Идея состояла в том, чтобы перегружать 32-битное число с плавающей точкой так же, как перегружается 16-битное. Нужно было сдвинуть значащую часть на некоторое число бит. Но битовые операции недоступны для Shader Model 2.0, и вдобавок использование более высокой модели шейдеров приводит к тому, что движок перестает предупреждать художников по эффектам о слишком сложных вычислениях для мобильных устройств и бюджет эффекта превышается.
Мы решили сделать максимально точный имитирующий алгоритм, который позволит получить необходимый результат.
- Добавляем некоторое большое число к исходному. Само это большое число определяется количеством знаков основного числа.
- Вычитаем его. Нужно обмануть компилятор, который автоматически сократит операции. Для этого мы вносим незначительную погрешность в число после увеличения.
Таким образом, немного изменяя шейдер, мы получаем визуально идентичное изображение для слабых устройств. Это позволяет нашим художникам контролировать процесс разработки эффектов и сокращает количество ошибок в работе.
Подсказка: frac в помощь
В работе над эффектами стремитесь к тому, чтобы все числа, ответственные за плавность эффекта и требующие точности, находились в диапазоне от 0 до 1. Используйте для этого операцию отсечения целой части — frac(x).
По сути, frac — это приведение числа с плавающей точкой к целому с последующим вычитанием этого целого числа из первоначального. То есть frac = x – floor(x).
Поскольку в этой операции используется изначальное число в исходном виде, потеря точности может происходить и на этапе передачи этого числа внутри шейдера. Поэтому мы рекомендуем использовать время, которое не увеличивается до больших значений. Хорошей практикой будет принудительное обнуление времени в моменты загрузки или в моменты, когда интерфейс игры перекрывает игровой уровень.
Проделанная работа позволила значительно сократить количество визуальных ошибок. Наши художники получили возможность контролировать процесс создания эффектов без длительной и сложной проверки на мобильных устройствах.
Комментарии (10)
Tutanhomon
05.04.2018 17:16+3Нужно обмануть компилятор, который автоматически сократит операции. Для этого мы вносим незначительную погрешность в число после увеличения.
Пример кода не помешал бы, пожалуй. Ибо очень долго думал над тем, что вы имеете в виду, прежде чем понял.Plarium Автор
06.04.2018 11:03Пример кода сильно зависит от текущей платформы движка и компилятора. Но общий подход такой:
например, пусть Х — наше число, а Y — достаточно большое число для перегрузки.
Если написать: X = (X+Y)-Y, то компилятор сократит Y и получится X=X.
А если сделать: X = (X+Y)*0.00001 — Y, то все будет хорошо, но мы добавили погрешность.
BasmanovDaniil
05.04.2018 18:18+1А примеры кода где?
Plarium Автор
06.04.2018 16:32Пример кода приведен в ответе выше.
Пусть Х — наше число, а Y — достаточно большое число для перегрузки.
Если написать: X = (X+Y)-Y, то компилятор сократит Y и получится X=X.
А если сделать: X = (X+Y)*0.00001 — Y, то все будет хорошо, но мы добавили погрешность.
iOrange
05.04.2018 20:21+2Я так понял что проблема была в том что текстурные координаты «уползали» до больших порядков и больше не влазили в разрядность?
Тогда править нужно на стороне хоста, особенно если вы так жестко лимитируете бюджет. Чем делать frac в шейдере уж лучше «обрезать» оффсет на хосте и засылать в шейдер нормализованное смещение.
В таком случае вы, в принципе, не ограничены точностью и в основном коде хоть double используйте.Plarium Автор
06.04.2018 11:07Именно так мы и делаем. Мы и обрезаем время на хосте и также имеем возможность проверить, помещаются ли наши числа после операций в самом шейдере в разрядность 16 бит.
iOrange
06.04.2018 17:55Лучше «обрезать» не время, а уже конечный параметр материала.
К примеру у вас есть некий «float2 uvOffset» — вот он и должен «обрезаться» на хосте, заодно и ALU сбережете.
Но мне осталось непонятно одно — вы в заголовке поставили вопрос «Возможна ли отладка шейдеров для мобильных устройств на ПК?» и так на него и не ответили.
N1Kav
06.04.2018 17:24Вспомним, как в двоичной системе выглядят числа с плавающей точкой.
Небольшая опечатка на картинке. «7 цифр. Значение. 32 бита». Должно быть 23 бита.
Извиняюсь за занудство)
А вообще статья полезная, спасибо.
Tutanhomon
Да, сталкивался с таким. Но решал через '%'. И тут забавная штука. Два шейдера в одном проекте. В одном процент работает, в другом — ругается. Не помню деталей, но помню что так и не допер, в чем была разница.