Cut draw calls, ranked by cost
SkillDev toolsUse when a PlayCanvas application submits too many draw calls or frame time is CPU-bound — repeated meshes, dense grids, always-on interface elements — before hand-writing mesh merging or custom renderers.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Cut draw calls, ranked by cost skill
What this skill tells your AI
The instructions your AI receives, as published by playcanvas/skills in skills/reduce-draw-calls/SKILL.md and read by ahel’s review.
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.
Measure first
Read app.stats.drawCalls.total (or the forward/depth/shadow breakdown) before changing
anything, or drop in MiniStats for a live overlay. Every rung below is proved against this number,
not against intuition about what "looks expensive."
Draw count alone does not identify the bottleneck. Test sensitivity to pixel cost by halving the
effective pixel ratio (Math.min(graphicsDevice.maxPixelRatio, window.devicePixelRatio)) and calling
app.resizeCanvas(). Verify the backbuffer shrank and compare frame times at the same scene state.
If frame time improves materially, prioritize resolution, multisampling and per-pixel shader cost.
Otherwise profile CPU and GPU work before choosing a rung; restore the original ratio after the test.
Rung 1: stop drawing invisible things
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 enabled 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.
Rung 2: BatchManager — merge without touching shaders
app.batcher.addGroup(name, dynamic, maxAabbSize), then set batchGroupId on each member's
component (render, sprite, or UI element). Use dynamic: false for geometry that never moves.
Contract: the batchGroupId setter only inserts into the batcher while entity.enabled is true,
and that flag requires both the local enable state and hierarchy attachment — set batchGroupId
after the entity is parented into the live tree, not before, or the member silently drops out of the
group. BatchManager.generate() runs once automatically on the app's first rendered frame; if group
membership changes afterward, call app.batcher.markGroupDirty(id) (or generate([id])) yourself.
An Engine-only app without the full Application bootstrap must register the class via
AppOptions.batchManager before it can batch anything.
Rung 3: hardware instancing — merge without touching layout
Reach for this once distinct materials or per-frame transform updates would defeat BatchManager.
Build a per-instance vertex buffer with VertexFormat.getDefaultInstancingFormat(device) (one mat4
per instance), call meshInstance.setInstancing(vb), and set instancingCount. A custom vertex
chunk needs its own INSTANCING code path with an identity-matrix fallback for the non-instanced
case (cross-reference override-shader-chunks).
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
setInstancing(vb, true) 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 find-examples.
Prove and report
Verify the rendered image is unchanged with verify-pixels before calling the change done —
merging or instancing must not move a single pixel. Re-measure app.stats.drawCalls afterward and
state the before/after counts in the change description; "fewer draw calls" without numbers is not
a result.
Signals
- GitHub stars
- 21
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
reduce-draw-calls- Source
- github.com/playcanvas/skills