PixiJS 源码解读:绘制矩形,底层都做了什么?

开发 前端
使用 PixiJS 简单易用的 API,我们可以在浏览器页面的 Canvas 元素上高性能地绘制图形,实现流畅的动画。它的底层是 WebGL。

大家好,我是前端西瓜哥,今天带大家看一下 PixiJS 的源码实现。

PixiJS 是一个非常流行的 Canvas 库,start 数将近 4w。

使用 PixiJS 简单易用的 API,我们可以在浏览器页面的 Canvas 元素上高性能地绘制图形,实现流畅的动画。它的底层是 WebGL。

用 PixiJS 绘制一个矩形,代码实现为:

const app = new PIXI.Application({
  width: 500,
  height: 300,
});
document.body.appendChild(app.view);

const graph = new PIXI.Graphics();
graph.beginFill(0xff0044); // 填充色
graph.drawRect(10, 10, 100, 80);
graph.endFill();

app.stage.addChild(graph);

渲染结果:

图片

这些代码的底层究竟做了什么呢?这次西瓜哥就带大家来一探究竟。

使用的 PixiJS 版本为 7.2.4。

Application 的初始化

首先是调用 Application 类的构造函数,创建 app 对象。

下面是 Application 构造函数的代码。

export class Application {
  // 创建 stage
  public stage: Container = new Container();
  // ...
  constructor(options) {
    options = Object.assign(
      {
        // 是否强制使用 Canvas 2D,否则如果支持 WebGL,用 WebGL
        // 默认为 false,且已经废弃 Canvas 2D,仅 pixi.js-legacy 可用
        forceCanvas: false, 
      },
      options
    );
 
    // 选择渲染器
    this.renderer = autoDetectRenderer(options);

    // 插件初始化
    Application._plugins.forEach((plugin) => {
      plugin.init.call(this, options);
    });
  }
}

主要做了以下几件事。

  1. 初始化 this.stage 为一个新的 Container 对象,将其作为根容器,之后我们绘制的矩形会放置于其下。
  2. 选择渲染器 renderer,有两种:Renderer(基于 WebGL) 和 CanvasRenderer(基于 Canvas 2D)。最新版 PixiJS 只内置了  Renderer。如果你希望在 WebGL 不可用时回退为 CanvasRenderer,需要改用 pixie.js-legacy 库。
  3. 调用 Renderer 的构造函数。它的属性 view 会指向一个 canvas 元素,Application 的 view 通过 getter 的代理方式拿到这个 view。
  4. 调用 Application 中注册插件的 init 方法,进行初始化。

Application 默认内置两个插件:

  • TickerPlugin:不停地在绘制下一帧前调用(基于 requestAnimationFrame)传入的回调函数,PixiJS 会在这里指定下一帧数要绘制的新内容。
  • ResizePlugin:监听容器尺寸变化,重绘画布。

图片

创建图形

const graph = new PIXI.Graphics();

创建一个 Graphics 对象。这个 Graphics 对象下可以绘制任何图形,这里我只绘制一个矩形。

graph.beginFill(0xff0044); // 填充色

该方法会给 Graphics 对象的 _fillStyle 设置为指定的颜色值。传入的颜色值会进行标准化(normalize)。

图片

Pixijs 实现有自己的风格:喜欢用类似 _varX 的方法保存 “私有” 变量,然后提供对应的 setter 和 getter 去读写这个内部变量。

getter 可能不提供,这样一个属性就会变成只读属性。有些 getter 里会做懒加载,在第一次读取的时候再初始化,比如 Texture.WHITE。

如果我们不指定颜色,这个 _fillStyle 会使用默认值,且其 visible 属性为 false,表示图形没有填充色,也会在之后的渲染阶段跳过填充的逻辑。

然后是创建一个矩形。

graph.drawRect(10, 10, 100, 80);

上面代码其实调用的是:

return this.drawShape(new PIXI.Rectangle(x, y, width, height));

首先创建一个 Rectangle 对象。

然后基于该 Rectangle 对象、之前设置的 fillStyle、lineStyle、matrix 创建一个 GraphicsData 对象,最后添加到给 rect._geometry.graphicsData 数组上。

图片

总之就是将这个矩形的数据记录下来,之后 PixiJS 会基于这些值构造出绘制 WebGL 可以直接使用的数据。

然后是重置填充色。

rect.endFill();

将 rect 的 _fillStyle 设置为默认值:

public reset() {
  this.color = 0xFFFFFF;
  this.alpha = 1;
  this.texture = Texture.WHITE;
  this.matrix = null;
  this.visible = false;
}

最后是把 rect 添加到容器 app.stage 下。

app.stage.addChild(rect);

对应的源码是:

export class Container extends DisplayObject {
  // ...

  addChild(...children) {
    if (children.length > 1) {
      // 有多个图形要添加,会遍历调用当前 addChild 方法
      for (let i = 0; i < children.length; i++) {
        this.addChild(children[i]);
      }
    } else {
      const child = children[0];
      if (child.parent) {
        child.parent.removeChild(child);
      }

      child.parent = this;
      this.sortDirty = true; // 表示没有排序

      child.transform._parentID = -1;

      this.children.push(child);

      this._boundsID++;

      // 触发子节点改变的相关事件
      this.onChildrenChange(this.children.length - 1); 
      this.emit("childAdded", child, this, this.children.length - 1);
      child.emit("added", this);
    }
    return children[0];
  }
}

至此,我们的矩形设置好属性并添加到图形树上。

下面是渲染环节。

绘制

还记得我们初始化 Application 时,初始化的两个插件吗?

其中一个就是 TickerPlugin,它是 raf(requestAnimationFrame)的封装,会在页面绘制下一帧要之前执行回调函数。

Application 初始化时,调用了TickerPlugin.init() 方法,将 renderer 的 render 方法绑定到 Ticker 上。这样,render 就会不断地被异步调用。

class TickerPlugin {
  static init(options) {
    Object.defineProperty(this, "ticker", {
      set(ticker) {
        // 将 app.render 函数传入 ticker 的回调列表
        ticker.add(this.render, this, UPDATE_PRIORITY.LOW);
      },
      // ...
    });
    
    // 触发 ticker setter
    this.ticker = options.sharedTicker ? Ticker.shared : new Ticker();
  }
  // ...
}

render 方法:

class Application {
  // ...
  public render() {
    this.renderer.render(this.stage);
  }
}

因为渲染的过程非常长,代码逻辑太多,各种细枝末节,这里只讲大致流程,之后会写一篇文章具体讲解。

  1. 递归 app.stage 下的子 graph 对象,将其变换矩阵与父容器的做矩阵乘法(父容器的 transfrom 会影响子节点),最终计算出所有节点的最终复合变换矩阵;
  2. 之前创建的 Rectangle 对象,它的 x、y、width、height,转换为 WebGL 顶点着色器(Vertex Shader)需要的 8 个顶点数据;
  3. 对顶点应用变换矩阵;
  4. 计算好的顶点和颜色的一些中间批量数据。最后在 BatchRenderer.drawBatches() 方法中,调用了  WebGL 的 API:gl.drawElements。

PixiJS 高性能的一个原因是减少 draw call,尽可能一次性批量(batch)提供大量顶点和片元给到 WebGL 去处理,充分利用 GPU 的并发计算能力。

责任编辑:姜华 来源: 前端西瓜哥
相关推荐

2023-10-09 07:49:33

PixiJSWebGL

2023-10-13 07:29:23

PixiJSRunner

2021-08-06 22:45:09

人工智能AI

2014-08-10 14:35:23

2021-01-03 10:37:50

前端开发技术

2023-06-08 08:16:33

TickerPixiJS

2017-06-26 14:52:45

微软智能云合作

2019-05-14 09:18:18

程序员PythonJava

2014-11-12 00:01:29

京东

2017-11-14 09:03:36

Spring Clou架构演进

2017-11-13 15:48:36

架构Spring Clou演进

2014-03-26 14:23:36

Microsoft AWindows Azu微软

2019-06-12 09:50:23

selectMySQLSQL

2022-07-26 07:47:47

SpringMVC

2022-08-02 07:55:28

MVCJersey

2022-07-26 07:47:09

SpringMVC

2023-03-02 07:44:39

pixijsWebGL

2022-07-29 09:12:14

Springservlet容器

2019-12-09 09:50:18

程序员技能开发者

2021-01-06 08:24:38

人工智能消费领域AI
点赞
收藏

51CTO技术栈公众号