Work the ladder in order and stop at the first rung that clears the budget. Each rung trades away
less flexibility than the next; skipping straight to a custom renderer or hand-written mesh merge
costs more engineering time than the draw calls it saves.
Read
app.stats.drawCalls.total
(or the
/
/
breakdown) before changing
anything, or drop in
for a live overlay. Every rung below is proved against this number,
not against intuition about what "looks expensive."
An element at opacity 0 still submits a draw call — its mesh instance exists and is still in a
layer, so the GPU processes it every frame for no visible result. Toggle
on the entity to
actually skip it. Check every element that is ever fully transparent, not only the ones on screen
when you're profiling — this rung is easy to skip precisely because nothing looks wrong.
app.batcher.addGroup(name, dynamic, maxAabbSize)
, then set
on each member's
component (render, sprite, or UI element). Use
for geometry that never moves.
Contract: the
setter only inserts into the batcher while
is true,
and that flag requires both the local enable state and hierarchy attachment — set
after the entity is parented into the live tree, not before, or the member silently drops out of the
group.
runs once automatically on the app's first rendered frame; if group
membership changes afterward, call
app.batcher.markGroupDirty(id)
(or
) yourself.
An Engine-only app without the full
bootstrap must register the class via
before it can batch anything.
Reach for this once distinct materials or per-frame transform updates would defeat
.
Build a per-instance vertex buffer with
VertexFormat.getDefaultInstancingFormat(device)
(one mat4
per instance), call
meshInstance.setInstancing(vb)
, and set
. A custom vertex
chunk needs its own
code path with an identity-matrix fallback for the non-instanced
case (cross-reference
).
Culling trade-off: instanced meshes cull as one unit against a single bounding volume — an
off-screen instance inside an otherwise-visible group still draws unless you opt in. Pass
and set a
RenderComponent#customAabb
spanning every instance's world
extent so the renderer has something correct to cull against instead of culling nothing or
culling the whole group by one instance's bounds.
Adapt the official recipes rather than deriving the buffer layout or vertex-shader wiring from
memory:
graphics/instancing-basic
for the format/buffer contract,
graphics/instancing-custom
for
the vertex-shader side. Locate both at the installed engine version via
.
Verify the rendered image is unchanged with
before calling the change done —
merging or instancing must not move a single pixel. Re-measure
afterward and
state the before/after counts in the change description; "fewer draw calls" without numbers is not
a result.