Interpretation of PixiJS source code: What does the bottom layer do when drawing a rectangle?

Hello everyone, I am the front-end watermelon brother. Today I will show you the source code implementation of PixiJS.

PixiJS is a very popular Canvas library with a start number of nearly 4w.

Using PixiJS’s easy-to-use API, we can draw graphics on the Canvas element of the browser page with high performance and achieve smooth animation. Its bottom layer is WebGL.

Use PixiJS to draw a rectangle, the code is implemented as:

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

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

app.stage.addChild(graph);

Rendering result:

What exactly does this code do under the hood? This time Brother Watermelon will take everyone to find out.

The PixiJS version used is 7.2.4.

Initialization of Application

The first is to call the constructor of the Application class to create an app object.

Below is the code for the Application constructor.

export class Application {<!-- -->
  // create stage
  public stage: Container = new Container();
  //...
  constructor(options) {<!-- -->
    options = Object. assign(
      {<!-- -->
        // Whether to force the use of Canvas 2D, otherwise if WebGL is supported, use WebGL
        // The default is false, and Canvas 2D has been deprecated, only pixi.js-legacy is available
        forceCanvas: false,
      },
      options
    );
\t
    // select renderer
    this.renderer = autoDetectRenderer(options);

    // plugin initialization
    Application._plugins.forEach((plugin) => {<!-- -->
      plugin.init.call(this, options);
    });
  }
}

Mainly did the following things.

  1. Initialize this.stage as a new Container object, use it as the root container, and then the rectangle we draw will be placed under it;

  2. Select the renderer renderer, there are two types: Renderer (based on WebGL) and CanvasRenderer (based on Canvas 2D). The latest version of PixiJS only has Renderer built in. If you wish to fall back to CanvasRenderer when WebGL is not available, you need to use the pixie.js-legacy library instead.

  3. Call the Renderer ‘s constructor. Its attribute view will point to a canvas element, and the Application view gets this view through the getter proxy;

  4. Call the init method of the registered plug-in in Application to initialize.

Application has two built-in plugins by default:

  • TickerPlugin: Call the incoming callback function (based on requestAnimationFrame) before drawing the next frame, where PixiJS will specify the new content to be drawn for the next frame number;
  • ResizePlugin: Monitor container size changes and redraw the canvas.

Create graphics

const graph = new PIXI. Graphics();

Create a Graphics object. Any graphics can be drawn under this Graphics object, here I only draw a rectangle.

graph.beginFill(0xff0044); // fill color

This method sets the Graphics object’s _fillStyle to the specified color value. The incoming color value will be normalized.

Pixijs implementation has its own style: it likes to save “private” variables in a way like _varX, and then provides corresponding setters and getters to read and write this internal variable.

A getter may not be provided, such that a property becomes read-only. Some getters will be lazy loaded and initialized when they are read for the first time, such as Texture.WHITE.

If we don’t specify a color, this _fillStyle will use the default value, and its visible attribute is false, indicating that the graphic has no fill color, and the filling logic will be skipped in the subsequent rendering stage.

Then create a rectangle.

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

The above code actually calls:

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

First create a Rectangle object.

Then create a GraphicsData object based on the Rectangle object, the previously set fillStyle, lineStyle, and matrix, and finally add it to the rect._geometry.graphicsData array.

In short, it is to record the data of this rectangle, and then PixiJS will construct data that can be directly used by drawing WebGL based on these values.

Then reset the fill color.

rect.endFill();

Set the rect’s _fillStyle to default:

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

Finally, add the rect to the container app.stage.

app.stage.addChild(rect);

The corresponding source code is:

export class Container extends DisplayObject {<!-- -->
  //...

  addChild(...children) {<!-- -->
    if (children. length > 1) {<!-- -->
      // There are multiple graphics to add, it will traverse and call the current addChild method
      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; // means no sorting

      child.transform._parentID = -1;

      this. children. push(child);

      this._boundsID++;

      // Trigger related events for child node changes
      this.onChildrenChange(this.children.length - 1);
      this.emit("childAdded", child, this, this.children.length - 1);
      child. emit("added", this);
    }
    return children[0];
  }
}

At this point, our rectangle has its attributes set and added to the graphics tree.

The following is the rendering part.

Draw

Remember the two plugins we initialized when we initialized the Application?

One of them is TickerPlugin, which is the encapsulation of raf (requestAnimationFrame), which will execute the callback function before the page draws the next frame.

When the Application is initialized, the TickerPlugin.init() method is called to bind the render method of the renderer to the Ticker. In this way, render will continue to be called asynchronously.

class TickerPlugin {<!-- -->
  static init(options) {<!-- -->
    Object.defineProperty(this, "ticker", {<!-- -->
      set(ticker) {<!-- -->
        // Pass the app.render function to the ticker's callback list
        ticker.add(this.render, this, UPDATE_PRIORITY.LOW);
      },
      //...
    });
    
    // trigger ticker setter
    this.ticker = options.sharedTicker ? Ticker.shared : new Ticker();
  }
  //...
}

render method:

class Application {<!-- -->
  //...
  public render() {<!-- -->
    this.renderer.render(this.stage);
  }
}

Because the rendering process is very long, there are too many code logics, and various details, here I only talk about the general process, and I will write an article to explain it in detail later.

  1. Recursively multiply the transformation matrix of the child graph object under app.stage with that of the parent container (transfrom of the parent container will affect the child nodes), and finally calculate the final composite transformation matrix of all nodes;
  2. The previously created Rectangle object, its x, y, width, height, is converted to the 8 vertex data required by the WebGL vertex shader (Vertex Shader);
  3. apply the transformation matrix to the vertices;
  4. Some intermediate batches of computed vertices and colors. Finally, in the BatchRenderer.drawBatches() method, the WebGL API: gl.drawElements is called.

One of the reasons for PixiJS’s high performance is to reduce draw calls, provide as many vertices and fragments as possible in one batch (batch) for WebGL to process, and make full use of the concurrent computing capabilities of the GPU.

End

I am the front-end watermelon brother, welcome to follow me and learn more front-end knowledge.