reckfellENGINE

Много объектов: MultiMesh или GPU-частицы

Полный проектный пример с двумя способами GPU-анимации и явными границами применения.

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

Выбрать подход

Нужна задачаПодход
Много копий одной геометрии с заданными позициямиMultiMesh
Визуальное движение множества экземпляров на GPUGpuParticles3D с process-шейдером
Индивидуальная логика, взаимодействие и физическое телоОтдельные Entity либо отдельная система игровой симуляции

Инстанс на GPU не становится Entity с callback, коллизией или сетевым ID. Пример показывает визуальную стаю, а не реализацию поведения тысяч игровых персонажей. Число 8192 — защитный предел этого примера, не лимит движка.

Запустить оба режима

Нужен C#-проект с Reckfell.NET.Sdk 1.0.0, net10.0 и рендерером Compatibility. Сохраните пять файлов ниже, выполните dotnet build, создайте уровень с корневым Entity3D и прикрепите SchoolShowcase.cs.

Сохраните уровень как school.rscene и запустите. По умолчанию видна стая из 512 рыб. Count меняет число экземпляров; ParticleMotion=false использует MultiMesh, true — GPU-частицы. Настройки задаются до запуска. На снимках ниже Count=2048; это реальные кадры движка, не результат замера FPS.

Реальная отрисовка 2 048 экземпляров

SchoolShaders.cs
C#
public static class SchoolShaders
{
    public const string Swim = """
shader_type spatial;
render_mode unshaded, cull_disabled;
uniform float clock = 0.0;
uniform float amplitude = 0.22;
varying float shade;
void vertex() {
    float phase = INSTANCE_CUSTOM.x * TAU;
    float tail = clamp((0.6 - VERTEX.x) / 1.2, 0.0, 1.0);
    VERTEX.y += sin(clock * (2.0 + INSTANCE_CUSTOM.y) + phase + VERTEX.x * 4.0)
        * amplitude * tail * tail;
    shade = 0.65 + INSTANCE_CUSTOM.x * 0.35;
}
void fragment() {
    ALBEDO = vec3(shade * 0.84, shade * 0.91, shade);
}
""";

    public const string Motion = """
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));
}
""";
}
SchoolMesh.cs
C#
using Reckfell;

public static class SchoolMesh
{
    public static ArrayMesh Create()
    {
        Vector3 head = new(0.6f, 0, 0);
        Vector3 upper = new(0.05f, 0.18f, 0);
        Vector3 lower = new(0.05f, -0.18f, 0);
        Vector3 tail = new(-0.35f, 0, 0);
        Vector3 finUp = new(-0.6f, 0.24f, 0);
        Vector3 finDown = new(-0.6f, -0.24f, 0);
        Vector3[] vertices = { head, upper, tail, head, tail, lower, tail, finUp, finDown };
        using var arrays = new Reckfell.Collections.Array();
        arrays.Resize((int)Mesh.ArrayType.Max);
        arrays[(int)Mesh.ArrayType.Vertex] = vertices;
        var mesh = new ArrayMesh();
        mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
        return mesh;
    }

    public static (int Columns, int Rows, Aabb Bounds) Layout(int count)
    {
        int columns = Mathf.CeilToInt(Mathf.Sqrt(count * 2));
        int rows = (count + columns - 1) / columns;
        float width = columns * 0.85f + 2;
        float height = rows * 0.6f + 2;
        return (columns, rows, new Aabb(new Vector3(-width / 2, -height / 2, -1),
            new Vector3(width, height, 2)));
    }
}
VertexSchool3D.cs
C#
using Reckfell;

public partial class VertexSchool3D : MultiMeshInstance3D
{
    public ShaderMaterial SwimMaterial { 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 } };
        var data = new MultiMesh
        {
            TransformFormat = MultiMesh.TransformFormatEnum.Transform3D,
            UseCustomData = true,
            Mesh = SchoolMesh.Create(),
            InstanceCount = count,
            CustomAabb = layout.Bounds
        };
        for (int i = 0; i < count; i++)
        {
            float x = (i % layout.Columns - (layout.Columns - 1) * 0.5f) * 0.85f;
            float y = (i / layout.Columns - (layout.Rows - 1) * 0.5f) * 0.6f;
            data.SetInstanceTransform(i, new Transform3D(Basis.Identity.Scaled(Vector3.One * 0.6f),
                new Vector3(x, y, 0)));
            data.SetInstanceCustomData(i, new Color(Mathf.PosMod(i * 0.6180339f, 1), 0.5f, 0, 1));
        }
        Multimesh = data;
        MaterialOverride = SwimMaterial;
        CastShadow = ShadowCastingSetting.Off;
        return true;
    }

    public void SetMotion(float clock, float amplitude)
    {
        SwimMaterial.SetShaderParameter("clock", clock);
        SwimMaterial.SetShaderParameter("amplitude", Mathf.Clamp(amplitude, 0, 0.4f));
    }
}
ParticleSchool3D.cs
C#
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));
    }
}
SchoolShowcase.cs
C#
using Reckfell;

public partial class SchoolShowcase : Entity3D
{
    [Export] public bool ParticleMotion { get; set; }
    [Export(PropertyHint.Range, "1,8192,1")] public int Count { get; set; } = 512;
    [Export] public bool Animate { get; set; } = true;
    public VertexSchool3D? Vertices { get; private set; }
    public ParticleSchool3D? Particles { get; private set; }
    public Camera3D Camera { get; private set; } = null!;
    private float _clock;

    public override void _Ready()
    {
        if (Count < 1 || Count > 8192)
        {
            ReckfellApi.PushError("SchoolShowcase.Count must be between 1 and 8192.");
            return;
        }
        var layout = SchoolMesh.Layout(Count);
        Camera = new Camera3D { Projection = Camera3D.ProjectionType.Orthogonal,
            Position = new Vector3(0, 0, 20), Current = true };
        AddChild(Camera);
        Vector2 size = GetViewport().GetVisibleRect().Size;
        float aspect = size.Y > 0 ? size.X / size.Y : 1;
        Camera.Size = Mathf.Max(layout.Bounds.Size.Y, layout.Bounds.Size.X / aspect);
        if (ParticleMotion)
        {
            Particles = new ParticleSchool3D();
            AddChild(Particles);
            Particles.TryBuild(Count);
        }
        else
        {
            Vertices = new VertexSchool3D();
            AddChild(Vertices);
            Vertices.TryBuild(Count);
        }
        SetMotion(0, 0.22f);
    }

    public override void _Process(double delta)
    {
        if (!Animate) return;
        _clock += (float)delta;
        SetMotion(_clock, 0.22f);
    }

    public void SetMotion(float clock, float amplitude)
    {
        Vertices?.SetMotion(clock, amplitude);
        Particles?.SetMotion(clock, amplitude);
    }
}

Ограничения примера

Шейдеры содержатся в строках SchoolShaders.cs, поэтому здесь не нужны внешние модели, текстуры или файлы шейдера. Рыба — собственная плоская сетка из трёх треугольников. Для другой геометрии пересчитайте масштаб, ось движения и границы.

Камера подбирает ортографический размер при _Ready по текущему Viewport. При последующем изменении пропорций окна пересчитайте размер отдельно. Animate=false позволяет менять время через SetMotion для воспроизводимого кадра.

Вершинная анимация · Движение частиц · Найти узкое место

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

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

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

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