Skip to content

Commit a340b8a

Browse files
authoredAug 13, 2019
Revert "Revert "Initialialize AABB Rectangle ""
1 parent c2e32d2 commit a340b8a

7 files changed

+248
-28
lines changed
 

‎.jsdoc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"private": true,
1616
"readme": "README.md",
1717
"recurse": true,
18-
"template": "node_modules/docdash"
18+
"template": "node_modules/docdash",
19+
"tutorials": "docs"
1920
}
2021
}

‎docs/Rectangle-AABB-Matrix.md

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Rectangle AABB Matrix
2+
3+
Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed by a model matrix.
4+
5+
-----
6+
7+
Every drawable is a 1 x 1 unit square that is rotated by its direction, scaled by its skin size and scale, and offset by its rotation center and position. The square representation is made up of 4 points that are transformed by the drawable properties. Often we want a shape that simplifies those 4 points into a non-rotated shape, a axis aligned bounding box.
8+
9+
One approach is to compare the x and y components of each transformed vector and find the minimum and maximum x component and the minimum and maximum y component.
10+
11+
We can start from this approach and determine an alternative one that prodcues the same output with less work.
12+
13+
Starting with transforming one point, here is a 3D point, `v`, transformation by a matrix, `m`.
14+
15+
```js
16+
const v0 = v[0];
17+
const v1 = v[1];
18+
const v2 = v[2];
19+
20+
const d = v0 * m[(0 * 4) + 3] + v1 * m[(1 * 4) + 3] + v2 * m[(2 * 4) + 3] + m[(3 * 4) + 3];
21+
dst[0] = (v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + v2 * m[(2 * 4) + 0] + m[(3 * 4) + 0]) / d;
22+
dst[1] = (v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + v2 * m[(2 * 4) + 1] + m[(3 * 4) + 1]) / d;
23+
dst[2] = (v0 * m[(0 * 4) + 2] + v1 * m[(1 * 4) + 2] + v2 * m[(2 * 4) + 2] + m[(3 * 4) + 2]) / d;
24+
```
25+
26+
As this is a 2D rectangle we can cancel out the third dimension, and the determinant, 'd'.
27+
28+
```js
29+
const v0 = v[0];
30+
const v1 = v[1];
31+
32+
dst = [
33+
v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + m[(3 * 4) + 0,
34+
v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + m[(3 * 4) + 1
35+
];
36+
```
37+
38+
Let's set the matrix points to shorter names for convenience.
39+
40+
```js
41+
const m00 = m[(0 * 4) + 0];
42+
const m01 = m[(0 * 4) + 1];
43+
const m10 = m[(1 * 4) + 0];
44+
const m11 = m[(1 * 4) + 1];
45+
const m30 = m[(3 * 4) + 0];
46+
const m31 = m[(3 * 4) + 1];
47+
```
48+
49+
We need 4 points with positive and negative 0.5 values so the square has sides of length 1.
50+
51+
```js
52+
let p = [0.5, 0.5];
53+
let q = [-0.5, 0.5];
54+
let r = [-0.5, -0.5];
55+
let s = [0.5, -0.5];
56+
```
57+
58+
Transform the points by the matrix.
59+
60+
```js
61+
p = [
62+
0.5 * m00 + 0.5 * m10 + m30,
63+
0.5 * m01 + 0.5 * m11 + m31
64+
];
65+
q = [
66+
-0.5 * m00 + -0.5 * m10 + m30,
67+
0.5 * m01 + 0.5 * m11 + m31
68+
];
69+
r = [
70+
-0.5 * m00 + -0.5 * m10 + m30,
71+
-0.5 * m01 + -0.5 * m11 + m31
72+
];
73+
s = [
74+
0.5 * m00 + 0.5 * m10 + m30,
75+
-0.5 * m01 + -0.5 * m11 + m31
76+
];
77+
```
78+
79+
With 4 transformed points we can build the left, right, top, and bottom values for the Rectangle. Each will use the minimum or the maximum of one of the components of all points.
80+
81+
```js
82+
const left = Math.min(p[0], q[0], r[0], s[0]);
83+
const right = Math.max(p[0], q[0], r[0], s[0]);
84+
const top = Math.max(p[1], q[1], r[1], s[1]);
85+
const bottom = Math.min(p[1], q[1], r[1], s[1]);
86+
```
87+
88+
Fill those calls with the vector expressions.
89+
90+
```js
91+
const left = Math.min(
92+
0.5 * m00 + 0.5 * m10 + m30,
93+
-0.5 * m00 + 0.5 * m10 + m30,
94+
-0.5 * m00 + -0.5 * m10 + m30,
95+
0.5 * m00 + -0.5 * m10 + m30
96+
);
97+
const right = Math.max(
98+
0.5 * m00 + 0.5 * m10 + m30,
99+
-0.5 * m00 + 0.5 * m10 + m30,
100+
-0.5 * m00 + -0.5 * m10 + m30,
101+
0.5 * m00 + -0.5 * m10 + m30
102+
);
103+
const top = Math.max(
104+
0.5 * m01 + 0.5 * m11 + m31,
105+
-0.5 * m01 + 0.5 * m11 + m31,
106+
-0.5 * m01 + -0.5 * m11 + m31,
107+
0.5 * m01 + -0.5 * m11 + m31
108+
);
109+
const bottom = Math.min(
110+
0.5 * m01 + 0.5 * m11 + m31,
111+
-0.5 * m01 + 0.5 * m11 + m31,
112+
-0.5 * m01 + -0.5 * m11 + m31,
113+
0.5 * m01 + -0.5 * m11 + m31
114+
);
115+
```
116+
117+
Pull out the `0.5 * m??` patterns.
118+
119+
```js
120+
const x0 = 0.5 * m00;
121+
const x1 = 0.5 * m10;
122+
const y0 = 0.5 * m01;
123+
const y1 = 0.5 * m11;
124+
125+
const left = Math.min(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30);
126+
const right = Math.max(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30);
127+
const top = Math.max(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31);
128+
const bottom = Math.min(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31);
129+
```
130+
131+
Now each argument for the min and max calls take an expression like `(a * x0 + b * x1 + m3?)`. As each expression has the x0, x1, and m3? variables we can split the min and max calls on the addition operators. Each new call has all the coefficients of that variable.
132+
133+
```js
134+
const left = Math.min(x0, -x0) + Math.min(x1, -x1) + Math.min(m30, m30);
135+
const right = Math.max(x0, -x0) + Math.max(x1, -x1) + Math.max(m30, m30);
136+
const top = Math.max(y0, -y0) + Math.max(y1, -y1) + Math.max(m31, m31);
137+
const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + Math.min(m31, m31);
138+
```
139+
140+
The min or max of two copies of the same value will just be that value.
141+
142+
```js
143+
const left = Math.min(x0, -x0) + Math.min(x1, -x1) + m30;
144+
const right = Math.max(x0, -x0) + Math.max(x1, -x1) + m30;
145+
const top = Math.max(y0, -y0) + Math.max(y1, -y1) + m31;
146+
const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + m31;
147+
```
148+
149+
The max of a negative and positive variable will be the absolute value of that variable. The min of a negative and positive variable will the negated absolute value of that variable.
150+
151+
```js
152+
const left = -Math.abs(x0) + -Math.abs(x1) + m30;
153+
const right = Math.abs(x0) + Math.abs(x1) + m30;
154+
const top = Math.abs(y0) + Math.abs(y1) + m31;
155+
const bottom = -Math.abs(y0) + -Math.abs(y1) + m31;
156+
```
157+
158+
Pulling out the negations of the absolute values, left and right as well as top and bottom are the positive or negative sum of the absolute value of the saled and rotated unit value.
159+
160+
```js
161+
const left = -(Math.abs(x0) + Math.abs(x1)) + m30;
162+
const right = Math.abs(x0) + Math.abs(x1) + m30;
163+
const top = Math.abs(y0) + Math.abs(y1) + m31;
164+
const bottom = -(Math.abs(y0) + Math.abs(y1)) + m31;
165+
```
166+
167+
We call pull out those sums and use them twice.
168+
169+
```js
170+
const x = Math.abs(x0) + Math.abs(x1);
171+
const y = Math.abs(y0) + Math.abs(y1);
172+
173+
const left = -x + m30;
174+
const right = x + m30;
175+
const top = y + m31;
176+
const bottom = -y + m31;
177+
```
178+
179+
This lets us arrive at our goal. Inlining some of our variables we get this block that will initialize a Rectangle to a unit square transformed by a matrix.
180+
181+
```js
182+
const m30 = m[(3 * 4) + 0];
183+
const m31 = m[(3 * 4) + 1];
184+
185+
const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]);
186+
const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]);
187+
188+
const left = -x + m30;
189+
const right = x + m30;
190+
const top = y + m31;
191+
const bottom = -y + m31;
192+
```

‎src/BitmapSkin.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,11 @@ class BitmapSkin extends Skin {
6262
/**
6363
* Get the bounds of the drawable for determining its fenced position.
6464
* @param {Array<number>} drawable - The Drawable instance this skin is using.
65+
* @param {?Rectangle} result - Optional destination for bounds calculation.
6566
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps.
6667
*/
67-
getFenceBounds (drawable) {
68-
return drawable.getAABB();
68+
getFenceBounds (drawable, result) {
69+
return drawable.getAABB(result);
6970
}
7071

7172
/**

‎src/Drawable.js

+19-20
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,10 @@ class Drawable {
451451
* This function applies the transform matrix to the known convex hull,
452452
* and then finds the minimum box along the axes.
453453
* Before calling this, ensure the renderer has updated convex hull points.
454+
* @param {?Rectangle} result optional destination for bounds calculation
454455
* @return {!Rectangle} Bounds for a tight box around the Drawable.
455456
*/
456-
getBounds () {
457+
getBounds (result) {
457458
if (this.needsConvexHullPoints()) {
458459
throw new Error('Needs updated convex hull points before bounds calculation.');
459460
}
@@ -462,18 +463,19 @@ class Drawable {
462463
}
463464
const transformedHullPoints = this._getTransformedHullPoints();
464465
// Search through transformed points to generate box on axes.
465-
const bounds = new Rectangle();
466-
bounds.initFromPointsAABB(transformedHullPoints);
467-
return bounds;
466+
result = result || new Rectangle();
467+
result.initFromPointsAABB(transformedHullPoints);
468+
return result;
468469
}
469470

470471
/**
471472
* Get the precise bounds for the upper 8px slice of the Drawable.
472473
* Used for calculating where to position a text bubble.
473474
* Before calling this, ensure the renderer has updated convex hull points.
475+
* @param {?Rectangle} result optional destination for bounds calculation
474476
* @return {!Rectangle} Bounds for a tight box around a slice of the Drawable.
475477
*/
476-
getBoundsForBubble () {
478+
getBoundsForBubble (result) {
477479
if (this.needsConvexHullPoints()) {
478480
throw new Error('Needs updated convex hull points before bubble bounds calculation.');
479481
}
@@ -485,9 +487,9 @@ class Drawable {
485487
const maxY = Math.max.apply(null, transformedHullPoints.map(p => p[1]));
486488
const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice);
487489
// Search through filtered points to generate box on axes.
488-
const bounds = new Rectangle();
489-
bounds.initFromPointsAABB(filteredHullPoints);
490-
return bounds;
490+
result = result || new Rectangle();
491+
result.initFromPointsAABB(filteredHullPoints);
492+
return result;
491493
}
492494

493495
/**
@@ -497,35 +499,32 @@ class Drawable {
497499
* which is tightly snapped to account for a Drawable's transparent regions.
498500
* `getAABB` returns a much less accurate bounding box, but will be much
499501
* faster to calculate so may be desired for quick checks/optimizations.
502+
* @param {?Rectangle} result optional destination for bounds calculation
500503
* @return {!Rectangle} Rough axis-aligned bounding box for Drawable.
501504
*/
502-
getAABB () {
505+
getAABB (result) {
503506
if (this._transformDirty) {
504507
this._calculateTransform();
505508
}
506509
const tm = this._uniforms.u_modelMatrix;
507-
const bounds = new Rectangle();
508-
bounds.initFromPointsAABB([
509-
twgl.m4.transformPoint(tm, [-0.5, -0.5, 0]),
510-
twgl.m4.transformPoint(tm, [0.5, -0.5, 0]),
511-
twgl.m4.transformPoint(tm, [-0.5, 0.5, 0]),
512-
twgl.m4.transformPoint(tm, [0.5, 0.5, 0])
513-
]);
514-
return bounds;
510+
result = result || new Rectangle();
511+
result.initFromModelMatrix(tm);
512+
return result;
515513
}
516514

517515
/**
518516
* Return the best Drawable bounds possible without performing graphics queries.
519517
* I.e., returns the tight bounding box when the convex hull points are already
520518
* known, but otherwise return the rough AABB of the Drawable.
519+
* @param {?Rectangle} result optional destination for bounds calculation
521520
* @return {!Rectangle} Bounds for the Drawable.
522521
*/
523-
getFastBounds () {
522+
getFastBounds (result) {
524523
this.updateMatrix();
525524
if (!this.needsConvexHullPoints()) {
526-
return this.getBounds();
525+
return this.getBounds(result);
527526
}
528-
return this.getAABB();
527+
return this.getAABB(result);
529528
}
530529

531530
/**

‎src/Rectangle.js

+25
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ class Rectangle {
5454
}
5555
}
5656

57+
/**
58+
* Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed
59+
* by a model matrix.
60+
* @param {Array.<number>} m A 4x4 matrix to transform the rectangle by.
61+
* @tutorial Rectangle-AABB-Matrix
62+
*/
63+
initFromModelMatrix (m) {
64+
// In 2D space, we will soon use the 2x2 "top left" scale and rotation
65+
// submatrix, while we store and the 1x2 "top right" that position
66+
// vector.
67+
const m30 = m[(3 * 4) + 0];
68+
const m31 = m[(3 * 4) + 1];
69+
70+
// "Transform" a (0.5, 0.5) vector by the scale and rotation matrix but
71+
// sum the absolute of each component instead of use the signed values.
72+
const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]);
73+
const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]);
74+
75+
// And adding them to the position components initializes our Rectangle.
76+
this.left = -x + m30;
77+
this.right = x + m30;
78+
this.top = y + m31;
79+
this.bottom = -y + m31;
80+
}
81+
5782
/**
5883
* Determine if this Rectangle intersects some other.
5984
* Note that this is a comparison assuming the Rectangle was

‎src/RenderWebGL.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const log = require('./util/log');
1616

1717
const __isTouchingDrawablesPoint = twgl.v3.create();
1818
const __candidatesBounds = new Rectangle();
19+
const __fenceBounds = new Rectangle();
1920
const __touchingColor = new Uint8ClampedArray(4);
2021
const __blendColor = new Uint8ClampedArray(4);
2122

@@ -1357,7 +1358,7 @@ class RenderWebGL extends EventEmitter {
13571358

13581359
const dx = x - drawable._position[0];
13591360
const dy = y - drawable._position[1];
1360-
const aabb = drawable._skin.getFenceBounds(drawable);
1361+
const aabb = drawable._skin.getFenceBounds(drawable, __fenceBounds);
13611362
const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2);
13621363

13631364
const sx = this._xRight - Math.min(FENCE_WIDTH, inset);
@@ -1627,14 +1628,14 @@ class RenderWebGL extends EventEmitter {
16271628
}
16281629

16291630
twgl.setUniforms(currentShader, uniforms);
1630-
1631+
16311632
/* adjust blend function for this skin */
16321633
if (drawable.skin.hasPremultipliedAlpha){
16331634
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
16341635
} else {
16351636
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
16361637
}
1637-
1638+
16381639
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
16391640
}
16401641

‎src/Skin.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,11 @@ class Skin extends EventEmitter {
146146
/**
147147
* Get the bounds of the drawable for determining its fenced position.
148148
* @param {Array<number>} drawable - The Drawable instance this skin is using.
149+
* @param {?Rectangle} result - Optional destination for bounds calculation.
149150
* @return {!Rectangle} The drawable's bounds.
150151
*/
151-
getFenceBounds (drawable) {
152-
return drawable.getFastBounds();
152+
getFenceBounds (drawable, result) {
153+
return drawable.getFastBounds(result);
153154
}
154155

155156
/**

0 commit comments

Comments
 (0)
Please sign in to comment.