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, отдельными материалами моделирования/отрисовки и функцией ручного выпуска:
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-проверки. Маленькие диагностические чтения пикселей не публикуются как художественные иллюстрации.