GPU-частицы: движение визуальной стаи
Процессный шейдер меняет TRANSFORM, а шейдер сетки читает CUSTOM как INSTANCE_CUSTOM.
Что меняется по сравнению с MultiMesh
В общем примере задайте ParticleMotion=true до запуска. GpuParticles3D управляет временем жизни и экземплярами, process-шейдер задаёт их Transform на GPU. Вершинный шейдер из предыдущего руководства отдельно двигает хвост.
ParticleSchool3D.cs
using Reckfell;
public partial class ParticleSchool3D : GpuParticles3D
{
public ShaderMaterial SwimMaterial { get; private set; } = null!;
public ShaderMaterial MotionMaterial { get; private set; } = null!;
public bool TryBuild(int count)
{
if (count < 1 || count > 8192) return false;
var layout = SchoolMesh.Layout(count);
SwimMaterial = new ShaderMaterial { Shader = new Shader { Code = SchoolShaders.Swim } };
MotionMaterial = new ShaderMaterial { Shader = new Shader { Code = SchoolShaders.Motion } };
MotionMaterial.SetShaderParameter("columns", layout.Columns);
MotionMaterial.SetShaderParameter("rows", layout.Rows);
Amount = count;
Lifetime = 60;
Explosiveness = 1;
FixedFps = 30;
UseFixedSeed = true;
Seed = 42;
LocalCoords = true;
VisibilityAabb = layout.Bounds.Grow(0.5f);
DrawPass1 = SchoolMesh.Create();
MaterialOverride = SwimMaterial;
ProcessMaterial = MotionMaterial;
CastShadow = ShadowCastingSetting.Off;
Emitting = true;
Restart();
return true;
}
public void SetMotion(float clock, float amplitude)
{
MotionMaterial.SetShaderParameter("clock", clock);
SwimMaterial.SetShaderParameter("clock", clock);
SwimMaterial.SetShaderParameter("amplitude", Mathf.Clamp(amplitude, 0, 0.4f));
}
}Полный process-шейдер
shader_type particles;
render_mode disable_force, disable_velocity;
uniform float clock = 0.0;
uniform int columns = 32;
uniform int rows = 16;
void start() {
CUSTOM = vec4(fract(float(INDEX) * 0.6180339), 0.5, 0.0, 1.0);
COLOR = vec4(1.0);
}
void process() {
float phase = CUSTOM.x * TAU;
float x = (float(INDEX % uint(columns)) - float(columns - 1) * 0.5) * 0.85;
float y = (float(INDEX / uint(columns)) - float(rows - 1) * 0.5) * 0.6;
TRANSFORM = mat4(vec4(0.6, 0.0, 0.0, 0.0), vec4(0.0, 0.6, 0.0, 0.0),
vec4(0.0, 0.0, 0.6, 0.0),
vec4(x + sin(clock + phase) * 0.35, y + cos(clock + phase) * 0.12, 0.0, 1.0));
}start задаёт фазу CUSTOM для слота INDEX, process записывает положение в TRANSFORM. На стороне сетки CUSTOM доступен как INSTANCE_CUSTOM. NUMBER и INDEX не следует считать постоянными идентификаторами игровых объектов: частицы перезапускаются и переиспользуют слоты.
disable_velocity отключает дополнительное перемещение по VELOCITY: положение уже вычисляется шейдером. clock передаётся из C#; в примере нет индивидуального поиска пути, столкновений, поведения стаи или доступа к каждой позиции из игровой логики.
Настройки и границы
DrawPass1 содержит собственную сетку, ProcessMaterial — particles-шейдер, MaterialOverride — spatial-шейдер её изображения. UseFixedSeed и Seed помогают воспроизводимости; наш процессный шейдер дополнительно использует детерминированный INDEX. Lifetime=60 и Explosiveness=1 задают запуск группы, а не бесконечную жизнь игровых персонажей.
VisibilityAabb включает область движения и запас. CaptureAabb читает состояние GPU и может отставать на несколько кадров; в Compatibility оценка дополнительно расширяется размером сетки. Не вызывайте его каждый кадр ради обычного отсечения и не считайте снимок гарантией для всех будущих положений и vertex-деформаций.
Проверить выбранный рендерер
Эта версия примера отрисована в Compatibility/OpenGL. Это не означает равную поддержку всех функций частиц: в этом пути не поддержаны trails, sub-emitters и ручной EmitParticle. Отдельные драйверные ограничения также могут отключать GPU-частицы. Для другой платформы проверяйте нужные функции и экспорт отдельно.