源码解读
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):
- mergeDirtyAABBs 将每个重绘图形的包围盒合并
- removedRBushNodeAABBs.map(...) 创建包围盒
- safeMergeAABB 合并所有脏矩形形成新的包围盒
- convertAABB2Rect 转换为脏矩形对象信息
- Math浮点清除计算
- clip 脏矩形裁切
- setTransform 坐标系移动
- searchDirtyObjects 寻找区域内所有需要重绘图形
- sort 根据z-index排序
- renderDisplayObject 遍历渲染队列执行图形渲染流程
- 存储脏矩形包围盒
- 清空渲染队列、恢复状态队列