UNPKG

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
'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