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 提供画布级上下文(如 renderingPluginscamerarenderingContext)。

3. 渲染阶段详解

每一帧的渲染流程严格遵循以下阶段序列:

syncHierarchyrenderDisplayObjectbeginFramebeforeRender/render/afterRenderendFrame
     |                    |                     |                         |                            |
  同步场景图         脏检查+裁剪+排序       帧开始回调              逐对象渲染                    帧结束/提交
        

以下是 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 / initAsyncSyncHook / AsyncParallelHook渲染器初始化(创建 GPU 上下文等)
脏检查dirtycheckSyncWaterfallHook判断对象是否发生变化需要重绘
视锥裁剪cullSyncWaterfallHook剔除视口外的对象
帧开始beginFrameSyncHook清除画布、准备绘制状态
渲染前beforeRenderSyncHook设置对象级渲染状态
渲染renderSyncHook执行实际绘制操作
渲染后afterRenderSyncHook清理对象级状态
帧结束endFrameSyncHook提交绘制结果到屏幕
拾取pick / pickSyncAsyncSeriesWaterfall / SyncWaterfall根据坐标查找命中的对象
销毁destroySyncHook释放 GPU 资源、清理状态

4. 脏矩形优化策略

脏矩形渲染是 @antv/g 在 Canvas2D 后端的核心性能优化策略。原理是:只重绘发生变化的区域,而非整个画布。

工作原理

  1. 标记脏对象:DisplayObject 的属性发生变化时,通过 renderable.dirty 标记为"脏"。同时 RenderingService.dirty()renderReasons 添加 DISPLAY_OBJECT_CHANGED
  2. 脏检查:renderDisplayObject() 遍历场景图时,启用 enableDirtyCheck 后,只有 renderable.dirtytruedirtyRectangleRenderingDisabled 时对象才进入渲染列表。
  3. 矩形合并:收集所有脏对象的包围盒,合并为最小的脏矩形区域。
  4. 局部重绘: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,因为在小规模场景中裁剪计算的开销可能大于收益。对于包含大量对象的场景,建议开启。

注意:HTML 图形默认禁用裁剪(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 变化 -> 完全重排
  // 其他原因 -> 增量排序
}

排序策略

排序实现了两种策略以优化性能:

  1. 完全重排:dirtyReasonZ_INDEX_CHANGED 时,对整个子节点列表执行 .sort(sortByZIndex)
  2. 增量排序:当子节点仅发生增删(非 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-canvasCanvasRenderingContext2D通用 2D 场景,兼容性最好
SVG@antv/g-svgSVG DOM 元素需要 DOM 交互、CSS 动画的场景
WebGL@antv/g-webglWebGL 1.0/2.0大量对象、3D 场景、GPU 加速
WebGPU@antv/g-webgpuWebGPU API次世代 GPU 渲染,计算着色器
CanvasKit@antv/g-canvaskitSkia/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 配置项

配置项默认值说明
enableDirtyChecktrue启用脏检查,仅渲染变化的对象
enableCullingfalse启用视锥裁剪
enableAutoRenderingtrue自动帧循环(false 需手动渲染)
enableDirtyRectangleRenderingtrue启用脏矩形局部重绘
enableDirtyRectangleRenderingDebugfalse可视化脏矩形调试
enableSizeAttenuationtrue3D 场景中物体大小随距离衰减
enableRenderingOptimizationfalse额外的渲染优化

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 渲染管线的完整架构:

  1. 帧循环:基于 RenderReason 的按需渲染,避免无变化时的无效重绘
  2. 渲染阶段:syncHierarchy → dirtycheck → cull → beginFrame → beforeRender/render/afterRender → endFrame 的完整流水线
  3. 优化策略:脏矩形局部重绘、视锥裁剪、增量排序,三重优化减少每帧的绘制工作量
  4. 多后端架构:通过 AbstractRenderer + AbstractRendererPlugin + RenderingPlugin 三层抽象,实现 Canvas2D/SVG/WebGL/WebGPU/CanvasKit 五种渲染后端的统一管理
  5. Hook 系统:基于 SyncHook/SyncWaterfallHook/AsyncParallelHook/AsyncSeriesWaterfallHook 四种类型,实现渲染管线的完全可插拔扩展