Срезаем геометрию волюмом пиксель в пиксель (Unreal Engine)
К сожалению на ютубе и на форумах нет хороших туториалов по этой теме. Как-то что-то там чё-то срезается, и норм. А мне нужно максимально точно и понятно.
Для моих задач в продакшен нужно только вращение волюма по Yaw, так что если нужно по всем 3м осям - пишите в комментах.
В двух словах про механику: На уровне есть какая-то геометрия, и на всей этой геометрии установлен один материал (для увеличения производительности и снижения DrawCall). Один прямоугольный участок мне нужно срезать так, чтобы не зацепить другую геометрию. Над геометрией для срезания помещается Блюпринт Актор внутри которого есть BoxCollisionVolume. Мы опускаем этот блюпринт, и позицию+поворот+размеры этого бокса отправляем в Material Parameter Collection (MPC). Во всех материалах геометрии учитываются эти параметры из MPC.
Material Parameter Collection такой:
Intensity (float1) - Интенсивность этого эффекта
Rotation (float1) - Поворот по Z (Yaw)
Location (float4) - Позиция бокса
BoundingBox (float4) - Размеры бокса в абсолютных величинах
RotationAxis (float4) - Ось поворота бокса (на будущее). Для текущего туториала достаточно вектора (0, 0, 1). Но в будущем сделаю полный поворот в 3д (наверное).
Шейдер CutGeometry_Volume (материал инстанц) на кубиках лежит вот такой:
И сам Блюпринт Актор (например, Cut_BP) который служит объёмом для срезания:
Конечно же в этом Акторе выключены все Тики, и у BoxCollisionVolume выключена абсолютно вся коллизия:
В этом Блюпринте есть 2 внешние переменные, которые можно крутить после помещения на уровень: Intensity и Bounds.
Интенсивность (Intensity) и Абсолютная позиция бокса (Box→WorldLocation) - просто передаётся в MPC.
Параметром Bounds мы изменяем размеры нашего BoxCollisionVolume, чтобы на уровне видно было что мы меняем:
BoxExtents для 3д-сцены и для шейдера к сожалению разные. Мы видим одно, а шейдер видит другое - он видит HalfExtents, так что я домножаю наши размеры на 2 и передаю в MPC.
Half Extends - это Объём, который можно описать вокруг геометрии меша.
По -X и +X выбираются ближайшие к пивоту меша точки, и берётся максимальная. Получается максимальная граница по X. Так же для Y. И так же для Z. В итоге получился объём, который принято делить на 2 и называть это HalfExtends =) Так во многих игровых движках: хочешь получить BoundingBox и никогда не знаешь что в итоге получишь: полный размер или половинный.
Дальше вращение по Z (Yaw):
Здесь я беру абсолютное вращение компонента Box, не смотря на то, какой относительный поворот компонента и отправляю в MPC. World Rotation точно даёт нужное вращение.
-180 и +180 - это возможный поворот в градусах на 3д сцене на уровне. Крутим Актора влево-вправо и получаем такие числа. Но шейдеру нужны не эти градусы, а относительное приведённое от 0..360 к 0..1 amount-число. MapRangeClamped привела какой-то угол в относительное значение.
Было -180 .. +180, стало -0.5 .. +0.5
Последний шаг - отправляю Ось вращения в шейдер:
Что теперь делает шейдер, когда получил все эти значения из MPC?
Для начала преобразовываем float4 в float3. RotationAxis в MPC лежит «цветом» это структура флоатов (R, G, B, A). Нода ComponentMask (которая на скриншоте называется Mask(RGB)) может убрать лишний для нас компонент A, и на выходе мы получили (R, G, B).
Передали в ноду RotateAboutAxis: ось вращения, угол вращения (от -0.5 до +0.5), точку вращения Pivot, и абсолютную позицию пикселя объекта в 3д. Мы вращаем один пиксель в этом пиксельном шейдере, который в Анриале называется Материал.
Ту позицию в 3д, которая получилась после поворота мы складываем абсолютной позицией и наконец-то получаем точку <A> для BoxMask. Второй точкой <B> будет позиция нашего BoxCollisionVolume из Актора.
BoxMask в пиксельном шейдере: между 2мя точками в 3д мире строится вектор направления, который не должен выйти за BoundingBox, в результате чего получается 3д-маска.
MaskEdgeFalloff - это на сколько далеко от границы будет градиентом расползаться маска. В моём примере от границы на дистанцию 5 пикселей будет малюсенький градиент.
При скалировании Актора шейдер перестаёт адекватно воспринимать границы, так что лучше Scale оставлять равным единице. Тогда всё будет пиксель в пиксель.
