pixi.js
Version:
<p align="center"> <a href="https://pixijs.com" target="_blank" rel="noopener noreferrer"> <img height="150" src="https://files.pixijs.download/branding/pixijs-logo-transparent-dark.svg?v=1" alt="PixiJS logo"> </a> </p> <br/> <p align="center">
522 lines (518 loc) • 19 kB
JavaScript
'use strict';
var Extensions = require('../extensions/Extensions.js');
var Matrix = require('../maths/matrix/Matrix.js');
var BindGroup = require('../rendering/renderers/gpu/shader/BindGroup.js');
var Geometry = require('../rendering/renderers/shared/geometry/Geometry.js');
var UniformGroup = require('../rendering/renderers/shared/shader/UniformGroup.js');
var Texture = require('../rendering/renderers/shared/texture/Texture.js');
var TexturePool = require('../rendering/renderers/shared/texture/TexturePool.js');
var types = require('../rendering/renderers/types.js');
var Bounds = require('../scene/container/bounds/Bounds.js');
var getRenderableBounds = require('../scene/container/bounds/getRenderableBounds.js');
var warn = require('../utils/logging/warn.js');
"use strict";
const quadGeometry = new Geometry.Geometry({
attributes: {
aPosition: {
buffer: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
format: "float32x2",
stride: 2 * 4,
offset: 0
}
},
indexBuffer: new Uint32Array([0, 1, 2, 0, 2, 3])
});
class FilterData {
constructor() {
/**
* Indicates whether the filter should be skipped.
* @type {boolean}
*/
this.skip = false;
/**
* The texture to which the filter is applied.
* @type {Texture}
*/
this.inputTexture = null;
/**
* The back texture used for blending, if required.
* @type {Texture | null}
*/
this.backTexture = null;
/**
* The list of filters to be applied.
* @type {Filter[]}
*/
this.filters = null;
/**
* The bounds of the filter area.
* @type {Bounds}
*/
this.bounds = new Bounds.Bounds();
/**
* The container to which the filter is applied.
* @type {Container}
*/
this.container = null;
/**
* Indicates whether blending is required for the filter.
* @type {boolean}
*/
this.blendRequired = false;
/**
* The render surface where the output of the filter is rendered.
* @type {RenderSurface}
*/
this.outputRenderSurface = null;
/**
* The offset of the output render surface.
* @type {PointData}
*/
this.outputOffset = { x: 0, y: 0 };
/**
* The global frame of the filter area.
* @type {{ x: number, y: number, width: number, height: number }}
*/
this.globalFrame = { x: 0, y: 0, width: 0, height: 0 };
}
}
class FilterSystem {
constructor(renderer) {
this._filterStackIndex = 0;
this._filterStack = [];
this._filterGlobalUniforms = new UniformGroup.UniformGroup({
uInputSize: { value: new Float32Array(4), type: "vec4<f32>" },
uInputPixel: { value: new Float32Array(4), type: "vec4<f32>" },
uInputClamp: { value: new Float32Array(4), type: "vec4<f32>" },
uOutputFrame: { value: new Float32Array(4), type: "vec4<f32>" },
uGlobalFrame: { value: new Float32Array(4), type: "vec4<f32>" },
uOutputTexture: { value: new Float32Array(4), type: "vec4<f32>" }
});
this._globalFilterBindGroup = new BindGroup.BindGroup({});
this.renderer = renderer;
}
/**
* The back texture of the currently active filter. Requires the filter to have `blendRequired` set to true.
* @readonly
*/
get activeBackTexture() {
return this._activeFilterData?.backTexture;
}
/**
* Pushes a filter instruction onto the filter stack.
* @param instruction - The instruction containing the filter effect and container.
* @internal
*/
push(instruction) {
const renderer = this.renderer;
const filters = instruction.filterEffect.filters;
const filterData = this._pushFilterData();
filterData.skip = false;
filterData.filters = filters;
filterData.container = instruction.container;
filterData.outputRenderSurface = renderer.renderTarget.renderSurface;
const colorTextureSource = renderer.renderTarget.renderTarget.colorTexture.source;
const rootResolution = colorTextureSource.resolution;
const rootAntialias = colorTextureSource.antialias;
if (filters.length === 0) {
filterData.skip = true;
return;
}
const bounds = filterData.bounds;
if (instruction.renderables) {
getRenderableBounds.getGlobalRenderableBounds(instruction.renderables, bounds);
} else if (instruction.filterEffect.filterArea) {
bounds.clear();
bounds.addRect(instruction.filterEffect.filterArea);
bounds.applyMatrix(instruction.container.worldTransform);
} else {
instruction.container.getFastGlobalBounds(true, bounds);
}
if (instruction.container) {
const renderGroup = instruction.container.renderGroup || instruction.container.parentRenderGroup;
const filterFrameTransform = renderGroup.cacheToLocalTransform;
if (filterFrameTransform) {
bounds.applyMatrix(filterFrameTransform);
}
}
this._calculateFilterBounds(filterData, renderer.renderTarget.rootViewPort, rootAntialias, rootResolution, 1);
if (filterData.skip) {
return;
}
const previousFilterData = this._getPreviousFilterData();
let globalResolution = rootResolution;
let offsetX = 0;
let offsetY = 0;
if (previousFilterData) {
offsetX = previousFilterData.bounds.minX;
offsetY = previousFilterData.bounds.minY;
globalResolution = previousFilterData.inputTexture.source._resolution;
}
filterData.outputOffset.x = bounds.minX - offsetX;
filterData.outputOffset.y = bounds.minY - offsetY;
const globalFrame = filterData.globalFrame;
globalFrame.x = offsetX * globalResolution;
globalFrame.y = offsetY * globalResolution;
globalFrame.width = colorTextureSource.width * globalResolution;
globalFrame.height = colorTextureSource.height * globalResolution;
filterData.backTexture = Texture.Texture.EMPTY;
if (filterData.blendRequired) {
renderer.renderTarget.finishRenderPass();
const renderTarget = renderer.renderTarget.getRenderTarget(filterData.outputRenderSurface);
filterData.backTexture = this.getBackTexture(renderTarget, bounds, previousFilterData?.bounds);
}
filterData.inputTexture = TexturePool.TexturePool.getOptimalTexture(
bounds.width,
bounds.height,
filterData.resolution,
filterData.antialias
);
renderer.renderTarget.bind(filterData.inputTexture, true);
renderer.globalUniforms.push({
offset: bounds
});
}
/**
* Applies filters to a texture.
*
* This method takes a texture and a list of filters, applies the filters to the texture,
* and returns the resulting texture.
* @param {object} params - The parameters for applying filters.
* @param {Texture} params.texture - The texture to apply filters to.
* @param {Filter[]} params.filters - The filters to apply.
* @returns {Texture} The resulting texture after all filters have been applied.
* @example
*
* ```ts
* // Create a texture and a list of filters
* const texture = new Texture(...);
* const filters = [new BlurFilter(), new ColorMatrixFilter()];
*
* // Apply the filters to the texture
* const resultTexture = filterSystem.applyToTexture({ texture, filters });
*
* // Use the resulting texture
* sprite.texture = resultTexture;
* ```
*
* Key Points:
* 1. padding is not currently supported here - so clipping may occur with filters that use padding.
* 2. If all filters are disabled or skipped, the original texture is returned.
*/
generateFilteredTexture({ texture, filters }) {
const filterData = this._pushFilterData();
this._activeFilterData = filterData;
filterData.skip = false;
filterData.filters = filters;
const colorTextureSource = texture.source;
const rootResolution = colorTextureSource.resolution;
const rootAntialias = colorTextureSource.antialias;
if (filters.length === 0) {
filterData.skip = true;
return texture;
}
const bounds = filterData.bounds;
bounds.addRect(texture.frame);
this._calculateFilterBounds(filterData, bounds.rectangle, rootAntialias, rootResolution, 0);
if (filterData.skip) {
return texture;
}
const globalResolution = rootResolution;
const offsetX = 0;
const offsetY = 0;
filterData.outputOffset.x = -bounds.minX;
filterData.outputOffset.y = -bounds.minY;
const globalFrame = filterData.globalFrame;
globalFrame.x = offsetX * globalResolution;
globalFrame.y = offsetY * globalResolution;
globalFrame.width = colorTextureSource.width * globalResolution;
globalFrame.height = colorTextureSource.height * globalResolution;
filterData.outputRenderSurface = TexturePool.TexturePool.getOptimalTexture(
bounds.width,
bounds.height,
filterData.resolution,
filterData.antialias
);
filterData.backTexture = Texture.Texture.EMPTY;
filterData.inputTexture = texture;
const renderer = this.renderer;
renderer.renderTarget.finishRenderPass();
this._applyFiltersToTexture(filterData, true);
const outputTexture = filterData.outputRenderSurface;
outputTexture.source.alphaMode = "premultiplied-alpha";
return outputTexture;
}
/** @internal */
pop() {
const renderer = this.renderer;
const filterData = this._popFilterData();
if (filterData.skip) {
return;
}
renderer.globalUniforms.pop();
renderer.renderTarget.finishRenderPass();
this._activeFilterData = filterData;
this._applyFiltersToTexture(filterData, false);
if (filterData.blendRequired) {
TexturePool.TexturePool.returnTexture(filterData.backTexture);
}
TexturePool.TexturePool.returnTexture(filterData.inputTexture);
}
/**
* Copies the last render surface to a texture.
* @param lastRenderSurface - The last render surface to copy from.
* @param bounds - The bounds of the area to copy.
* @param previousBounds - The previous bounds to use for offsetting the copy.
*/
getBackTexture(lastRenderSurface, bounds, previousBounds) {
const backgroundResolution = lastRenderSurface.colorTexture.source._resolution;
const backTexture = TexturePool.TexturePool.getOptimalTexture(
bounds.width,
bounds.height,
backgroundResolution,
false
);
let x = bounds.minX;
let y = bounds.minY;
if (previousBounds) {
x -= previousBounds.minX;
y -= previousBounds.minY;
}
x = Math.floor(x * backgroundResolution);
y = Math.floor(y * backgroundResolution);
const width = Math.ceil(bounds.width * backgroundResolution);
const height = Math.ceil(bounds.height * backgroundResolution);
this.renderer.renderTarget.copyToTexture(
lastRenderSurface,
backTexture,
{ x, y },
{ width, height },
{ x: 0, y: 0 }
);
return backTexture;
}
/**
* Applies a filter to a texture.
* @param filter - The filter to apply.
* @param input - The input texture.
* @param output - The output render surface.
* @param clear - Whether to clear the output surface before applying the filter.
*/
applyFilter(filter, input, output, clear) {
const renderer = this.renderer;
const filterData = this._activeFilterData;
const outputRenderSurface = filterData.outputRenderSurface;
const filterUniforms = this._filterGlobalUniforms;
const uniforms = filterUniforms.uniforms;
const outputFrame = uniforms.uOutputFrame;
const inputSize = uniforms.uInputSize;
const inputPixel = uniforms.uInputPixel;
const inputClamp = uniforms.uInputClamp;
const globalFrame = uniforms.uGlobalFrame;
const outputTexture = uniforms.uOutputTexture;
if (outputRenderSurface === output) {
outputFrame[0] = filterData.outputOffset.x;
outputFrame[1] = filterData.outputOffset.y;
} else {
outputFrame[0] = 0;
outputFrame[1] = 0;
}
outputFrame[2] = input.frame.width;
outputFrame[3] = input.frame.height;
inputSize[0] = input.source.width;
inputSize[1] = input.source.height;
inputSize[2] = 1 / inputSize[0];
inputSize[3] = 1 / inputSize[1];
inputPixel[0] = input.source.pixelWidth;
inputPixel[1] = input.source.pixelHeight;
inputPixel[2] = 1 / inputPixel[0];
inputPixel[3] = 1 / inputPixel[1];
inputClamp[0] = 0.5 * inputPixel[2];
inputClamp[1] = 0.5 * inputPixel[3];
inputClamp[2] = input.frame.width * inputSize[2] - 0.5 * inputPixel[2];
inputClamp[3] = input.frame.height * inputSize[3] - 0.5 * inputPixel[3];
globalFrame[0] = filterData.globalFrame.x;
globalFrame[1] = filterData.globalFrame.y;
globalFrame[2] = filterData.globalFrame.width;
globalFrame[3] = filterData.globalFrame.height;
if (output instanceof Texture.Texture)
output.source.resource = null;
const renderTarget = this.renderer.renderTarget.getRenderTarget(output);
renderer.renderTarget.bind(output, !!clear);
if (output instanceof Texture.Texture) {
outputTexture[0] = output.frame.width;
outputTexture[1] = output.frame.height;
} else {
outputTexture[0] = renderTarget.width;
outputTexture[1] = renderTarget.height;
}
outputTexture[2] = renderTarget.isRoot ? -1 : 1;
filterUniforms.update();
if (renderer.renderPipes.uniformBatch) {
const batchUniforms = renderer.renderPipes.uniformBatch.getUboResource(filterUniforms);
this._globalFilterBindGroup.setResource(batchUniforms, 0);
} else {
this._globalFilterBindGroup.setResource(filterUniforms, 0);
}
this._globalFilterBindGroup.setResource(input.source, 1);
this._globalFilterBindGroup.setResource(input.source.style, 2);
filter.groups[0] = this._globalFilterBindGroup;
renderer.encoder.draw({
geometry: quadGeometry,
shader: filter,
state: filter._state,
topology: "triangle-list"
});
if (renderer.type === types.RendererType.WEBGL) {
renderer.renderTarget.finishRenderPass();
}
}
/**
* Multiply _input normalized coordinates_ to this matrix to get _sprite texture normalized coordinates_.
*
* Use `outputMatrix * vTextureCoord` in the shader.
* @param outputMatrix - The matrix to output to.
* @param {Sprite} sprite - The sprite to map to.
* @returns The mapped matrix.
*/
calculateSpriteMatrix(outputMatrix, sprite) {
const data = this._activeFilterData;
const mappedMatrix = outputMatrix.set(
data.inputTexture._source.width,
0,
0,
data.inputTexture._source.height,
data.bounds.minX,
data.bounds.minY
);
const worldTransform = sprite.worldTransform.copyTo(Matrix.Matrix.shared);
const renderGroup = sprite.renderGroup || sprite.parentRenderGroup;
if (renderGroup && renderGroup.cacheToLocalTransform) {
worldTransform.prepend(renderGroup.cacheToLocalTransform);
}
worldTransform.invert();
mappedMatrix.prepend(worldTransform);
mappedMatrix.scale(
1 / sprite.texture.frame.width,
1 / sprite.texture.frame.height
);
mappedMatrix.translate(sprite.anchor.x, sprite.anchor.y);
return mappedMatrix;
}
destroy() {
}
_applyFiltersToTexture(filterData, clear) {
const inputTexture = filterData.inputTexture;
const bounds = filterData.bounds;
const filters = filterData.filters;
this._globalFilterBindGroup.setResource(inputTexture.source.style, 2);
this._globalFilterBindGroup.setResource(filterData.backTexture.source, 3);
if (filters.length === 1) {
filters[0].apply(this, inputTexture, filterData.outputRenderSurface, clear);
} else {
let flip = filterData.inputTexture;
const tempTexture = TexturePool.TexturePool.getOptimalTexture(
bounds.width,
bounds.height,
flip.source._resolution,
false
);
let flop = tempTexture;
let i = 0;
for (i = 0; i < filters.length - 1; ++i) {
const filter = filters[i];
filter.apply(this, flip, flop, true);
const t = flip;
flip = flop;
flop = t;
}
filters[i].apply(this, flip, filterData.outputRenderSurface, clear);
TexturePool.TexturePool.returnTexture(tempTexture);
}
}
_calculateFilterBounds(filterData, viewPort, rootAntialias, rootResolution, paddingMultiplier) {
const renderer = this.renderer;
const bounds = filterData.bounds;
const filters = filterData.filters;
let resolution = Infinity;
let padding = 0;
let antialias = true;
let blendRequired = false;
let enabled = false;
let clipToViewport = true;
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
resolution = Math.min(resolution, filter.resolution === "inherit" ? rootResolution : filter.resolution);
padding += filter.padding;
if (filter.antialias === "off") {
antialias = false;
} else if (filter.antialias === "inherit") {
antialias && (antialias = rootAntialias);
}
if (!filter.clipToViewport) {
clipToViewport = false;
}
const isCompatible = !!(filter.compatibleRenderers & renderer.type);
if (!isCompatible) {
enabled = false;
break;
}
if (filter.blendRequired && !(renderer.backBuffer?.useBackBuffer ?? true)) {
warn.warn("Blend filter requires backBuffer on WebGL renderer to be enabled. Set `useBackBuffer: true` in the renderer options.");
enabled = false;
break;
}
enabled = filter.enabled || enabled;
blendRequired || (blendRequired = filter.blendRequired);
}
if (!enabled) {
filterData.skip = true;
return;
}
if (clipToViewport) {
bounds.fitBounds(0, viewPort.width / rootResolution, 0, viewPort.height / rootResolution);
}
bounds.scale(resolution).ceil().scale(1 / resolution).pad((padding | 0) * paddingMultiplier);
if (!bounds.isPositive) {
filterData.skip = true;
return;
}
filterData.antialias = antialias;
filterData.resolution = resolution;
filterData.blendRequired = blendRequired;
}
_popFilterData() {
this._filterStackIndex--;
return this._filterStack[this._filterStackIndex];
}
_getPreviousFilterData() {
let previousFilterData;
let index = this._filterStackIndex - 1;
while (index > 1) {
index--;
previousFilterData = this._filterStack[index];
if (!previousFilterData.skip) {
break;
}
}
return previousFilterData;
}
_pushFilterData() {
let filterData = this._filterStack[this._filterStackIndex];
if (!filterData) {
filterData = this._filterStack[this._filterStackIndex] = new FilterData();
}
this._filterStackIndex++;
return filterData;
}
}
/** @ignore */
FilterSystem.extension = {
type: [
Extensions.ExtensionType.WebGLSystem,
Extensions.ExtensionType.WebGPUSystem
],
name: "filter"
};
exports.FilterSystem = FilterSystem;
//# sourceMappingURL=FilterSystem.js.map