New Render Pipeline (Kiwi.js v1.1.0)

Hello, and welcome to the first in a series of in-depth articles about Kiwi.js v1.1.0, now available for use at www.kiwijs.org. I’m Ben Richards, and I’ve been elbow-deep inside this beast for a couple of months. I figure I’ll show you around.

Who Needs to Read This Post?

Advanced users. If you want to write plugins to create custom GameObjects, new renderers, or related subjects, this is important. If you’re curious about WebGL rendering, or want to squeeze more performance from your games using best practices, you’ll find this interesting. If you think Kiwi does everything you need already – and we hope you do! – then this is probably too much information.

What’s a Render Pipeline?

It’s a piece of jargon. It refers to the process whereby objects are translated from the numbers you set in memory (like ninja.x = 600), into images on the screen (like a ninja standing on the right). From the user’s perspective, this happens automatically. But from an engine point of view, there are a lot of steps we must follow to make those images appear. As your data moves through those steps it is transformed, much like an industrial product streaming through a pipe. And much like that industrial product, we’d like to pump as fast as possible.

Kiwi.js has two complete pipelines under the hood. The first, the canvas renderer, uses the canvas element as defined by the HTML5 specification. This has the widest availability, and is easiest to implement. The second renderer uses WebGL, through which your browser talks directly to the graphics hardware, allowing faster and more flexible graphics. For obvious reasons we default to the WebGL renderer. However, some browsers do not yet support WebGL, so for maximum compatibility we provide the canvas renderer.

What’s a Batch Renderer?

How did you…? Never mind, it’s a good question. Batch rendering is key to getting the most out of Kiwi.js WebGL performance.

You see, drawing WebGL graphics has a cost. We could draw every sprite in the game one by one, but this would be very slow. Instead, we use two pieces of information to create batches. All renderable objects have an atlas (the texture file) and a renderer (the system that draws it to the screen). A batch is formed when two game objects that would be drawn in sequence have the same atlas and renderer. Kiwi.js automatically loads everything in a batch onto the video card in one operation, ensuring we make as few calls to the card as possible.

Obviously, you can optimise this process by putting together your game properly. This means proper scene graph management. The scene graph is simply an ordered hierarchy of everything visible in the current State, and it is drawn beginning to end. This means the last object you add to the State is drawn on top, and so on.

Consider a scene constructed thus, where every object type has its own texture:

  1. Ninja
  2. Ronin
  3. Background
  4. Ronin
  5. Ninja
  6. Ronin
  7. Player

This is a terrible scene graph, and I should be ashamed for ever making it. The first two characters are behind the background, so you’ll never see them. And the various ninja and ronin are not added in any particular order, so they will all form individual batches. There are 7 objects, and it will take 7 batches to render them all. In a large scene, this could cause considerable slowdown.

Instead, consider using Groups thus:

  1. Background
  2. Ninja Group
    1. Ninja
    2. Ninja
  3. Ronin Group
    1. Ronin
    2. Ronin
    3. Ronin
  4. Player

This is a much better scene graph. Now we can add enemies to dedicated groups, and the game engine will automatically collate them into efficient batches. This scene will render with 4 batches.

You can also improve performance by combining atlases. An atlas correlates to a single file. When you create your images and sprite sheets, consider combining them into larger files. If ninja and ronin shared a sprite sheet, the optimised scene could put them into the same batch, reducing the batch count to 3!

Just be careful when combining atlases. Some devices cannot handle resolution above 2048×2048, so regard this as the absolute limit.

Now that we know about batches, we can talk about pipelines.

The Old Render Pipeline

Kiwi.js version 1.0.0 was a fine piece of code. We held onto its canvas renderer essentially unchanged, and almost all of the WebGL pipeline remains intact.

Here’s how things went down for a single batch under WebGL renderer 1.0.1:

  • Switch texture to the current batch’s texture. This makes the texture available in video memory.
  • Switch renderer to the current batch’s renderer. This sets up shaders on the video card.
  • Clear current renderer. This primes data arrays to receive new information.
  • Call renderGL() on all members of the batch, in sequence. The main purpose of this method is to deliver information to the current renderer’s data arrays. It is also a good place to perform just-in-time operations such as texture updates.
  • Call refreshTextureGL() on the current texture atlas. This ensures that the texture is properly updated in video memory.
  • Call draw() on the current renderer. This tells the video card to draw everything to the screen.

Reasons For Change

We first noticed problems when examining bugs in the TextField object. This object does something unusual: you can change its text mid-game. And we asked ourselves, where does this re-drawn text actually appear in the pipeline?

You see, the TextField only redraws itself in its renderGL() method. This happens after the texture is switched. This could be a problem: if the texture that was just uploaded is no longer correct, can we trust the graphics we draw to be timely?

In addition, while it’s not mentioned above, the v1.0.1 pipeline actually had two pipelines. Some entities could not render as batches, and were sent to a slightly different sequence of steps to achieve the same result. This seemed like extra effort and complexity.

The New Render Pipeline

We decided to re-order the render pipeline. After all, all the pieces worked fine. They just needed a little bit of re-ordering.

Here’s the new pipeline:

  • Switch renderer to the current batch’s renderer.
  • Clear current renderer.
  • Call renderGL() on all members of the batch, in sequence.
  • Switch texture to the current batch’s texture.
  • Switch blend mode to the current renderer’s blend mode. This is a new feature and will be examined in more detail in a future article.
  • Call draw() on the current renderer.

This new order doesn’t look very different, but it makes several operations much clearer. We could consider it as an even simpler series of steps:

  • SHADERS
  • PER-ENTITY
  • TEXTURE
  • BLEND MODE
  • DRAW

This clearly delineates the responsibility of each step, and makes it clear what should be done where in the process.

In addition, the pipeline was unified into a single flow for both batchable and non-batchable objects. After all, a non-batchable object is just a batch of length 1.

Implications For Game Objects

You may now perform any number of useful operations in the renderGL() method, and they will be reflected in the rendered graphics. Some illustrative examples follow.

Dynamic Textures

You can redraw your textures every frame, perhaps by drawing on an HTML5 canvas, or by rendering video or capturing a webcam feed. This is how we render text efficiently; because there are so many parameters to change (the text itself, size, alignment, colour, font etc), we cannot redraw it every time one parameter changes. Instead we mark it as dirty, and then during renderGL() we do a single redraw operation. This data travels to the video card and all is well. If the object is hidden, it doesn’t even get to that step! Minimum effort, maximum performance.

Protip: Duplicate an atlas for peak performance! If you want to duplicate the same text field into several places, such as a series of identical signs or monitors in a level, try something like this:

Per-Object Shaders

Sometimes you want to update a shader based on an entity’s own properties and circumstances. But don’t shaders come before per-entity calls? Yes, they do. Does that mean you can’t set shader uniforms in renderGL()?

Actually, it’s fine.

You can continue to set shader uniforms in renderGL(). However, be aware that this only makes sense if you are rendering a single object, because all objects in a batch are processed in a single step before the batch goes to draw. If you are designing a game object that does this, you must design a non-batch renderer. This need simply have the property isBatchRenderer = false. We’ll be talking a little more about renderer design in a later article.

We create our Particle Effects Plugin as a non-batch renderer. Numerous particles are rendered as part of a single game object. As part of this trickery, we need to set unique animation parameters on that object.

In Review

Today we’ve discussed batch renderers, and how to get the most out of your scenes; and how the renderGL() method on game objects is a useful place to run per-entity operations. I hope you find it useful.

Benjamin D. Richards

Share the joy

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">