Суть такова:
Разрабатывается игра, в которой игрок может использовать различные заклинания. Эти заклинания, воздействуя на поверхность уровня, могут накладывать на неё определённый эффект. Например, ледяная глыба создаёт пятно, покрытое льдом, а шар кислоты - оставляет на земле лужу кислоты. При этом, естественно, всё это должно влиять на геймплей: кислота обжигает, по льду можно скользить, а вода проводит электричество от заклинаний молнии.
К примеру, хорошая, годная реализация чего-то похожего имеется в Baldur's Gate 3. А в Portal 2 можно разбрызгивать по поверхностям гель разных типов, который меняет их физические свойства.
Вопрос следующий: каким способом это лучше реализовать?
Как уже пробовал делать я
Разумеется, я уже пытался изобрести велосипед написать собственную реализацию. Она даже как-то работает, но я уверен, можно сделать что-то получше.
В общем, в памяти создаётся 3D-текстура, куда записываются индексы типов поверхности: 0 - обычная поверхность, 1 - огонь, 2 - вода, и так далее. Каждому текселю условно ставится в соответствие один метр мировых координат. Шейдеры сэмплят эту текстуру для создания визуальной части, а для использования игровой логикой эти же данные дополнительно хранятся в обычном плоском массиве, построчно.
Минусы такого подхода:
- Недостаток разрешения. Конечно, я добавляю некоторый шум к краям, но всё равно видна некоторая квадратность получаемых пятен:
- Если пятно надо нарисовать на относительно тонкой стене, или на полу возле неё - оно появится с обеих сторон, что выглядит странно.
- Не будет корректно работать с движущимися объектами (например, лифты).
- Даже при низком разрешении текстуры - жрёт много памяти и медленно обновляется. Из-за этого приходится ограничивать зону вокруг игрока, в которой система работает. Если игрок отошёл достаточно далеко - пятна на земле просто стираются.
Ещё одна возможная реализация
Вместо 3D текстуры использовать обычные плоские текстуры, размапанные по дополнительному UV каналу.
Недостатки:
- Этот дополнительный канал нужно заранее готовить, что усложнит прототипирование уровней.
- Производительность выглядит сомнительной. В игре планируется весьма большое число врагов, вплоть до нескольких сотен в одном сражении. Им всем нужно будет знать, на каком типе поверхности они стоят в каждом кадре. Если для каждого врага переводить позицию в UV и сэмплить текстуру - скорее всего, лаги будут легендарными.
В общем, нужны более удачные решения. Может быть, существуют уже какие-то статьи на эту тему (я, правда, не нашёл), или у кого-то есть личный опыт? Буду рад любым советам, а то без этой механики разработка ощутимо буксует.
Мне кажется это то что тебе нужно:
Decals and projectors
Легко использовать, легко контролировать, хорошо выглядят.
Я использую Builtin Pipeline, и там нормальных декалей нет, есть только унылый Projector. Начинал проект с URP, но по определённым причинам пришлось вернуться к Builtin.
Есть, конечно, другие реализации, которые будут работать.
Но как понять, что персонаж стоит на декали? Наверное, можно для каждой создавать триггеры, но это во-первых будет нагружать физику, а во-вторых - триггеры будут пролезать сквозь стены.
Ещё проблемы начинаются когда несколько декалей разных типов накладываются друг на друга. По задумке, в этом случае разные среды должны реагировать друг с другом. Например, при наложении огня и льда образуется мокрая поверхность. При реализации через декали это выглядит крайне непростой задачей.
Информации мало, чтобы о чём-то судить.
> Эти заклинания, воздействуя на поверхность уровня,
А что такое вообще эта "поверхность уровня"? Есть ли стилизация? Может, работаем в вокселях (условно MineCraft), и подход различен от реализма.
А уровень из чего состоит? Единый mesh (terrain), или совокупность объектов(props)?
BooTheJudge
> ледяная глыба создаёт пятно, покрытое льдом, а шар кислоты - оставляет на земле лужу кислоты.
А какие ограничения?
- Можно ли кислоту вылить на потолок?
- Жидкостные эффекты растекаются?
- Жидкостные эффекты капают?
- Может ли один эффект примениться на несколько поверхностей (земля, бочка и кирпич)?
BooTheJudge
> унылый Projector
А чем унылый-то? Рабочее решение. Ну или можно попросить нейронки screen-space decals сделать, но там нужен буфер глубины + свои приколы.
BooTheJudge
> Но как понять, что персонаж стоит на декали? Наверное, можно для каждой создавать триггеры, но это во-первых будет нагружать физику, а во-вторых - триггеры будут пролезать сквозь стены.
Тогда зададимся вопросом: а что значит воздействие эффекта? Как я понял - ступни персонажа находятся в зоне действия эффекта. Понять это наиболее оптимальнее как раз-таки через физику, т.к. подход наиболее близок к C++ ядру движка, и PhysX уже имеет тонну оптимизаций. Пролезание триггеров через стены - генерируй зоны триггера процедурно, а именно пятно контакта эффекта выдавливай по нормали поверхности.
Мысли по решению
1. Статичные эффекты можно делать прожектором/декалями. Контакт описывай простыми BoxCollider. Не стоит смотреть на этот подход с высока, решение вполне рабочее и рынкопригодное.
2. Развитие - через процедурные декали. Есть ассет Decalery от Mr F (автор есть на форуме). Особенно интересен третий скриншот:
Но там вопросы по мульти-объектному наложению. Skinned-mesh вроде заявлен, что прямо подкупает.
Контакт тут посложнее. Поставляется ассет, как я понял, с исходниками. Можно подоткнуться к ним, вытянуть генерируемую сетку, вывернуть нормали и выдавить на желаемое расстояние действия эффекта.
3. Растекание в рамках объекта можно сделать через Fluid Flow 2. Но это чисто текстурки. Точный контакт сделать трудно.
4. Растекание по большим площадям - подход 2 + использовать, как ни странно, физику уровня. Есть репозиторий, делающий по уровню процедурные лианы. Как вариант - точка, где должна быть лиана, делать новую декаль. И в рамках алгоритма поиска делать ограничения (не затекать с пола на стены).
5. Коллизии эффектов - развитие 4. Сам эффект сделать актором растекания, у которого есть дочерние ноды, которые описывают зону действия эффекта. И при растекании эффекта узнавать габариты ноды, и опрашивать соседние на предмет коллизии. А саму коллизию можно разрешать кастомными правилами; условно огонь проигрывает воде/морозу, но выигрывает у органики. Усекание зоны работы эффекта можно, тупо отрезав меш по плоскости контакта, типа такого подхода.
Dan398
Спасибо за развёрнутый ответ.
Dan398
> А что такое вообще эта "поверхность уровня"? Есть ли стилизация? Может, работаем в вокселях (условно MineCraft), и подход различен от реализма.
>
> А уровень из чего состоит? Единый mesh (terrain), или совокупность объектов(props)?
Мультяшная стилизация есть, с ней пока ещё экспериментирую. Вокселей нет, уровни состоят из мешей, импортированных из fbx, в том числе и ландшафты.
Dan398
> - Можно ли кислоту вылить на потолок?
С точки зрения визуала - будет, конечно, логичнее, если пятна будут действовать и на стены, и на потолок. Но для геймплея достаточно и того, чтобы действовало только на полы.
Dan398
> - Жидкостные эффекты растекаются?
> - Жидкостные эффекты капают?
Нет, это лишнее уже.
За ссылки отдельное спасибо, про Decalery раньше не слышал, хотя Bakery пользуюсь давно. Генератор лиан тоже может пригодиться, в том числе и по прямому назначению.
Буду, пожалуй, думать в направлении декалей с триггерами.
Итак, спустя полтора месяца, я наконец это сделал.
Видео с результатом:
В итоге реализация получилась гибридной: некая смесь декалей и рисования по текстурам.
Сначала производится автоматическая подготовка сцены. Скрипт проходит по указанным мешам и выбирает из них полигоны, которые смотрят лицевой стороной вверх (с настраиваемым допуском по углу наклона). Объединяя все эти полигоны, мы получаем поверхность, на которую будут накладываться пятна. В идеале планируется собирать полигоны со специального меша для коллизий, но, пока он не готов, сгодится и обычная видимая геометрия. У созданного меша генерируется второй UV-канал с помощью встроенных средств Unity.
Полученная поверхность, по сути, является одной большой декалью, и отрисовывается она через CommandBuffer, как обычные Deferred декали. За отправную точку я взял демо-проект отсюда. Он древний, но рабочий.
Шейдер для отрисовки декали используется непростой: он принимает одноканальную текстуру, в которой каждый цвет соответствует определённому типу поверхности. Важно, чтобы текстура была линейной и у неё была отключена фильтрация. Взяв тексель из этой текстуры по второму UV-каналу, шейдер использует его как индекс, чтобы взять из текстурных массивов уже те текстуры, которые нужно непосредственно нарисовать на поверхности (альбедо, нормали, спекуляр и эмиссию).
Изменение типов поверхности в процессе игры сводится к рисованию по вышеупомянутой одноканальной текстуре. Здесь я за основу взял вот этот проект, хоть и основательно его переработав. Как упоминалось в стартовом посте, типы поверхности должны "реагировать" друг с другом. За это отвечает шейдер, который рисует на текстуре. Он берёт старый цвет текселя и цвет, который надо нарисовать, и использует эти два значения как координаты для выбора итогового значения из текстуры-таблицы. Эта текстура генерируется скриптом на основе значений, задаваемых вручную в инспекторе:
С визуальной частью на этом, в принципе, всё. Но как же быть с игровой логикой? Ведь скриптам нужно знать, где какой тип поверхности, а считывать текстуру каждый кадр - это очень медленно.
Для решения этой проблемы содержимое текстуры вытягивается обратно в оперативную память при помощи Async GPU Readback. Происходит это через короткие интервалы времени, при условии того, что на протяжении этого интервала в текстуре кто-то рисовал. Полученные данные размещаются в плоском массиве типа byte.
Далее, когда какой-то скрипт хочет узнать тип поверхности, он делает Raycast только по слою, отведённому для нашей декали. При попадании в поверхность, он получает координаты UV2, из которых нетрудно вычислить требуемый индекс в плоском массиве. Таким образом, возврат данных из GPU происходит максимум один раз за фиксированный интервал, а все остальные скрипты уже работают исключительно с оперативной памятью.
Как можно заметить, хитрая декаль распространяется только на полы и некрутые склоны, но это не так и критично. Для стен и потолков можно использовать обычные, чисто декоративные декали.
В целом, я страшно доволен результатом. Теперь можно наконец-то заняться разработкой непосредственно самой игры.
Выглядит недурно, молодца.