v8 Migration Guide
Welcome to the PixiJS v8 Migration Guide! This document is designed to help you smoothly transition your projects from PixiJS v7 to the latest and greatest PixiJS v8. Please follow these steps to ensure a successful migration.
Table of Contents
1. Introduction
PixiJS v8 introduces several exciting changes and improvements that dramatically enhance the performance of the renderer. While we've made efforts to keep the migration process as smooth as possible, some breaking changes are inevitable. This guide will walk you through the necessary steps to migrate your PixiJS v7 project to PixiJS v8.
2. Breaking Changes
Before diving into the migration process, let's review the breaking changes introduced in PixiJS v8. Make sure to pay close attention to these changes as they may impact your existing codebase.
Should I Upgrade?
Generally, the answer is yes! But currently, there may be reasons that suggest it's best not to upgrade just yet. Ask yourself the following question:
- Does your project leverage existing Pixi libraries that have not yet been migrated to v8? We are working hard to migrate our key libraries to v8 but did not want this to be a blocker for those who are using pure Pixi. This means some libraries will not have a v8 counterpart just yet. It's best to hold off on migration if this is the case for you.
Migrated
- Filters
- Sound
- Gif
- Storybook
- UI
-
- Open Games
Migrating Right Now:
- React
- Spine (esoteric version)
To Be Migrated:
- Pixi layers (rather than migrating this, we will likely incorporate it directly into PixiJS v8 as a feature)
New Package Structure
Since version 5, PixiJS has utilized individual sub-packages to organize its codebase into smaller units. However, this approach led to issues, such as conflicting installations of different PixiJS versions, causing complications with internal caches.
In v8, PixiJS has reverted to a single-package structure. While you can still import specific parts of PixiJS, you only need to install the main package.
Old:
import { Application } from '@pixi/app';
import { Sprite } from '@pixi/sprite';
New:
import { Application, Sprite } from 'pixi.js';
Custom Builds
PixiJS uses an "extensions" system to add renderer functionality. By default, PixiJS includes many extensions for a comprehensive out-of-the-box experience. However, for full control over features and bundle size, you can manually import specific PixiJS components.
// imported by default
import 'pixi.js/accessibility';
import 'pixi.js/app';
import 'pixi.js/events';
import 'pixi.js/filters';
import 'pixi.js/sprite-tiling';
import 'pixi.js/text';
import 'pixi.js/text-bitmap';
import 'pixi.js/text-html';
import 'pixi.js/graphics';
import 'pixi.js/mesh';
import 'pixi.js/sprite-nine-slice';
// not added by default, everyone needs to import these manually
import 'pixi.js/advanced-blend-modes';
import 'pixi.js/unsafe-eval';
import 'pixi.js/prepare';
import 'pixi.js/math-extras';
import 'pixi.js/dds';
import 'pixi.js/ktx';
import 'pixi.js/ktx2';
import 'pixi.js/basis';
import { Application } from 'pixi.js';
const app = new Application();
await app.init({
manageImports: false, // disable importing the above extensions
});
When initializing the application, you can disable the auto-import feature, preventing PixiJS from importing any extensions automatically. You'll need to import them manually, as demonstrated above.
It should also be noted that the pixi.js/text-bitmap
, also add Assets
loading functionality.
Therefore if you want to load bitmap fonts BEFORE initialising the renderer, you will need to import this extension.
import 'pixi.js/text-bitmap';
import { Assets, Application } from 'pixi.js';
await Assets.load('my-font.fnt'); // If 'pixi.js/text-bitmap' is not imported, this will not load
await new Application().init();
Async Initialisation
PixiJS will now need to be initialised asynchronously. With the introduction of the WebGPU renderer PixiJS will now need to be awaited before being used
Old:
import { Application } from 'pixi.js';
const app = new Application();
// do pixi things
New:
import { Application } from 'pixi.js'
const app = new Application();
(async () => {
await app.init({
// application options
});
// do pixi things
})()
With this change it also means that the ApplicationOptions
object can now be passed into the init
function instead of the constructor.
** Texture adjustments **
Textures structures have been modified to simplify what was becoming quite a mess behind the scenes in v7. Textures no longer know or manage loading of resources. This needs to be done upfront by you or the assets manager. Textures expect full loaded resources only. This makes things so much easier to manage as the validation of a texture can essentially be done at construction time and left at that! BaseTexture no longer exists. In stead we now have a variety of TextureSources available. A texture source combines the settings of a texture with how to upload and use that texture. In v8 there are the following texture sources:
TextureSource - a vanilla texture that you can render too or upload however you wish. (used mainly by render textures) ImageSource - a texture source that contains an image resource of some kind (eg ImageBitmap or html image) CanvasSource - a canvas source that contains a canvas. Used mainly for rendering canvases or rendering to a canvas (webGPU) VideoSource - a texture source that contains a video. Takes care of updating the texture eon the GPU to ensure that they stay in sync. BufferSource - a texture source that contains a buffer. What ever you want really! make sure your buffer type and format are compatible! CompressedSource - a texture source that handles compressed textures. Used by the GPU compressed texture formats.
Whilst the majority of the time Assets
will return Textures you may want to make you own! More power to ya!
To create a texture source the signature differs from baseTexture. example:
const image = new Image();
image.onload = function(){
// create a texture source
const source = new ImageSource({
resource: image,
});
// create a texture
const texture = new Texture({
source
});
}
image.src = 'myImage.png';
Graphics API Overhaul
There are a few key changes to the Graphics API. In fact this is probably the most changed part of v8. We have added deprecations where possible but below is the rundown of changes:
- Instead of beginning a fill or a stroke and then building a shape, v8 asks you to build your shape and then stroke / fill it. The terminology of
Line
has been replaced with the terminology ofStroke
Old:
// red rect
const graphics = new Graphics()
.beginFill(0xFF0000)
.drawRect(50, 50, 100, 100)
.endFill();
// blur rect with stroke
const graphics2 = new Graphics()
.lineStyle(2, 'white')
.beginFill('blue')
.circle(530, 50, 140, 100)
.endFill();
New:
// red rect
const graphics = new Graphics()
.rect(50, 50, 100, 100)
.fill(0xFF0000);
// blur rect with stroke
const graphics2 = new Graphics()
.rect(50, 50, 100, 100)
.fill('blue')
.stroke({width:2, color:'white'});
- Shape functions have been renamed. Each drawing function has been simplified into a shorter version of its name. They have the same parameters though:
v7 API Call | v8 API Equivalent |
---|---|
drawChamferRect | chamferRect |
drawCircle | circle |
drawEllipse | ellipse |
drawFilletRect | filletRect |
drawPolygon | poly |
drawRect | rect |
drawRegularPolygon | regularPoly |
drawRoundedPolygon | roundPoly |
drawRoundedRect | roundRect |
drawRoundedShape | roundShape |
drawStar | star |
- fills functions expect
FillStyle
options or a color, rather than a string of parameters. This also replacesbeginTextureFill
Old:
const rect = new Graphics()
.beginTextureFill({texture:Texture.WHITE, alpha:0.5, color:0xFF0000})
.drawRect(0, 0, 100, 100)
.endFill()
.beginFill(0xFFFF00, 0.5)
.drawRect(100, 0, 100, 100)
.endFill();
New:
const rect = new Graphics()
.rect(0, 0, 100, 100)
.fill({texture:Texture.WHITE, alpha:0.5, color:0xFF0000})
.rect(100, 0, 100, 100)
.fill({color:0xFFFF00, alpha:0.5});
- stokes functions expect
StrokeStyle
options or a color, rather than a string of parameters. This also replaceslineTextureStyle
Old:
const rect = new Graphics()
.lineTextureStyle({texture:Texture.WHITE, width:10, color:0xFF0000})
.drawRect(0, 0, 100, 100)
.endFill()
.lineStyle(2, 0xFEEB77);
.drawRect(100, 0, 100, 100)
.endFill();
New:
const rect = new Graphics()
.rect(0, 0, 100, 100)
.stroke({texture:Texture.WHITE, width:10, color:0xFF0000})
.rect(100, 0, 100, 100)
.stroke({color:0xFEEB77, width:2});
- holes now make use of a new
cut
function. As withstroke
andfill
,cut
acts on the previous shape. Old:
const rectAndHole = new Graphics()
.beginFill(0x00FF00)
.drawRect(0, 0, 100, 100)
.beginHole()
.drawCircle(50, 50, 20)
.endHole()
.endFill();
New:
const rectAndHole = new Graphics()
.rect(0, 0, 100, 100)
.fill(0x00FF00)
.circle(50, 50, 20)
.cut();
GraphicsGeometry
has been replaced withGraphicsContext
this allows for sharing ofGraphics
data more efficiently.
Old:
const rect = new Graphics()
.beginFill(0xFF0000)
.drawRect(50, 50, 100, 100)
.endFill();
const geometry = rect.geometry;
const secondRect = new Graphics(geometry);
New:
const context = new GraphicsContext()
.rect(50, 50, 100, 100)
.fill(0xFF0000);
const rect = new Graphics(context);
const secondRect = new Graphics(context);
Shader changes
As we now need to accommodate both WebGL and WebGPU shaders, the wey they are constructed has been tweaked to take this into account. The main differences you will notice (this is for shaders in general) is that Textures are no longer considered uniforms (as in they cannot be included in a uniform group). Instead we have the concept of resources. A resource can be a few things including:
- TextureSource - A source texture
myTexture.source
- TextureStyle - A texture style
myTexture.style
- UniformGroup - A collection of number based uniforms
myUniforms = new UniformGroup({})
- BufferResource - A buffer that is treated as a uniform group (advanced)
creating a webgl only shader now looks like this:
old
const shader = PIXI.Shader.from(vertex, fragment, uniforms);
new
just WebGL
const shader = Shader.from({
gl: { vertex, fragment },
resources, // resource used from above including uniform groups
});
WebGL and WebGPU
const shader = Shader.from({
gl: { vertex, fragment },
gpu: {
vertex: {
entryPoint: 'mainVert',
source,
},
fragment: {
entryPoint: 'mainFrag',
source,
},
},
resources, // resource used from above including uniform groups
});
Uniforms are also constructed in a slightly different way. When creating them, you now provide the type of variable you want it to be.
old
const uniformGroup = new UniformGroup({
uTime:1,
});
uniformGroup.uniforms.uTime = 100;
new
const uniformGroup = new UniformGroup({
uTime:{value:1, type:'f32',
});
uniformGroup.uniforms.uTime = 100; // still accessed the same!
The best way to play and fully and get to know this new setup please check out the Mesh and Shader examples:
old: v7 example new: v8 example
Filters
Filters work almost exactly the same, unless you are constructing a custom one. If this is the case, the shader changes mentioned above need to taken into account.
old
const filter = new Filter(vertex, fragment, {
uTime: 0.0,
});
new
const filter = new Filter({
glProgram: GlProgram.from({
fragment,
vertex,
}),
resources: {
timeUniforms: {
uTime: { value: 0.0, type: 'f32' },
},
},
});
old: v7 example new: v8 example
If you're using the community filters, note that the @pixi/filter-*
packages are no-longer maintained for v8, however, you can import directly from the pixi-filters
package as sub-modules.
*old
import { AdjustmentFilter } from '@pixi/filter-adjustment';
*new
import { AdjustmentFilter } from 'pixi-filters/adjustment';
ParticleContainer
ParticleContainer
has been reworked in v8 to allow for far more particles than before. There are a few key changes you should be aware of:
A ParticleContainer
no longer accepts sprites as its children. Instead, it requires a Particle
class (or an object that implements the IParticle
interface), which follows this interface:
export interface IParticle
{
x: number;
y: number;
scaleX: number;
scaleY: number;
anchorX: number;
anchorY: number;
rotation: number;
color: number;
texture: Texture;
}
The reason for this change is that sprites come with many extra properties and events that are generally unnecessary when dealing with large numbers of particles. This approach explicitly removes any ambiguity we had in v7, such as "Why doesn't my sprite work with filters?" or "Why can't I nest children in my sprites?" It’s a bit more predictable. Additionally, due to the lightweight nature of particles, this means we can render far more of them!
So, no functionality is lost—just an API tweak with a massive performance boost!
Particles are also not stored in the children
array of the ParticleContainer
, as particles are not technically part of the scene graph (for performance reasons). Instead, they are stored in a flat list called particleChildren
, which is part of the ParticleContainer
class. You can modify this array directly for extra speed, or you can use the addParticle
and removeParticle
methods to manage your particles.
Another optimization is that ParticleContainer
does not calculate its own bounds, as doing so would negate the performance gains we've created! Instead, it's up to you to provide a boundsArea
when initializing the ParticleContainer
.
OLD
const container = new ParticleContainer();
for (let i = 0; i < 100000; i++) {
const particle = new Sprite(texture);
container.addChild(particle);
}
NEW
const container = new ParticleContainer();
for (let i = 0; i < 100000; i++) {
const particle = new Particle(texture);
container.addParticle(particle);
}
with a bounds area
const container = new ParticleContainer({
boundsArea:new Rectangle(0,0,500,500)
});
Other Breaking Changes
-
DisplayObject
has been removed.Container
is now the base class for all PixiJS objects. -
updateTransform
has been removed as nodes no longer contain any rendering logicWe do recognise that many people used this function to do custom logic every frame, so we have added a new
onRender
function that can be used for this purpose.Old:
class MySprite extends Sprite {
constructor() {
super();
this.updateTransform();
}
updateTransform() {
super.updateTransform();
// do custom logic
}
}New:
class MySprite extends Sprite {
constructor() {
super();
this.onRender = this._onRender.bind(this);
}
_onRender() {
// do custom logic
}
} -
Mipmap generation changes
- The BaseTexture
mipmap
property has been renamed toautoGenerateMipmaps
. - Mipmaps for
RenderTextures
have been adjusted so that developer is responsible for updating them mipmaps. Mipmap generation can be expensive, and due to the new reactive way we handle textures we do not want to accidentally generate mipmaps when they are not required.
- The BaseTexture
const myRenderTexture = RenderTexture.create({width:100, height:100, autoGenerateMipmaps:true});
// do some rendering..
renderer.render({target:myRenderTexture, container:scene});
// now refresh mipmaps when you are ready
myRenderTexture.source.updateMipmaps();
- Due to the new way PixiJS handles things internally, sprites no longer get notified if a texture's UVs have been modified. The best practice is not to modify texture UVs once they have been created. It's best to have textures ready to go (they are inexpensive to create and store).
- Sometimes, you might want to employ a special technique that animates the UVs. In this last instance, you will be responsible for updating the sprite (it's worth noting that it may update automatically - but due to the new optimizations, this will not be guaranteed). Updating the source data (e.g., a video texture) will, however, always be reflected immediately.
const texture = await Assets.load('bunny.png');
const sprite = new Sprite(texture);
texture.frame.width = texture.frame.width/2;
texture.update();
// guarantees the texture changes will be reflected on the sprite
sprite.onViewUpdate();
// alternatively you can hooke into the sprites event
texture.on('update', ()=>{sprite.onViewUpdate});
The act of adding and removing the event when a sprite's texture was changed led to an unacceptable performance drop, especially when swapping many textures (imagine shooting games with lots of keyframe textures swapping). This is why we now leave that responsibility to the user.
-
New Container culling approach
With this version of PixiJS we have changed how the
cullable
property works on containers. Previously culling was done for you automatically during the render loop. However, we have moved this logic out and provided users the ability to control when culling happens themselves.With this change we have added a couple of new properties:
cullable
- Whether or not the container can be culledcullArea
- A cull area that will be used instead of the bounds of the containercullableChildren
- Whether or not the containers children can be culled. This can help optimise large scenes
New:
const container = new Container();
const view = new Rectangle(0, 0, 800, 600);
container.cullable = true;
container.cullArea = new Rectangle(0,0,400,400);
container.cullableChildren = false;
Culler.shared.cull(myContainer, view);
renderer.render(myContainer);There is also a
CullerPlugin
that can be used to automatically callCuller.shared.cull
every frame if you want to simulate the old behaviour.import {extensions, CullerPlugin} from 'pixi.js'
extensions.add(CullerPlugin) -
Renamed several mesh classes
- renamed
SimpleMesh
->MeshSimple
- renamed
SimplePlane
->MeshPlane
- renamed
SimpleRope
->MeshRope
- renamed
-
Deprecations for
Assets
removedOld:
import { Assets } from 'pixi.js';
Assets.add('bunny', 'bunny.png');New:
import { Assets } from 'pixi.js';
Assets.add({ alias: 'bunny', src: 'bunny.png' }); -
settings
object has been removedOld:
import { settings, BrowserAdapter } from 'pixi.js';
settings.RESOLUTION = 1;
settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = false;
settings.ADAPTER = BrowserAdapter;New:
import { AbstractRenderer, DOMAdapter, BrowserAdapter } from 'pixi.js';
// Can also be passed into the renderer directly e.g `autoDetectRenderer({resolution: 1})`
AbstractRenderer.defaultOptions.resolution = 1;
// Can also be passed into the renderer directly e.g `autoDetectRenderer({failIfMajorPerformanceCaveat: false})`
AbstractRenderer.defaultOptions.failIfMajorPerformanceCaveat = false;
// See below for more information about changes to the adapter
DOMAdapter.set(BrowserAdapter); -
Adapter and Web Worker Changes
-
settings.ADAPTER
has been removed and replaced withDOMAdapter
DOMAdapter
is a static class that can be used to set the adapter for the entire application- PixiJS has two adapters built in
BrowserAdapter
andWebWorkerAdapter
BrowserAdapter
is the default adapter and is used when running in the browserWebWorkerAdapter
is used when running in a web worker
Old:
import { settings, WebWorkerAdapter } from 'pixi.js';
settings.ADAPTER = WebWorkerAdapter;
settings.ADAPTER.createCanvas();New:
import { DOMAdapter, WebWorkerAdapter } from 'pixi.js';
DOMAdapter.set(WebWorkerAdapter);
DOMAdapter.get().createCanvas();
-
-
Application type now accepts Renderer instead of view by @Zyie in https://github.com/pixijs/pixijs/pull/9740
This is to allow
app.renderer
to be typed correctlyOld:
const app = new Application<HTMLCanvasElement>();
New:
// WebGL or WebGPU renderer
const app = new Application<Renderer<HTMLCanvasElement>>();
// WebGL specific renderer
const app = new Application<WebGLRenderer<HTMLCanvasElement>>();
// WebGPU specific renderer
const app = new Application<WebGPURenderer<HTMLCanvasElement>>();
-
Texture.from
no longer will load a texture from a URL.When using
Texture.from
you will need to pass in a source such asCanvasSource
/ImageSource
/VideoSource
or a resource such asHTMLImageElement
/HTMLCanvasElement
/HTMLVideoElement
or a string that has been loaded throughAssets.load
Old:
import { Texture } from 'pixi.js';
const texture = Texture.from('https://i.imgur.com/IaUrttj.png');New:
import { Assets, Texture } from 'pixi.js';
await Assets.load('https://i.imgur.com/IaUrttj.png');
const texture = Texture.from('https://i.imgur.com/IaUrttj.png');
-
The
Ticker
's callback will now pass theTicker
instance instead of the delta time. This is to allow for more control over what unit of time is used.Old:
Ticker.shared.add((dt)=> {
bunny.rotation += dt
});New:
Ticker.shared.add((ticker)=> {
bunny.rotation += ticker.deltaTime;
}); -
Text parsers have been renamed
TextFormat
->bitmapFontTextParser
XMLStringFormat
->bitmapFontXMLStringParser
XMLFormat
->bitmapFontXMLParser
-
The default
eventMode
is nowpassive
instead ofauto
-
utils
has been removed. All the functions are available as direct imports.Old:
import { utils } from 'pixi.js';
utils.isMobile.any();New:
import { isMobile } from 'pixi.js';
isMobile.any(); -
container.getBounds()
now returns aBounds
object instead of aRectangle
object. You can access the rectangle by usingcontainer.getBounds().rectangle
instead.Old:
const bounds = container.getBounds();
New:
const bounds = container.getBounds().rectangle;
-
container.cacheAsBitmap
has been replaced withcontainer.cacheAsTexture()
. They do the same things, except we changed the namecacheAsTexture
as the Bitmap terminology is not really relevant to PixiJS.Old:
container.cacheAsBitmap = true;
New:
container.cacheAsTexture(true);
3. Deprecated Features
Certain features from PixiJS v7 have been deprecated in v8. While they will still work, it's recommended to update your code to use the new alternatives. Refer to the deprecated features section for details on what to replace them with.
-
Leaf nodes no longer allow children
Only
Containers
can have children. This means thatSprite
,Mesh
,Graphics
etc can no longer have children.To replicate the old behaviour you can create a
Container
and add the leaf nodes to it.Old:
const sprite = new Sprite();
const spriteChild = new Sprite();
sprite.addChild(spriteChild);New:
const container = new Container();
const sprite = new Sprite();
const spriteChild = new Sprite();
container.addChild(sprite);
container.addChild(spriteChild); -
Application.view
replaced withApplication.canvas
Old:
const app = new Application({ view: document.createElement('canvas') });
document.body.appendChild(app.view);New:
const app = new Application();
await app.init({ view: document.createElement('canvas') });
document.body.appendChild(app.canvas); -
NineSlicePlane
renamed toNineSliceSprite
-
SCALE_MODES
replaced withScaleMode
stringsSCALE_MODES.NEAREST
->'nearest'
,SCALE_MODES.LINEAR
->'linear'
,
-
WRAP_MODES
replaced withWrapMode
stringsWRAP_MODES.CLAMP
->'clamp-to-edge'
,WRAP_MODES.REPEAT
->'repeat'
,WRAP_MODES.MIRRORED_REPEAT
->'mirror-repeat'
,
-
DRAW_MODES
replaced withTopology
stringsDRAW_MODES.POINTS
->'point-list'
,DRAW_MODES.LINES
->'line-list'
,DRAW_MODES.LINE_STRIP
->'line-strip'
,DRAW_MODES.TRIANGLES
->'triangle-list'
,DRAW_MODES.TRIANGLE_STRIP
->'triangle-strip'
,
-
Constructors have largely been changed to accept objects instead of multiple arguments
Old:
const blurFilter = new BlurFilter(8, 4, 1, 5);
const displacementFilter = new DisplacementFilter(sprite, 5);
const meshGeometry = new MeshGeometry(vertices, uvs, index);
const mesh = new Mesh(geometry, shader, state, drawMode);
const plane = new PlaneGeometry(width, height, segWidth, segHeight);
const nineSlicePlane = new NineSlicePlane(texture, leftWidth, topHeight, rightWidth, bottomHeight);
const tileSprite = new TileSprite(texture, width, height);
const text = new Text('Hello World', style);
const bitmapText = new BitmapText('Hello World', style);
const htmlText = new HTMLText('Hello World', style);New:
const blurFilter = new BlurFilter({
blur: 8,
quality: 4,
resolution: 1,
kernelSize: 5,
});
const displacementFilter = new DisplacementFilter({
sprite,
scale: 5,
});
const meshGeometry = new MeshGeometry({
positions: vertices,
uvs,
indices: index,
topology: 'triangle-list';
shrinkBuffersToFit: boolean;
});
const mesh = new Mesh({
geometry
shader
texture
});
const plane = new PlaneGeometry({
width,
height,
verticesX: segWidth,
verticesY: segHeight,
});
const nineSliceSprite = new NineSliceSprite({
texture,
leftWidth,
topHeight,
rightWidth,
bottomHeight,
});
const tileSprite = new TileSprite({
texture,
width,
height,
});
const text = new Text({
text: 'Hello World',
style,
});
const bitmapText = new BitmapText({
text:'Hello World',
style,
});
const htmlText = new HTMLText({
text:'Hello World',
style,
}); -
container.name
is nowcontainer.label