reckfellENGINE

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
C#
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
C#
using Reckfell;

public partial class CountedMesh3D : MeshInstance3D
{
    public int ProcessTicks { get; private set; }
    public override void _Process(double delta) => ProcessTicks++;
}
OptimizationLab3D.cs
C#
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

Ближняя сфера с автоматически выбранным LOD и счётчиком примитивов

Дальняя сфера: меньше переданных треугольников

На проверенной конфигурации 512×384 получены одинаковые числа в Compatibility/OpenGL и Forward+/Vulkan:

КонфигурацияПримитивы
MeshLodThreshold=0, полная геометрия4224
Порог 1, камера на Z=61056
Порог 1, камера на Z=60132

Ноль отключает выбор упрощённых уровней. Он вернул полную геометрию и на дальней дистанции. Увеличение 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 прежний: профилируйте остальные затраты; уменьшение треугольников не останавливает логику и физику.

ImporterMesh API · Occlusion culling

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

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

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

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