Level of detail

Usage guide of the render.lod kit: what it decides (mesh level by the size shown, culling, impostor, shadow, update rate), turning it on, the profiles, categories and tags with their defaults, the frame-time budget, per-entity tags and the passport, how each engine applies the levels, events, service, debug.

Source: module-kits:kits/render_lod/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs render-lodMCP tool: gripforge_docs { "topic": "render-lod" }

Level of detail in your game (render.lod) #

Decide, for every object of a game, which mesh level to show, whether to draw it at all, whether it casts a shadow and how often it updates, from one model that behaves the same on the web, Godot, Unity, Unreal and the native player. Readable offline: gripforge docs render-lod, or the MCP tool gripforge_docs (topic render-lod).

What it is for #

A model drawn far away should not cost what it costs up close. render.lod is the run-time half of that:

  • it decides: for each managed object and the position of the viewer, the level (0 is the full mesh), visible or culled, impostor or not, shadow or not, updated every how many ticks;
  • it does not draw and does not simplify: each engine applies the answer with its own mechanism, and the levels themselves come from the build (gripforge lod, see gripforge docs lod). With a single mesh the kit still culls, cuts shadows and slows updates.

It needs no other kit. It is separate from render.optimize (instancing, merging, culling off camera): the two do not depend on each other.

Turn it on #

render.lod is installed, with the defaults below, in every new game. Existing games receive it the next time they are opened. To add it, tune it or remove it on a game of your own:

gripforge_gamekit_install   { project_id: "gkp_…", id: "render.lod" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "render.lod", config: { "profile": "medium" } }
gripforge_gamekit_configure { project_id: "gkp_…", id: "render.lod", enabled: false }   # off, kept installed
gripforge_gamekit_remove    { project_id: "gkp_…", id: "render.lod" }

The same calls are in the MCP of your agent. You only write the keys that differ from the defaults. The full reference of the kit (config, service, events) is gripforge kit api render.lod.

What decides a level #

For an object at a given size on the screen, with its rule:

  • Level: the number of thresholds it has passed. By default (metric: screen) the thresholds are fractions of the height of the view the object covers (its bounding sphere over the height the view spans at its distance), so a big wall far away keeps its detail and a small barrier near loses it, at every resolution. metric: distance reads distances in metres instead.
  • Culling: beyond drawDistance, or smaller than minScreenSize.
  • Impostor: beyond impostorDistance, an object that has one shows it; one that has none keeps its last level.
  • Shadows: none beyond shadowDistance.
  • Update rate: beyond each updateDistances[i] the object updates every updateIntervals[i] ticks (animation, AI).
  • Instance budget: per category, only the maxInstances nearest visible instances stay.

Every threshold is a band (hysteresis, 0.1 by default) so an object at the edge does not flicker between two levels.

Rules: profiles, categories, tags #

One rule shape everywhere (-1 and [] mean "inherit"). The rule of an object is the chain built-in profile, then profiles[profile], then categories[category], then tags[tag] (alphabetical), then the rule of the model's own passport. scale multiplies along the chain; every other key replaces.

KeyMeaning
scalemultiplies every distance (divides the screen sizes)
distances / screenSizesmetres / fractions of the screen height at which level 1, 2, 3… start
drawDistancemetres; 0 = never culled by distance
minScreenSizefraction of the screen height under which the object is culled; 0 = off
impostorDistancemetres; 0 = no impostor
shadowDistancemetres; 0 = no limit
updateDistances, updateIntervalsmetres, and ticks between two updates beyond each
maxInstancesvisible instances of the category; 0 = unlimited
minLevel, maxLevelthe most and the least detailed level allowed

Built-in profiles (the player picks one in the Graphics tab when settings.graphics is installed):

ProfilescreenSizesdrawDistanceminScreenSizeshadowDistance
low0.5, 0.22, 0.12000.01535
medium0.4, 0.16, 0.073000.00870
high, custom0.3, 0.12, 0.054000.004120

By default vegetation uses scale 0.75, vehicles 1.25 and structures 2.5 (structures are never culled for their size); props and characters use the profile as it is.

An entity belongs to the category named by its description, else by a tag lod.<category> (props, vegetation, characters, vehicles, structures), else by the first category whose match list holds its kind or one of its tags. An entity that matches nothing is left alone (manageAll: true manages it as a prop). The entity tagged player and any entity tagged lod.off are never managed.

Settings and their defaults #

KeyDefaultMeaning
profilehighprofile before the player chooses one
metricscreenscreen or distance
viewercamerameasure from the camera (the player until the renderer gives it), or always from the player
fov60vertical field of view for screen sizes, until the renderer gives the camera's
hysteresis0.1width of the threshold bands
interval0.1seconds between two evaluations
followViewDistancetruedraw distances follow the player's view distance (settings.graphics)
manageAllfalsemanage entities that match no category, as props
playerOptiontrueshow the profile row in the Graphics tab
debugfalsepublish the stats variables and show the overlay
matchlists per categorykinds and tags that put an entity in a category
profiles, categories, tagsinheritrule overrides
budgetoffframe-time budget (below)

A forest game on modest machines:

{
  "profile": "medium",
  "categories": { "vegetation": { "scale": 0.6, "shadowDistance": 25, "maxInstances": 1500 } },
  "tags": { "landmark": { "drawDistance": 0, "maxLevel": 1 } },
  "budget": { "enabled": true, "targetMs": 16.7 }
}

Frame-time budget #

Off by default. With budget.enabled, the frame times are averaged over window frames: an average above targetMs * (1 + tolerance) lowers the scale of every distance by step, down to minScale; one below targetMs * (1 - headroom) raises it, up to maxScale. With budget.measure: false the engine measures nothing and only reportFrame(ms) feeds it (tests, replays), so the same inputs always give the same decisions.

Per object, per model #

  • Tags on an entity: lod.off (full mesh, never managed), lod.level.<n> (always level n), lod.<category>. At run time: forceEntity(id, n), forceEntity(id, 'off'), and forceEntity(id, null) to give it back to its size.
  • The passport of the model (section kits.lod, from gripforge lod and the Library): the levels of the model (ratios, fit), impostor (the model may be shown as an impostor), castShadows: false (never casts a shadow), and its own rule. A game overrides it key by key in its asset_passports document. See gripforge docs lod.

Where the levels come from #

The kit consumes levels, it makes none. Make them with gripforge lod (or the MCP tool gripforge_lod_build) when the asset arrives, then each engine finds them: on the web the bundle lists the levels of each slot and a level is loaded the first time it is asked; a Godot, Unity or Unreal project made by gripforge game-export carries them in the model it imports.

Engines #

The kit decides; each engine applies the levels baked into the model (extra meshes gf_lod.<mesh>.<k>, made by gripforge lod) with its own mechanism:

EngineHow the levels are switchedA level fixed by tag or forceEntity
Webchildren swapped per level, a level loaded the first time it is asked; visible and castShadow for culling and shadows; the animation mixer advances every updateEvery ticksthe level shown
Native playerthe same decisions, from the Rust modulethe level shown
Godotsibling mesh instances of the master, switched by the engine's visibility ranges: the kit's screen sizes become distances for the camera's field of view, the hysteresis band becomes the range marginthe only one shown, without range
Unitysibling renderers of the master switched by a LODGroup whose transition heights are the kit's screen sizesLODGroup.ForceLOD
Unrealthe LODs of the slot's static mesh, made at import with the screen sizes of the passport; the engine's own LOD selection switches them for every field of viewthe component's forced LOD

Godot and Unity show a child named Impostor when the model says so; the web and Unreal draw no impostor yet (the object keeps its last level, the decision is the same everywhere). Unreal meshes that use Nanite keep their own geometry level; the kit still culls, cuts shadows and slows updates.

Hooks, mods and events #

render.lod is a pure service: it decides nothing of the game, so it has no hook. A mod may override profile, metric, viewer, fov, hysteresis, interval, followViewDistance, manageAll, match, profiles, categories, tags and budget (not playerOption nor debug); the profile the player chose in the Graphics tab still wins over a modded one. Scripts and rules can listen to:

EventPayload
lod.changedentity, level, visible, shadows, impostor, updateEvery, category, reason
lod.group.changedthe same for a registered group of instances
lod.profileprofile
lod.budgetscale, averageMs
lod.pinsepoch (the fixed levels changed)

Service #

For a HUD, an instanced renderer or a test: profile(), setProfile(p), rule(category, tags), state(entity) ({ category, distance, level, visible, shadows, impostor, updateEvery, reason }), levelOf(entity) (-1 when culled), isVisible, castsShadow, updateEvery, shouldUpdate, evaluate(category, distance, radius, levels) (a one-off decision), addGroup(id, { category, position, radius, count, levels, impostor, tags }) with moveGroup, removeGroup, groupState, setViewer([x, y, z]), setFov(deg), reportFrame(ms), budget(), stats(), statsLines(lang), setDebug(on), refresh(). Variables: lod.profile, lod.scale, lod.debug, and with debug on lod.objects, lod.visible, lod.culled, lod.triangles.

Debug #

debug: true (or setDebug(true)) publishes the stats variables and shows an overlay: objects per level, culled, impostors, shadow casters, estimated triangles. stats() always answers, for a HUD of your own.

When it does not do what you expect #

SymptomCause and fix
Nothing changes levelthe model has no levels: run gripforge lod; or the entity matches no category (manageAll, a tag lod.<category>)
An object never loses detailtagged lod.off, pinned by lod.level.<n>, or maxLevel: 0 in a rule
Far objects disappear too earlyminScreenSize or drawDistance of its category; raise scale or set them to 0
Levels flicker at the edgeraise hysteresis
The player's choice is ignored by a modon purpose: the profile chosen in the Graphics tab wins