Как баг в моей игре помог мне лучше понять работу GPU

Я недавно выпустил обновление для Blackshift, добавив в игру новые типы песчаных тайлов:

Всё работало исправно, пока пользователи не начали присылать отчёты о странных графических артефактах:

Пришлось отложить вечерние планы и заняться отладкой.

В основе всех песчаных блоков лежит единая модель — сегментированная плоскость. Вершинный шейдер деформирует сетку, создавая рельеф, а фрагментный шейдер отрисовывает затенение на стыках между соседними элементами1.

Тайл песка до того, как за него взялся вершинный шейдер2
Песчаный тайл до применения вершинного шейдера2

Чтобы корректно накладывать тени, фрагментный шейдер анализирует карту соседства. Это 8-битное целое число (integer), где каждый бит соответствует одному из направлений.

В случае этого тайла шейдер получает значение 238 и отрисовывает тени вдоль южного ребра и северо-восточного угла.
При значении 238 шейдер активирует тени вдоль южной грани и северо-восточного угла.

Как и остальные элементы игры, тайлы рендерятся через GPU-инстансинг: вся группа объектов рисуется за один вызов. GPU получает массив трансформаций для каждого экземпляра, используя одну и ту же базовую модель. Данные о соседстве передаются вместе с матрицей трансформации3.

Так как bgfx поддерживает для данных инстансов только float, я преобразовываю 8-битный integer в число с плавающей запятой перед отправкой в буфер.

Вершинный шейдер считывает этот float и передает его фрагментному, который конвертирует значение обратно в integer, чтобы выделить необходимые биты. Погрешности float для диапазона 0–255 обычно не критичны, поэтому я был уверен в надежности этого метода: если CPU пишет 238.0f, шейдер должен получить те же 238.0f4.

Но где же тогда прятался баг?

* * *

Первым делом я заподозрил Z-конфликты (z-fighting), но это было лишено смысла: поверхность едина. Отключение Z-буферизации лишь подтвердило мои подозрения — проблема была в другом.

Затем я обратил внимание на предпросмотр уровней. В GUI Blackshift миниатюры рендерятся иначе, и, как выяснилось, на них артефактов не было. Это была важная зацепка.

Главное различие в том, что для миниатюр инстансинг отключен. Однако тщательная проверка кода инстансинга показала, что это ложный след — даже при его отключении ошибка сохранялась.

Вернувшись к работе с float, я осознал: когда значение передается из вершинного шейдера во фрагментный, GPU выполняет интерполяцию. Поскольку все вершины треугольника имеют одинаковый «соседский» float, я полагал, что результат будет стабильным. И на моем железе он был таковым.

Проблема кроется в перспективной коррекции интерполяции5. На некоторых GPU вычисления с плавающей запятой приводят к микроскопическим отклонениям. В итоге значение 238.0f может превратиться в 237.999…, что при обратной конвертации в integer дает 237 вместо 238, полностью меняя паттерн теней.

Решение оказалось простым: при записи в буфер я добавил смещение:

(float)adjacency + 0.5f

Это нивелирует погрешности, гарантируя попадание в нужный диапазон целого числа6.

Почему же проблема не возникала на миниатюрах? Ответ прост.

Ответ

Миниатюры рендерятся с использованием ортогональной проекции, которая не требует перспективной коррекции интерполяции.

Заключение

Важный урок: в графическом конвейере передача одинаковых значений в вершины не гарантирует идентичности данных для каждого фрагмента треугольника. Особенности работы GPU с перспективной проекцией могут вносить непредсказуемые искажения на разном оборудовании.

Сноски

1. Это своего рода «псевдо-AO». По сути, вся компьютерная графика — это набор компромиссов и иллюзий.

2. Четыре полосы по краям отвечают за стыковку с соседними тайлами. Если сосед отсутствует, шейдер убирает их из кадра.

3. Хранение соседства в текстуре было бы альтернативой, но это усложнило бы архитектуру при одновременной отрисовке нескольких уровней в списке.

4. Использование float обусловлено ограничениями API bgfx и поддержкой старого оборудования.

5. Подробнее об этом можно почитать здесь.

6. Я не мог воспользоваться квалификатором flat, так как вынужден поддерживать устаревшие версии OpenGL.

 

Источник

Читайте также