From 88ddb75cde234932d407af7a024d56368752dbed Mon Sep 17 00:00:00 2001 From: "yuqi.pyq" Date: Fri, 14 Jul 2023 13:46:07 +0800 Subject: [PATCH] fix: merge simple path into a single draw call --- .../src/PickingPlugin.ts | 4 +- .../src/meshes/Instanced.ts | 97 ++++++++-- .../src/meshes/InstancedPath.ts | 40 +++- .../src/meshes/Line.ts | 7 +- .../src/renderer/Path.ts | 2 +- site/examples/perf/webgl/demo/arrow.js | 175 ++++++++++++++++++ site/examples/perf/webgl/demo/meta.json | 19 +- .../examples/perf/webgl/demo/rounded-rects.js | 99 ++++++++++ site/examples/shape/polyline/demo/polyline.js | 5 +- 9 files changed, 411 insertions(+), 37 deletions(-) create mode 100644 site/examples/perf/webgl/demo/arrow.js create mode 100644 site/examples/perf/webgl/demo/rounded-rects.js diff --git a/packages/g-plugin-device-renderer/src/PickingPlugin.ts b/packages/g-plugin-device-renderer/src/PickingPlugin.ts index 90f9d4df5..bc97bf2b5 100644 --- a/packages/g-plugin-device-renderer/src/PickingPlugin.ts +++ b/packages/g-plugin-device-renderer/src/PickingPlugin.ts @@ -90,8 +90,8 @@ export class PickingPlugin implements RenderingPlugin { renderingService.hooks.pick.tapPromise( PickingPlugin.tag, - (result: PickingResult) => { - throw new Error('Async version is not implemented.'); + async (result: PickingResult) => { + return this.pick(result); }, ); } diff --git a/packages/g-plugin-device-renderer/src/meshes/Instanced.ts b/packages/g-plugin-device-renderer/src/meshes/Instanced.ts index a355c64cd..8737ce77f 100644 --- a/packages/g-plugin-device-renderer/src/meshes/Instanced.ts +++ b/packages/g-plugin-device-renderer/src/meshes/Instanced.ts @@ -6,7 +6,7 @@ import type { Tuple4Number, } from '@antv/g-lite'; import { CSSRGB, isPattern, isCSSRGB, parseColor, Shape } from '@antv/g-lite'; -import { mat4 } from 'gl-matrix'; +import { mat4, vec3 } from 'gl-matrix'; import { BufferGeometry, GeometryEvent } from '../geometries'; import type { LightPool } from '../LightPool'; import type { Fog } from '../lights'; @@ -80,7 +80,7 @@ export enum VertexAttributeLocation { } /** - * Instanced mesh + * Draw call. */ export abstract class Instanced { /** @@ -141,10 +141,20 @@ export abstract class Instanced { protected lightReceived = false; /** - * + * Divisor of instanced array. */ protected divisor = 1; + /** + * Account for anchor and merge it into modelMatrix. + */ + protected mergeAnchorIntoModelMatrix = false; + + /** + * Create a new instance when exceed. + */ + protected maxInstance = 5000; + protected abstract createMaterial(objects: DisplayObject[]): void; get instance() { @@ -235,6 +245,10 @@ export abstract class Instanced { return true; } + if (this.objects.length >= this.maxInstance) { + return false; + } + // Path / Polyline could be rendered as Line if ( this.instance.nodeName !== object.nodeName && @@ -303,18 +317,18 @@ export abstract class Instanced { ]; } - if (this.clipPathTarget) { - // account for target's rts - mat4.copy(modelMatrix, object.getLocalTransform()); - fillColor = [255, 255, 255, 255]; - mat4.mul( - modelMatrix, - this.clipPathTarget.getWorldTransform(), - modelMatrix, - ); - } else { - mat4.copy(modelMatrix, object.getWorldTransform()); - } + // if (this.clipPathTarget) { + // // account for target's rts + // mat4.copy(modelMatrix, object.getLocalTransform()); + // fillColor = [255, 255, 255, 255]; + // mat4.mul( + // modelMatrix, + // this.clipPathTarget.getWorldTransform(), + // modelMatrix, + // ); + // } else { + // mat4.copy(modelMatrix, object.getWorldTransform()); + // } mat4.mul( modelViewMatrix, this.context.camera.getViewTransform(), @@ -325,6 +339,30 @@ export abstract class Instanced { // @ts-ignore object.renderable3D?.encodedPickingColor) || [0, 0, 0]; + if (this.mergeAnchorIntoModelMatrix) { + const { anchor } = object.parsedStyle as ParsedBaseStyleProps; + let translateX = 0; + let translateY = 0; + let translateZ = 0; + const contentBounds = object.getGeometryBounds(); + if (contentBounds) { + const { halfExtents } = contentBounds; + translateX = -halfExtents[0] * anchor[0] * 2; + translateY = -halfExtents[1] * anchor[1] * 2; + translateZ = -halfExtents[2] * (anchor[2] || 0) * 2; + } + + mat4.mul( + modelMatrix, + object.getWorldTransform(), // apply anchor + mat4.fromTranslation( + modelMatrix, + vec3.fromValues(translateX, translateY, translateZ), + ), + ); + } else { + mat4.copy(modelMatrix, object.getWorldTransform()); + } packedModelMatrix.push(...modelMatrix); packedFillStroke.push( packUint8ToFloat(fillColor[0], fillColor[1]), @@ -798,11 +836,32 @@ export abstract class Instanced { ); } else if (name === 'modelMatrix') { const packed: number[] = []; + const modelMatrix = mat4.create(); objects.forEach((object) => { - const modelMatrix = mat4.copy( - mat4.create(), - object.getWorldTransform(), - ); + if (this.mergeAnchorIntoModelMatrix) { + const { anchor } = object.parsedStyle; + let translateX = 0; + let translateY = 0; + let translateZ = 0; + const contentBounds = object.getGeometryBounds(); + if (contentBounds) { + const { halfExtents } = contentBounds; + translateX = -halfExtents[0] * anchor[0] * 2; + translateY = -halfExtents[1] * anchor[1] * 2; + translateZ = -halfExtents[2] * (anchor[2] || 0) * 2; + } + + mat4.mul( + modelMatrix, + object.getWorldTransform(), // apply anchor + mat4.fromTranslation( + modelMatrix, + vec3.fromValues(translateX, translateY, translateZ), + ), + ); + } else { + mat4.copy(modelMatrix, object.getWorldTransform()); + } packed.push(...modelMatrix); }); diff --git a/packages/g-plugin-device-renderer/src/meshes/InstancedPath.ts b/packages/g-plugin-device-renderer/src/meshes/InstancedPath.ts index f81beec0e..e01fd4952 100644 --- a/packages/g-plugin-device-renderer/src/meshes/InstancedPath.ts +++ b/packages/g-plugin-device-renderer/src/meshes/InstancedPath.ts @@ -43,26 +43,50 @@ export class InstancedPathMesh extends Instanced { const { path: { absolutePath }, } = object.parsedStyle as ParsedPathStyleProps; - if ( - absolutePath.length === 2 && - absolutePath[0][0] === 'M' && - (absolutePath[1][0] === 'C' || - absolutePath[1][0] === 'A' || - absolutePath[1][0] === 'Q') - ) { + if (absolutePath.length >= 2 && absolutePath.length <= 5) { return true; } } return false; } + protected mergeAnchorIntoModelMatrix = true; + + private segmentNum = -1; + + private calcSegmentNum(object: DisplayObject) { + return ( + object.parsedStyle as ParsedPathStyleProps + ).path.absolutePath.reduce((prev, cur) => { + let segment = 0; + if (cur[0] === 'M') { + segment = 0; + } else if (cur[0] === 'L') { + segment = 1; + } else if (cur[0] === 'A' || cur[0] === 'Q' || cur[0] === 'C') { + segment = SEGMENT_NUM; + } else if (cur[0] === 'Z') { + segment = 1; + } + return prev + segment; + }, 0); + } + /** + * Paths with the same number of vertices should be merged. + */ shouldMerge(object: DisplayObject, index: number) { const shouldMerge = super.shouldMerge(object, index); if (!shouldMerge) { return false; } - return true; + if (this.segmentNum === -1) { + this.segmentNum = this.calcSegmentNum(this.instance); + } + + const segmentNum = this.calcSegmentNum(object); + + return this.segmentNum === segmentNum; } createMaterial(objects: DisplayObject[]): void { diff --git a/packages/g-plugin-device-renderer/src/meshes/Line.ts b/packages/g-plugin-device-renderer/src/meshes/Line.ts index 3ec79751e..2d5c6aadd 100644 --- a/packages/g-plugin-device-renderer/src/meshes/Line.ts +++ b/packages/g-plugin-device-renderer/src/meshes/Line.ts @@ -20,7 +20,7 @@ import { import { arcToCubic } from '@antv/util'; import earcut from 'earcut'; import { mat4, vec3 } from 'gl-matrix'; -import { CullMode, Format, VertexBufferFrequency } from '../platform'; +import { Format, VertexBufferFrequency } from '../platform'; import { RENDER_ORDER_SCALE } from '../renderer/Batch'; import frag from '../shader/line.frag'; import vert from '../shader/line.vert'; @@ -190,8 +190,8 @@ export class LineMesh extends Instanced { }); } else if (name === 'lineDash') { this.material.setUniforms({ - [Uniform.DASH]: lineDash[0] || 0, - [Uniform.GAP]: lineDash[1] || 0, + [Uniform.DASH]: (lineDash && lineDash[0]) || 0, + [Uniform.GAP]: (lineDash && lineDash[1]) || 0, }); } else if (name === 'lineDashOffset') { this.material.setUniforms({ @@ -299,7 +299,6 @@ export class LineMesh extends Instanced { [Uniform.VISIBLE]: visibility === 'visible' ? 1 : 0, [Uniform.Z_INDEX]: instance.sortable.renderOrder * RENDER_ORDER_SCALE, }); - this.material.cullMode = CullMode.None; } createGeometry(objects: DisplayObject[]): void { diff --git a/packages/g-plugin-device-renderer/src/renderer/Path.ts b/packages/g-plugin-device-renderer/src/renderer/Path.ts index 594a5b677..bf1408b82 100644 --- a/packages/g-plugin-device-renderer/src/renderer/Path.ts +++ b/packages/g-plugin-device-renderer/src/renderer/Path.ts @@ -67,7 +67,7 @@ export class PathRenderer extends Batch { } if (index === 3) { - return isOneCommandCurve; + return !isLine && isOneCommandCurve; } return true; diff --git a/site/examples/perf/webgl/demo/arrow.js b/site/examples/perf/webgl/demo/arrow.js new file mode 100644 index 000000000..8f3a3dc9e --- /dev/null +++ b/site/examples/perf/webgl/demo/arrow.js @@ -0,0 +1,175 @@ +import { Canvas, CanvasEvent, Path } from '@antv/g'; +import { Renderer as WebGLRenderer } from '@antv/g-webgl'; +import * as lil from 'lil-gui'; +import Stats from 'stats.js'; +import { vec2 } from 'gl-matrix'; + +const getControlPoint = (startPoint, endPoint, percent, offset) => { + const point = { + x: (1 - percent) * startPoint.x + percent * endPoint.x, + y: (1 - percent) * startPoint.y + percent * endPoint.y, + }; + + let tangent = [0, 0]; + vec2.normalize(tangent, [ + endPoint.x - startPoint.x, + endPoint.y - startPoint.y, + ]); + + if (!tangent || (!tangent[0] && !tangent[1])) { + tangent = [0, 0]; + } + const perpendicular = [-tangent[1] * offset, tangent[0] * offset]; // 垂直向量 + point.x += perpendicular[0]; + point.y += perpendicular[1]; + return point; +}; + +// create a renderer +const webglRenderer = new WebGLRenderer(); + +// create a canvas +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 500, + renderer: webglRenderer, +}); + +for (let i = 0; i < 1000; i++) { + const arrowMarker = new Path({ + style: { + path: 'M 10,10 L -10,0 L 10,-10 Z', + stroke: '#1890FF', + anchor: '0.5 0.5', + transformOrigin: 'center', + }, + }); + + const p1 = { x: Math.random() * 600 * 2, y: Math.random() * 500 * 2 }; + const p2 = { x: Math.random() * 600 * 2, y: Math.random() * 500 * 2 }; + const cp = getControlPoint(p1, p2, 0.5, 30); + const path = new Path({ + style: { + lineWidth: 1, + stroke: '#54BECC', + d: [ + ['M', p1.x, p1.y], + ['Q', cp.x, cp.y, p2.x, p2.y], + ], + markerEnd: arrowMarker, + }, + }); + canvas.appendChild(path); + + path.addEventListener('mouseenter', () => { + path.style.stroke = 'red'; + }); + path.addEventListener('mouseleave', () => { + path.style.stroke = '#54BECC'; + }); +} + +for (let i = 0; i < 5000; i++) { + const arrowMarker = new Path({ + style: { + path: 'M 10,10 L -10,0 L 10,-10', + stroke: '#1890FF', + anchor: '0.5 0.5', + transformOrigin: 'center', + }, + }); + + const p1 = { x: Math.random() * 600 * 2, y: Math.random() * 500 * 2 }; + const p2 = { x: Math.random() * 600 * 2, y: Math.random() * 500 * 2 }; + const cp1 = getControlPoint(p1, p2, 0.2, 30); + const cp2 = getControlPoint(p1, p2, 0.8, 30); + const path = new Path({ + style: { + lineWidth: 1, + stroke: '#54BECC', + d: [ + ['M', p1.x, p1.y], + ['C', cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y], + ], + markerEnd: arrowMarker, + }, + }); + canvas.appendChild(path); + path.addEventListener('mouseenter', () => { + path.style.stroke = 'red'; + }); + path.addEventListener('mouseleave', () => { + path.style.stroke = '#54BECC'; + }); +} +// stats +const stats = new Stats(); +stats.showPanel(0); +const $stats = stats.dom; +$stats.style.position = 'absolute'; +$stats.style.left = '0px'; +$stats.style.top = '0px'; +const $wrapper = document.getElementById('container'); +$wrapper.appendChild($stats); + +const camera = canvas.getCamera(); +canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + if (stats) { + stats.update(); + } + + // manipulate camera instead of the root of canvas + camera.rotate(0, 0, 1); +}); + +// update Camera's zoom +// @see https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js +const minZoom = 0; +const maxZoom = Infinity; +canvas + .getContextService() + .getDomElement() // g-canvas/webgl 为 ,g-svg 为 + .addEventListener( + 'wheel', + (e) => { + e.preventDefault(); + let zoom; + if (e.deltaY < 0) { + zoom = Math.max(minZoom, Math.min(maxZoom, camera.getZoom() / 0.95)); + } else { + zoom = Math.max(minZoom, Math.min(maxZoom, camera.getZoom() * 0.95)); + } + camera.setZoom(zoom); + }, + { passive: false }, + ); + +// GUI +const gui = new lil.GUI({ autoPlace: false }); +$wrapper.appendChild(gui.domElement); + +const transformFolder = gui.addFolder('style'); +const transformConfig = { + lineWidth: 1, + lineDash: 10, + lineDashOffset: 0, +}; +transformFolder + .add(transformConfig, 'lineWidth', 0, 20) + .onChange((lineWidth) => { + const paths = canvas.document.querySelectorAll('path'); + paths.forEach((path) => { + path.style.lineWidth = lineWidth; + }); + }); +transformFolder.add(transformConfig, 'lineDash', 0, 50).onChange((dash) => { + const paths = canvas.document.querySelectorAll('path'); + paths[0].style.lineDash = [dash, dash]; +}); +transformFolder + .add(transformConfig, 'lineDashOffset', 0, 50) + .onChange((lineDashOffset) => { + const paths = canvas.document.querySelectorAll('path'); + paths[0].style.lineDashOffset = lineDashOffset; + }); diff --git a/site/examples/perf/webgl/demo/meta.json b/site/examples/perf/webgl/demo/meta.json index 3925240ec..4956ab2d5 100644 --- a/site/examples/perf/webgl/demo/meta.json +++ b/site/examples/perf/webgl/demo/meta.json @@ -16,6 +16,14 @@ }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sQH_T5LdKdcAAAAAAAAAAAAADmJ7AQ/original" }, + { + "filename": "rounded-rects.js", + "title": { + "zh": "大量圆角矩形", + "en": "Instanced Rounded Rect" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sQH_T5LdKdcAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "images.js", "title": { @@ -29,7 +37,16 @@ "title": { "zh": "包含一条曲线命令的 Path", "en": "Instanced Simple Path(one-command curve)" - } + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CvgZSYvSp3IAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "arrow.js", + "title": { + "zh": "大量箭头", + "en": "Instanced Arrow" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8F-XSbz8EIkAAAAAAAAAAAAADmJ7AQ/original" }, { "filename": "circle-path.js", diff --git a/site/examples/perf/webgl/demo/rounded-rects.js b/site/examples/perf/webgl/demo/rounded-rects.js new file mode 100644 index 000000000..c1afd924d --- /dev/null +++ b/site/examples/perf/webgl/demo/rounded-rects.js @@ -0,0 +1,99 @@ +import { Canvas, CanvasEvent, Rect } from '@antv/g'; +import { Renderer as WebGLRenderer } from '@antv/g-webgl'; +import { Renderer as WebGPURenderer } from '@antv/g-webgpu'; +import * as lil from 'lil-gui'; +import Stats from 'stats.js'; + +// create a renderer +const webglRenderer = new WebGLRenderer(); +const webgpuRenderer = new WebGPURenderer({ + shaderCompilerPath: '/glsl_wgsl_compiler_bg.wasm', +}); + +// create a canvas +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 500, + renderer: webglRenderer, +}); + +canvas.addEventListener(CanvasEvent.READY, () => { + for (let i = 0; i < 5; i++) { + const rect = new Rect({ + style: { + x: Math.random() * 600, + y: Math.random() * 500, + width: 200, + height: 100, + fill: '#C6E5FF', + stroke: '#5B8FF9', + lineWidth: 1, + radius: [0, 4, 8, 16], + }, + }); + canvas.appendChild(rect); + } +}); + +// stats +const stats = new Stats(); +stats.showPanel(0); +const $stats = stats.dom; +$stats.style.position = 'absolute'; +$stats.style.left = '0px'; +$stats.style.top = '0px'; +const $wrapper = document.getElementById('container'); +$wrapper.appendChild($stats); + +const camera = canvas.getCamera(); +canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + if (stats) { + stats.update(); + } + + // manipulate camera instead of the root of canvas + camera.rotate(0, 0, 1); +}); + +// update Camera's zoom +// @see https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js +const minZoom = 0; +const maxZoom = Infinity; +canvas + .getContextService() + .getDomElement() // g-canvas/webgl 为 ,g-svg 为 + .addEventListener( + 'wheel', + (e) => { + e.preventDefault(); + let zoom; + if (e.deltaY < 0) { + zoom = Math.max(minZoom, Math.min(maxZoom, camera.getZoom() / 0.95)); + } else { + zoom = Math.max(minZoom, Math.min(maxZoom, camera.getZoom() * 0.95)); + } + camera.setZoom(zoom); + }, + { passive: false }, + ); + +// GUI +const gui = new lil.GUI({ autoPlace: false }); +$wrapper.appendChild(gui.domElement); +const rendererFolder = gui.addFolder('renderer'); +const rendererConfig = { + renderer: 'webgl', +}; +rendererFolder + .add(rendererConfig, 'renderer', ['webgl', 'webgpu']) + .onChange((rendererName) => { + let renderer; + if (rendererName === 'webgl') { + renderer = webglRenderer; + } else if (rendererName === 'webgpu') { + renderer = webgpuRenderer; + } + canvas.setRenderer(renderer); + }); +rendererFolder.open(); diff --git a/site/examples/shape/polyline/demo/polyline.js b/site/examples/shape/polyline/demo/polyline.js index 1fc8a8926..308036176 100644 --- a/site/examples/shape/polyline/demo/polyline.js +++ b/site/examples/shape/polyline/demo/polyline.js @@ -33,7 +33,7 @@ const canvas = new Canvas({ container: 'container', width: 600, height: 500, - renderer: canvasRenderer, + renderer: webglRenderer, }); // create a line @@ -59,7 +59,8 @@ const polyline = new Polyline({ style: { points, stroke: '#1890FF', - lineWidth: 2, + lineWidth: 20, + lineCap: 'round', cursor: 'pointer', }, });