reduce-draw-calls

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cut draw calls, ranked by cost

减少绘制调用的方法(按成本排序)

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."
在进行任何修改前,先查看
app.stats.drawCalls.total
(或
forward
/
depth
/
shadow
的细分数据),或者引入
MiniStats
来显示实时数据面板。下面的每一种方法都需要基于这个数据来验证效果,而不是凭直觉判断哪些内容“看起来开销大”。

Rung 1: stop drawing invisible things

方法1:停止绘制不可见元素

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.
透明度为0的元素仍然会提交绘制调用——它的网格实例依然存在且属于某个图层,因此GPU每帧都会处理它,但不会产生任何可见效果。通过切换实体的
enabled
属性来真正跳过它的绘制。检查所有曾经完全透明的元素,而不仅仅是你分析时屏幕上的元素——正因为视觉上没有任何问题,这个方法很容易被忽略。

Rung 2:
BatchManager
— merge without touching shaders

方法2:使用
BatchManager
——无需修改着色器即可合并

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.
调用
app.batcher.addGroup(name, dynamic, maxAabbSize)
,然后为每个成员的组件(渲染、精灵或UI元素)设置
batchGroupId
。对于不会移动的几何体,使用
dynamic: false
注意事项:只有当
entity.enabled
为true时,设置
batchGroupId
才会将元素加入到批处理管理器中,而该属性同时要求实体处于本地启用状态且已加入层级结构——要在实体被添加到活跃层级树之后再设置
batchGroupId
,否则该元素会被静默地排除在批处理组之外。
BatchManager.generate()
会在应用首次渲染帧时自动运行一次;如果之后批处理组成员发生变化,需要手动调用
app.batcher.markGroupDirty(id)
(或
generate([id])
)。对于未完整初始化
Application
的纯Engine应用,必须先通过
AppOptions.batchManager
注册该类,才能进行批处理操作。

Rung 3: hardware instancing — merge without touching layout

方法3:硬件实例化——无需修改布局即可合并

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
.
当不同材质或每帧变换更新会导致
BatchManager
失效时,可以使用此方法。通过
VertexFormat.getDefaultInstancingFormat(device)
创建每个实例的顶点缓冲区(每个实例对应一个mat4矩阵),调用
meshInstance.setInstancing(vb)
并设置
instancingCount
。自定义顶点块需要有自己的
INSTANCING
代码路径,并为非实例化情况提供单位矩阵作为回退(参考
override-shader-chunks
)。
剔除权衡:实例化网格会作为一个整体,基于单个包围盒进行剔除——除非你主动配置,否则即使组内某个实例不在屏幕范围内,只要整个组可见,该实例仍会被绘制。传入
setInstancing(vb, true)
并设置
RenderComponent#customAabb
以覆盖所有实例的世界范围,这样渲染器就能基于正确的范围进行剔除,而不是不做剔除或仅根据单个实例的边界剔除整个组。
建议参考官方示例,而不是凭记忆推导缓冲区布局或顶点着色器连接:
graphics/instancing-basic
示例介绍了格式/缓冲区的规范,
graphics/instancing-custom
示例介绍了顶点着色器相关内容。可以通过
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.
在确认修改完成前,使用
verify-pixels
验证渲染图像没有变化——合并或实例化操作不能改变任何一个像素的位置。修改后重新测量
app.stats.drawCalls
,并在变更说明中记录修改前后的数值;只说“减少了绘制调用”而没有具体数字不能算作有效结果。