关于d3force下webgl,canvas,svg的渲染压力测试

916 阅读3分钟

前言

  在常见图谱,流程图等场景中,基于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倍屏

  1. webgl 16577ms
  2. canvas 21420ms
  3. svg 26877ms

分辨率898*587 2倍屏

  1. webgl 16367ms
  2. canvas 21077ms
  3. svg 27037ms

从开始运行d3force碰撞到结束可以发现,性能的瓶颈主要还是每帧渲染的压力,而没有到d3force的极限,所以通过开启worker单独运行物理引擎还会快很多,另外也还可以通过offscreenCanvas进行离线渲染,同时减少帧数为20帧单倍像素,也可以进行加速。

毋庸置疑的是webgl确实会块很多,webgl比canvas快乐20%以上, canvas比svg快乐30%左右。

优化策略

webgl 还可以使用drawInstanceArrays渲染,并且单倍分辨率渲染。另外使用worker跑非异步物理引擎的d3-force或offscreencanvas渲染,还可以进一步提高渲染性能。

但另一方面,当渲染数据量到达这个程度,或许产品交互上的优化会更加有必要。

demo如下,各位可以尝试自己测试下性能。

(吐槽一下掘金的编辑器做的好烂。。。)