reckfellENGINE

Particle: состояние, движение и sub-emitter

Полные шейдеры и C#-настройка, флаги рождения, USERDATA и ограничения RD.

На этой странице

Particle-шейдер моделирует состояние GPU-частиц до их отрисовки. Назначайте его в ProcessMaterial у GpuParticles2D/GpuParticles3D. Материал меша или спрайта затем рисует результат через spatial/canvas_item. CpuParticles не исполняет этот шейдер моделирования, хотя его изображение тоже рисуется GPU.

Два материала выполняют разные задачи

ProcessMaterial содержит ShaderMaterial с shader_type particles либо готовый ParticleProcessMaterial. DrawPass1 у GpuParticles3D содержит меш с обычным материалом поверхности. Назначение particle-шейдера вместо spatial-материала не создаёт изображение.

Полный шейдер задаёт начальные значения только для данных, не переданных эмиссией, сохраняет возраст в USERDATA1 и прекращает активность после LIFETIME:

Шейдер
shader_type particles;
void start() {
    if (RESTART_POSITION) TRANSFORM[3] = EMISSION_TRANSFORM[3];
    if (RESTART_ROT_SCALE) {
        TRANSFORM[0] = EMISSION_TRANSFORM[0];
        TRANSFORM[1] = EMISSION_TRANSFORM[1];
        TRANSFORM[2] = EMISSION_TRANSFORM[2];
    }
    if (RESTART_VELOCITY) VELOCITY = vec3(1.0, 0.0, 0.0);
    if (RESTART_COLOR) COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    if (RESTART_CUSTOM) CUSTOM = vec4(0.0);
    USERDATA1 = vec4(0.0);
}
void process() {
    USERDATA1.x += DELTA;
    CUSTOM.x = USERDATA1.x;
    if (CUSTOM.x >= LIFETIME) ACTIVE = false;
}

В этом примере VELOCITY двигает TRANSFORM автоматически; дополнительное TRANSFORM[3].xyz += VELOCITY * DELTA удвоит перемещение. Для сохранения входных данных ручного выпуска или sub-emitter нужны проверки RESTART_*: безусловное присваивание COLOR в start() затрёт переданный цвет.

Полный материал отрисовки читает цвет частицы, а CUSTOM получает через INSTANCE_CUSTOM в vertex() и varying. display=0 показывает COLOR, 1 — CUSTOM.yzw, 2 — диагностический цвет накопленного возраста:

Шейдер
shader_type spatial;
render_mode unshaded, cull_disabled;
uniform int display = 0;
varying vec4 particle_custom;
void vertex() {
    particle_custom = INSTANCE_CUSTOM;
}
void fragment() {
    if (display == 1) {
        ALBEDO = particle_custom.yzw;
    } else if (display == 2) {
        ALBEDO = particle_custom.x > 0.25 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
    } else {
        ALBEDO = COLOR.rgb;
    }
}

Если используется StandardMaterial3D, включите VertexColorUseAsAlbedo, чтобы видеть COLOR частицы. CUSTOM не превращается в цвет автоматически. Раскладка CUSTOM у собственного шейдера определяется вашим кодом; не смешивайте её с соглашениями ParticleProcessMaterial для угла, возраста и кадров анимации.

Полная настройка из C#

Сохраните ParticleSetup.cs и оба шейдера выше. Помощник создаёт остановленный эмиттер с QuadMesh, отдельными материалами моделирования/отрисовки и функцией ручного выпуска:

C#
using Reckfell;

public static class ParticleSetup
{
    public static GpuParticles3D Create(Shader simulation, Shader surface)
    {
        return new GpuParticles3D
        {
            Emitting = false,
            Amount = 1,
            Lifetime = 10,
            FixedFps = 60,
            LocalCoords = false,
            UseFixedSeed = true,
            Seed = 42,
            ProcessMaterial = new ShaderMaterial { Shader = simulation },
            DrawPass1 = new QuadMesh
            {
                Size = new Vector2(0.2f, 0.2f),
                Material = new ShaderMaterial { Shader = surface },
            },
            VisibilityAabb = new Aabb(new Vector3(-4, -4, -4), new Vector3(8, 8, 8)),
        };
    }

    public static void Emit(GpuParticles3D target, Transform3D transform,
        Vector3 velocity, Color color, Color custom)
    {
        uint flags = (uint)(GpuParticles3D.EmitFlags.Position |
            GpuParticles3D.EmitFlags.RotationScale | GpuParticles3D.EmitFlags.Velocity |
            GpuParticles3D.EmitFlags.Color | GpuParticles3D.EmitFlags.Custom);
        target.EmitParticle(transform, velocity, color, custom, flags);
    }
}

Загрузите шейдеры через ResourceLoader.Load<Shader>("asset://MotionParticle.reckfellshader") и аналогичный вызов для ParticleSurface. Создайте эмиттер, добавьте его в дерево, затем вызовите ParticleSetup.Emit с Transform3D.Identity, Vector3.Right, Colors.Red и данными CUSTOM. Для display=1 значение custom new Color(0, 0, 1, 0) даёт зелёный, потому что материал читает yzw. Здесь Color — контейнер четырёх компонент данных, а не требование использовать цветовую семантику CUSTOM.

Для просмотра подойдёт Camera3D у (0,0,5), смотрящая вдоль −Z. Частица движется по +X и может выйти из кадра; подберите VisibilityAabb под всю траекторию. Невидимость из-за границы/камеры не означает ACTIVE=false. Пример использует маленький диагностический quad, не готовый художественный эффект.

start(), process() и время

start() вызывается при начале/перезапуске активной частицы. process() обрабатывает активное состояние шага, включая первый шаг с RESTART=true. DELTA — время моделирования этого шага, не обещание одного вызова на каждый кадр изображения. При FixedFps>0 моделирование идёт фиксированными шагами; Interpolate сглаживает отображение между ними. FractDelta уточняет долю шага при рождении. InterpToEnd — отдельное значение INTERPOLATE_TO_END для шейдера; собственный алгоритм должен использовать его сам.

LIFETIME — настройка длительности, а не текущий возраст. Счётчик в USERDATA1.x явно накапливает DELTA и управляет ACTIVE. При SpeedScale=0 проверенный эмиттер сохраняет положение/состояние; положительная скорость продолжает моделирование. TIME — общее время рендерера с Engine.TimeScale и сбросом rendering/limits/time/time_rollover_secs (обычно 3600 секунд), а не возраст отдельной частицы. Не заменяйте им локальный счётчик.

TRANSFORM, VELOCITY, COLOR, CUSTOM и используемые USERDATA1…6 хранятся в буфере между шагами. Инициализируйте нужные значения при старте. MASS в текущем RD — локальное значение шага с начальным 1, оно не сохраняется как USERDATA: проверка задаёт 2 в start(), а последующие шаги снова получают 1.

Выберите один способ перемещения

Если меняете только VELOCITY, используйте автоматическое перемещение. Для полного контроля над TRANSFORM включите disable_velocity и интегрируйте позицию самостоятельно:

Шейдер
shader_type particles;
render_mode disable_velocity;
void start() {
    if (RESTART_POSITION) TRANSFORM[3] = EMISSION_TRANSFORM[3];
    if (RESTART_ROT_SCALE) {
        TRANSFORM[0] = EMISSION_TRANSFORM[0];
        TRANSFORM[1] = EMISSION_TRANSFORM[1];
        TRANSFORM[2] = EMISSION_TRANSFORM[2];
    }
    if (RESTART_VELOCITY) VELOCITY = vec3(1.0, 0.0, 0.0);
    if (RESTART_COLOR) COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    if (RESTART_CUSTOM) CUSTOM = vec4(0.0);
    USERDATA1 = vec4(0.0);
}
void process() {
    USERDATA1.x += DELTA;
    TRANSFORM[3].xyz += VELOCITY * DELTA;
    CUSTOM.x = USERDATA1.x;
    if (CUSTOM.x >= LIFETIME) ACTIVE = false;
}

В текущем RD автоматическое смещение активной частицы вычисляется до пользовательского process(). Изменённая в process() скорость участвует в следующем автоматическом шаге. Написанный вручную интегратор может выбрать другой порядок; не считайте их численно одинаковыми при ускорениях и столкновениях.

EMISSION_TRANSFORM помогает инициализировать мировую позицию при LocalCoords=false. Не применяйте его повторно к уже переданному мировому TRANSFORM. При LocalCoords=true используется локальная система эмиттера; перенос схемы между режимами требует согласовать позиции и скорости.

Режимы: проверенное поведение текущего RD

РежимДействие и ограничение
disable_velocityОтключает автоматическое смещение по VELOCITY. Сам вектор остаётся доступен вашему коду.
collision_use_scaleУчитывает масштаб TRANSFORM в размере столкновений частиц; это не PhysicsBody3D и не создание CollisionShape3D.
keep_dataСинтаксис принимается, но проверенный RD Restart всё равно очищает COLOR. Не рассчитывайте на сохранение состояния этим флагом в данной версии.
disable_forceСинтаксис принимается, но проверенный RD продолжает выдавать ненулевой ATTRACTOR_FORCE. Если сила не нужна, не применяйте её в своём process().

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

Встроенные значения

in — только чтение, inout — доступные начальные данные с возможностью изменения. Общие значения доступны в start()/process(); USERDATA — записываемые vec4, а не read-only. TIME и PI/TAU/E доступны глобально.

Общее значениеНазначение
in float LIFETIME / DELTAДлительность / время текущего шага моделирования.
in uint NUMBER / INDEXНомер эмиссии / индекс слота внутри системы; не ID Entity.
in mat4 EMISSION_TRANSFORMПреобразование эмиттера для выбранной системы координат.
in vec3 EMITTER_VELOCITYСкорость эмиттера; наследование задаёт ваш алгоритм.
in uint RANDOM_SEEDБазовый seed для собственного детерминированного генератора.
in float AMOUNT_RATIOДоля AmountRatio; это float. Готовый материал использует её для отбора эмиссии, собственный шейдер должен учитывать её сам.
in float INTERPOLATE_TO_ENDЗначение InterpToEnd в диапазоне 0–1; не самостоятельное время жизни.
inout bool ACTIVEАктивность частицы; false прекращает её обработку/отрисовку.
inout vec4 COLORЦвет, передаваемый материалу отрисовки.
inout vec3 VELOCITYСкорость для автоматического или собственного интегратора.
inout mat4 TRANSFORMПреобразование; позиция в TRANSFORM[3].xyz.
inout vec4 CUSTOMЧетыре компоненты для следующего шага и INSTANCE_CUSTOM материала отрисовки.
inout vec4 USERDATA1…USERDATA6Дополнительные сохраняемые данные моделирования. Не становятся отдельными атрибутами материала меша автоматически.
inout float MASSМасса для расчётов шага, начальное значение 1; не сохраняется между шагами текущего RD.
in uint FLAG_EMIT_POSITION / FLAG_EMIT_ROT_SCALE / FLAG_EMIT_VELOCITY / FLAG_EMIT_COLOR / FLAG_EMIT_CUSTOMБитовые флаги данных для emit_subparticle().

Только start()

RESTART_POSITION, RESTART_ROT_SCALE, RESTART_VELOCITY, RESTART_COLOR и RESTART_CUSTOM имеют тип bool. true означает, что соответствующие данные нужно инициализировать; при переданном флаге ручной/sub-эмиссии связанное значение уже присутствует. Проверяйте каждый флаг отдельно: наличие позиции не означает наличия скорости или цвета.

Только process()

ЗначениеНазначение
in bool RESTARTПервый шаг после рождения/перезапуска.
in bool COLLIDEDЕсть контакт с коллайдером GPU-частиц.
in vec3 COLLISION_NORMALНормаль контакта; без контакта нулевая.
in float COLLISION_DEPTHГлубина проникновения, не длина нормализованной нормали.
in vec3 ATTRACTOR_FORCEНакопленная сила аттракторов. Самостоятельно применяйте её в формуле скорости, если она нужна.

Эти контакты не создают обычные сигналы PhysicsBody3D для каждой частицы. Собственный particle-шейдер должен задать реакцию: наличие COLLIDED само по себе не является готовым отскоком.

Sub-emitter: флаги и результат

Дополнительная встроенная функция bool emit_subparticle(mat4 xform, vec3 velocity, vec4 color, vec4 custom, uint flags) ставит частицу в очередь связанного sub-emitter. Она доступна в start()/process(). Обычные пользовательские функции языка шейдеров тоже разрешены; это не запрет писать свои функции.

Создайте оба GpuParticles3D в дереве, задайте первому SubEmitter = GetPathTo(secondary), а второму — собственный ProcessMaterial и меш. Цель в примере не испускает частицы автоматически. Полный шейдер источника делает одну попытку на RESTART: зелёный означает принятую эмиссию, красный — отказ.

Шейдер
shader_type particles;
render_mode disable_velocity;
void start() {
    TRANSFORM = EMISSION_TRANSFORM;
    VELOCITY = vec3(0.0);
    COLOR = vec4(1.0, 0.0, 0.0, 1.0);
}
void process() {
    if (RESTART) {
        mat4 child = TRANSFORM;
        child[3].x += 1.0;
        uint flags = FLAG_EMIT_POSITION | FLAG_EMIT_ROT_SCALE |
            FLAG_EMIT_VELOCITY | FLAG_EMIT_COLOR | FLAG_EMIT_CUSTOM;
        bool accepted = emit_subparticle(child, vec3(0.0),
            vec4(0.0, 0.0, 1.0, 1.0), vec4(0.0), flags);
        COLOR = accepted ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
    }
}

Флаги определяют, какие компоненты цели уже заданы. Без FLAG_EMIT_COLOR её RESTART_COLOR будет true, и целевой start() должен выбрать цвет. true от emit_subparticle означает принятие очередью, а не гарантию видимого пикселя: проверьте материал, позицию, границы и ёмкость цели. Функция не создаёт дочерний Entity на каждую частицу. См. настройку цепочки.

Диагностика

СимптомЧто проверить
Нет изображенияРаздельные ProcessMaterial/DrawPass, активность, меш, камеру и VisibilityAabb.
Цвет процесса не виденCOLOR в собственном материале или VertexColorUseAsAlbedo в StandardMaterial3D.
Ручной цвет/скорость пропадаютБезусловную перезапись в start() и соответствующие RESTART_*/флаги.
Движение вдвое быстрееАвтоматический VELOCITY вместе с ручным смещением TRANSFORM.
Возраст сбрасывается каждый шагИнициализацию счётчика в process() вместо start().
Данные MASS не сохраняютсяИспользуйте USERDATA/CUSTOM для постоянного состояния.
keep_data или disable_force не дают ожидаемого эффектаОграничения текущего RD в таблице режимов; задайте алгоритм явно.
Sub-emitter ничего не показываетОтносительный путь, созданную цель, ёмкость, bool результата, флаги и материал цели.

Проверены четыре полных шейдера и C#-помощник на Windows Forward+/Vulkan: движение, COLOR/CUSTOM, накопление USERDATA, остановка скорости, ACTIVE, ручная эмиссия и sub-emitter, текущие keep_data/disable_force и MASS. Настройки шага проверены на приёме значений, не как полный анализ точности интеграции. Все столкновения/масштабы, trails, 2D, прочие платформы/рендереры и производительность не имеют здесь полной runtime-проверки. Маленькие диагностические чтения пикселей не публикуются как художественные иллюстрации.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.