reckfellENGINE

Mesh LOD: fewer distant triangles

Prepare index LODs, measure renderer selection and persist a mesh with its LOD data.

On this page

Separate LOD from visibility

Automatic mesh LOD selects a shorter index array of the same mesh. The object remains in the world while fewer triangles are submitted. This differs from switching near/far models: no separate entity is created for each level.

LOD does not stop _Process, change collision or unload resources. It helps geometry-related costs; scripts, physics and every material cost do not disappear automatically.

Run the complete example

Complete C# setup. Use SDK 1.0.0, net10.0 and an Entity3D-rooted .rscene level. Save all three files below, run dotnet build and attach OptimizationLab3D.cs to the root. Select Preview=MeshLod before startup. Code creates the camera, sphere and counter without external assets.

The △ counter displays PrimitivesInFrame for the Visible pass. All 3D meshes in this fixture use triangles, so it counts triangles here. It is not FPS, entity count or memory usage.

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);
    }
}

Verify LOD generation

GeneratedLodMesh adds the indexed sphere arrays as an ImporterMesh surface and calls GenerateLods before the first GetMesh. GetSurfaceLodCount should exceed 0; additional index arrays should be shorter than the base and divisible by 3.

Source skips surfaces without indices. Calling GenerateLods alone does not establish success. normalMergeAngle is in degrees, 60 in the example; current implementation does not use normalSplitAngle. The static sphere passes an empty bone-transform array.

GetMesh caches an ArrayMesh. Do not retrieve it early expecting repeated reads to rebuild it automatically after late LOD generation. Assign the prepared resource to Target.Mesh.

Measure renderer selection

Near sphere with automatically selected LOD and primitive counter

Distant sphere submitting fewer triangles

The checked 512×384 configuration produces identical counts in Compatibility/OpenGL and Forward+/Vulkan:

ConfigurationPrimitives
MeshLodThreshold=0, full geometry4224
Threshold 1, camera Z=61056
Threshold 1, camera Z=60132

Zero disables simplified-level selection and restores full geometry even at a distance. Raising instance LodBias from 1 to 8 retains more distant detail.

GetSurfaceLodSize is an error/scale metric, not a fixed switching distance in meters. Projection, viewport dimensions, object scale and threshold also affect selection. Do not reuse one test mesh’s thresholds across assets without checking silhouette and motion.

For comparisons, use the same GetRenderInfo Visible pass and wait for frame updates after changes. Do not mix Canvas/Shadow counters or present fewer primitives as guaranteed FPS gains.

Persist and use the result

Save the prepared ArrayMesh through ResourceSaver to .rdata. Check Error.Ok and reload: the native test reloads without cache and obtains the same distant LOD through actual rendering. Saving only base vertices is not equivalent to preserving the resource’s LOD data.

Generation performs geometry work. Prepare production resources ahead of time rather than simplifying every frame. The example verifies a static procedural sphere in the current local build, not imported character, bone, blend-shape, UV-seam or export-template quality.

A separate StaticBody3D/SphereShape3D supplies collision. A ray still hits it at distant LOD, and CountedMesh3D continues _Process. Visual index selection is not physics LOD.

Diagnosis

  • Primitive count stays constant: check LOD data, MeshLodThreshold, LodBias, distance and the selected viewport.
  • GetSurfaceLodCount is 0: check indexed triangle input and preparation order.
  • Mesh looks damaged: inspect silhouette, normals and material; shorter indices alone do not prove acceptable quality.
  • FPS is unchanged: profile other costs; fewer triangles do not stop logic or physics.

ImporterMesh API · Occlusion culling

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.