Рендеринг процедурных поверхностей с использованием технологии маршировки лучей на основе функций полей расстояний со знаком

Напиши заголовок типа на диссертацию тянет.

Я в своей голове
слизька

"Что по полигонам?" - извечный вопрос рендеринга в реальном времени. Ведь модели становятся всё детальнее с каждым годом. И под каждую вершину нам надо запускать вершинный шейдер, преобразовывать трёхмерные координаты в экранные и в конце концов, брать данные о вершине из памяти (с конечными объёмом и пропускной способностью). А из-за особенности рендеринга на видеокартах маленький размер треугольников может крайне негативно сказываться на итоговой производительности (можете за подробностями погуглить quad overshading или overdraw).

Поэтому все мы знаем старую как мир систему LODов, а теперь у нас есть ещё и анриловские наниты. Всё, чтобы переваривать детализированные модели легче дизайнеру, железу и незаметнее для игрока.

Наниты, к сожалению, могут давать очень заметные артефакты при использовании. Например, для смещения мирового положения(WPO) в шейдере

Но что если мы сократим число вершин у модели до минимума и при этом сохраним высокую детализацию. Более того, идеальную детализацию, создадим модель из совершенных сфер, цилиндров и кубов. Но обо всём по порядку.

Поля расстояний со знаком(SDF)

Кто имеет дело с UE, мог уже встречать эти загадочные три буквы. Например, в Lumen.

Объяснить, что такое SDF и функция на её основе, довольно просто. Давайте возьмём сферу. Сфера в своей функции определяется через центр и радиус. При вычислении функции SDF сферы в произвольной точке пространства, можно получить 3 результата:

  • Положительное значение, если точка находится за пределами сферы(чем дальше от сферы, тем больше)
  • 0, если лежит на поверхности сферы
  • Отрицательное значение, если внутри сферы

Как вы понимаете, это позволяет вычислить расстояние до ближайшей поверхности в пространстве. Если мы получаем ноль, значит, вот она, родимая, и нашлась. А вот как мы эту точку найдём, узнаем далее.

Маршировка лучей

Простая маршировка

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

Луч как бы "марширует", отсюда и название(Ray Marching или Ray Stepping)
Луч как бы "марширует", отсюда и название(Ray Marching или Ray Stepping)

Если мы получаем результат меньше порогового значения(например, 0.001), значит луч находится совсем рядом с поверхностью или даже попадает внутрь, а значит, мы нашли, что нам отрисовать в данном пикселе. Если луч не найдёт ничего по достижении заданного расстояния или количества шагов, мы останавливаем маршировщик.

Сферическая маршировка

Алгоритм с заданной длиной шага создаёт много проблем. Если задать слишком большую длину, луч будет просто промахиваться, а если слишком маленькую, то потребуется много шагов.

Поэтому давно уже придумали динамическую длину шага, для которой даже не требуется дополнительных расчётов.

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

С приближением к искомой точке шаг будет уменьшаться
С приближением к искомой точке шаг будет уменьшаться

Сильные стороны

Не только минимизацией полигонов данный набор технологий силён.

Бесплатное копирование

Создавая, например, инстансы статической сетки, вы будете забивать ими ресурсы видеокарты. Что уже для тысячи копий приведёт к ощутимым просадкам, а для сложных мешей рискует просто забить память под завязку.

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

Бесконечное число кубов в одной текстуре
Бесконечное число кубов в одной текстуре

Булевы операции

Тридэшники знакомы с ними. Сложение и вычитание фигур друг из друга. Иногда даже плавное, как в "мета" фигурах, например. Вне условного блендера такие выкрутасы очень ограничены. Плавные мета-объекты технически можно сделать, но это потребует большого количества вершин. В заголовке вы уже могли увидеть, как это работает здесь внутри простого куба.

Деформация

Помимо задания идеальных примитивов навроде сфер и кубов, их поверхности можно придать любую форму. Выдавливать рельеф, скручивать, в общем, делать всё, что можно представить в виде функции:

Уот такая вот загогулина
Уот такая вот загогулина

И бонусом быстрое получение мягких теней:

Не советую делать по представленному здесь алгоритму. Данная реализация при смягчении тени её раздувает. На Shadertoy есть и более интересные примеры. Ну а моя реализация - секрет фирмы.

Слабые стороны

Очевидно, самая слабая сторона в том, что в каждом кадре нужно искать поверхности через маршировку. А ещё тени к ним, отражения к ним... И как с этим ни бьются в целых научных работах, отрендерить всё в 4 шага на луч не получается. Да и тяжёлые циклы в фрагментных шейдерах бяка.

Одна из главных проблем: постоянные застревания в локальных минимумах на пути к искомой точке
Одна из главных проблем: постоянные застревания в локальных минимумах на пути к искомой точке

Но есть и хитрее проблема:

Неевклидовы расстояния

Снова просто вставлю Куилеза, который расскажет о проблеме эллипсов:

Но ими всё не ограничивается. Сообщать неевклидовы расстояния может и та самая деформация. Функции деформации в разных точках пространства сообщают разные значения. И данная разница искажает радиусы для сферического трассировщика. То есть буквально искажает пространство.

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

Вместо практической части

Для начала, мне не очень хочется здесь расписывать, что там по итогу в анрил енжине, когда я уже сделал плагин с документацией, поэтому просто дам ссылку, если кому-то НЕРЕАЛЬНО интересно:

Демо

Отдельно стоит отметить решения вышеназванных проблем. Например, вопрос поиска процедурной поверхности частично решается приближением к ней формы меша. Участники демосцены любят флексить тем, как внутри простого кубика генерируют целые миры. Но такой подход как раз и создаёт вышеуказанные локальные минимумы для маршировщика, а также бесполезную беготню лучей по пустотам. Поэтому разумный подход здесь: вернуть немного работы для вершинного шейдера. Он в любом случае не является узким местом, а вот работы для пиксельного сократится. Неиронично, где могло потребоваться 64-96 шагов луча, можно будет обойтись в 4-16. К тому же приближенная лоу-поли версия вашего процедурного объекта вполне может сгодиться, например, для прокси теней(подробнее о подобном в документации к плагину)

Также определённо не стоит помещать весь мир в один шейдер. Чем больше функций фигур участвует в маршировке, тем, очевидно, каждый шаг луча дороже. Поэтому разумно разбивать поиск на несколько Marching Space, как я их назвал. В самом деле, зачем лучу постоянно проверять наличие здесь сферы, которая от него находится в километре и точно никогда не окажется в данном пикселе. Или постоянно отсекать дальние объекты, что тоже тратит ресурсы. Из минусов тот факт, что поверхности из разных пространств маршировки друг друга не видят. Отсюда невозможность друг друга затенять и отражать, например. В анриле это частично решается lit шейдингом.

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

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

Например, задонатить автору 😎

В любом случае, спасибо за прочтение, если кто-то реально прочитал.

33
4
1
1
6 комментариев