Display optimisation

Usage guide of the render.optimize kit: the levers (instancing, merge, cull, streaming, kept static shadows, warm) and their defaults, departures per entity (passport section, rules, tags, setEntity, the hook render.optimize.entity), how each engine applies them, debug.

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

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

Display optimisation of your game (render.optimize) #

One set of switches over what an engine already knows to make a scene cheap to draw: models loaded once and shared, static scenery merged, what is off camera hidden, far zones of a level hidden, shadows of what does not move kept. Readable offline: gripforge docs render-optimize, or the MCP tool gripforge_docs (topic render-optimize).

What it is for #

The kit decides, the engine applies with its own mechanisms. You do not write a batching or culling pass: you say which levers are on, and for the few objects that must be treated differently, how. Nothing is unloaded unless you ask for it (streaming.unload), and nothing is recompiled: the kit is data the engine reads.

It is separate from render.lod (which mesh level to show): they do not depend on each other. They meet only in the engine, each reading its own data. The level of detail of one entity (forced, fixed, none) stays render.lod's: tags lod.level.<n> and lod.off.

Turn it on #

render.optimize is installed, with the defaults below, in every new game, and strongly recommended; existing games receive it at their next opening. A game declares only what departs from the defaults:

gripforge_gamekit_install   { project_id: "gkp_…", id: "render.optimize" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "render.optimize",
                              config: { "merge": { "instanceTriangles": 0 }, "streaming": { "unload": true } } }
gripforge_gamekit_configure { project_id: "gkp_…", id: "render.optimize", enabled: false }   # off, kept installed

enabled: false inside the config turns every lever off for the game (the engine draws the files as they are). The full reference is gripforge kit api render.optimize.

The levers and their defaults #

LeverWhat the engine doesDefault
instancinga model is loaded once; every copy shares its meshes and materials (one handle, one batch); identical contents from different files share one handle tooon
mergethe static parts of a scene that share a material become one mesh, by chunkson
cullwhat is off camera is hidden (frustum culling); nothing is unloadedon
streaminga level drawn zone by zone shows the zones around the camera; the others are hidden after linger secondson, unload: false
staticShadowsthe shadow of the static scenery is drawn once and kept (WebGPU)off
warmthe pipelines of what appears during play are made while the game loadson

Each lever has enabled (default true; false: the engine does as if the lever did not exist) and these keys:

KeyDefaultMeaning
merge.instanceTriangles400a mesh drawn several times is merged only up to this many triangles; above it stays instanced; 0 = never merge copies
merge.partTriangles4000no part above this many triangles is merged (it keeps its own culling)
merge.vertices60000vertices of one merged mesh at most
streaming.unloadfalsetrue: a zone far from the camera is unloaded (spawned again on the way back) instead of hidden
streaming.linger4seconds a zone that left the ring stays shown
streaming.perFrame8looks of zone pieces made per frame at most
staticShadows.snap0.125how far the kept shadow reaches past the view, as a fraction of its side
warm.models32models drawn ahead at most
rules{}policies by entity kind or tag (below)

staticShadows is off by default because the frame that draws the kept shadow again has no moving shadow: turn it on when the game can afford that frame.

Departures per entity #

An entity departs from the levers with three switches:

  • alwaysShown: never hidden off camera, never streamed out (a sky, a landmark seen through a window);
  • instancing: false: its meshes and materials are its own and it is not merged (a model a kit recolours per copy);
  • static: true or false (mode: "static" / "dynamic"): static is merged with the scenery and its shadow kept; dynamic is never merged (anything that moves).

They are read in this order, each over the previous one:

  1. the passport of the model, section kits.optimize (keys alwaysShown, instancing, static, mode): the GripForge passport, then the game's override in its asset_passports document: { "id": "<slot>", "passport": { "kits": { "optimize": { "mode": "dynamic" } } } };
  2. the rules: the config key rules and the registry optimize_rules, by entity kind or tag. A rule is { "id": "door", "match": ["door", "gate"], "mode": "dynamic" } (when match is empty, id is what it matches). A kit, a game or a mod adds one;
  3. the tags of the entity: optimize.alwaysShown, optimize.noInstancing, optimize.static, optimize.dynamic, and optimize.off (all three at once: shown, not instanced, dynamic);
  4. setEntity(id, policy) at run time (setEntity(id, null) clears it);
  5. the hook render.optimize.entity: a tap sees { entity, kind, alwaysShown, instancing, static }, may change the three switches, or cancel the decision, in which case the entity follows the levers.
{ "rules": { "door": { "static": false }, "skybox": { "alwaysShown": true } } }

Hooks, mods, registries #

WhatNameDetail
Hookrender.optimize.entitymutable alwaysShown, instancing, static; cancellable; web and native player
Registryoptimize_rulespolicies by kind or tag; inherited by games, extended by kits and mods
Moddable configenabled, instancing, merge, cull, streaming, staticShadows, warm, rulesa mod changes them like the game does
Eventoptimize.changed { epoch }a lever or the policy of an entity changed; engines read state() again

Service #

state() ({ epoch, enabled, settings, entities }: the levers in the engine's shape and only the entities that depart from them), settings(), lever(name), setLever(name, value | null), enabled(), setEnabled(on), policy(id), setEntity(id, policy | null), rules(), addRule(kindOrTag, policy), stats(), epoch(). The native player reads state() as its view render.optimize.state, again only on optimize.changed, never every frame.

Engines #

The kit decides; each engine applies the levers with its own tools, and the three switches of an entity (alwaysShown, instancing, static) mean the same everywhere.

EngineInstancingOff cameraalwaysShowninstancing: false
Web and native playercopies share one handle; static parts merged; zones streamed; kept static shadows; pipelines warmedhidden, not unloadednever hidden or streamed outmeshes and materials of its own
Godotcopies share the meshes of a template and are drawn as instanceshidden by the engine's frustum and occlusion cullingbounds that cover the world, no occlusionmeshes of its own
Unityone template instantiated, materials with GPU instancinghidden by the engine's cullingbounds that cover the world, no occlusionmeshes of its own
Unrealcopies share the static mesh, drawn as instanced drawshidden by the engine's cullingbounds that cover the world, no distance culla mesh of its own

Merging, zone streaming, kept shadows and warming are levers of the web and native players; the engines keep their own for those.

Debug #

The debug inspector (F3) has a mode Display optimisation: the levers (switch them there to compare), the entities that depart from them, and the engine's own counters: triangles, draws, instances (copies sharing a mesh), meshes hidden off camera, zones hidden, parts merged. setLever from a script does the same.

When it does not do what you expect #

SymptomCause and fix
A moving object looks cut or stays in placeit was merged as static: mark it dynamic (rule, passport mode, tag optimize.dynamic)
A recoloured model recolours every copycopies share their material: instancing: false for that model
Something disappears when seen through a windowculling saw it off camera: alwaysShown
The moving shadow is missing for a framestaticShadows redraws the kept shadow on that frame: leave it off, or accept it
Memory does not go downstreaming hides zones, it unloads only with streaming.unload: true