pixijs: cacheAsBitmap and filters bug

There is bug with caching filtered object if it is some out of screen. For example, we have sprite width=100, height=400 with coords x = 100, y = -50. It is -50 pixels out of screen. If it has filter and cacheAsBitmap, there will be wrong output. PIXI version is 5.1.5 Code sample:

	app = new PIXI.Application({
		  width: 210,
		  height: 400,
		  listening: false
	});
document.body.appendChild(app.view);
points = Array();
points.push(10,10);
points.push(100,10);
points.push(100,200);
points.push(10,400);
var container = new PIXI.Container();
var sprite = new PIXI.Sprite(); 
var graphics = new PIXI.Graphics();
app.stage.addChild(container);
container.addChild(sprite);
sprite.addChild(graphics);

graphics.lineStyle(2, 0xffffff);
graphics.beginFill(0xFF3300);
graphics.moveTo(points[0], points[1]);
for (var l=2;l<points.length;l+=2) graphics.lineTo(points[l], points[l+1]);
graphics.lineTo(points[0], points[1]);

var graphics2 = new PIXI.Graphics();
graphics2.lineStyle(2, 0xffffff);
graphics2.beginFill(0xFF3300);
graphics2.moveTo(points[0], points[1]);
for (var l=2;l<points.length;l+=2) graphics2.lineTo(points[l], points[l+1]);
graphics2.lineTo(points[0], points[1]);
app.stage.addChild(graphics2);

var blur_filter = new PIXI.filters.BlurFilter(0.5, 1);
blur_filter.repeatEdgePixels   = true;
graphics.filters = [blur_filter];
graphics.y = -100;
graphics.x = 100;

container.cacheAsBitmap = true;
app.renderer.render(app.stage);

setInterval(function () {
	container.cacheAsBitmap = false;
	if (!graphics.filters) graphics.filters = [blur_filter]; else {graphics.filters = null;graphics.y += 5;};
	container.cacheAsBitmap = true;
	if (graphics.y>10)
	{
		graphics.y = -100;
	}
}, 1000);

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 16 (4 by maintainers)

Most upvoted comments

You can try my CacheContainer. It has better compatibility with filters and and pixi-layers than cacheAsBitmap . It depends on this PR though, or filter padding will get ignored and thus cropped: https://github.com/pixijs/pixi.js/pull/6306

Create a container, add your stuff to it and call updateCache(true) once to enable caching, then call it again whenever the cache should be updated.


import * as pixi from 'pixi.js';

const PIXI_LAYERS = 'parentLayer' in (new pixi.Container() as any);

interface RenderingContext {
    renderer: pixi.Renderer;
    renderTexture: pixi.RenderTexture;
    filterTarget?: pixi.RenderTexture;
    sourceFrame: pixi.Rectangle;
    destinationFrame: pixi.Rectangle;
    activeLayer: any;
    transform: pixi.Matrix;
}

function STORE_RENDERING_CONTEXT(renderer: pixi.Renderer): RenderingContext {
    renderer.batch.flush();

    let transform = renderer.projection.transform;

    if (transform) {
        transform = transform.clone();
    }

    const filterStack = renderer.filter.defaultFilterStack;

    let filterTarget;

    if (filterStack.length) {
        filterTarget = filterStack[filterStack.length - 1].renderTexture;
    }

    return {
        renderer,
        renderTexture: renderer.renderTexture.current,
        sourceFrame: renderer.renderTexture.sourceFrame.clone(),
        destinationFrame: renderer.renderTexture.destinationFrame.clone(),
        activeLayer: PIXI_LAYERS ? (renderer as any)._activeLayer : null,
        filterTarget,
        transform
    };
}

function RESTORE_RENDERING_CONTEXT(context: RenderingContext) {
    context.renderer.projection.transform = context.transform;
    context.renderer.renderTexture.bind(context.renderTexture, context.sourceFrame, context.destinationFrame);

    const filterStack = context.renderer.filter.defaultFilterStack;

    if (context.filterTarget) {
        filterStack[filterStack.length - 1].renderTexture = context.filterTarget;
    }

    if (PIXI_LAYERS) {
        (context.renderer as any)._activeLayer = context.activeLayer;
    }
}

const tmpParent = new pixi.Container();

export class CacheContainer extends pixi.Container {
    private cacheSprite: pixi.Sprite;
    private cacheEnabled = false;

    private renderTexture: pixi.RenderTexture;

    private shouldUpdate = true;

    private sprites: pixi.Sprite[] = [];

    public blendMode: number = pixi.BLEND_MODES.NORMAL;

    public tint = 0xFFFFFF;

    public constructor() {
        super();
    }

    public createSprite() {
        const sprite = new pixi.Sprite();

        sprite.roundPixels = true;

        sprite.on('destroy', () => {
            const index = this.sprites.indexOf(sprite);

            if (index !== -1) {
                this.sprites.splice(index, 1);
            }
        });

        this.sprites.push(sprite);

        return sprite;
    }

    public destroy(...args: Parameters<typeof pixi.Container.prototype.destroy>) {
        super.destroy(...args);

        if (!this.renderTexture) {
            return;
        }

        this.renderTexture.destroy(true);

        this.renderTexture = null;
        this.cacheSprite = null;
    }

    public updateCache(enabled?: boolean) {
        this.shouldUpdate = true;

        if (enabled !== null && enabled !== undefined) {
            this.cacheEnabled = enabled;
        }
    }

    private createRenderTexture(width: number, height: number) {
        if (this.renderTexture) {
            if (width === this.renderTexture.width && height === this.renderTexture.height) {
                return;
            }

            this.renderTexture.destroy(true);
        }

        this.renderTexture = pixi.RenderTexture.create({
            resolution: pixi.settings.RESOLUTION,
            width,
            height
        });

        for (const sprite of this.sprites) {
            sprite.texture = this.renderTexture;
        }
    }

    public render(renderer: pixi.Renderer) {
        if (!this.visible || this.worldAlpha <= 0 || !this.renderable) {
            return;
        }

        if (!this.cacheEnabled) {
            return super.render(renderer);
        }

        if (!this.cacheSprite) {
            this.cacheSprite = this.createSprite();
        }

        this.renderToTexture(renderer);

        const {
            worldAlpha,
            blendMode,
            transform
        } = this as any;

        this.cacheSprite.transform.updateTransform(transform);
        this.cacheSprite.blendMode = blendMode;
        this.cacheSprite.tint = this.tint;
        (this.cacheSprite as any).worldAlpha = worldAlpha;

        if (PIXI_LAYERS) {
            (this.cacheSprite as any).parentLayer = (this as any).parentLayer;
            (this.cacheSprite as any)._activeParentLayer = (this as any)._activeParentLayer;
        }

        this.cacheSprite.render(renderer);
    }

    private renderToTexture(renderer: pixi.Renderer) {
        if (!this.shouldUpdate) {
            return;
        }

        (this as any)._boundsID++;

        this.shouldUpdate = false;

        const oldContext = STORE_RENDERING_CONTEXT(renderer);

        let parentLayer,
            activeLayer;

        const alpha = this.alpha;
        const parent = this.parent;

        if (PIXI_LAYERS) {
            parentLayer = (this as any).parentLayer;
            activeLayer = (this as any)._activeParentLayer;

            (this as any).parentLayer = null;
            (this as any)._activeParentLayer = null;
        }

        this.alpha = 1;

        tmpParent.transform.setFromMatrix(this.parent.transform.worldTransform);
        (this as any).parent = tmpParent;

        this.updateTransform();

        const bounds = this.getBounds(false).clone();

        bounds.ceil(pixi.settings.RESOLUTION);

        this.createRenderTexture(bounds.width, bounds.height);

        (this.renderTexture as any).filterFrame = bounds;

        renderer.renderTexture.bind(this.renderTexture, bounds);
        renderer.renderTexture.clear([0, 0, 0, 0]);

        const filterStack = renderer.filter.defaultFilterStack;

        if (filterStack.length) {
            filterStack[filterStack.length - 1].renderTexture = renderer.renderTexture.current;
        }

        super.render(renderer);

        renderer.batch.flush();

        RESTORE_RENDERING_CONTEXT(oldContext);

        this.renderTexture.update();

        this.alpha = alpha;
        (this as any).parent = parent;

        if (PIXI_LAYERS) {
            (this as any).parentLayer = parentLayer;
            (this as any)._activeParentLayer = activeLayer;
        }

        this.updateTransform();

        const worldTransform = this.transform.worldTransform.clone();
        worldTransform.translate(-bounds.x, -bounds.y);
        worldTransform.invert();

        for (const sprite of this.sprites) {
            sprite.transform.setFromMatrix(worldTransform);
            if (sprite.parent) {
                sprite.updateTransform();
            }
        }
    }
}

export default CacheContainer;

@laino

Absolute legend! Just wanted to thank you for this solution!