- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
- 624
- 625
- 626
- 627
- 628
- 629
- 630
- 631
- 632
- 633
- 634
- 635
import { RenderTexturePool } from '../renderTexture/RenderTexturePool';
import { Quad } from '../utils/Quad';
import { QuadUv } from '../utils/QuadUv';
import { Rectangle, Matrix, Point } from '@pixi/math';
import { UniformGroup } from '../shader/UniformGroup';
import { DRAW_MODES, CLEAR_MODES, MSAA_QUALITY } from '@pixi/constants';
import { FilterState } from './FilterState';
import type { ISystem } from '../ISystem';
import type { Filter } from './Filter';
import type { IFilterTarget } from './IFilterTarget';
import type { ISpriteMaskTarget } from './spriteMask/SpriteMaskFilter';
import type { RenderTexture } from '../renderTexture/RenderTexture';
import type { Renderer } from '../Renderer';
const tempPoints = [new Point(), new Point(), new Point(), new Point()];
const tempMatrix = new Matrix();
/**
* System plugin to the renderer to manage filters.
*
* ## Pipeline
*
* The FilterSystem executes the filtering pipeline by rendering the display-object into a texture, applying its
* filters in series, and the last filter outputs into the final render-target.
*
* The filter-frame is the rectangle in world space being filtered, and those contents are mapped into
* `(0, 0, filterFrame.width, filterFrame.height)` into the filter render-texture. The filter-frame is also called
* the source-frame, as it is used to bind the filter render-textures. The last filter outputs to the `filterFrame`
* in the final render-target.
*
* ## Usage
*
* PIXI.Container#renderAdvanced is an example of how to use the filter system. It is a 3 step process:
*
* **push**: Use PIXI.FilterSystem#push to push the set of filters to be applied on a filter-target.
* **render**: Render the contents to be filtered using the renderer. The filter-system will only capture the contents
* inside the bounds of the filter-target. NOTE: Using PIXI.Renderer#render is
* illegal during an existing render cycle, and it may reset the filter system.
* **pop**: Use PIXI.FilterSystem#pop to pop & execute the filters you initially pushed. It will apply them
* serially and output to the bounds of the filter-target.
* @memberof PIXI
*/
export class FilterSystem implements ISystem
{
/**
* List of filters for the FilterSystem
* @member {object[]}
*/
public readonly defaultFilterStack: Array<FilterState>;
/** A pool for storing filter states, save us creating new ones each tick. */
public statePool: Array<FilterState>;
/** Stores a bunch of POT textures used for filtering. */
public texturePool: RenderTexturePool;
/** Whether to clear output renderTexture in AUTO/BLIT mode. See PIXI.CLEAR_MODES. */
public forceClear: boolean;
/**
* Old padding behavior is to use the max amount instead of sum padding.
* Use this flag if you need the old behavior.
* @default false
*/
public useMaxPadding: boolean;
/** A very simple geometry used when drawing a filter effect to the screen. */
protected quad: Quad;
/** Quad UVs */
protected quadUv: QuadUv;
/**
* Active state
* @member {object}
*/
protected activeState: FilterState;
/**
* This uniform group is attached to filter uniforms when used.
* @property {PIXI.Rectangle} outputFrame -
* @property {Float32Array} inputSize -
* @property {Float32Array} inputPixel -
* @property {Float32Array} inputClamp -
* @property {number} resolution -
* @property {Float32Array} filterArea -
* @property {Float32Array} filterClamp -
*/
protected globalUniforms: UniformGroup;
/** Temporary rect for math. */
private tempRect: Rectangle;
public renderer: Renderer;
/**
* @param renderer - The renderer this System works for.
*/
constructor(renderer: Renderer)
{
this.renderer = renderer;
this.defaultFilterStack = [{}] as any;
this.texturePool = new RenderTexturePool();
this.texturePool.setScreenSize(renderer.view);
this.statePool = [];
this.quad = new Quad();
this.quadUv = new QuadUv();
this.tempRect = new Rectangle();
this.activeState = {} as any;
this.globalUniforms = new UniformGroup({
outputFrame: new Rectangle(),
inputSize: new Float32Array(4),
inputPixel: new Float32Array(4),
inputClamp: new Float32Array(4),
resolution: 1,
// legacy variables
filterArea: new Float32Array(4),
filterClamp: new Float32Array(4),
}, true);
this.forceClear = false;
this.useMaxPadding = false;
}
/**
* Pushes a set of filters to be applied later to the system. This will redirect further rendering into an
* input render-texture for the rest of the filtering pipeline.
* @param {PIXI.DisplayObject} target - The target of the filter to render.
* @param filters - The filters to apply.
*/
push(target: IFilterTarget, filters: Array<Filter>): void
{
const renderer = this.renderer;
const filterStack = this.defaultFilterStack;
const state = this.statePool.pop() || new FilterState();
const renderTextureSystem = this.renderer.renderTexture;
let resolution = filters[0].resolution;
let multisample = filters[0].multisample;
let padding = filters[0].padding;
let autoFit = filters[0].autoFit;
// We don't know whether it's a legacy filter until it was bound for the first time,
// therefore we have to assume that it is if legacy is undefined.
let legacy = filters[0].legacy ?? true;
for (let i = 1; i < filters.length; i++)
{
const filter = filters[i];
// let's use the lowest resolution
resolution = Math.min(resolution, filter.resolution);
// let's use the lowest number of samples
multisample = Math.min(multisample, filter.multisample);
// figure out the padding required for filters
padding = this.useMaxPadding
// old behavior: use largest amount of padding!
? Math.max(padding, filter.padding)
// new behavior: sum the padding
: padding + filter.padding;
// only auto fit if all filters are autofit
autoFit = autoFit && filter.autoFit;
legacy = legacy || (filter.legacy ?? true);
}
if (filterStack.length === 1)
{
this.defaultFilterStack[0].renderTexture = renderTextureSystem.current;
}
filterStack.push(state);
state.resolution = resolution;
state.multisample = multisample;
state.legacy = legacy;
state.target = target;
state.sourceFrame.copyFrom(target.filterArea || target.getBounds(true));
state.sourceFrame.pad(padding);
const sourceFrameProjected = this.tempRect.copyFrom(renderTextureSystem.sourceFrame);
// Project source frame into world space (if projection is applied)
if (renderer.projection.transform)
{
this.transformAABB(
tempMatrix.copyFrom(renderer.projection.transform).invert(),
sourceFrameProjected
);
}
if (autoFit)
{
state.sourceFrame.fit(sourceFrameProjected);
if (state.sourceFrame.width <= 0 || state.sourceFrame.height <= 0)
{
state.sourceFrame.width = 0;
state.sourceFrame.height = 0;
}
}
else if (!state.sourceFrame.intersects(sourceFrameProjected))
{
state.sourceFrame.width = 0;
state.sourceFrame.height = 0;
}
// Round sourceFrame in screen space based on render-texture.
this.roundFrame(
state.sourceFrame,
renderTextureSystem.current ? renderTextureSystem.current.resolution : renderer.resolution,
renderTextureSystem.sourceFrame,
renderTextureSystem.destinationFrame,
renderer.projection.transform,
);
state.renderTexture = this.getOptimalFilterTexture(state.sourceFrame.width, state.sourceFrame.height,
resolution, multisample);
state.filters = filters;
state.destinationFrame.width = state.renderTexture.width;
state.destinationFrame.height = state.renderTexture.height;
const destinationFrame = this.tempRect;
destinationFrame.x = 0;
destinationFrame.y = 0;
destinationFrame.width = state.sourceFrame.width;
destinationFrame.height = state.sourceFrame.height;
state.renderTexture.filterFrame = state.sourceFrame;
state.bindingSourceFrame.copyFrom(renderTextureSystem.sourceFrame);
state.bindingDestinationFrame.copyFrom(renderTextureSystem.destinationFrame);
state.transform = renderer.projection.transform;
renderer.projection.transform = null;
renderTextureSystem.bind(state.renderTexture, state.sourceFrame, destinationFrame);
renderer.framebuffer.clear(0, 0, 0, 0);
}
/** Pops off the filter and applies it. */
pop(): void
{
const filterStack = this.defaultFilterStack;
const state = filterStack.pop();
const filters = state.filters;
this.activeState = state;
const globalUniforms = this.globalUniforms.uniforms;
globalUniforms.outputFrame = state.sourceFrame;
globalUniforms.resolution = state.resolution;
const inputSize = globalUniforms.inputSize;
const inputPixel = globalUniforms.inputPixel;
const inputClamp = globalUniforms.inputClamp;
inputSize[0] = state.destinationFrame.width;
inputSize[1] = state.destinationFrame.height;
inputSize[2] = 1.0 / inputSize[0];
inputSize[3] = 1.0 / inputSize[1];
inputPixel[0] = Math.round(inputSize[0] * state.resolution);
inputPixel[1] = Math.round(inputSize[1] * state.resolution);
inputPixel[2] = 1.0 / inputPixel[0];
inputPixel[3] = 1.0 / inputPixel[1];
inputClamp[0] = 0.5 * inputPixel[2];
inputClamp[1] = 0.5 * inputPixel[3];
inputClamp[2] = (state.sourceFrame.width * inputSize[2]) - (0.5 * inputPixel[2]);
inputClamp[3] = (state.sourceFrame.height * inputSize[3]) - (0.5 * inputPixel[3]);
// only update the rect if its legacy..
if (state.legacy)
{
const filterArea = globalUniforms.filterArea;
filterArea[0] = state.destinationFrame.width;
filterArea[1] = state.destinationFrame.height;
filterArea[2] = state.sourceFrame.x;
filterArea[3] = state.sourceFrame.y;
globalUniforms.filterClamp = globalUniforms.inputClamp;
}
this.globalUniforms.update();
const lastState = filterStack[filterStack.length - 1];
this.renderer.framebuffer.blit();
if (filters.length === 1)
{
filters[0].apply(this, state.renderTexture, lastState.renderTexture, CLEAR_MODES.BLEND, state);
this.returnFilterTexture(state.renderTexture);
}
else
{
let flip = state.renderTexture;
let flop = this.getOptimalFilterTexture(
flip.width,
flip.height,
state.resolution
);
flop.filterFrame = flip.filterFrame;
let i = 0;
for (i = 0; i < filters.length - 1; ++i)
{
if (i === 1 && state.multisample > 1)
{
flop = this.getOptimalFilterTexture(
flip.width,
flip.height,
state.resolution
);
flop.filterFrame = flip.filterFrame;
}
filters[i].apply(this, flip, flop, CLEAR_MODES.CLEAR, state);
const t = flip;
flip = flop;
flop = t;
}
filters[i].apply(this, flip, lastState.renderTexture, CLEAR_MODES.BLEND, state);
if (i > 1 && state.multisample > 1)
{
this.returnFilterTexture(state.renderTexture);
}
this.returnFilterTexture(flip);
this.returnFilterTexture(flop);
}
// lastState.renderTexture is blitted when lastState is popped
state.clear();
this.statePool.push(state);
}
/**
* Binds a renderTexture with corresponding `filterFrame`, clears it if mode corresponds.
* @param filterTexture - renderTexture to bind, should belong to filter pool or filter stack
* @param clearMode - clearMode, by default its CLEAR/YES. See PIXI.CLEAR_MODES
*/
bindAndClear(filterTexture: RenderTexture, clearMode: CLEAR_MODES = CLEAR_MODES.CLEAR): void
{
const {
renderTexture: renderTextureSystem,
state: stateSystem,
} = this.renderer;
if (filterTexture === this.defaultFilterStack[this.defaultFilterStack.length - 1].renderTexture)
{
// Restore projection transform if rendering into the output render-target.
this.renderer.projection.transform = this.activeState.transform;
}
else
{
// Prevent projection within filtering pipeline.
this.renderer.projection.transform = null;
}
if (filterTexture && filterTexture.filterFrame)
{
const destinationFrame = this.tempRect;
destinationFrame.x = 0;
destinationFrame.y = 0;
destinationFrame.width = filterTexture.filterFrame.width;
destinationFrame.height = filterTexture.filterFrame.height;
renderTextureSystem.bind(filterTexture, filterTexture.filterFrame, destinationFrame);
}
else if (filterTexture !== this.defaultFilterStack[this.defaultFilterStack.length - 1].renderTexture)
{
renderTextureSystem.bind(filterTexture);
}
else
{
// Restore binding for output render-target.
this.renderer.renderTexture.bind(
filterTexture,
this.activeState.bindingSourceFrame,
this.activeState.bindingDestinationFrame
);
}
// Clear the texture in BLIT mode if blending is disabled or the forceClear flag is set. The blending
// is stored in the 0th bit of the state.
const autoClear = (stateSystem.stateId & 1) || this.forceClear;
if (clearMode === CLEAR_MODES.CLEAR
|| (clearMode === CLEAR_MODES.BLIT && autoClear))
{
// Use framebuffer.clear because we want to clear the whole filter texture, not just the filtering
// area over which the shaders are run. This is because filters may sampling outside of it (e.g. blur)
// instead of clamping their arithmetic.
this.renderer.framebuffer.clear(0, 0, 0, 0);
}
}
/**
* Draws a filter using the default rendering process.
*
* This should be called only by Filter#apply.
* @param filter - The filter to draw.
* @param input - The input render target.
* @param output - The target to output to.
* @param clearMode - Should the output be cleared before rendering to it
*/
applyFilter(filter: Filter, input: RenderTexture, output: RenderTexture, clearMode?: CLEAR_MODES): void
{
const renderer = this.renderer;
// Set state before binding, so bindAndClear gets the blend mode.
renderer.state.set(filter.state);
this.bindAndClear(output, clearMode);
// set the uniforms..
filter.uniforms.uSampler = input;
filter.uniforms.filterGlobals = this.globalUniforms;
// TODO make it so that the order of this does not matter..
// because it does at the moment cos of global uniforms.
// they need to get resynced
renderer.shader.bind(filter);
// check to see if the filter is a legacy one..
filter.legacy = !!filter.program.attributeData.aTextureCoord;
if (filter.legacy)
{
this.quadUv.map(input._frame, input.filterFrame);
renderer.geometry.bind(this.quadUv);
renderer.geometry.draw(DRAW_MODES.TRIANGLES);
}
else
{
renderer.geometry.bind(this.quad);
renderer.geometry.draw(DRAW_MODES.TRIANGLE_STRIP);
}
}
/**
* 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 {PIXI.Sprite} sprite - The sprite to map to.
* @returns The mapped matrix.
*/
calculateSpriteMatrix(outputMatrix: Matrix, sprite: ISpriteMaskTarget): Matrix
{
const { sourceFrame, destinationFrame } = this.activeState;
const { orig } = sprite._texture;
const mappedMatrix = outputMatrix.set(destinationFrame.width, 0, 0,
destinationFrame.height, sourceFrame.x, sourceFrame.y);
const worldTransform = sprite.worldTransform.copyTo(Matrix.TEMP_MATRIX);
worldTransform.invert();
mappedMatrix.prepend(worldTransform);
mappedMatrix.scale(1.0 / orig.width, 1.0 / orig.height);
mappedMatrix.translate(sprite.anchor.x, sprite.anchor.y);
return mappedMatrix;
}
/** Destroys this Filter System. */
destroy(): void
{
this.renderer = null;
// Those textures has to be destroyed by RenderTextureSystem or FramebufferSystem
this.texturePool.clear(false);
}
/**
* Gets a Power-of-Two render texture or fullScreen texture
* @param minWidth - The minimum width of the render texture in real pixels.
* @param minHeight - The minimum height of the render texture in real pixels.
* @param resolution - The resolution of the render texture.
* @param multisample - Number of samples of the render texture.
* @returns - The new render texture.
*/
protected getOptimalFilterTexture(minWidth: number, minHeight: number, resolution = 1,
multisample: MSAA_QUALITY = MSAA_QUALITY.NONE): RenderTexture
{
return this.texturePool.getOptimalTexture(minWidth, minHeight, resolution, multisample);
}
/**
* Gets extra render texture to use inside current filter
* To be compliant with older filters, you can use params in any order
* @param input - renderTexture from which size and resolution will be copied
* @param resolution - override resolution of the renderTexture
* @param multisample - number of samples of the renderTexture
*/
getFilterTexture(input?: RenderTexture, resolution?: number, multisample?: MSAA_QUALITY): RenderTexture
{
if (typeof input === 'number')
{
const swap = input;
input = resolution as any;
resolution = swap;
}
input = input || this.activeState.renderTexture;
const filterTexture = this.texturePool.getOptimalTexture(input.width, input.height, resolution || input.resolution,
multisample || MSAA_QUALITY.NONE);
filterTexture.filterFrame = input.filterFrame;
return filterTexture;
}
/**
* Frees a render texture back into the pool.
* @param renderTexture - The renderTarget to free
*/
returnFilterTexture(renderTexture: RenderTexture): void
{
this.texturePool.returnTexture(renderTexture);
}
/** Empties the texture pool. */
emptyPool(): void
{
this.texturePool.clear(true);
}
/** Calls `texturePool.resize()`, affects fullScreen renderTextures. */
resize(): void
{
this.texturePool.setScreenSize(this.renderer.view);
}
/**
* @param matrix - first param
* @param rect - second param
*/
private transformAABB(matrix: Matrix, rect: Rectangle): void
{
const lt = tempPoints[0];
const lb = tempPoints[1];
const rt = tempPoints[2];
const rb = tempPoints[3];
lt.set(rect.left, rect.top);
lb.set(rect.left, rect.bottom);
rt.set(rect.right, rect.top);
rb.set(rect.right, rect.bottom);
matrix.apply(lt, lt);
matrix.apply(lb, lb);
matrix.apply(rt, rt);
matrix.apply(rb, rb);
const x0 = Math.min(lt.x, lb.x, rt.x, rb.x);
const y0 = Math.min(lt.y, lb.y, rt.y, rb.y);
const x1 = Math.max(lt.x, lb.x, rt.x, rb.x);
const y1 = Math.max(lt.y, lb.y, rt.y, rb.y);
rect.x = x0;
rect.y = y0;
rect.width = x1 - x0;
rect.height = y1 - y0;
}
private roundFrame(
frame: Rectangle,
resolution: number,
bindingSourceFrame: Rectangle,
bindingDestinationFrame: Rectangle,
transform?: Matrix
)
{
if (frame.width <= 0 || frame.height <= 0 || bindingSourceFrame.width <= 0 || bindingSourceFrame.height <= 0)
{
return;
}
if (transform)
{
const { a, b, c, d } = transform;
// Skip if skew/rotation present in matrix, except for multiple of 90° rotation. If rotation
// is a multiple of 90°, then either pair of (b,c) or (a,d) will be (0,0).
if ((Math.abs(b) > 1e-4 || Math.abs(c) > 1e-4)
&& (Math.abs(a) > 1e-4 || Math.abs(d) > 1e-4))
{
return;
}
}
transform = transform ? tempMatrix.copyFrom(transform) : tempMatrix.identity();
// Get forward transform from world space to screen space
transform
.translate(-bindingSourceFrame.x, -bindingSourceFrame.y)
.scale(
bindingDestinationFrame.width / bindingSourceFrame.width,
bindingDestinationFrame.height / bindingSourceFrame.height)
.translate(bindingDestinationFrame.x, bindingDestinationFrame.y);
// Convert frame to screen space
this.transformAABB(transform, frame);
// Round frame in screen space
frame.ceil(resolution);
// Project back into world space.
this.transformAABB(transform.invert(), frame);
}
}