【问题标题】:PIXI.js performance optimizationPIXI.js 性能优化
【发布时间】:2021-05-14 17:27:08
【问题描述】:

我使用 canvas 2dContext 构建了一个 fractal clock,但发现当层数增加到 6 以上时出现性能问题。为了提高性能并增加层数限制,我重新构建了 the same clock using PIXI.js

我遇到的问题是性能提升很小 - 在我的桌面上,它从 2dContext 的最多 6 层变为 PIXI 的 7 层。

我在处理 PIXI 解决方案的方式上是否存在明显的问题,可能会限制其性能?

我怀疑最好的解决方案是将所有计算转移到着色器中,并从样板 webgl 开始。

保存点击链接的代码:

const config = {
  numLevels: 7, // bottleneck here, higher means poor performance
  levelAngleOffset: 0,
  levelScale: .7,
  levelOpacityMultiplier: .7,
  levelHueRotation: 40,

  initialRadius: .4,
  initialOpacity: 1,
  initialHue: 0,

  dialOpacity: .1,
  handOpacity: 1,

  secondHandLength: .9,
  secondHandWidth: .015,
  minuteHandLength: .8,
  minuteHandWidth: .03,
  hourHandLength: .6,
  hourHandWidth: .05,

  // set by code
  width: 0,
  height: 0,
};

// https://stackoverflow.com/a/44134328/3282374
function hslToHex(h, s, l) {
  l /= 100;
  const a = s * Math.min(l, 1 - l) / 100;
  const f = n => {
    const k = (n + h / 30) % 12;
    const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
    return Math.round(255 * color).toString(16).padStart(2, '0');   // convert to Hex and prefix "0" if needed
  };
  return `0x${f(0)}${f(8)}${f(4)}`;
}

class Hand extends PIXI.Graphics {
  constructor(x,y,length, rotation, width, opacity) {
    super();
    
    this.length = length;
    
    this.lineStyle({
      width: width, 
      color: 0xFFFFFF,
      alpha: config.handOpacity * opacity,
      cap: PIXI.LINE_CAP.ROUND
    });
    this.moveTo(0, 0);
    this.lineTo(0, -length);
    this.rotation = rotation;
    this.x = x;
    this.y = y;
    this.endFill();
  }
  
  setClock(clock) {
    this.hasClock = true;
    this.clock = clock;
    
    clock.x = 0;
    clock.y = this.length;

    this.addChild(clock);
  }

  update(rotation, angles) {
    this.rotation = rotation;
    
    if (this.hasClock) {
      this.clock.update(angles, 0, -this.length, rotation);
    }
  }
}

class Clock extends PIXI.Graphics {
  constructor(cx, cy, radius, rotation, hue, opacity) {
    super();

    this.lineStyle(0);     
    this.beginFill(hslToHex(hue, 100, 50), opacity * config.dialOpacity);
    this.drawCircle(0, 0, radius);
    this.endFill();
    this.x = cx;
    this.y = cy;
    this.rotation = rotation;

    this.hourHand = new Hand(0, 0, config.hourHandLength * radius, 0, config.hourHandWidth * radius, opacity);
    this.minuteHand = new Hand(0, 0, config.minuteHandLength * radius, 0, config.minuteHandWidth * radius, opacity);
    this.secondHand = new Hand(0, 0, config.secondHandLength * radius, 0, config.secondHandWidth * radius, opacity);

    this.addChild(this.hourHand, this.minuteHand, this.secondHand);
  }
  
  setChildClocks(hour, minute, second) {
    this.hourHand.setClock(hour);
    this.minuteHand.setClock(minute);
    this.secondHand.setClock(second);
  }

  update(angles, cx, cy, rotation) {
    this.x = cx;
    this.y = cy;
    this.rotation = rotation;

    this.hourHand.update(angles.hour, angles);
    this.minuteHand.update(angles.minute, angles);
    this.secondHand.update(angles.second, angles);
  }
}

function getTimeAngles() {
  const time = new Date();

  const millisecond = time.getMilliseconds();
  const second = time.getSeconds() + millisecond / 1000;
  const minute = time.getMinutes() + second / 60;
  const hour = time.getHours() % 12 + minute / 60;

  const hourAngle = Math.PI * 2 * hour / 12;
  const minuteAngle = Math.PI * 2 * minute / 60;
  const secondAngle = Math.PI * 2 * second / 60;

  return {
    hour: hourAngle,
    minute: minuteAngle,
    second: secondAngle
  };
}

let clock;

function initClock() {
  const center = Math.min(config.width, config.height) / 2;
  clock = new Clock(
    center, 
    center, 
    center * config.initialRadius, 
    Math.PI / 2,     
    config.initialHue, 
    config.initialOpacity);

  let level = 0;
  let clocks = [clock];
  while (level < config.numLevels) {
    level++;
    const nextClocks = [];
    for (const parent of clocks) {
      const children = [];
      for (var i  = 0; i < 3; i++) {
        const child = new Clock(
          center, 
          center, 
          center * config.initialRadius * config.levelScale**level, 
          0, 
          config.initialHue + config.levelHueRotation * level, 
          config.initialOpacity * config.levelOpacityMultiplier ** level);
        
        children.push(child);
      }
      parent.setChildClocks(...children);
      nextClocks.push(...children);
    }
    clocks = nextClocks;
  }
}

function step() {
  const angles = getTimeAngles();
  clock.update(angles, config.width/2, config.height/2, 0);
}

function init() {
  PIXI.utils.skipHello();
  
  const app = new PIXI.Application({ antialias: true, transparent: true });
  document.body.appendChild(app.view);

  const canvas = document.querySelector('canvas');

  const resize = () => {
    const {width, height } = canvas.getBoundingClientRect();
    config.width = width;
    config.height = height;
    app.renderer.resize(width, height);
  }

  window.addEventListener('resize', resize);
  resize();

  initClock();

  app.stage.addChild(clock);
  app.ticker.add(step);
}

init();
html, body {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #222;
}

canvas {
  width: 99vw;
  height: 99vh;
}
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.3.9/pixi.min.js"&gt;&lt;/script&gt;

【问题讨论】:

    标签: javascript pixi.js


    【解决方案1】:

    我在处理 PIXI 解决方案的方式上是否存在明显的问题,可能会限制其性能?

    您的性能问题来自 CPU,而不是 GPU。对于深度 8,在我的 PC(英特尔酷睿 i7-10700F)上绘制单个帧需要 60 毫秒,因为 CPU 必须完成大量工作(深入遍历不断扩展的树)。

    切换到 Pixi 无济于事,因为您在 CPU 上做的工作量相同。为了让你的分形时钟在更大的深度上工作得很好,你必须在 GPU 上进行编程。 WebGL 允许您通过编写着色器来做到这一点。

    您需要重新考虑您的程序顺序算法并将其转换为片段着色器,其指令一次并行执行多个像素(片段)。这不是一项简单的任务,需要一些练习。

    但是,根据您希望分形的外观方式,您可能仍然使用画布。您可以只绘制一个级别,然后将此图像数据复制到其他位置,以及平移/旋转/缩放,而不是从头开始重新绘制每个分支。详情请见getImageData

    但是,如果分形的更深节点需要进行仿射变换或简单的基于像素的操作无法实现的操作,则必须使用我描述的自定义片段着色器解决方案。

    有关在 WebGL 中使用着色器可以做什么的灵感,请参阅ShaderToy。有a section for fractals as well。这是clock similar to yours(如下图所示)。

    【讨论】:

    • 是的,我对编写着色器有点熟悉,正如问题中提到的那样,我认为这是最好的方法。我曾考虑尝试重复像素剪辑,但拒绝了它,因为我希望颜色在每个级别都发生变化,并且循环通过 pixelData 似乎有点重。不过可能值得调查。
    • 像素裁剪方法的另一个问题是每个级别的每个时钟的放置和旋转仍然需要在每个步骤中重新计算。所以也许毕竟不是性能提升。
    猜你喜欢
    • 2019-10-11
    • 2016-12-24
    • 2013-03-17
    • 2016-05-14
    • 2011-10-17
    • 2014-01-08
    • 1970-01-01
    • 2017-06-02
    • 2013-03-31
    相关资源
    最近更新 更多