G4.0源码解析(二)

源码解读

RenderingService中定义了所有生命周期钩子,如下:

php 复制代码
  hooks = {
    /**
     * called before any frame rendered
     */
    init: new AsyncParallelHook<[]>(),
    /**
     * only dirty object which has sth changed will be rendered
     */
    dirtycheck: new SyncWaterfallHook<[DisplayObject | null]>(['object']),
    /**
     * do culling
     */
    cull: new SyncWaterfallHook<[DisplayObject | null, ICamera]>([
      'object',
      'camera',
    ]),
    /**
     * called at beginning of each frame, won't get called if nothing to re-render
     */
    beginFrame: new SyncHook<[]>([]),
    /**
     * called before every dirty object get rendered
     */
    beforeRender: new SyncHook<[DisplayObject]>(['objectToRender']),
    /**
     * called when every dirty object rendering even it's culled
     */
    render: new SyncHook<[DisplayObject]>(['objectToRender']),
    /**
     * called after every dirty object get rendered
     */
    afterRender: new SyncHook<[DisplayObject]>(['objectToRender']),
    endFrame: new SyncHook<[]>([]),
    destroy: new SyncHook<[]>([]),
    /**
     * use async but faster method such as GPU-based picking in `g-plugin-device-renderer`
     */
    pick: new AsyncSeriesWaterfallHook<[PickingResult], PickingResult>([
      'result',
    ]),

    /**
     * Unsafe but sync version of pick.
     */
    pickSync: new SyncWaterfallHook<[PickingResult], PickingResult>(['result']),
    /**
     * used in event system
     */
    pointerDown: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerUp: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerMove: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerOut: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerOver: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerWheel: new SyncHook<[InteractivePointerEvent]>(['event']),
    pointerCancel: new SyncHook<[InteractivePointerEvent]>(['event']),
  };

render和renderDisplayObject是这个文件中两个核心的方法,render方法通过tapable的生命周期把渲染过程所有要做的事情串联起来,在这里我们称之为渲染管线。

kotlin 复制代码
export class RenderingService {
  // 该方法执行了整个渲染过程
  // 其中调用了生命周期钩子(插件中会注册钩子),来完成渲染管线
  render(canvasConfig: Partial<CanvasConfig>) {
    this.stats.total = 0;
    this.stats.rendered = 0;
    this.zIndexCounter = 0;

    const { renderingContext } = this.context;

    // 计算并同步图形矩阵
    this.globalRuntime.sceneGraphService.syncHierarchy(renderingContext.root);
    // 触发BOUNDS_CHANGED事件
    this.globalRuntime.sceneGraphService.triggerPendingEvents();

    if (renderingContext.renderReasons.size && this.inited) {
      // 批量渲染根节点下所有图形对象
      this.renderDisplayObject(
        renderingContext.root,
        canvasConfig,
        renderingContext,
      );

      // 执行所有注册的beginFrame钩子(全局搜索beginFrame.tap可查到注册的位置)
      this.hooks.beginFrame.call();

      // renderListCurrentFrame用来存储当前帧所有要重新渲染的图形对象
      // 存储过程在renderDisplayObject中完成
      renderingContext.renderListCurrentFrame.forEach((object) => {
      	// 执行所有注册的beforeRender钩子
        this.hooks.beforeRender.call(object);
      	// 执行所有注册的render钩子
        this.hooks.render.call(object);
      	// 执行所有注册的afterRender钩子
        this.hooks.afterRender.call(object);
      });

      // 执行所有注册的endFrame钩子
      this.hooks.endFrame.call();
      // 清空脏图形对象栈
      renderingContext.renderListCurrentFrame = [];
      renderingContext.renderReasons.clear();
    }

    // console.log('stats', this.stats);
  }

  // 批量渲染图形的方法
  private renderDisplayObject(
    displayObject: DisplayObject,
    canvasConfig: Partial<CanvasConfig>,
    renderingContext: RenderingContext,
  ) {
    const { enableDirtyCheck, enableCulling } =
      canvasConfig.renderer.getConfig();
    // 批量更新样式属性,相当于konva的setAttr
    // *** 这里涉及到更新包围盒的处理逻辑,具体代码在styleValueRegistry》processProperties中执行
    this.globalRuntime.styleValueRegistry.recalc(displayObject);

    // TODO: relayout

    // 获取脏节点
    const objectChanged = enableDirtyCheck
      ? this.hooks.dirtycheck.call(displayObject)
      : displayObject;
    if (objectChanged) {
      // 做一次剔除过滤
      const objectToRender = enableCulling
        ? this.hooks.cull.call(objectChanged, this.context.camera)
        : objectChanged;

      // 要渲染的图形放到renderListCurrentFrame栈中
      if (objectToRender) {
        this.stats.rendered++;
        renderingContext.renderListCurrentFrame.push(objectToRender);
      }
    }

    // 移除渲染脏标记
    displayObject.renderable.dirty = false;
    displayObject.sortable.renderOrder = this.zIndexCounter++;

    this.stats.total++;

    // sort is very expensive, use cached result if posible
    const sortable = displayObject.sortable;
    let renderOrderChanged = false;
    // 按先后渲染顺序排序
    if (sortable.dirty) {
      sortable.sorted = displayObject.childNodes.slice().sort(sortByZIndex);
      renderOrderChanged = true;
      sortable.dirty = false;
    }

    // recursive rendering its children
    (sortable.sorted || displayObject.childNodes).forEach(
      (child: DisplayObject) => {
        // 递归渲染子节点
        this.renderDisplayObject(child, canvasConfig, renderingContext);
      },
    );

    // 触发RENDER_ORDER_CHANGED事件
    if (renderOrderChanged) {
      displayObject.forEach((child: DisplayObject) => {
        this.renderOrderChangedEvent.target = child;
        this.renderOrderChangedEvent.detail = {
          renderOrder: child.sortable.renderOrder,
        };
        child.ownerDocument.defaultView.dispatchEvent(
          this.renderOrderChangedEvent,
          true,
        );
      });
    }
  }
}

接下来我们继续看每个生命周期钩子都有哪些插件注册以及做了什么

目前存在5个钩子(beginFrame、beforeRender、render、afterRender、endFrame),其中beforeRender、render、afterRender是每个图形对象触发render前后要做的事情,而beginFrame、endFrame是当前帧这一批脏对象渲染前后的钩子,具体渲染的操作在endFrame中执行。

beginFrame

beginFrame钩子与2D相关的只有CanvasRenderPlugin中

arduino 复制代码
// 注册beginFrame钩子
renderingService.hooks.beginFrame.tap(CanvasRendererPlugin.tag, () => {
  const context = contextService.getContext();
  const dpr = contextService.getDPR();
  const { width, height } = config;
  const { dirtyObjectNumThreshold, dirtyObjectRatioThreshold } =
    this.canvasRendererPluginOptions;

  // total是记录当前帧需要重新渲染的图形数量(超总量的80%直接进行画布重绘)
  const { total, rendered } = renderingService.getStats();
  const ratio = rendered / total;

  // 大于渲染数量阙值的直接全屏重绘 (500个或 0.8比例)
  this.clearFullScreen =
    renderingService.disableDirtyRectangleRendering() ||
    (rendered > dirtyObjectNumThreshold &&
     ratio > dirtyObjectRatioThreshold);

  // 画布清屏
  if (context) {
    context.resetTransform();
    if (this.clearFullScreen) {
      this.clearRect(
        context,
        0,
        0,
        width * dpr,
        height * dpr,
        config.background,
      );
    }
  }
});

beforeRender

目前没有地方注册

render

render钩子与2D相关在CanvasRenderPlugin中

kotlin 复制代码
renderingService.hooks.render.tap(
  CanvasRendererPlugin.tag,
  (object: DisplayObject) => {
    if (!this.clearFullScreen) {
      // 放入渲染队列中
      this.renderQueue.push(object);
    }
  },
);

在阅读源码的过程中,发现上面代码的renderQueue和renderListCurrentFrame两个参数的意义并没什么不同,都是作为当前帧要渲染的图形队列,可能区别在于renderListCurrentFrame是相对RenderingService环境的,而renderQueue是相对CanvasRenderPlugin环境的。

afterRender

ini 复制代码
renderingService.hooks.afterRender.tap(CullingPlugin.tag, (object: DisplayObject) => {
  object.cullable.visibilityPlaneMask = -1;
});

endFrame

整个流程中最重要的绘制过程发生在endFrame中

kotlin 复制代码
// 核心渲染过程
renderingService.hooks.endFrame.tap(CanvasRendererPlugin.tag, () => {
  const context = contextService.getContext();
  // clear & clip dirty rectangle
  const dpr = contextService.getDPR();
  mat4.fromScaling(this.dprMatrix, vec3.fromValues(dpr, dpr, 1));
  mat4.multiply(this.vpMatrix, this.dprMatrix, camera.getOrthoMatrix());

  if (this.clearFullScreen) {
    // 如果是全屏重绘,根据zIndex渲染每个图形
    renderByZIndex(renderingContext.root, context);
  } else {
    // merge removed AABB
    // 合并所有renderQueue(脏图形对象队列)中的包围盒形成新的包围盒
    // 获取本帧渲染要移除的图形包围盒
    // 对上述包围盒做合并,形成一个大的包围盒
    const dirtyRenderBounds = this.safeMergeAABB(
      this.mergeDirtyAABBs(this.renderQueue),
      ...this.removedRBushNodeAABBs.map(({ minX, minY, maxX, maxY }) => {
        const aabb = new AABB();
        aabb.setMinMax(
          vec3.fromValues(minX, minY, 0),
          vec3.fromValues(maxX, maxY, 0),
        );
        return aabb;
      }),
    );
    this.removedRBushNodeAABBs = [];

    if (AABB.isEmpty(dirtyRenderBounds)) {
      this.renderQueue = [];
      return;
    }

    // 转换为脏矩形,获取x/y/width/height信息
    const dirtyRect = this.convertAABB2Rect(dirtyRenderBounds);
    const { x, y, width, height } = dirtyRect;

    // tl、tr、bl、br分别是获取的包围盒四个点的x/y坐标
    const tl = vec3.transformMat4(
      this.vec3a,
      vec3.fromValues(x, y, 0),
      this.vpMatrix,
    );
    const tr = vec3.transformMat4(
      this.vec3b,
      vec3.fromValues(x + width, y, 0),
      this.vpMatrix,
    );
    const bl = vec3.transformMat4(
      this.vec3c,
      vec3.fromValues(x, y + height, 0),
      this.vpMatrix,
    );
    const br = vec3.transformMat4(
      this.vec3d,
      vec3.fromValues(x + width, y + height, 0),
      this.vpMatrix,
    );

    // 下面的Math计算是为了解决局部刷新时清空的区域不足,会留下一些残影的问题
    // 讲解文章看这里:https://www.yuque.com/antv/ou292n/bi8nix
    const minx = Math.min(tl[0], tr[0], br[0], bl[0]);
    const miny = Math.min(tl[1], tr[1], br[1], bl[1]);
    const maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
    const maxy = Math.max(tl[1], tr[1], br[1], bl[1]);

    const ix = Math.floor(minx);
    const iy = Math.floor(miny);
    const iwidth = Math.ceil(maxx - minx);
    const iheight = Math.ceil(maxy - miny);

    // 脏矩形区域清空
    context.save();
    this.clearRect(context, ix, iy, iwidth, iheight, config.background);
    context.beginPath();
    context.rect(ix, iy, iwidth, iheight);

    // 在这个脏矩形区域内渲染要重绘的图形,所以要执行clip
    context.clip();

    // @see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformations
    context.setTransform(
      this.vpMatrix[0],
      this.vpMatrix[1],
      this.vpMatrix[4],
      this.vpMatrix[5],
      this.vpMatrix[12],
      this.vpMatrix[13],
    );

    // debug draw dirty rectangle
    const { enableDirtyRectangleRenderingDebug } =
      config.renderer.getConfig();
    if (enableDirtyRectangleRenderingDebug) {
      canvas.dispatchEvent(
        new CustomEvent(CanvasEvent.DIRTY_RECTANGLE, {
          dirtyRect: {
            x: ix,
            y: iy,
            width: iwidth,
            height: iheight,
          },
        }),
      );
    }

    // 通过rBushNodes获取要渲染的图形
    const dirtyObjects = this.searchDirtyObjects(dirtyRenderBounds);

    // 绘制图形
    dirtyObjects
      // sort by z-index
      .sort((a, b) => a.sortable.renderOrder - b.sortable.renderOrder)
      .forEach((object) => {
        // 过滤未显示的和不在视口内的图形
        if (object && object.isVisible() && !object.isCulled()) {
          // 渲染单个图形对象 - renderDisplayObject
          this.renderDisplayObject(
            object,
            context,
            this.context,
            this.restoreStack,
          );
        }
      });

    context.restore();

    // 缓存脏矩形的包围盒
    this.renderQueue.forEach((object) => {
      this.saveDirtyAABB(object);
    });

    // clear queue
    this.renderQueue = [];
  }

  // pop restore stack, eg. root -> parent -> child
  this.restoreStack.forEach(() => {
    context.restore();
  });
  // clear restore stack
  this.restoreStack = [];
});

最后来看CanvasRendererPlugin::renderDisplayObject的内部处理过程

scss 复制代码
// 渲染图形对象
renderDisplayObject(
  object: DisplayObject,
  context: CanvasRenderingContext2D,
  canvasContext: CanvasContext,
  restoreStack: DisplayObject[],
) {
  const nodeName = object.nodeName;

  // restore to its ancestor

  const parent = restoreStack[restoreStack.length - 1];
  if (
    parent &&
    !(
      object.compareDocumentPosition(parent) & Node.DOCUMENT_POSITION_CONTAINS
    )
  ) {
    context.restore();
    restoreStack.pop();
  }

  // 这里去g-plugin-canvas-renderer/src/index.ts中获取具体图形的样式渲染器,相当于去拿konva中具体图形的_sceneFunc方法
  const styleRenderer = this.context.styleRendererFactory[nodeName];
  // 这里去g-plugin-canvas-path-generator/src/index.ts中获取具体图形的路径渲染器(路径绘制方法)
  const generatePath = this.pathGeneratorFactory[nodeName];

  // 有需要裁切的图形优先执行绘制方法
  const { clipPath } = object.parsedStyle as ParsedBaseStyleProps;
  if (clipPath) {
    this.applyWorldTransform(context, clipPath);

    // generate path in local space
    const generatePath = this.pathGeneratorFactory[clipPath.nodeName];
    if (generatePath) {
      context.save();

      // save clip
      restoreStack.push(object);

      context.beginPath();
      generatePath(context, clipPath.parsedStyle);
      context.closePath();
      context.clip();
    }
  }

  // 优先为context设置fill & stroke属性
  if (styleRenderer) {
    this.applyWorldTransform(context, object);

    context.save();

    // apply attributes to context
    this.applyAttributesToContext(context, object);
  }

  // 绘制路径图形
  if (generatePath) {
    context.beginPath();
    generatePath(context, object.parsedStyle);
    if (
      object.nodeName !== Shape.LINE &&
      object.nodeName !== Shape.PATH &&
      object.nodeName !== Shape.POLYLINE
    ) {
      context.closePath();
    }
  }

  // 调用具体图形的样式渲染方法(填充、描边、阴影等)
  if (styleRenderer) {
    styleRenderer.render(
      context,
      object.parsedStyle,
      object,
      canvasContext,
      this,
    );

    // restore applied attributes, eg. shadowBlur shadowColor...
    context.restore();
  }

  // finish rendering, clear dirty flag
  object.renderable.dirty = false;
}

流程总结

渲染过程:

beginFrame -> (beforeRender -> render -> afterRender) -> endFrame

括号内为每个节点的渲染过程

局部重绘流程(endFrame):

  1. mergeDirtyAABBs 将每个重绘图形的包围盒合并
  2. removedRBushNodeAABBs.map(...) 创建包围盒
  3. safeMergeAABB 合并所有脏矩形形成新的包围盒
  4. convertAABB2Rect 转换为脏矩形对象信息
  5. Math浮点清除计算
  6. clip 脏矩形裁切
  7. setTransform 坐标系移动
  8. searchDirtyObjects 寻找区域内所有需要重绘图形
  9. sort 根据z-index排序
  10. renderDisplayObject 遍历渲染队列执行图形渲染流程
  11. 存储脏矩形包围盒
  12. 清空渲染队列、恢复状态队列
相关推荐
余生H2 天前
即时可玩web小游戏(二):打砖块(支持移动端版) - 集成InsCode快来阅读并即时体验吧~
前端·javascript·inscode·canvas·h5游戏
普兰店拉马努金9 天前
【Canvas与图标】牛皮纸文件袋图标
canvas·图标·文件袋·牛皮纸
德育处主任11 天前
前端啊,拿Lottie炫个动画吧
前端·svg·canvas
GDAL12 天前
深入剖析Canvas的getBoundingClientRect:精准定位与交互事件实现
canvas
剑亦未配妥14 天前
使用js和canvas、html实现简单的俄罗斯方块小游戏
前端·javascript·canvas·1024程序员节
howard200516 天前
2.1 HTML5 - Canvas标签
html5·canvas
普兰店拉马努金1 个月前
【Canvas与标牌】立入禁止标牌
canvas·警示·标识·立入禁止
Anlige1 个月前
Javascript:使用canvas画二维码矩阵
javascript·canvas·qrcode
牛老师讲GIS1 个月前
分享一个从图片中提取色卡的实现
canvas·提取颜色
云樱梦海1 个月前
OpenAI 推出 Canvas 工具,助力用户与 ChatGPT 协作写作和编程
人工智能·chatgpt·canvas