MultiMeshInstance3D: много экземпляров одного меша
Поле из 64 кубов, порядок выделения, цвета и редакторский Populate Surface.
Запустить поле
Добавьте GeometryStage3D.cs и MultiMeshFieldLab3D.cs. Прикрепите MultiMeshFieldLab3D к корню Entity3D отдельного C#-уровня 1.0.0, задайте viewport 640×420, соберите и нажмите F6. Отобразятся 64 куба общей геометрии с разной высотой и цветом. UseBulkUpload изначально выключен; другой путь записи разобран в руководстве по буферу.
GeometryStage3D.cs
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
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 и реальной отрисовке.