Mesh LOD: меньше треугольников вдали
Подготовить индексные уровни детализации, измерить выбор renderer и сохранить ресурс с LOD.
Отделить LOD от видимости
Автоматический mesh LOD выбирает более короткий индексный массив того же меша. Объект остаётся в мире, но в отрисовку передаётся меньше треугольников. Это другой механизм, чем переключение ближней и дальней моделей: здесь не создаются отдельные Entity для каждого уровня.
LOD не останавливает _Process, не меняет коллизию и не выгружает ресурс. Он полезен, когда стоимость действительно связана с геометрией; время скриптов, физики и всех материалов само по себе не исчезает.
Запустить полный пример
Выполните настройку C#. Нужны SDK 1.0.0, net10.0 и уровень .rscene с Entity3D-корнем. Сохраните три файла ниже, выполните dotnet build и назначьте OptimizationLab3D.cs корню. До запуска выберите Preview=MeshLod. Камера, сфера и счётчик создаются кодом; внешние ассеты не нужны.
Счётчик △ показывает PrimitivesInFrame для Visible-прохода. В этом уровне все 3D-меши треугольные, поэтому он соответствует числу треугольников. Это не FPS, число Entity или расход памяти.
GeneratedLodMesh.cs
using Reckfell;
public static class GeneratedLodMesh
{
public static ImporterMesh Source()
{
var sphere = new SphereMesh { Radius = 1, Height = 2, RadialSegments = 64, Rings = 32 };
var source = new ImporterMesh();
source.AddSurface(Mesh.PrimitiveType.Triangles, sphere.SurfaceGetArrays(0));
source.GenerateLods(60, 0, new Reckfell.Collections.Array());
return source;
}
}CountedMesh3D.cs
using Reckfell;
public partial class CountedMesh3D : MeshInstance3D
{
public int ProcessTicks { get; private set; }
public override void _Process(double delta) => ProcessTicks++;
}OptimizationLab3D.cs
using Reckfell;
public partial class OptimizationLab3D : Entity3D
{
public enum PreviewMode { MeshLod, Occlusion }
[Export] public PreviewMode Preview { get; set; } = PreviewMode.MeshLod;
public CountedMesh3D Target { get; private set; } = null!;
public Camera3D Camera { get; private set; } = null!;
public MeshInstance3D Wall { get; private set; } = null!;
public OccluderInstance3D Occluder { get; private set; } = null!;
private Label counter = null!;
public override void _Ready()
{
var material = new StandardMaterial3D
{
AlbedoColor = Colors.White,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
};
Target = new CountedMesh3D { Mesh = GeneratedLodMesh.Source().GetMesh(), MaterialOverride = material };
AddChild(Target);
var body = new StaticBody3D();AddChild(body);
body.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = 1 } });
Wall = new MeshInstance3D
{
Mesh = new BoxMesh { Size = new Vector3(5, 5, 0.2f) },
Position = new Vector3(0, 0, 2), Visible = Preview == PreviewMode.Occlusion,
MaterialOverride = new StandardMaterial3D
{
AlbedoColor = new Color(0.08f, 0.25f, 1),
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
}
};
Occluder = new OccluderInstance3D
{
Occluder = new QuadOccluder3D { Size = new Vector2(4.9f, 4.9f) },
Position = new Vector3(0, 0, 2), Visible = Preview == PreviewMode.Occlusion
};
AddChild(Wall);AddChild(Occluder);
Camera = new Camera3D { Current = true, Position = new Vector3(0, 0, 6), Fov = 60, Far = 200 };
AddChild(Camera);
var canvas = new CanvasLayer();AddChild(canvas);
counter = new Label { Position = new Vector2(12, 12) };canvas.AddChild(counter);
GetViewport().UseOcclusionCulling = Preview == PreviewMode.Occlusion;
}
public override void _Process(double delta)
{
counter.Text = "△ " + GetViewport().GetRenderInfo(Viewport.RenderInfoType.Visible, Viewport.RenderInfo.PrimitivesInFrame);
}
}Проверить, что LOD действительно создан
GeneratedLodMesh берёт индексные массивы сферы и добавляет поверхность ImporterMesh. GenerateLods вызывается до первого GetMesh. Затем GetSurfaceLodCount должен быть больше 0, а дополнительные индексные массивы — короче базового и иметь длину, кратную 3.
По исходникам поверхности без индексов не получают эти уровни. Сам вызов GenerateLods не доказывает успех. Параметр normalMergeAngle задаётся в градусах, в примере 60; normalSplitAngle текущая реализация не использует. Для статической сферы массив преобразований костей пуст.
GetMesh кэширует ArrayMesh. Не получайте меш заранее в ожидании, что повторное чтение автоматически пересоберёт его после поздней генерации LOD. Готовый ресурс назначается Target.Mesh.
Измерить выбор renderer
На проверенной конфигурации 512×384 получены одинаковые числа в Compatibility/OpenGL и Forward+/Vulkan:
| Конфигурация | Примитивы |
|---|---|
| MeshLodThreshold=0, полная геометрия | 4224 |
| Порог 1, камера на Z=6 | 1056 |
| Порог 1, камера на Z=60 | 132 |
Ноль отключает выбор упрощённых уровней. Он вернул полную геометрию и на дальней дистанции. Увеличение LodBias экземпляра с 1 до 8 сохранило больше деталей вдали.
GetSurfaceLodSize возвращает метрику ошибки/масштаба, а не фиксированную дистанцию переключения в метрах. На выбор также влияют проекция, размер viewport, масштаб объекта и порог. Не переносите пороги одного тестового меша на все ассеты без проверки силуэта и движения.
Для своего сравнения используйте GetRenderInfo с одним и тем же Visible-проходом и дождитесь обновления кадров после изменения. Не смешивайте его с Canvas/Shadow и не объявляйте уменьшение примитивов гарантированным приростом FPS.
Сохранить и использовать
Полученный ArrayMesh можно сохранить через ResourceSaver в .rdata. Проверяйте Error.Ok и загрузку: нативный тест загрузил ресурс без кэша и повторно получил тот же дальний LOD по фактической отрисовке. Сохранение только исходных вершин без LOD-данных не равно сохранению готового ресурса с уровнями.
Генерация требует работы с геометрией. Для готового проекта подготавливайте ресурс заранее, а не повторяйте упрощение на каждом кадре. Пример проверяет статическую процедурную сферу в текущей локальной сборке; качество импортированных персонажей, костей, blend shapes, UV-швов и экспортных сборок отдельно не подтверждается.
Физическая сфера создана отдельным StaticBody3D с SphereShape3D. Луч продолжил попадать в неё при дальнем LOD, а CountedMesh3D продолжил _Process. Изменение визуальных индексов не является физическим LOD.
Диагностика
- Число примитивов не меняется: проверьте наличие LOD, MeshLodThreshold, LodBias, дистанцию и выбранный viewport.
- GetSurfaceLodCount равен 0: проверьте индексную треугольную поверхность и порядок подготовки данных.
- Меш выглядит повреждённым: оцените силуэт, нормали и материал; меньший массив индексов сам по себе не доказывает приемлемое качество.
- FPS прежний: профилируйте остальные затраты; уменьшение треугольников не останавливает логику и физику.