Pixi.js: How to achieve high FPS while rendering lots of text and updating its position?

Created on 25 Sep 2020  ·  10Comments  ·  Source: pixijs/pixi.js

Hi, I am currently investigating PixiJS for a new project and I really like it so far! There are some requirements regarding the performance of the application. The really tricky part is that this will result in lots of displayed text labels which are constantly updated. I could not find a way to further improve the performance and my last hope is to finde some help in this community.

Goal

1500 text labels
60 position updates per second
60 FPS

Pixi Playground

I already created a minimal example using BitMapText resulting in ~28 FPS on MacBook Pro.
https://www.pixiplayground.com/#/edit/rLbAN_xrw7yUU_cg_c8Xv

Do you have any idea to improve this? Thanks a lot in advance!

🤔 Question

Most helpful comment

Thanks, it somehow worked generating a texture from the text and using it on a sprite. But I currently do not understand why and I can't reproduce it in a playground. 😄

However the application now updates all 1500 label positions every frame with 60FPS. Furthermore each second the text content gets updated. This leads to a short freeze every second. So the next step will be to render those text updates asynchronous, e.g. in a web worker. I am currently reading about ImageBitmap and OffscreenCanvas. This might be interesting to others, so I will share my progress.

All 10 comments

Are you _sure_ you need 1500 on screen at _all_ times? Culling offscreen labels will help a lot with performance. Pixi doesn't do any culling out of the box, but there are a few community plugins (e.g., @pixi-essentials/cull by @SukantPal) that might help you here.

Are all 1500 labels unique? If there are many duplicates, you can convert it into a Sprite using a RenderTexture and share references at the expensive of more memory.

@ChristophWalter The example you've shown could potentially benefit from culling as @bigtimebuddy mentioned. 1500 labels each with 112 characters is _a lot_ to render. There are reasons why your example cannot be used to suggest relevant optimization, for example the text is mostly overlapping. I don't think your project will be displaying jumbled text that is so dense it can't be read by the user.

Setting MESH.BATCHABLE_SIZE to a higher value like 200 might help. I didn't throughly test this because it still powers through 45 FPS after a 6x CPU slowdown on my iMac.

I haven't profiled the example too much. However, if you're working on a commercial project & this turns out to be GPU-side bottleneck, you might consider an out-of-order that renders each letter (all As, then all Bs, all Cs, etc.) together to improve texture locality, developing a tile-engine and/or doing a diff-rect optimization that renders the portion of the screen that has _changed_ (i.e. based on the 60 updates out of 1500 labels). This are just ideas and should be taken as such.

Thank you for your help!

In reality we will use culling and it will be very unlikely to display that many labels at the same time. But for benchmark reasons we need to compete against non-web applications. We already questioned these requirements, but it would help a lot if we can show that webgl can handle this as well.

The labels will be unique. But they might not change that often. The example doesn't change the text content at all. So there might be some potential to optimize. I noticed that the FPS stay the same even if I do not update the positions (playground). Is there a way to rerender the texts only when they change?

Setting MESH.BATCHABLE_SIZE did not change anything on my side. I will take a look at your other ideas or recommend to buy iMacs 😏 I am not that much into rendering, so these ideas really help me!

Ah, if you really are optimization that specific example then the supreme method would be to do what @bigtimebuddy said, with one change - use a Mesh and update that mesh directly (instead of using 1500 sprites).

This provides two key benefits:

  • This will eliminate the overhead of the batch renderer’s buffering phase.
  • The number of vertices will be cut by 112x (4/text instead of 4/character). That brings down the vertices to just 6K.
  • Only one DisplayObject in the scene.

So basically, the process would look like:

  1. Render the BitmapText into a RenderTexture
  2. Create a Mesh with 1500*4 vertices.
  3. Animate the vertices directly. You must be careful because there are four vertices per instance (i.e. rectangles). So you would calculate the position for the first vertex. Then the other three would be (x + width, height), (x + width, y + height), (x, y + height).

This should be a straightforward process. Please tell us how it goes!

Hey, I tried to implement your suggestion. Converting the BitmapText to a texture seemed to work. But I am currently stuck trying to display the texture in a mesh. I tried using a PIXI.Mesh and PIXI.SimpleMesh. Do I need to create my own shader or can I use PIXI.MeshMaterial?

const bitmapFontText = new PIXI.BitmapText(
    'Lorem ipsum dolor\nsit amet consetetur\nsadipscing elitr sed', 
    { font: '55px Desyrel', align: 'left' }
);

const texture =  PIXI.RenderTexture.create({ width: 800, height: 600 });
app.renderer.render(bitmapFontText, texture);

const vertices = [
    -0.5, -0.5,
    0.5, -0.5,
    0.5, 0.5,
    -0.5, 0.5
];
const uvs = [
    0, 0,
    1, 0,
    1, 1,
    0, 1,
];
const indices = [0, 1, 2, 0, 2, 3];
const geometry = new PIXI.MeshGeometry(vertices, uvs, indices);
const shader = new PIXI.MeshMaterial(texture);

const mesh = new PIXI.Mesh(geometry, shader);
app.stage.addChild(mesh);

https://www.pixiplayground.com/#/edit/7RHqFti0tdSzw-6iOtylk

--
_Edit_: Fixed it. The vertices need to represent pixel coordinates. _Next step_: Use more than one texture in the mesh.

const vertices = [
    0, 0,
    500, 0,
    500, 500,
    0, 500
];

--
_Edit 2_: This will only work when using one texture for all labels, right? I might have expressed myself wrong. The labels will be unique, but the contents will not change in every frame.

I noticed that the FPS stay the same even if I do not update the positions (playground). Is there a way to rerender the texts only when they change?

Will a PIXI.Mesh help with this? Do you know any resource where I can read about the rendering process of PixiJS?

@ChristophWalter You can create a renderer yourself (instead of using an Application), setup a ticker loop which calls Renderer.render _only_ when your scene changes.

You can also pass autoStart: false to the Application options and then call app.render() when something changes. Same diff.

Thanks, it somehow worked generating a texture from the text and using it on a sprite. But I currently do not understand why and I can't reproduce it in a playground. 😄

However the application now updates all 1500 label positions every frame with 60FPS. Furthermore each second the text content gets updated. This leads to a short freeze every second. So the next step will be to render those text updates asynchronous, e.g. in a web worker. I am currently reading about ImageBitmap and OffscreenCanvas. This might be interesting to others, so I will share my progress.

Short update as I stopped investigating some weeks ago
I rendered the text in a web worker which actually worked quite straight forward:

// render-text-worker.js
onmessage = function(event) {
    const textLines = event.data.text.split(/\n/);
    const index = event.data.index;
    const offscreen = new OffscreenCanvas(150,45);
    const ctx = offscreen.getContext("2d");
    ctx.font = "15px monospace";
    textLines.forEach((text, index) => {
        ctx.fillText(text, 0, 15 + index * 15)
    })
    const imageBitmap = offscreen.transferToImageBitmap();
    postMessage({imageBitmap, index});
};
// index.js
const worker = new Worker("render-text-worker.js");
const callbacks ={}
function getLabelTexture(index, text, callback) {
  callbacks[index] = callback
  worker.postMessage({ index, text });
}
worker.onmessage = function({ data }) {
  const callback = callbacks[data.index]
  callback(PIXI.Texture.from(data.imageBitmap));
}

Unfortunately there is no improvement for my use case. The frames still drop while rendering the texts. Maybe due to the work required for transferring the image bitmaps from the worker.

For our specific use case we ware able to recommend reducing the text length of each label to achieve the performance requirements.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ivanpopelyshev picture ivanpopelyshev  ·  33Comments

tobireif picture tobireif  ·  24Comments

SukantPal picture SukantPal  ·  27Comments

manudurgoni picture manudurgoni  ·  24Comments

bigtimebuddy picture bigtimebuddy  ·  24Comments