渲染管线与多后端架构
深入 @antv/g 的帧循环机制、渲染服务核心、脏矩形优化、视锥裁剪、Z-Index 排序,以及 Canvas2D/SVG/WebGL/WebGPU/CanvasKit 多后端抽象层。
1. 帧循环(Frame Loop)机制
@antv/g 采用"按需渲染"的帧循环策略。并非每一帧都重绘整个画布,而是通过 RenderReason 机制判断是否需要重新渲染:
// packages/g-lite/src/services/RenderingContext.ts
enum RenderReason {
CAMERA_CHANGED, // 相机参数变化
DISPLAY_OBJECT_CHANGED, // 场景图中有对象发生变化
NONE, // 无需渲染
}
interface RenderingContext {
root: Group; // 场景图根节点
force: boolean; // 强制渲染
renderReasons: Set<RenderReason>; // 渲染原因集合
renderListCurrentFrame: DisplayObject[]; // 当前帧需渲染的对象列表
unculledEntities: number[]; // 未被裁剪的实体
dirty: boolean;
}
当 renderReasons 集合为空时,帧循环跳过渲染。只有当对象属性变更(触发 DISPLAY_OBJECT_CHANGED)或相机变化(触发 CAMERA_CHANGED)时才会执行实际渲染。
enableAutoRendering 配置项可以控制是否启用自动帧循环。设为 false 后需手动调用 canvas.render() 触发渲染,适用于需要精确控制渲染时机的场景。
2. RenderingService 核心服务
RenderingService 是渲染管线的中枢,位于 packages/g-lite/src/services/RenderingService.ts。它维护了完整的 Hook 系统和统计信息:
// RenderingService 核心结构
class RenderingService {
constructor(
private globalRuntime: GlobalRuntime,
private context: CanvasContext,
) {}
private stats = {
total: 0, // 场景图中总对象数
rendered: 0, // 当前帧实际渲染的对象数
};
private zIndexCounter = 0; // 全局渲染顺序计数器
private inited = false;
hooks = {
init: new SyncHook(),
initAsync: new AsyncParallelHook(),
dirtycheck: new SyncWaterfallHook(), // 脏检查
cull: new SyncWaterfallHook(), // 视锥裁剪
beginFrame: new SyncHook(),
beforeRender: new SyncHook(), // 每个对象渲染前
render: new SyncHook(), // 渲染对象
afterRender: new SyncHook(), // 每个对象渲染后
endFrame: new SyncHook(),
destroy: new SyncHook(),
pick: new AsyncSeriesWaterfallHook(), // 拾取(异步)
pickSync: new SyncWaterfallHook(), // 拾取(同步)
};
}
RenderingService 接受两个依赖注入参数:GlobalRuntime 提供全局运行时服务(如 sceneGraphService),CanvasContext 提供画布级上下文(如 renderingPlugins、camera、renderingContext)。
3. 渲染阶段详解
每一帧的渲染流程严格遵循以下阶段序列:
syncHierarchy → renderDisplayObject → beginFrame → beforeRender/render/afterRender → endFrame | | | | | 同步场景图 脏检查+裁剪+排序 帧开始回调 逐对象渲染 帧结束/提交
以下是 render() 方法的核心执行流程:
// RenderingService.render() 核心逻辑(简化)
render(canvas: Canvas, frame: XRFrame, rerenderCallback: () => void) {
// 1. 重置统计
this.stats.total = 0;
this.stats.rendered = 0;
this.zIndexCounter = 0;
// 2. 同步场景图层级关系
this.globalRuntime.sceneGraphService.syncHierarchy(root);
this.globalRuntime.sceneGraphService.notifyMutationObservers(canvas);
// 3. 仅在有渲染原因且已初始化时执行
if (renderReasons.size && this.inited) {
// 4. 判断是否禁用脏矩形渲染
renderingContext.dirtyRectangleRenderingDisabled =
this.disableDirtyRectangleRendering();
// 5. 遍历场景图:脏检查 + 裁剪 + 排序
this.renderDisplayObject(root, canvasConfig, renderingContext);
// 6. 帧开始
this.hooks.beginFrame.call(frame);
// 7. 逐对象渲染
renderListCurrentFrame.forEach((object) => {
this.hooks.beforeRender.call(object);
this.hooks.render.call(object);
this.hooks.afterRender.call(object);
});
// 8. 帧结束(提交绘制结果)
this.hooks.endFrame.call(frame);
// 9. 清理当前帧状态
renderingContext.renderListCurrentFrame = [];
renderingContext.renderReasons.clear();
}
}
阶段职责详表
| 阶段 | Hook | 类型 | 职责 |
|---|---|---|---|
| 初始化 | init / initAsync | SyncHook / AsyncParallelHook | 渲染器初始化(创建 GPU 上下文等) |
| 脏检查 | dirtycheck | SyncWaterfallHook | 判断对象是否发生变化需要重绘 |
| 视锥裁剪 | cull | SyncWaterfallHook | 剔除视口外的对象 |
| 帧开始 | beginFrame | SyncHook | 清除画布、准备绘制状态 |
| 渲染前 | beforeRender | SyncHook | 设置对象级渲染状态 |
| 渲染 | render | SyncHook | 执行实际绘制操作 |
| 渲染后 | afterRender | SyncHook | 清理对象级状态 |
| 帧结束 | endFrame | SyncHook | 提交绘制结果到屏幕 |
| 拾取 | pick / pickSync | AsyncSeriesWaterfall / SyncWaterfall | 根据坐标查找命中的对象 |
| 销毁 | destroy | SyncHook | 释放 GPU 资源、清理状态 |
4. 脏矩形优化策略
脏矩形渲染是 @antv/g 在 Canvas2D 后端的核心性能优化策略。原理是:只重绘发生变化的区域,而非整个画布。
工作原理
- 标记脏对象:当
DisplayObject的属性发生变化时,通过renderable.dirty标记为"脏"。同时RenderingService.dirty()向renderReasons添加DISPLAY_OBJECT_CHANGED。 - 脏检查:在
renderDisplayObject()遍历场景图时,启用enableDirtyCheck后,只有renderable.dirty为true或dirtyRectangleRenderingDisabled时对象才进入渲染列表。 - 矩形合并:收集所有脏对象的包围盒,合并为最小的脏矩形区域。
- 局部重绘:在
beginFrame时,只清除脏矩形区域;在渲染阶段只绘制与脏矩形相交的对象。
// 脏检查核心逻辑(来自 renderDisplayObject 内部函数)
function internalRenderSingleDisplayObject(object: DisplayObject) {
const { renderable, sortable } = object;
// 启用脏检查时,只有 dirty 对象才进入渲染管线
const objectChanged = enableDirtyCheck
? (renderable.dirty || dirtyRectangleRenderingDisabled
? object
: null)
: object;
let objectToRender = null;
if (objectChanged) {
// 启用裁剪时,进一步过滤视口外的对象
objectToRender = enableCulling
? this.hooks.cull.call(objectChanged, camera)
: objectChanged;
if (objectToRender) {
stats.rendered += 1;
renderListCurrentFrame.push(objectToRender);
}
}
// 重置脏标记
object.dirty(false);
sortable.renderOrder = zIndexCounter++;
stats.total += 1;
}
禁用脏矩形渲染的条件
disableDirtyRectangleRendering() {
const { enableDirtyRectangleRendering } = renderer.getConfig();
return (
!enableDirtyRectangleRendering ||
renderingContext.renderReasons.has(RenderReason.CAMERA_CHANGED)
);
}
当相机参数变化时(如平移/缩放),整个视口内容都可能改变,脏矩形优化失去意义,此时退化为全量渲染。
enableDirtyRectangleRenderingDebug: true 可以在画布上可视化脏矩形区域,Canvas 会触发 CanvasEvent.DIRTY_RECTANGLE 事件。
5. 视锥裁剪(Culling)
视锥裁剪通过 cull Hook 实现。当对象的包围盒完全在视口之外时,cull Hook 返回 null,该对象被跳过不渲染。
// cull hook 的使用(在 renderDisplayObject 中)
objectToRender = enableCulling
? this.hooks.cull.call(objectChanged, this.context.camera)
: objectChanged;
// cull hook 签名
cull: new SyncWaterfallHook<[DisplayObject, ICamera], DisplayObject>()
裁剪策略由各渲染器插件注册。例如 Canvas2D 渲染器通过比较对象的 AABB(轴对齐包围盒)与相机视锥来判断可见性。
裁剪流程
Camera Viewport
+========================+
| |
| [Object A] 可见 | [Object C] 不可见
| | (视口外,被裁剪)
| [Object B] |
| |
+========================+
[Object D] 部分可见 -> 保留渲染
裁剪通过 RendererConfig.enableCulling 配置开关控制。默认为 false,因为在小规模场景中裁剪计算的开销可能大于收益。对于包含大量对象的场景,建议开启。
cullable.enable = false),因为 DOM 元素由浏览器自身管理可见性。
6. Z-Index 排序机制
渲染顺序决定了图形的前后遮挡关系。@antv/g 使用 Sortable 组件管理排序:
// 排序相关数据结构
interface Sortable {
renderOrder: number; // 全局渲染顺序(每帧递增分配)
dirty: boolean; // 是否需要重新排序
sorted: DisplayObject[]; // 排序后的子节点列表
dirtyChildren: DisplayObject[]; // 需要重新插入的子节点
dirtyReason?: SortReason; // 脏原因
}
enum SortReason {
Z_INDEX_CHANGED, // zIndex 变化 -> 完全重排
// 其他原因 -> 增量排序
}
排序策略
排序实现了两种策略以优化性能:
- 完全重排:当
dirtyReason为Z_INDEX_CHANGED时,对整个子节点列表执行.sort(sortByZIndex) - 增量排序:当子节点仅发生增删(非 zIndex 变化)时,仅对
dirtyChildren做二分插入(sortedIndex),避免全量排序的 O(n log n) 开销
// sort 方法核心逻辑(来自源码)
private sort(displayObject: DisplayObject, sortable: Sortable) {
if (
sortable.sorted?.length > 0 &&
sortable.dirtyReason !== SortReason.Z_INDEX_CHANGED
) {
// 增量排序:只处理变化的子节点
sortable.dirtyChildren.forEach((child) => {
// 从排序列表中移除
const sortIndex = sortable.sorted.indexOf(child);
if (sortIndex > -1) sortable.sorted.splice(sortIndex, 1);
// 二分查找正确位置并插入
const index = sortedIndex(sortable.sorted, child);
sortable.sorted.splice(index, 0, child);
});
} else {
// 完全重排
sortable.sorted = displayObject.childNodes.slice().sort(sortByZIndex);
}
// 优化:如果没有任何 zIndex != 0 的子节点,清空排序列表
if (
displayObject.childNodes.filter(
(child) => child.parsedStyle.zIndex
).length === 0
) {
sortable.sorted = [];
}
}
场景图遍历顺序
遍历使用基于栈的迭代方式(非递归),优先使用排序后的列表:
// 场景图遍历(使用栈替代递归)
const stack = [displayObject];
while (stack.length > 0) {
const currentObject = stack.pop();
internalRenderSingleDisplayObject(currentObject);
// 优先使用排序后的子节点列表
const objects = currentObject.sortable?.sorted?.length > 0
? currentObject.sortable.sorted
: currentObject.childNodes;
// 逆序入栈,保证正确的渲染顺序
for (let i = objects.length - 1; i >= 0; i--) {
stack.push(objects[i]);
}
}
7. 多渲染后端架构
@antv/g 的核心设计理念是渲染后端可替换。同一套场景图 API 可以无缝切换不同的渲染实现:
@antv/g-lite (核心抽象层)
|
+----------+----------+----------+-----------+
| | | | |
g-canvas g-svg g-webgl g-webgpu g-canvaskit
Canvas2D SVG DOM WebGL WebGPU Skia/WASM
| 渲染器 | 包名 | 技术栈 | 适用场景 |
|---|---|---|---|
| Canvas2D | @antv/g-canvas | CanvasRenderingContext2D | 通用 2D 场景,兼容性最好 |
| SVG | @antv/g-svg | SVG DOM 元素 | 需要 DOM 交互、CSS 动画的场景 |
| WebGL | @antv/g-webgl | WebGL 1.0/2.0 | 大量对象、3D 场景、GPU 加速 |
| WebGPU | @antv/g-webgpu | WebGPU API | 次世代 GPU 渲染,计算着色器 |
| CanvasKit | @antv/g-canvaskit | Skia/WASM | 高质量矢量渲染,跨平台一致性 |
每个渲染器包内部组织为多个插件:
// g-canvas 包的内部结构示例
packages/g-canvas/src/
├── Canvas2DContextService.ts // Canvas2D 上下文创建
├── ContextRegisterPlugin.ts // 上下文注册插件
├── index.ts
└── plugins/
├── canvas-renderer/ // Canvas2D 绘制实现
├── path-generator/ // 路径生成器
└── picker/ // 拾取(碰撞检测)
8. AbstractRenderer 抽象层
AbstractRenderer 是所有渲染器的基类,定义了统一的插件管理接口和配置体系:
// packages/g-lite/src/AbstractRenderer.ts
class AbstractRenderer implements IRenderer {
clipSpaceNearZ = ClipSpaceNearZ.NEGATIVE_ONE;
private plugins: RendererPlugin[] = [];
private config: RendererConfig;
constructor(config?: Partial<RendererConfig>) {
this.config = {
enableDirtyCheck: true,
enableCulling: false,
enableAutoRendering: true,
enableDirtyRectangleRendering: true,
enableDirtyRectangleRenderingDebug: false,
enableSizeAttenuation: true,
enableRenderingOptimization: false,
...config,
};
}
registerPlugin(plugin: RendererPlugin) { ... }
unregisterPlugin(plugin: RendererPlugin) { ... }
getPlugin(name: string) { ... }
getPlugins() { ... }
getConfig() { ... }
setConfig(config: Partial<RendererConfig>) { ... }
}
RendererConfig 配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
enableDirtyCheck | true | 启用脏检查,仅渲染变化的对象 |
enableCulling | false | 启用视锥裁剪 |
enableAutoRendering | true | 自动帧循环(false 需手动渲染) |
enableDirtyRectangleRendering | true | 启用脏矩形局部重绘 |
enableDirtyRectangleRenderingDebug | false | 可视化脏矩形调试 |
enableSizeAttenuation | true | 3D 场景中物体大小随距离衰减 |
enableRenderingOptimization | false | 额外的渲染优化 |
AbstractRendererPlugin 插件基类
渲染器的每个功能模块都封装为 AbstractRendererPlugin,它提供了注册和清理 RenderingPlugin(Hook 回调)的方法:
abstract class AbstractRendererPlugin<T> implements RendererPlugin {
context: CanvasContext & T;
protected plugins = [];
protected addRenderingPlugin(plugin: RenderingPlugin) {
this.plugins.push(plugin);
this.context.renderingPlugins.push(plugin);
}
protected removeAllRenderingPlugins() {
this.plugins.forEach((plugin) => {
const index = this.context.renderingPlugins.indexOf(plugin);
if (index >= 0) this.context.renderingPlugins.splice(index, 1);
});
}
abstract name: string;
abstract init(runtime: GlobalRuntime): void;
abstract destroy(runtime: GlobalRuntime): void;
}
每个 RenderingPlugin 通过 apply() 方法注册到 RenderingService 的各个 Hook 上:
interface RenderingPlugin {
apply: (context: RenderingPluginContext, runtime: GlobalRuntime) => void;
}
// RenderingPluginContext = CanvasContext & GlobalRuntime
// 合并了画布上下文和全局运行时的完整接口
9. 渲染器切换机制
@antv/g 的 API 设计使得渲染器切换只需更改一行导入:
import { Canvas } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Renderer as SVGRenderer } from '@antv/g-svg';
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
// 切换渲染器只需更换这一行
const renderer = new CanvasRenderer();
// const renderer = new SVGRenderer();
// const renderer = new WebGLRenderer();
const canvas = new Canvas({
container: 'container',
width: 600,
height: 400,
renderer,
});
// 之后的所有图形操作完全相同
const circle = new Circle({ style: { cx: 100, cy: 100, r: 50, fill: 'red' } });
canvas.appendChild(circle);
ClipSpaceNearZ
不同 GPU API 的裁剪空间 Z 轴范围不同。AbstractRenderer 通过 clipSpaceNearZ 属性标识:
enum ClipSpaceNearZ {
ZERO, // [0, 1] - WebGPU / Vulkan / DirectX / Metal
NEGATIVE_ONE, // [-1, 1] - WebGL / OpenGL
}
这个差异在 WebGL 和 WebGPU 渲染器中影响投影矩阵的构建。默认值为 NEGATIVE_ONE(兼容 WebGL),WebGPU 渲染器会覆写为 ZERO。
10. Hook 系统:beforeRender / afterRender
Hook 系统是渲染管线可扩展的关键。每个渲染后端通过注册 RenderingPlugin 来接入管线:
// 渲染插件示例:注册到各 Hook
const myRenderingPlugin: RenderingPlugin = {
apply(context: RenderingPluginContext) {
const { renderingService } = context;
// 初始化
renderingService.hooks.init.tap('MyPlugin', () => {
// 创建 GPU 上下文、编译着色器等
});
// 帧开始:清除画布
renderingService.hooks.beginFrame.tap('MyPlugin', () => {
// ctx.clearRect(0, 0, width, height)
});
// 对象渲染前
renderingService.hooks.beforeRender.tap('MyPlugin', (object) => {
// ctx.save()
// 应用变换矩阵、设置全局透明度等
});
// 实际渲染
renderingService.hooks.render.tap('MyPlugin', (object) => {
// 根据 object.nodeName 调用对应的绘制函数
// Circle -> ctx.arc(), Rect -> ctx.fillRect() 等
});
// 对象渲染后
renderingService.hooks.afterRender.tap('MyPlugin', (object) => {
// ctx.restore()
});
// 帧结束
renderingService.hooks.endFrame.tap('MyPlugin', () => {
// 提交绘制缓冲区
});
// 拾取
renderingService.hooks.pick.tapPromise('MyPlugin', async (result) => {
// 碰撞检测:根据点击坐标查找命中对象
return { ...result, picked: [hitObject] };
});
},
};
Hook 类型对比
| Hook 类型 | 执行方式 | 返回值 | 用途 |
|---|---|---|---|
SyncHook | 同步串行 | 无 | 通知型(beginFrame, endFrame) |
SyncWaterfallHook | 同步串行 | 链式传递 | 可过滤(dirtycheck, cull, pickSync) |
AsyncParallelHook | 异步并行 | 无 | 异步初始化 |
AsyncSeriesWaterfallHook | 异步串行 | 链式传递 | 异步拾取(GPU picking) |
事件相关 Hook
RenderingService 还注册了指针事件相关的 Hook,用于桥接底层浏览器事件和 @antv/g 的事件系统:
// 指针事件 Hook(均为 SyncHook<InteractivePointerEvent>)
pointerDown, pointerUp, pointerMove,
pointerOut, pointerOver, pointerWheel,
pointerCancel, click
小结
本模块覆盖了 @antv/g 渲染管线的完整架构:
- 帧循环:基于
RenderReason的按需渲染,避免无变化时的无效重绘 - 渲染阶段:syncHierarchy → dirtycheck → cull → beginFrame → beforeRender/render/afterRender → endFrame 的完整流水线
- 优化策略:脏矩形局部重绘、视锥裁剪、增量排序,三重优化减少每帧的绘制工作量
- 多后端架构:通过
AbstractRenderer+AbstractRendererPlugin+RenderingPlugin三层抽象,实现 Canvas2D/SVG/WebGL/WebGPU/CanvasKit 五种渲染后端的统一管理 - Hook 系统:基于 SyncHook/SyncWaterfallHook/AsyncParallelHook/AsyncSeriesWaterfallHook 四种类型,实现渲染管线的完全可插拔扩展