跳到主要内容

绘制顺序与性能

本页介绍界面的绘制方式和性能优化。引擎、编辑器、React 与 Web Components 使用相同绘制机制。

界面如何绘制​

每个图像和文本元素都是网格实例,默认位于 UI 图层,除非指定其他图层:

  • UI 图层最后绘制。 默认顺序中,它位于 World、Skybox 和 Immediate 之后,因此界面覆盖场景。
  • 仅包含透明对象。 不透明部分不在渲染顺序中,因此不混合的材质不会在此绘制,详见自定义材质。
  • 按元素绘制顺序排序,即遵循层级,而非距离,详见绘制顺序。
  • 元素不写入深度。 屏幕空间元素也不测试深度,直接覆盖摄像机整个视图;世界空间元素则与场景进行深度测试。
  • 不受后处理影响。 使用 CameraFrame 的摄像机会在效果之后绘制 UI 图层,因此界面不受模糊、泛光和色调映射影响。

绘制顺序​

同一屏幕的元素按层级深度优先绘制:先父项,再子项;实体的所有后代绘制完成后,才绘制下一个同级项。因此子项覆盖父项,靠后的同级项覆盖靠前同级项及其全部后代。

蓝、橙、绿三张卡片按层级顺序依次覆盖,橙卡的白色子徽章位于橙卡之上、绿卡之下

要让元素置顶,将其设为父项最后一个子项:

// Draw the window over its siblings from the next frame on
selectedWindow.reparent(selectedWindow.parent);

屏幕层级变化后,绘制顺序会在下一帧绘制前更新。元素 drawOrder 保存其位置,最高 8 位包含屏幕优先级。

React 中,无论 JSX 中的位置如何,新 <Entity> 都添加在已有同级项之后。保持列表顺序的方法详见创建布局组。

界面中的粒子​

位于屏幕空间屏幕层级中的粒子系统,若设为屏幕空间并放在 UI 图层,会与元素一起遵循层级顺序。添加后应调用屏幕的 syncDrawOrder():

sparkles.addComponent('particlesystem', {
screenSpace: true,
layers: [app.scene.layers.getLayerByName('UI').id],
numParticles: 50,
lifetime: 1
});
screen.screen.syncDrawOrder();
粒子系统

多屏幕与优先级​

屏幕重叠时,priority 决定覆盖顺序,范围为 0 到 127,较高者绘制在上方,也先接收输入。相同优先级的屏幕会按各自层级位置交错绘制,因此应给重叠屏幕不同优先级,例如 HUD 为 0、菜单为 10、对话框为 20。

pauseMenu.screen.priority = 10;

Screen 组件的 Priority 和 <pc-screen> 的 priority 设置相同值。

图层与摄像机​

每个渲染 UI 图层的摄像机都会在其视图上绘制全部屏幕空间屏幕。小地图或画中画等第二摄像机会让 HUD 绘制两次,应从不需要显示 HUD 的摄像机移除 UI 图层:

const uiLayer = app.scene.layers.getLayerByName('UI');
minimapCamera.camera.layers = minimapCamera.camera.layers.filter(id => id !== uiLayer.id);

摄像机 rect 仅覆盖部分画布时,屏幕空间界面会被压缩到该区域,因为其分辨率始终等于整个画布。

将元素移到其他图层时,要为 layers 赋予新数组,原地修改数组不生效。

在界面上方绘制三维内容​

有两种方式:将模型渲染到纹理,再由图像元素显示,这样可遵循界面顺序和遮罩,详见将三维内容渲染为图像;或将模型放到 UI 之后的图层,先清除深度缓冲,避免被场景遮挡:

const overUi = new pc.Layer({ name: 'Over UI', clearDepthBuffer: true });
app.scene.layers.push(overUi);
camera.camera.layers = [...camera.camera.layers, overUi.id];

trophy.render.layers = [overUi.id];

世界空间界面与后处理​

UI 图层上的世界空间元素在场景后绘制,并对场景进行深度测试。使用 CameraFrame 时,UI 在后处理后绘制,没有场景深度,因此会穿墙显示,也不受效果影响。要将其纳入场景,应在 World 后添加独立图层,使用 Manual 排序。World 自身按距离排序透明对象,会打乱同一屏幕的元素顺序:

// A layer for world-space UI, drawn with the scene and in the hierarchy's order
const worldLayer = app.scene.layers.getLayerByName('World');
const worldUi = new pc.Layer({ name: 'World UI', transparentSortMode: pc.SORTMODE_MANUAL });
app.scene.layers.insertTransparent(worldUi, app.scene.layers.getTransparentIndex(worldLayer) + 1);
camera.camera.layers = [...camera.camera.layers, worldUi.id];

// Move the elements of a world-space screen to it
for (const node of sign.find(node => !!node.element)) {
node.element.layers = [worldUi.id];
}

编辑器中,在设置的 LAYERS 区域添加图层,将 Transparent Sort 设为 Manual,再将透明部分拖到渲染顺序的 World 之后。最后,将图层添加到摄像机和元素的 Layers。

减少绘制调用​

每个图像和文本元素通常需要一次绘制调用。共享批处理组、材质且位于同一图层时,可合并为更少调用,规则详见批处理。

  • 批处理组。 为同一面板或 HUD 的元素设置相同组,代码使用 batchGroupId,编辑器使用 Batch Group。
  • 纹理图集。 不同纹理的图像无法合并,宜将图标和面板作为同一纹理图集的精灵绘制。
  • 遮罩。 内外元素分开批处理,详见遮罩开销。
  • 变化的元素。 修改组内任意图像或文本属性,例如文字或颜色,会在下一帧重建整个组。计时器等频繁变化的元素应单独使用小组,或不参与批处理。

更新文本​

设置 text 会重新布局并重建网格。少量标签开销不大,但每帧更新大量标签会累积成本,应仅在值改变时设置:

let shownScore = -1;
app.on('update', () => {
if (score !== shownScore) {
scoreLabel.element.text = String(score);
shownScore = score;
}
});

通过 rangeStart 和 rangeEnd 显示部分文字不会重新布局,比修改 text 更便宜,详见逐字显示。

布局开销​

布局组在子项添加、移除、启用、禁用或变更尺寸时重新布局。因此,每帧改变一个子项尺寸会让整组每帧重新布局。动画元素宜放在布局组外,或通过 LayoutChild 启用 Exclude from Layout。

填充率​

元素的全部像素都会绘制和混合,包括透明像素。全屏背景、暗角和面板等大型叠层会让同一像素重复绘制,尤其影响高像素比移动设备。应减少大型透明图,裁掉不可见区域,并参考像素比选择渲染分辨率。

剔除​

屏幕空间屏幕的 cull 默认开启,完全在摄像机视图外的元素会跳过绘制。遮罩内完全超出遮罩的元素也会被跳过,例如滚动视图长列表中滚出视口的条目。屏幕创建时会根据 screenSpace 设置 cull,传给 addComponent 不生效,需要之后修改。世界空间屏幕元素则像其他场景内容一样按摄像机视野剔除。

另请参阅​