В этой статье мы расскажем об использовании чисел с плавающей точкой при отладке шейдеров для мобильных устройств на ПК. Мы уверены, что опыт краснодарской студии Plarium будет полезен для вас вне зависимости от того, какой движок вы используете.
![image](https://habrastorage.org/webt/ib/ip/ih/ibipihbkvckj6hlptnpwhgh_a8w.jpeg)
Как мы выявили проблему
В процессе создания эффектов мы постоянно сталкивались с тем, что некоторые из них со временем рассыпаются на пиксели.
![image](https://habrastorage.org/webt/fj/6a/eo/fj6aeo26ntcnl68y99tzonvmt0g.jpeg)
Мы исследовали эту проблему и определили причины ее возникновения: оказалось, далеко не все гаджеты подвержены артефактам. В основном это касается старых мобильных устройств или моделей со слабыми характеристиками. Причем некоторые с виду одинаковые эффекты то работали как следует, то теряли плавность движений и приобретали пикселизацию.
В ходе анализа технической документации видеопроцессоров устройств, находящихся в группе риска, мы выяснили, что не все гаджеты имеют полную поддержку 32-битных чисел с плавающей точкой. Именно это вызывает проблемы с артефактами и изображением.
А как там в двоичной системе?
Вспомним, как в двоичной системе выглядят числа с плавающей точкой.
![image](https://habrastorage.org/webt/il/qs/r0/ilqsr0pyk1jxbk4nvtmsxkgyhdu.jpeg)
Можно представить такое число как контейнер для хранения фиксированного количества значащих цифр и запятой в любом месте этого числа. Очевидно, что большие числа имеют меньшую точность.
Например, у нас есть значащее число — 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
Да, сталкивался с таким. Но решал через '%'. И тут забавная штука. Два шейдера в одном проекте. В одном процент работает, в другом — ругается. Не помню деталей, но помню что так и не допер, в чем была разница.