MultiMesh: буфер, границы и цена обновления
Упаковка 3D-матрицы по строкам, 20 float на экземпляр и ограничения группового отображения.
Запустить bulk-запись
Добавьте GeometryStage3D.cs и MultiMeshFieldLab3D.cs, прикрепите MultiMeshFieldLab3D к Entity3D-корню C#-уровня 1.0.0 и включите UseBulkUpload. Viewport — 640×420. После сборки и F6 поле должно выглядеть так же, как при отдельных SetInstance-вызовах: 64 куба с теми же преобразованиями и цветами.
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");
}
}Формат одного элемента
В текущем API 3D-преобразование занимает 12 float. При UseColors и UseCustomData добавляются по четыре: в примере stride = 20. Размер Buffer равен InstanceCount × stride. Неверный размер отклоняется, а не автоматически преобразуется движком.
| Смещение внутри элемента | Содержимое |
|---|---|
| 0..3 | Basis.Row0.X/Y/Z, Origin.X |
| 4..7 | Basis.Row1.X/Y/Z, Origin.Y |
| 8..11 | Basis.Row2.X/Y/Z, Origin.Z |
| 12..15 | Цвет R/G/B/A, если UseColors |
| 16..19 | Custom R/G/B/A, если UseCustomData |
Это строки, не три подряд записанных столбца Basis. У 2D-формата преобразование занимает восемь float; не переносите его layout в этот 3D-буфер. В стенде проверены обратное чтение матрицы, цветов/custom data и совпадающее видимое поле после обоих вариантов загрузки.
Что bulk-запись меняет
BuildBuffer вычисляет данные на CPU, затем Buffer передаёт их целиком. Это сокращает число отдельных вызовов API, но всё равно выделяет массив и передаёт данные renderer. Getter Buffer может потребовать синхронизации/чтения обратно; не используйте его каждый кадр как бесплатное хранилище игры. Держите собственные CPU-данные, если они нужны постоянно.
В примере 1–256 элементов — выбранный предел стенда, не максимальная ёмкость API. Он не измеряет ускорение bulk-записи. Профилируйте одинаковое количество объектов, одинаковый кадр и целевую сборку; фиксируйте стоимость подготовки/передачи и GPU, а не только один вызов setter.
Отсечение и активные элементы
Отсечение использует общую границу MultiMesh. Одно огромное поле способно рисовать много невидимых элементов из-за попадания общей AABB в камеру. Разбивайте мир на группы подходящего размера и проверяйте результат измерениями. Количество материалов, поверхностей и проходов, включая тени, влияет на число вызовов отрисовки; обещание «всегда один draw call» неверно.
VisibleInstanceCount показывает непрерывный префикс, не произвольную маску живых объектов. Для плотного списка перемещайте активные данные в начало либо ведите несколько групп. Это не заменяет игровую идентичность Entity, физику или систему стриминга уровней.
Проверены размер/формат буфера, layout до выделения, индивидуальные и bulk-данные, CustomAabb, ёмкость и префикс видимости. Реальная отрисовка дополнительно проверяет одинаковый bulk-результат и исчезновение поля при VisibleInstanceCount = 0. Другие renderer, motion-vector/interpolation режимы и производительность пока не подтверждены этим стендом.