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.
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, seegripforge 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: distancereads distances in metres instead. - Culling: beyond
drawDistance, or smaller thanminScreenSize. - 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 everyupdateIntervals[i]ticks (animation, AI). - Instance budget: per category, only the
maxInstancesnearest 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.
| Key | Meaning |
|---|---|
scale | multiplies every distance (divides the screen sizes) |
distances / screenSizes | metres / fractions of the screen height at which level 1, 2, 3… start |
drawDistance | metres; 0 = never culled by distance |
minScreenSize | fraction of the screen height under which the object is culled; 0 = off |
impostorDistance | metres; 0 = no impostor |
shadowDistance | metres; 0 = no limit |
updateDistances, updateIntervals | metres, and ticks between two updates beyond each |
maxInstances | visible instances of the category; 0 = unlimited |
minLevel, maxLevel | the most and the least detailed level allowed |
Built-in profiles (the player picks one in the Graphics tab when settings.graphics is installed):
| Profile | screenSizes | drawDistance | minScreenSize | shadowDistance |
|---|---|---|---|---|
low | 0.5, 0.22, 0.1 | 200 | 0.015 | 35 |
medium | 0.4, 0.16, 0.07 | 300 | 0.008 | 70 |
high, custom | 0.3, 0.12, 0.05 | 400 | 0.004 | 120 |
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 #
| Key | Default | Meaning |
|---|---|---|
profile | high | profile before the player chooses one |
metric | screen | screen or distance |
viewer | camera | measure from the camera (the player until the renderer gives it), or always from the player |
fov | 60 | vertical field of view for screen sizes, until the renderer gives the camera's |
hysteresis | 0.1 | width of the threshold bands |
interval | 0.1 | seconds between two evaluations |
followViewDistance | true | draw distances follow the player's view distance (settings.graphics) |
manageAll | false | manage entities that match no category, as props |
playerOption | true | show the profile row in the Graphics tab |
debug | false | publish the stats variables and show the overlay |
match | lists per category | kinds and tags that put an entity in a category |
profiles, categories, tags | inherit | rule overrides |
budget | off | frame-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'), andforceEntity(id, null)to give it back to its size. - The passport of the model (section
kits.lod, fromgripforge lodand 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 ownrule. A game overrides it key by key in itsasset_passportsdocument. Seegripforge 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:
| Engine | How the levels are switched | A level fixed by tag or forceEntity |
|---|---|---|
| Web | children 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 ticks | the level shown |
| Native player | the same decisions, from the Rust module | the level shown |
| Godot | sibling 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 margin | the only one shown, without range |
| Unity | sibling renderers of the master switched by a LODGroup whose transition heights are the kit's screen sizes | LODGroup.ForceLOD |
| Unreal | the 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 view | the 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:
| Event | Payload |
|---|---|
lod.changed | entity, level, visible, shadows, impostor, updateEvery, category, reason |
lod.group.changed | the same for a registered group of instances |
lod.profile | profile |
lod.budget | scale, averageMs |
lod.pins | epoch (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 #
| Symptom | Cause and fix |
|---|---|
| Nothing changes level | the model has no levels: run gripforge lod; or the entity matches no category (manageAll, a tag lod.<category>) |
| An object never loses detail | tagged lod.off, pinned by lod.level.<n>, or maxLevel: 0 in a rule |
| Far objects disappear too early | minScreenSize or drawDistance of its category; raise scale or set them to 0 |
| Levels flicker at the edge | raise hysteresis |
| The player's choice is ignored by a mod | on purpose: the profile chosen in the Graphics tab wins |