Devblog. Часть 5 — Использование системы партиклов для создания сложных эффектов

Девблог

Недавно мы добавили в EdenSpark системы партиклов — функцию, которую очень часто запрашивали. Она обеспечивает симуляцию большого количества частиц на ГПУ. У этих частиц множество общих свойств, что даёт серьёзный выигрыш в производительности по сравнению с ручной симуляцией через сценарные ноды. Кроме того, для создания эффекта не нужно писать код.

Сегодня мы поговорим о предыстории этой системы, а затем пройдём туториал по созданию довольно сложного эффекта.

Предыстория

Мы — игровая компания, поэтому у нас, разумеется, есть собственная система эффектов. Речь идёт о системе ModFX, которая публично доступна в репозитории Dagor Engine. Она довольно сложная, содержит огромное количество параметров, многие из которых специфичны для игр, имеет собственный инструмент создания эффектов и в целом заточена под производительность в условиях наших игр.

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

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

Наконец, мы добавили контуры, попиксельный выбор и возможность редактирования в реальном времени. Сейчас изменение параметров приводит к перезапуску потока партиклов, но мы работаем над редактированием в реальном времени, а также над эффектами искажения и столкновениями частиц.

Текущая система уже позволяет создавать множество эффектов, и мы покажем это в следующем туториале.

Туториал: эффект фейерверка

Мы будем делать трёхэтапный эффект фейерверка: подъём снаряда, основной взрыв и вторичный взрыв в конце. Вот как это выглядит:

Начнём с подъёма снаряда. Для этого нам нужно создать эффект следа и скрипт, который будет перемещать эмиттер, так как движение эмиттера — часть игровой логики.

Поднимающийся снаряд

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

Чтобы частицы вели себя как искры, они должны начать двигаться. Для этого мы включим начальную скорость через motion.velocity, которая по умолчанию разбрасывает частицы от центра эмиттера, и применим гравитацию, чтобы они падали вниз.

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

С этими настройками поведение частиц выглядит так:

Чтобы превратить их в настоящие искры, нам нужно создать для них гладкую круглую форму, добавить цвет и сделать их самосветящимися. Для формы мы будем использовать простую гауссовскую текстуру

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

С дополнительной настройкой радиуса частиц и цвета мы получаем такой эффект — он уже выглядит как след искр:

Теперь нужно собрать всё в настоящий снаряд. Добавляем новый компонент, который будет перемещать ноду, симулировать небольшую физику и создавать вторичные префабы — это понадобится нам для создания многоэтапного эффекта. Мы также сразу сделаем время жизни рандомизируемым, потому что для вторичного взрыва нам понадобится случайная задержка.

require engine.core

def random_point_on_sphere() : float3 {
    let z = 1.0 - 2.0 * random_float()
    let r = sqrt(max(0.0, 1.0 - z * z))
    let phi = 2.0 * PI * random_float()
    return float3(r * cos(phi), r * sin(phi), z)
}

struct SubFireworkSetup {
    prefab : PrefabId
    count : int
}

class FireworkBehavior : Component {
    subFireworks : array<SubFireworkSetup>

    lifeTimeRange : float2
    speed : float
    drag : float // Speed reduction coefficient.
    acceleration : float3 // Gravity or other constant acceleration in world space.

    private velocity : float3
    private lifeTime : float

    def override on_initialize() {
        velocity = nodeId.worldRotation * float3(0, speed, 0)
        lifeTime = lerp(lifeTimeRange.x, lifeTimeRange.y, random_float())
    }

    def override on_update() {
        let dt = get_delta_time()

        lifeTime -= dt
        if (lifeTime <= 0.0) {
            launch_sub_fireworks()
            remove_node(nodeId)
            return
        }

        velocity += acceleration * dt - velocity * drag * dt
        nodeId.worldPosition += velocity * dt
    }

    def private launch_sub_fireworks() {
        for (sub in subFireworks) {
            for (i in range(sub.count)) {
                let dir = random_point_on_sphere()
                sub.prefab.instantiate_prefab(NodeData(
                    position = nodeId.worldPosition,
                    rotation = quat4(UP, dir)
                ))
            }
        }
    }
}

После добавления этого компонента в префаб фейерверка и настройки параметров получаем следующее:

Можно заметить, что когда эмиттер угасает, весь след мгновенно исчезает. Чтобы это исправить, мы можем изменить свойство stopBehavior системы частиц на ParticleStopBehavior.DeleteEmitter. Теперь, когда нода угасает, эмиссия останавливается, но эффект продолжает жить, пока все частицы не исчерпаются:

Основной взрыв

Теперь, когда у нас есть компонент поведения фейерверка, мы можем просто создать префабы вторичных фейерверков и добавить их в соответствующий список для запуска:

Здесь мы можем добавить более заметные следы, которые можно симулировать с помощью режима рендеринга лент (ribbon). Он включается через параметр render.shape.kind и настраивается с помощью поля ribbon. Добавим также интересную раскраску через градиент на протяжении жизни частицы, чтобы сделать эффект более разнообразным. Для текстуры мы используем плоскую версию гауссовской текстуры (по сути, текстура может быть высотой всего в один пиксель):

После настройки остальных параметров получаем такой след:

В сочетании с нашим эффектом это выглядит так:

Вторичный взрыв

Теперь не хватает только эффекта вторичного взрыва, который дополнит картину. Здесь мы используем режим эмиссии burst и добавим задержку через spawn.delay:

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

Что дальше

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

В ParticleSystem есть множество других параметров, которые мы не рассмотрели в этом туториале — исследуйте их самостоятельно! Эффект от большинства из них виден сразу, а если что-то непонятно, всегда можно обратиться к документации. Теперь это просто: каждое поле и значение перечисления имеют подсказку прямо в редакторе.

Надеемся, этот туториал поможет вам как отправная точка для создания ваших собственных красивых эффектов!