иерархическая окклюзия z буфера что это satisfactory
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 3 из 6
Улучшение кода
Official translation (with a bit of polishing) is available here.
А что потом? Я разобрал весь материал!
В статьях 7 и 8 мы поговорим о программировании непосредственно под OpenGL. Есть ненулевая вероятность получить краткий курс OpenCL/CUDA в статьях 9+.
Удаление невидимых поверхностей
Знакомьтесь, это мой друг z-buffer головы абстрактного африканца. Он нам поможет убрать визуальные артефакты отбрасывания задних граней, которые у нас оставались в прошлой статье.
Кстати, не могу не упомянуть, что эта модель, которую я использую в хвост и в гриву, была любезно предоставлена замечательным Vidar Rapp.
Мы её можем использовать исключительно в рамках обучения рендерингу. Это очень качественная модель, с которой я варварски обошёлся, но я обещаю вернуть ей глаза!
В теории можно не отбрасывать невидимые грани, а просто рисовать всё подряд, начав с самых задних, и заканчивая передними.
Это называется алгоритмом художника. К сожалению, он весьма затратен, на каждое изменение положения камеры нужно пересортировывать сцену. А бывают ещё и динамические сцены… Но даже не это основная проблема. Проблема в том, что не всегда это можно сделать.
Перед отрисовкой головы отрисуем что попроще
Давайте представим себе простейшую сцену из трёх треугольников, камера смотрит сверху вниз, мы проецируем наши треугольники на белый экран:
Вот так должен выглядеть рендер этой сцены:
Синяя грань — она за красной или перед? Ни то, ни то. Алгоритм художника здесь ломается. Ну, то есть, можно синюю грань разбить на две, одна часть перед красной, другая за. А та, что перед красной, ещё на две — перед зелёной и за зелёной… Думаю, достаточно ясно, что в сценах с миллионами треугольников это быстро становится непростой задачей. Да, у неё есть решения, например, пользоваться двоичными разбиениями пространства, заодно это помогает и для сортировки при смене положения камеры, но давайте не будем себе усложнять жизнь!
Три измерения — это слишком. Y-buffer!
Давайте потеряем одно из измерений, рассмотрим двумерную сцену, полученную пересечением нашей сцены и жёлтой плоскости разреза:
То есть, наша сцена состоит из трёх отрезков (пересечение жёлтой плоскости и каждого из треугольников), а её рендер — это картинка
той же ширины, что и нормальный рендер, но в один пиксель высотой:
Снимок кода, как обычно, на гитхабе. Поскольку у нас сцена двумерная, то её очень просто нарисовать, это просто три вызова функции line(), которую мы запрограммировали в самый первый раз.
Вот так выглядит наша двумерная сцена, наша задача посмотреть на эти отрезки сверху.
Давайте теперь её рендерить. Напоминаю, рендер — это картинка шириной во всю сцену и высотой в один пиксель. В моём коде я её объявил высотой в 16, но это чтобы не ломать глаза, рассматривая один пиксель на экранах высокого разрешения. Функция rasterize пишет только в первую строчку картинки render.
Итак, я объявил загадочный массив ybuffer ровно в размер нашего экрана (width, 1). Этот массив инициализирован минус бесконечностью. Затем я передаю в функцию rasterize и картинку render, и этот загадочный массив. Как выглядит сама функция?
Очень-очень просто: я прохожу по всем x-координатам между p0.x и p1.x и вычисляю соответствующую y-координату нашей линии.
Затем я проверяю, что у нас в массиве ybuffer по этой координате x. Если текущий пиксель ближе к камере, нежели то, что там сохранено,
то я и его рисую в картинке, и ставлю новую y-координату в игрек-буфере.
Давайте разбираться поэтапно: после вызова растеризатора для первой (красной) линии вот что мы имеем в памяти:
содержимое экрана:
содержимое y-буфера:
Здесь мерзким фиолетовым цветом отмечена минус бесконечность, это те места, где ни одного пикселя ещё нарисовано не было.
Всё остальное градациями серого, т.к. ybuffer это не цвет, а глубина данного пикселя. Чем белее, тем ближе к камере был данный нарисованный на экране пиксель.
Дальше мы рисуем зелёную линию, вот память после вызова её растеризатора:
Ну и напоследок синюю:
Поздравляю вас, мы нарисовали нашу двумерную сцену! Ещё раз полюбуемся на финальный рендер:
Три измерения — это в самый раз. Z-buffer!
Снимок кода на гитхабе.
Внимание: в этой статье я использую ту же самую версию растеризатора треугольника, что и в предыдущей. Улучшенная версия растеризатора (проход всех пикселей описывающего прямоугольника) будет вскорости любезно предоставлена и описана в отдельной статье уважаемым gbg! Stay tuned.
Поскольку у нас экран теперь двумерный, то z-буфер тоже должен быть двумерным:
Я упаковал двумерный массив в одномерный, конвертировать можно как обычно:
из двух координат в одну:
Затем в коде я прохожу по всем треугольникам и делаю вызов растеризатора, передавая ему и картинку, и z-буфер.
Это просто ужасно, насколько код похож на растеризатор из прошлой статьи. Что изменилось? (Используйте vimdiff и посмотрите).
Vec2 был заменён на Vec3 в вызове функции и сделана проверка if (zbuffer[idx]
Всё! Вот наш настоящий рендер без огрехов отсечения невидимых поверхностей:
Обратите внимание, что backface culling в моём коде оставлен:
Он не является необходимым для получения этой картинки, это только ускорение вычислений.
Стоп, мы только что интерполировали z-координату. А можно добавить ещё чего в нагрузку?
Текстуры! Это будет домашняя работа.
Вот пример того, что должно получиться:
Настройки
Это незавершённая статья Она содержит неполную информацию Вы можете помочь Satisfactory вики, дополнив её. |
Пользовательские настройки доступны в основном меню ( Esc ).
Почти все пункты понятно переведены и снабжены всплывающими подсказками, так что не возникает сомнений в их назначении.
Игровой процесс
Переключение языка интерфейса. Поддерживается 26 языков. Голосовые сообщения АНА остаются на английском, но субтитры переводятся.
Качество сети
Определяет максимальную полосу пропускания сети, которую игра может использовать для игры в режиме мультиплеера.
Покачивание головой
Регулирует степень покачивания головы пионера при ходьбе.
Сила оптического эффекта при движении камеры (%)
Интервал автосохранений в минутах
Задаёт частоту автоматических сохранений. Задайте нулевое значение, если хотите отключить автосохранения. Количество слотов для автоматического сохранения можно изменить только в конфигурационных файлах.
Уведомления об автосохранении
Включает уведомление с таймером обратного отсчёта за 10 секунд до начала автоматического сохранения.
Отправлять данные об игре
Разрешает отправку внутриигровой диагностики в Coffee Stain Studios.
Режим арахнофобии
Уведомления о перерыве
Должно выдавать предупреждение после двух часов непрерывной игры. Это функциональность перестала работать после патча 0.3.3.0
Ставить опоры при отпускании кнопки мыши
Если стоит галочка, то для установки опор будет необходимо сначала зажать кнопку основного действия (), после чего можно будет настроить высоту, угол поворота или наклона. Для окончательного построения будет необходимо отпустить кнопку.
Автоматическое соблюдение отступов при строительстве
При попытке построить ноый объект слишком близко к уже существующим, новый объект будет немногшо перемещён, чтобы соблюсти минимальные отступы от уже существующих конструкций.
Выполнять наклон опоры по оси Х
Если стоит галочка вам понадобится удерживать клавишу привязки ( Ctrl ), чтобы вращать опору перемещением мыши.
Детальное управление уровнями громкости.
Иерархический буфер глубин
Краткий обзор
Иерархический буфер глубин — это многоуровневый буфер глуби (Z-буфер), используемый как ускоряющая структура (acceleration structure) для запросов глубин. Как и в случае mip-цепочек текстур, размеры каждого уровня обычно являются результатами деления на степени двойки размеров буфера полного разрешения. В этой статье я расскажу о двух способах генерации иерархического буфера глубин из буфера полного разрешения.
Сначала я покажу, как генерировать полную mip-цепочку для буфера глубин, сохраняющую точность запросов глубин в пространстве координат текстуры (или NDC) даже для размеров буферов глубин, не равных степеням двойки. (В Интернете мне встречались примеры кода, не гарантирующие этой точности, что усложняет выполнение точных запросов на высоких mip-уровнях.)
Затем для случаев, в которых требуется только один уровень даунсэмплинга, я продемонстрирую, как сгенерировать этот уровень при помощи одного вызова вычислительного (compute) шейдера, использующего атомарные операции в общей памяти рабочей группы. Для моего приложения, где требуется только разрешение 1/16 x 1/16 (mip-уровень 4), способ с вычислительным шейдером в 2-3 раза быстрее, чем обычный подход с даунсэмплингом mip-цепочки в несколько проходов.
Введение
Иерархические глубины (также называемые Hi-Z) — это часто используемая в 3D-графике техника. Она используется для ускорения усечения невидимой геометрии (occlusion culling) (в CPU, а также в GPU), вычислений отражений в экранном пространстве, объёмного тумана и многого другого.
Кроме того, в GPU Hi-Z часто реализуется как часть конвейера растеризации. Быстрые операции поиска Hi-Z в кэшах на чипе позволяют целиком пропускать тайлы фрагментов, если они полностью закрыты ранее отрендеренными примитивами.
Базовая идея Hi-Z заключается в ускорении операций запросов глубин благодаря считыванию из буферов меньшего разрешения. Это быстрее, чем считывание из буфера глубин полного разрешения, по двум причинам:
Буферы Hi-Z чаще всего запрашиваются почти сразу на выходе, чтобы избежать дальнейшей обработки и выполнения более точных операций поиска в буфере полного разрешения. Например, если мы храним значения max для неинвертированного буфера глубин (в которых чем больше значение глубины, тем дальше находится объект), то можем быстро совершенно точно определить, перекрыта ли конкретная позиция в экранном пространстве буфером глубин (если её координата Z > значения (max), сохранённого в какой-то более высоком уровне (то есть с пониженным разрешением) буфера Hi-Z).
Благодаря этому тест глубин для видимых частиц в общем случае становится малозатратной операцией. (Для перекрытых геометрией частиц он более затратен, но это нас устраивает, потому что это не вызывает затрат на рендеринг, поэтому частицы по-прежнему требуют мало вычислений.)
Благодаря тому, что поиск сначала выполняется в буфере глубин пониженного разрешения (как сказано выше), время рендеринга частиц снижается максимум на 35% по сравнению со случаем, когда поиск выполняется только в буфере полного разрешения. Поэтому для моего приложения Hi-Z очень выгоден.
Теперь мы рассмотрим две техники генерирования иерархического буфера глубин.
Техника 1: генерация полной Mip-цепочки
Во многих областях использования Hi-Z требуется создание полной mip-цепочки буфера глубин. Например, при выполнении occlusion culling с помощью Hi-Z ограничивающий объём проецируется в экранное пространство и спроецированный размер используется для выбора подходящего mip-уровня (чтобы в каждой проверке перекрытия участвовало фиксированное количество текселов).
Однако в случае буферов глубин, размеры которых не соответствуют степеням двойки, всё сложнее. Так как Hi-Z для буферов глубин часто строится из стандартных разрешений экрана (которые редко являются степенями двойки), нам нужно найти надёжное решение этой задачи.
Итак, что же конкретно будет означать получаемое нами значение отдельного тексела на mip-уровне N? Это должно быть минимальное значение ( min ) из всех текселов полноэкранного буфера глубин, занимающего то же пространство в (нормализованном) пространстве координат текстуры.
Пример чётных размеров уровней: 6 текселов на этом уровне уменьшаются до 3 на более высоком уровне. Размеры координат текстур каждого из трёх текселов высокого уровня точно накладываются на каждые два тексела нижнего уровня. (Точки — это центры текселов, а квадраты — это размеры координаты текстуры при использовании фильтрации ближайших соседей.)
В случае нечётных размеров уровней (а у буферов полного разрешения, размер которых не является степенью двойки, будет хотя бы один уровень с нечётным размером) всё становится сложнее. Для уровня N-1 нечётного размера размер следующего уровня (N) будет равен
, то есть
.
Это означает, что теперь у нас нет чёткого сопоставления «2 к 1» текселов уровня N-1 к текселам уровня N. Теперь размер координаты текстуры каждого тексела на уровне N накладывается на размер 3 текселов на уровне N-1.
Пример нечётных размеров уровня: 7 текселов этого уровня уменьшаются до 3 текселов на следующем уровне. Размеры координаты текстуры трёх текселов высокого уровня накладываются на размеры трёх текселов из нижнего уровня.
Описание выше для простоты было представлено всего в одном измерении. В двух измерениях, если обе размерности уровня N-1 являются чётными, то область текселов 2×2 на уровне N-1 сопоставляется с одним текселом на уровне N. Если одна из размерностей нечётна, то область 2×3 или 3×2 на уровне N-1 сопоставляется с одним текселом на уровне N. Если нечётны обе размерности, то следует также учитывать «угловой» тексел, то есть область 3×3 на уровне N-1 сопоставляется с одним текселом на уровне N.
Пример кода
Показанный ниже код шейдера на GLSL реализует описанный нами алгоритм. Он должен выполняться для каждого последующего mip-начиная с уровня 1 (уровень 0 — это уровень полного разрешения).
Изъяны этого кода
Производительность
Я использовал приведённый выше фрагментный шейдер для генерации двух полных mip-цепочек для двух (по одному на каждый глаз) буферов глубин в своём VR-движке. В тесте разрешение для каждого глаза составляло 1648×1776, что привело к созданию 10 дополнительных уменьшенных mip-уровней (а значит, 10 проходов). На генерацию полной цепочки для обоих глаз потребовалось 0,25 мс на NVIDIA GTX 980 и 0,30 мс на AMD R9 290.
Mip-уровни 4, 5 и 6, сгенерированные из буфера глубин, соответствующего показанному выше цветовому буферу. (Стоит учесть, что бОльшая часть сцены прозрачна, а значит, не влияет на буфер глубин.) Mip-уровень 4 — первый, в котором размеры (103×111) не делятся нацело на 2.
Альтернативный способ генерации mip-цепочки
Задача описанного выше алгоритма заключается в сохранении точности запросов глубины в пространстве координат текстуры (или NDC). Для полноты (и потому, что я отказался от этой гарантии в представленной ниже технике 2) я хотел бы продемонстрировать ещё один способ, который мне встречался (например, в этой статье).
Учтите, что, как и предыдущий, этот альтернативный способ предназначен для работы с буферами полного разрешения, размеры которых не являются степенями двойки (но, разумеется, они работают и с размерами, равными степеням двойки).
В этом альтернативном способе при даунсэмплинге уровня с нечётной шириной (или высотой) вместо добавления для каждого выходного тексела дополнительного столбца (или строки) текселов из предыдущего (нижнего) уровня, мы выполняем эту операцию только для выходных текселов с максимальными индексами («крайних» текселов). Единственное, что это изменяет в представленном выше фрагментном шейдере — задание значений shouldIncludeExtraColumnFromPreviousLevel и shouldIncludeExtraRowFromPreviousLevel :
Из-за этого крайние текселы с наибольшим индексом становятся очень «толстыми», так как каждое деление на 2 нечётной размерности приводит к тому, что они занимают пропорционально всё больший интервал нормализованного пространства координат текстуры.
Недостаток такого подхода заключается в том, что становится сложнее выполнять запросы глубин высоких mip-уровней. Вместо того, чтобы просто использовать нормализованные координаты текстуры, мы сначала должны определить тексел полного разрешения, соответствующий этим координатам, а затем перенести координаты этого тексела на координаты соответствующего mip-уровня, запрос которого выполняется.
Показанный ниже фрагмент кода выполняет перенос из пространства NDC в координаты текселов на mip-уровне higherMipLevel :
Техника 2: генерация одного уровня Hi-Z при помощи вычислительного шейдера
Генерация полной mip-цепочки выполняется довольно быстро, но меня немного напрягало то, что моё приложение генерирует все эти уровни, а использует только один из них (уровень 4). Кроме устранения этой небольшой неэффективности, мне также хотелось посмотреть, насколько можно всё ускорить, если использовать для генерации только нужного мне уровня один вызов вычислительного шейдера (compute shader). (Стоит заметить, что моё приложение может останавливаться на уровне 4 при использовании решения с многопроходным фрагментным шейдером, поэтому в конце этого раздела я использовал его как основу для сравнения времени выполнения.)
В большинстве случаев использования Hi-Z требуется только один уровень глубин, поэтому я считаю, что такая ситуация встречается часто. Я написал вычислительный шейдер под собственные специфические требования (генерирование уровня 4, имеющего разрешение 1/16 x 1/16 от исходного). Похожий код можно использовать для генерации разных уровней.
Вычислительный шейдер хорошо подходит для решения этой задачи, потому что он может использовать общую память рабочей группы для обмена данными между потоками. Каждая рабочая группа отвечает за один выходной тексел (уменьшенного даунсэмплингом буфера), а потоки рабочей группы разделяют работу по вычислению min соответствующих текселов полного разрешения, обмениваясь результатами через общую память.
Я попробовал два основных решения на основе вычислительных шейдеров. В первом каждый поток вызывал atomicMin для одной переменной общей памяти.
Самым быстрым из всех этих протестированных решений с atomicMin и на NVIDIA, и на AMD оказалось решение с блоками потоков 4×4 (при котором каждый поток сам получает область текселов размером 4×4). Я не совсем понимаю, почему этот вариант оказался самым быстрым, но возможно, он отражает компромисс между конкуренцией атомарных операций и вычислениями в независимых потоках. Стоит также заметить, что размер рабочей группы 4×4 использует всего 16 потоков на warp/wave (а возможно использовать ещё и 32 или 64), что любопытно. В показанном ниже примере реализован этот подход.
В качестве альтернативы использованию atomicMin я попытался выполнять параллельное уменьшение при помощи техник, использованных в этой активно цитируемой презентации NVIDIA. (Основная идея заключается в использовании массива общей памяти того же размера, что и количество потоков в рабочей группе, а также проходов для последовательного совместного вычисления min минимумов каждого потока, пока не будет получен окончательный минимум всей рабочей группы.)
Пример кода
При таком способе проще всего не стараться поддерживать соответствие в нормализованном пространстве координат текстуры между текселами уменьшенных буферов и полного разрешения. Можно просто выполнять преобразования из координат текселов полного разрешения в координаты текселов уменьшенного разрешения:
В моём случае (генерирование эквивалента mip-уровня 4) downscalingFactor равен 16.
Как сказано выше, этот вычислительный шейдер GLSL реализует решение с atomicMin с размером рабочих групп 4×4, где каждый поток получает из буфера полного разрешения область текселов размером 4×4. Получившийся уменьшенный буфер глубин значений min равен 1/16 x 1/16 от размера буфера полного разрешения (с округлением вверх, когда размеры полного разрешения не делятся на 16 нацело).
Производительность
Я использовал приведённый выше вычислительный шейдер для обработки буфера глубин полного разрешения с теми же размерами, которые использовались для генерации полной mip-цепочки (буферы 1648×1776 для каждого глаза). Он выполняется за 0,12 мс на NVIDIA GTX 980 и за 0,08 мс на AMD R9 290. Если сравнивать со временем генерации только mip-уровней 1–4 (0,22 мс на NVIDIA, 0,25 мс AMD), то решение с вычислительным шейдером оказалось на 87% быстрее у GPU NVIDIA и на 197% быстрее, чем у GPU AMD.
В абсолютных величинах ускорение не такое уж и большое, но важны каждые 0,1 мс, особенно в VR 🙂
Иератичная Z-буферизация для отбраковки окклюзии
Я читаю раздел отсечки окклюзии в Real-Time Rendering 3rd Edition, и я не мог понять, как он работает. Некоторые вопросы:
Как получается “Z-пирамида”? Зачем нам нужно многократное разрешение Z-буфера? В книге он отображается следующим образом (слева):
Является ли структура Octree одинаковой Octree, которая используется для общего отбраковки и рендеринга усечения? Или это специализированный Octree, сделанный только для техники отбраковки окклюзии?
Более общий вопрос: в предыдущем разделе (а также здесь), термин Query Occlusion Query описывается как “рендеринг упрощенный ограничивающий объем объекта и сравнение его глубины приводит к Z-буфере, возвращая количество видимых пикселей”. Какие функции в OpenGL связаны с этой концепцией окклюзионного запроса?
Является ли эта методика стандартом для отсечения окклюзии открытого мира?
Иерархический Z-буфер полезен в ситуациях, когда большие близлежащие объекты могут перекрывать множество более мелких объектов. Примером может служить визуализация внутренней части здания или горного ландшафта.
Когда визуализируется близлежащий объект, он устанавливает пиксель где-нибудь на более низком уровне разрешения Z-буферной пирамиды на значение, близкое к глубине. Когда визуализируется объект меньшего размера, его ограничивающий прямоугольник можно сначала проверить по этому пикселю и отбраковать целиком.
Чтобы извлечь выгоду из иерархического Z-буфера, нам нужно визуализировать пейзаж, начиная с ближайших объектов. Для этого можно использовать такие приемы, как октреи или BSP.
Кроме того, наличие октри под рукой позволяет отбирать целые ветки дерева на основе расстояния до их bbox, а не отдельных объектов или треугольников.
Иерархические Z-буферы были реализованы аппаратно на некоторых ранних Radeon. Однако я не слышал об этом в настоящее время.
Запросы окклюзии, с другой стороны, обычно используются. Они, по сути, дают аналогичные преимущества.