前言
在常见图谱,流程图等场景中,基于2d的形状渲染是非常高频的场景。而在这类高频场景中,高密度点,高密度线往往是对图形能力挑战最大的地方。针对这类问题,我们通常会考虑在渲染层,算法层,交互层进行优化。本文则是在渲染方面针对2d可选的三类渲染方式进行横向对比,从而得出三类渲染方式的性能情况,帮助选择出最适合场景的渲染器。
场景
- 点数:5000半径为15的圆
- 线:5000随机连接线的关系网络
- 浏览器:chrome 96
- 系统环境:macOs monterey
- 测试机器:Macbook air m1
- 渲染尺寸:canvas和webgl由于retina屏需要2倍渲染
实现
d3force配置
const simulation = d3.forceSimulation(points)
.force("link", d3.forceLink(links))
.force("charge", d3.forceManyBody())
.force("collide", d3.forceCollide(30).strength(0.2).iterations(5))
.force("center", d3.forceCenter(width/2,height/2))
.alpha(0.2)
webgl
这里使用自研的MiniGL 2d渲染库(github.com/mizy/MiniGL…
const colors = [[0.1,0.1,0.4],[0.4,0.1,0.1],[0.1,0.4,0.1]]
const app = new MiniGL({
container:document.querySelector("#root")
});
app.init();
const width = document.querySelector("#root").clientWidth;
const height = document.querySelector("#root").clientHeight;
const points = [];
const size = [];
const links = [];
const number = 5000;
for(let i = 0;i<number;i+=1){
const x = Math.random()*width;
const y = Math.random()*height
points.push(
{
id:i,
x,y,
position:{x,y},
color:[x/width,y/height,i/width,1],
size:30
}
);
}
const linkPoints = [];
for(let i = 0;i<number;i+=1){
const source = Math.floor(Math.random()*number);
const target = Math.floor(Math.random()*number);
links.push(
{
source,
target
}
);
linkPoints.push({position:points[source]},{position:points[target]})
}
app.canvas.point.setData(points);
app.canvas.line.config.color = [0.8,0.1,0,1];
app.canvas.line.uniformData.z.value = 0.8;
app.canvas.line.setData(linkPoints);
simulation.on("tick",()=>{
const vertex = [];
points.forEach(item=>{
vertex.push(item.x,item.y)
})
const linksPoints = [];
linkPoints.forEach(item=>{
linksPoints.push(item.position.x,item.position.y)
})
app.canvas.point.updateBufferData(vertex, 'position');
app.canvas.line.updateBufferData(linksPoints, 'position');
});
app.canvas.line.drawType = 'LINES';
const time = new Date().getTime();
simulation.on("end",()=>{
console.log(new Date().getTime() - time)
})
这里webgl主要通过bufferSubData来更新缓冲区的顶点数据,另外一种方式是使用webgl2的instanceArrays 来更改偏移值渲染。但从原理上想,本质上都需要更新5000232bit的内存数据,效果应该差不多。另外数据构造上还有一定优化空间。
渲染效果图(圆边缘做了步进模糊用来优化锯齿,所以有泛白)
canvas
渲染逻辑代码
simulation.on("tick",()=>{
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
links.forEach((d)=>{
ctx.moveTo(d.source.x, d.source.y);
ctx.lineTo(d.target.x, d.target.y);
});
ctx.strokeStyle = "#aaa";
ctx.stroke();
ctx.strokeStyle = "#fff";
for (let d of points) {
ctx.beginPath();
ctx.moveTo(d.x + 30, d.y);
ctx.arc(d.x, d.y, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(245,22,144,1)';
ctx.fill();
ctx.stroke();
}
});
渲染效果图
\
svg
渲染逻辑代码
const link = svg.append("g")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.attr("stroke-opacity", 0.8)
.selectAll("line")
.data(links)
.join(a=>a.append("line"))
.attr("stroke", "#f20666")
.attr("stroke-width", 1);
const node = svg.append("g")
.selectAll("circle")
.data(nodes)
.join(a=>a.append("circle"))
.attr("fill", "#23fa93")
.attr("stroke", "#ffffff")
.attr("r", 15)
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
document.querySelector("#root").appendChild(svg.node());
渲染效果图(边缘加了stroke白色)
\
最终数据对比
分辨率1343*780 2倍屏
- webgl 16577ms
- canvas 21420ms
- svg 26877ms
分辨率898*587 2倍屏
- webgl 16367ms
- canvas 21077ms
- svg 27037ms
从开始运行d3force碰撞到结束可以发现,性能的瓶颈主要还是每帧渲染的压力,而没有到d3force的极限,所以通过开启worker单独运行物理引擎还会快很多,另外也还可以通过offscreenCanvas进行离线渲染,同时减少帧数为20帧单倍像素,也可以进行加速。
毋庸置疑的是webgl确实会块很多,webgl比canvas快乐20%以上, canvas比svg快乐30%左右。
优化策略
webgl 还可以使用drawInstanceArrays渲染,并且单倍分辨率渲染。另外使用worker跑非异步物理引擎的d3-force或offscreencanvas渲染,还可以进一步提高渲染性能。
但另一方面,当渲染数据量到达这个程度,或许产品交互上的优化会更加有必要。
demo如下,各位可以尝试自己测试下性能。
(吐槽一下掘金的编辑器做的好烂。。。)