reckfellENGINE

MultiMeshInstance3D: много экземпляров одного меша

Поле из 64 кубов, порядок выделения, цвета и редакторский Populate Surface.

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

Запустить поле

Добавьте GeometryStage3D.cs и MultiMeshFieldLab3D.cs. Прикрепите MultiMeshFieldLab3D к корню Entity3D отдельного C#-уровня 1.0.0, задайте viewport 640×420, соберите и нажмите F6. Отобразятся 64 куба общей геометрии с разной высотой и цветом. UseBulkUpload изначально выключен; другой путь записи разобран в руководстве по буферу.

64 экземпляра одного BoxMesh с индивидуальными преобразованиями и цветами

GeometryStage3D.cs
C#
using Reckfell;

public static class GeometryStage3D
{
    public static StandardMaterial3D Material(bool vertexColors = false, bool unshaded = false)
    {
        return new StandardMaterial3D
        {
            AlbedoColor = vertexColors ? Colors.White : new Color(0.24f, 0.44f, 1),
            VertexColorUseAsAlbedo = vertexColors,
            ShadingMode = unshaded ? BaseMaterial3D.ShadingModeEnum.Unshaded : BaseMaterial3D.ShadingModeEnum.PerPixel,
            Roughness = 0.8f
        };
    }

    public static void Setup(Entity3D root, string ru, string en)
    {
        root.AddChild(new WorldEnvironment { Environment = new Reckfell.Environment
        {
            BackgroundMode = Reckfell.Environment.BGMode.Color,
            BackgroundColor = new Color(0.035f, 0.04f, 0.05f),
            AmbientLightSource = Reckfell.Environment.AmbientSource.Color,
            AmbientLightColor = Colors.White, AmbientLightEnergy = 0.5f
        }});
        root.AddChild(new DirectionalLight3D { RotationDegrees = new Vector3(-55, -25, 0), LightEnergy = 1.2f });
        root.AddChild(new MeshInstance3D
        {
            Mesh = new BoxMesh { Size = new Vector3(8, 0.05f, 6) }, Position = new Vector3(0, -0.12f, 0),
            MaterialOverride = new StandardMaterial3D { AlbedoColor = new Color(0.10f, 0.11f, 0.13f), Roughness = 1 }
        });
        var camera = new Camera3D { Position = new Vector3(4, 4.8f, 6), Current = true, Fov = 45 };
        root.AddChild(camera); camera.LookAt(new Vector3(0, 0.35f, 0), Vector3.Up);
        var canvas = new CanvasLayer(); root.AddChild(canvas);
        var label = new Label { Position = new Vector2(24, 20), Text = TranslationServer.GetLocale().StartsWith("ru") ? ru : en };
        label.AddThemeFontSizeOverride("font_size", 22); canvas.AddChild(label);
    }
}
MultiMeshFieldLab3D.cs
C#
using System;
using Reckfell;

public partial class MultiMeshFieldLab3D : Entity3D
{
    [Export] public int Count { get; set; } = 64;
    [Export] public bool UseBulkUpload { get; set; }
    public MultiMesh Field { get; private set; } = null!;
    private MultiMeshInstance3D _visual = null!;

    public static Transform3D TransformAt(int index)
    {
        float height = 0.35f + (index % 5) * 0.12f;
        var basis = Basis.Identity.Scaled(new Vector3(0.32f, height, 0.32f));
        return new Transform3D(basis, new Vector3((index % 8 - 3.5f) * 0.5f, height / 2, (index / 8 - 3.5f) * 0.5f));
    }

    public static Color ColorAt(int index) => index % 4 == 0 ? Colors.White : new Color(0.24f, 0.44f, 1);
    public static Color DataAt(int index) => new(index / 255.0f, 0, 0, 1);

    public static float[] BuildBuffer(int count)
    {
        if (count < 1 || count > 256) throw new ArgumentOutOfRangeException(nameof(count));
        float[] buffer = new float[count * 20];
        for (int i = 0; i < count; i++)
        {
            Transform3D t = TransformAt(i); Color color = ColorAt(i), data = DataAt(i); int p = i * 20;
            buffer[p] = t.Basis.Row0.X; buffer[p + 1] = t.Basis.Row0.Y; buffer[p + 2] = t.Basis.Row0.Z; buffer[p + 3] = t.Origin.X;
            buffer[p + 4] = t.Basis.Row1.X; buffer[p + 5] = t.Basis.Row1.Y; buffer[p + 6] = t.Basis.Row1.Z; buffer[p + 7] = t.Origin.Y;
            buffer[p + 8] = t.Basis.Row2.X; buffer[p + 9] = t.Basis.Row2.Y; buffer[p + 10] = t.Basis.Row2.Z; buffer[p + 11] = t.Origin.Z;
            buffer[p + 12] = color.R; buffer[p + 13] = color.G; buffer[p + 14] = color.B; buffer[p + 15] = color.A;
            buffer[p + 16] = data.R; buffer[p + 17] = data.G; buffer[p + 18] = data.B; buffer[p + 19] = data.A;
        }
        return buffer;
    }

    public Error Rebuild(int count, bool bulk)
    {
        if (_visual is null) return Error.Unconfigured;
        if (count < 1 || count > 256) return Error.InvalidParameter;
        _visual.Multimesh = null;
        Field?.Dispose();
        Field = new MultiMesh
        {
            TransformFormat = MultiMesh.TransformFormatEnum.Transform3D,
            UseColors = true, UseCustomData = true,
            Mesh = new BoxMesh { Size = Vector3.One, Material = GeometryStage3D.Material(vertexColors: true) },
            InstanceCount = count
        };
        if (bulk) Field.Buffer = BuildBuffer(count);
        else for (int i = 0; i < count; i++)
        {
            Field.SetInstanceTransform(i, TransformAt(i));
            Field.SetInstanceColor(i, ColorAt(i));
            Field.SetInstanceCustomData(i, DataAt(i));
        }
        int rows = (count + 7) / 8;
        Field.CustomAabb = new Aabb(new Vector3(-2, -0.05f, -2), new Vector3(4, 1.5f, Math.Max(4, rows * 0.5f)));
        Field.VisibleInstanceCount = -1;
        _visual.Multimesh = Field;
        Count = count; UseBulkUpload = bulk;
        return Error.Ok;
    }

    public override void _Ready()
    {
        GeometryStage3D.Setup(this, "MultiMesh: общая геометрия, свои данные", "MultiMesh: shared geometry, per-instance data");
        _visual = new MultiMeshInstance3D(); AddChild(_visual);
        if (Rebuild(Count, UseBulkUpload) != Error.Ok) ReckfellApi.PrintErr("MultiMesh count must be 1..256");
    }
}

MultiMesh хранит данные экземпляров. MultiMeshInstance3D показывает ресурс через свойство Multimesh — именно так оно называется в C# SDK. Каждый элемент не является отдельной Entity, не получает автоматически скрипт, физику или собственный Mesh. Для разных геометрий/материалов разделяйте группы.

Порядок настройки

TransformFormat, UseColors и UseCustomData задавайте при InstanceCount = 0. Затем назначьте Mesh и выделите InstanceCount. При ненулевом количестве смена этих настроек отклоняется движком. VisibleInstanceCount не меняет ёмкость: −1 показывает все элементы, 0 — ни одного, N — первые N.

SetInstanceTransform задаёт преобразование в локальном пространстве MultiMeshInstance3D, SetInstanceColor — цвет. Материал примера включает VertexColorUseAsAlbedo. INSTANCE_CUSTOM содержит дополнительные значения для своего шейдера; стандартный материал не превращает эти данные в анимацию сам.

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

Разместить по поверхности в редакторе

В проверенных исходниках редактора инструмент находится в меню MultiMesh выбранной MultiMeshInstance3D: Populate Surface, диалог Populate MultiMesh. Подготовьте MeshInstance3D с геометрией поверхности и MeshInstance3D с исходным мешем. В Target Surface выберите поверхность, в Source Mesh — источник. Пути разрешаются относительно выбранной MultiMeshInstance3D, например ../Ground и ../Source. Скрытый Source можно использовать как источник ресурса, чтобы не показывать лишнюю копию.

Mesh Up Axis выбирает исходную верхнюю ось; для обычного BoxMesh это Y. Amount задаёт количество, Scale — масштаб; Random Rotation/Tilt/Scale добавляют разброс. Начните с малого Amount и нулевого разброса. Target должен иметь треугольные грани; пустой Mesh или неверный путь вызывают ошибку. Если Source Mesh не указан, инструмент пытается взять Mesh текущего MultiMesh.

Populate создаёт распределение по площадям граней и назначает новый MultiMesh выбранной Entity. Проверенный метод не регистрирует собственный шаг Undo/Redo; сохраните прежний ресурс отдельно, если его нужно оставить. Это не детерминированная генерация по сохранённому seed и не добавление физических объектов. Последовательность меню сверена с исходниками; действия мышью в нативном редакторе здесь ещё не проверены. Программный вариант выше проверен в runtime и реальной отрисовке.

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

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

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

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