Skip to content

Commit 008dc5b

Browse files
authored
Merge pull request #419 from cwillisf/coordinates-fixups-2
Adjust CPU `isTouchingColor` to match GPU results (again)
2 parents 9177705 + b304ea8 commit 008dc5b

File tree

8 files changed

+127
-104
lines changed

8 files changed

+127
-104
lines changed

src/Drawable.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ const __isTouchingPosition = twgl.v3.create();
2424
* @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix
2525
*/
2626
const getLocalPosition = (drawable, vec) => {
27-
// Transfrom from world coordinates to Drawable coordinates.
27+
// Transform from world coordinates to Drawable coordinates.
2828
const localPosition = __isTouchingPosition;
2929
const v0 = vec[0];
3030
const v1 = vec[1];
3131
const m = drawable._inverseMatrix;
32-
// var v2 = v[2];
3332
const d = (v0 * m[3]) + (v1 * m[7]) + m[15];
3433
// The RenderWebGL quad flips the texture's X axis. So rendered bottom
3534
// left is 1, 0 and the top right is 0, 1. Flip the X axis so
@@ -342,7 +341,7 @@ class Drawable {
342341
// Drawable configures a 3D matrix for drawing in WebGL, but most values
343342
// will never be set because the inputs are on the X and Y position axis
344343
// and the Z rotation axis. Drawable can bring the work inside
345-
// _calculateTransform and greatly reduce the ammount of math and array
344+
// _calculateTransform and greatly reduce the amount of math and array
346345
// assignments needed.
347346

348347
const scale0 = this._skinScale[0];
@@ -625,11 +624,6 @@ class Drawable {
625624
*/
626625
static sampleColor4b (vec, drawable, dst) {
627626
const localPosition = getLocalPosition(drawable, vec);
628-
if (localPosition[0] < 0 || localPosition[1] < 0 ||
629-
localPosition[0] > 1 || localPosition[1] > 1) {
630-
dst[3] = 0;
631-
return dst;
632-
}
633627
const textColor =
634628
// commenting out to only use nearest for now
635629
// drawable.useNearest ?

src/RenderWebGL.js

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,7 @@ class RenderWebGL extends EventEmitter {
267267
this._yBottom = yBottom;
268268
this._yTop = yTop;
269269

270-
// swap yBottom & yTop to fit Scratch convention of +y=up
271-
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
270+
this._projection = this._makeOrthoProjection(xLeft, xRight, yBottom, yTop);
272271

273272
this._setNativeSize(Math.abs(xRight - xLeft), Math.abs(yBottom - yTop));
274273
}
@@ -292,6 +291,20 @@ class RenderWebGL extends EventEmitter {
292291
this.emit(RenderConstants.Events.NativeSizeChanged, {newSize: this._nativeSize});
293292
}
294293

294+
/**
295+
* Build a projection matrix for Scratch coordinates. For example, `_makeOrthoProjection(-240,240,-180,180)` will
296+
* mean the lower-left pixel is at (-240,-179) and the upper right pixel is at (239,180), matching Scratch 2.0.
297+
* @param {number} xLeft - the left edge of the projection volume (-240)
298+
* @param {number} xRight - the right edge of the projection volume (240)
299+
* @param {number} yBottom - the bottom edge of the projection volume (-180)
300+
* @param {number} yTop - the top edge of the projection volume (180)
301+
* @returns {module:twgl/m4.Mat4} - a projection matrix containing [xLeft,xRight) and (yBottom,yTop]
302+
*/
303+
_makeOrthoProjection (xLeft, xRight, yBottom, yTop) {
304+
// swap yBottom & yTop to fit Scratch convention of +y=up
305+
return twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
306+
}
307+
295308
/**
296309
* Create a new bitmap skin from a snapshot of the provided bitmap data.
297310
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
@@ -529,7 +542,7 @@ class RenderWebGL extends EventEmitter {
529542
* Returns the position of the given drawableID in the draw list. This is
530543
* the absolute position irrespective of layer group.
531544
* @param {number} drawableID The drawable ID to find.
532-
* @return {number} The postion of the given drawable ID.
545+
* @return {number} The position of the given drawable ID.
533546
*/
534547
getDrawableOrder (drawableID) {
535548
return this._drawList.indexOf(drawableID);
@@ -543,7 +556,7 @@ class RenderWebGL extends EventEmitter {
543556
* "go to back": setDrawableOrder(id, 1); (assuming stage at 0).
544557
* "go to front": setDrawableOrder(id, Infinity);
545558
* @param {int} drawableID ID of Drawable to reorder.
546-
* @param {number} order New absolute order or relative order adjusment.
559+
* @param {number} order New absolute order or relative order adjustment.
547560
* @param {string=} group Name of layer group drawable belongs to.
548561
* Reordering will not take place if drawable cannot be found within the bounds
549562
* of the layer group.
@@ -714,7 +727,7 @@ class RenderWebGL extends EventEmitter {
714727

715728
/**
716729
* Check if a particular Drawable is touching a particular color.
717-
* Unlike touching drawable, if the "tester" is invisble, we will still test.
730+
* Unlike touching drawable, if the "tester" is invisible, we will still test.
718731
* @param {int} drawableID The ID of the Drawable to check.
719732
* @param {Array<int>} color3b Test if the Drawable is touching this color.
720733
* @param {Array<int>} [mask3b] Optionally mask the check to this part of Drawable.
@@ -738,7 +751,7 @@ class RenderWebGL extends EventEmitter {
738751

739752
// if there are just too many pixels to CPU render efficiently, we need to let readPixels happen
740753
if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) {
741-
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
754+
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id), bounds, color3b, mask3b);
742755
}
743756

744757
const drawable = this._allDrawables[drawableID];
@@ -747,24 +760,24 @@ class RenderWebGL extends EventEmitter {
747760
const hasMask = Boolean(mask3b);
748761

749762
// Scratch Space - +y is top
750-
for (let y = bounds.bottom; y <= bounds.top; y++) {
751-
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
752-
return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom);
763+
for (let y = 0; y < bounds.height; ++y) {
764+
if (bounds.width * y * (candidates.length + 1) >= maxPixelsForCPU) {
765+
return this._isTouchingColorGpuFin(bounds, color3b, y);
753766
}
754-
for (let x = bounds.left; x <= bounds.right; x++) {
755-
point[1] = y;
756-
point[0] = x;
767+
for (let x = 0; x < bounds.width; ++x) {
768+
point[0] = bounds.left + x; // bounds.left <= point[0] < bounds.right
769+
point[1] = bounds.top - y; // bounds.bottom < point[1] <= bounds.top ("flipped")
757770
// if we use a mask, check our sample color...
758771
if (hasMask ?
759772
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
760773
drawable.isTouching(point)) {
761774
RenderWebGL.sampleColor3b(point, candidates, color);
762775
if (debugCanvasContext) {
763776
debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
764-
debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1);
777+
debugCanvasContext.fillRect(x, y, 1, 1);
765778
}
766779
// ...and the target color is drawn at this pixel
767-
if (colorMatches(color, color3b, 0)) {
780+
if (colorMatches(color3b, color, 0)) {
768781
return true;
769782
}
770783
}
@@ -794,7 +807,7 @@ class RenderWebGL extends EventEmitter {
794807
// Limit size of viewport to the bounds around the target Drawable,
795808
// and create the projection matrix for the draw.
796809
gl.viewport(0, 0, bounds.width, bounds.height);
797-
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
810+
const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom);
798811

799812
let fillBackgroundColor = this._backgroundColor;
800813

@@ -877,7 +890,7 @@ class RenderWebGL extends EventEmitter {
877890
const candidates = this._candidatesTouching(drawableID,
878891
// even if passed an invisible drawable, we will NEVER touch it!
879892
candidateIDs.filter(id => this._allDrawables[id]._visible));
880-
// if we are invisble we don't touch anything.
893+
// if we are invisible we don't touch anything.
881894
if (candidates.length === 0 || !this._allDrawables[drawableID]._visible) {
882895
return false;
883896
}
@@ -910,7 +923,7 @@ class RenderWebGL extends EventEmitter {
910923

911924
/**
912925
* Convert a client based x/y position on the canvas to a Scratch 3 world space
913-
* Rectangle. This creates recangles with a radius to cover selecting multiple
926+
* Rectangle. This creates rectangles with a radius to cover selecting multiple
914927
* scratch pixels with touch / small render areas.
915928
*
916929
* @param {int} centerX The client x coordinate of the picking location.
@@ -1027,7 +1040,7 @@ class RenderWebGL extends EventEmitter {
10271040
for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) {
10281041

10291042
// Check candidates in the reverse order they would have been
1030-
// drawn. This will determine what candiate's silhouette pixel
1043+
// drawn. This will determine what candidate's silhouette pixel
10311044
// would have been drawn at the point.
10321045
for (let d = candidateIDs.length - 1; d >= 0; d--) {
10331046
const id = candidateIDs[d];
@@ -1111,7 +1124,7 @@ class RenderWebGL extends EventEmitter {
11111124
// Limit size of viewport to the bounds around the target Drawable,
11121125
// and create the projection matrix for the draw.
11131126
gl.viewport(0, 0, bounds.width, bounds.height);
1114-
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
1127+
const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom);
11151128

11161129
gl.clearColor(0, 0, 0, 0);
11171130
gl.clear(gl.COLOR_BUFFER_BIT);
@@ -1186,7 +1199,7 @@ class RenderWebGL extends EventEmitter {
11861199
const pickY = bounds.top - scratchY;
11871200

11881201
gl.viewport(0, 0, bounds.width, bounds.height);
1189-
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
1202+
const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom);
11901203

11911204
gl.clearColor.apply(gl, this._backgroundColor);
11921205
gl.clear(gl.COLOR_BUFFER_BIT);
@@ -1255,8 +1268,7 @@ class RenderWebGL extends EventEmitter {
12551268
}
12561269

12571270
/**
1258-
* Filter a list of candidates for a touching query into only those that
1259-
* could possibly intersect the given bounds.
1271+
* Filter a list of candidates for a touching query into only those that could possibly intersect the given bounds.
12601272
* @param {int} drawableID - ID for drawable of query.
12611273
* @param {Array<int>} candidateIDs - Candidates for touching query.
12621274
* @return {?Array< {id, drawable, intersection} >} Filtered candidates with useful data.
@@ -1267,8 +1279,7 @@ class RenderWebGL extends EventEmitter {
12671279
if (bounds === null) {
12681280
return result;
12691281
}
1270-
// iterate through the drawables list BACKWARDS - we want the top most item to be the first we check
1271-
for (let index = candidateIDs.length - 1; index >= 0; index--) {
1282+
for (let index = 0; index < candidateIDs.length; ++index) {
12721283
const id = candidateIDs[index];
12731284
if (id !== drawableID) {
12741285
const drawable = this._allDrawables[id];
@@ -1428,7 +1439,7 @@ class RenderWebGL extends EventEmitter {
14281439

14291440
// Limit size of viewport to the bounds around the stamp Drawable and create the projection matrix for the draw.
14301441
gl.viewport(0, 0, bounds.width, bounds.height);
1431-
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
1442+
const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom);
14321443

14331444
gl.clearColor(0, 0, 0, 0);
14341445
gl.clear(gl.COLOR_BUFFER_BIT);
@@ -1517,7 +1528,7 @@ class RenderWebGL extends EventEmitter {
15171528
* can skip superfluous extra state calls when it is already in that
15181529
* region. Since one region may be entered from within another a exit
15191530
* handle can also be registered that is called when a new region is about
1520-
* to be entered to restore a common inbetween state.
1531+
* to be entered to restore a common in-between state.
15211532
*
15221533
* @param {any} regionId - id of the region to enter
15231534
* @param {function} enter - handle to call when first entering a region
@@ -1649,7 +1660,7 @@ class RenderWebGL extends EventEmitter {
16491660
*
16501661
* The determinant is useful in this case to know if AC is counter
16511662
* clockwise from AB. A positive value means the AC is counter
1652-
* clockwise from AC. A negative value menas AC is clockwise from AB.
1663+
* clockwise from AC. A negative value means AC is clockwise from AB.
16531664
*
16541665
* @param {Float32Array} A A 2d vector in space.
16551666
* @param {Float32Array} B A 2d vector in space.
@@ -1754,16 +1765,15 @@ class RenderWebGL extends EventEmitter {
17541765
* Sample a "final" color from an array of drawables at a given scratch space.
17551766
* Will blend any alpha values with the drawables "below" it.
17561767
* @param {twgl.v3} vec Scratch Vector Space to sample
1757-
* @param {Array<Drawables>} drawables A list of drawables with the "top most"
1758-
* drawable at index 0
1768+
* @param {Array<Drawables>} drawables A list of drawables with the "bottom most" drawable at index 0
17591769
* @param {Uint8ClampedArray} dst The color3b space to store the answer in.
17601770
* @return {Uint8ClampedArray} The dst vector with everything blended down.
17611771
*/
17621772
static sampleColor3b (vec, drawables, dst) {
17631773
dst = dst || new Uint8ClampedArray(3);
17641774
dst.fill(0);
17651775
let blendAlpha = 1;
1766-
for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) {
1776+
for (let index = drawables.length - 1; blendAlpha !== 0 && index >= 0; --index) {
17671777
/*
17681778
if (left > vec[0] || right < vec[0] ||
17691779
bottom > vec[1] || top < vec[0]) {

src/Silhouette.js

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
let __SilhouetteUpdateCanvas;
1212

1313
/**
14-
* Internal helper function (in hopes that compiler can inline). Get a pixel
15-
* from silhouette data, or 0 if outside it's bounds.
14+
* Internal helper function (in hopes that compiler can inline). Get the alpha value for a texel in the silhouette
15+
* data, or 0 if outside it's bounds.
1616
* @private
17-
* @param {Silhouette} silhouette - has data width and height
18-
* @param {number} x - x
19-
* @param {number} y - y
17+
* @param {Silhouette} $0 - has data, width, and height
18+
* @param {number} x - X position in texels (0..width).
19+
* @param {number} y - Y position in texels (0..height).
2020
* @return {number} Alpha value for x/y position
2121
*/
2222
const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
23-
// 0 if outside bouds, otherwise read from data.
23+
// 0 if outside bounds, otherwise read from data.
2424
if (x >= width || y >= height || x < 0 || y < 0) {
2525
return 0;
2626
}
@@ -39,14 +39,14 @@ const __cornerWork = [
3939

4040
/**
4141
* Get the color from a given silhouette at an x/y local texture position.
42-
* @param {Silhouette} The silhouette to sample.
43-
* @param {number} x X position of texture (0-1).
44-
* @param {number} y Y position of texture (0-1).
45-
* @param {Uint8ClampedArray} dst A color 4b space.
46-
* @return {Uint8ClampedArray} The dst vector.
42+
* @param {Silhouette} $0 - The silhouette to sample.
43+
* @param {number} x - X position in texels (0..width).
44+
* @param {number} y - Y position in texels (0..height).
45+
* @param {Uint8ClampedArray} dst - A color 4b space.
46+
* @return {Uint8ClampedArray} - The dst vector.
4747
*/
4848
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
49-
// 0 if outside bouds, otherwise read from data.
49+
// 0 if outside bounds, otherwise read from data.
5050
if (x >= width || y >= height || x < 0 || y < 0) {
5151
return dst.fill(0);
5252
}
@@ -110,7 +110,7 @@ class Silhouette {
110110
}
111111

112112
this._colorData = imageData.data;
113-
// delete our custom overriden "uninitalized" color functions
113+
// delete our custom overridden "uninitialized" color functions
114114
// let the prototype work for itself
115115
delete this.colorAtNearest;
116116
delete this.colorAtLinear;
@@ -124,12 +124,10 @@ class Silhouette {
124124
* @returns {Uint8ClampedArray} dst
125125
*/
126126
colorAtNearest (vec, dst) {
127-
return getColor4b(
128-
this,
129-
Math.floor(vec[0] * (this._width - 1)),
130-
Math.floor(vec[1] * (this._height - 1)),
131-
dst
132-
);
127+
const x = Math.round(vec[0] * this._width);
128+
const y = Math.round(vec[1] * this._height);
129+
const color = getColor4b(this, x, y, dst);
130+
return color;
133131
}
134132

135133
/**

src/shaders/sprite.frag

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ uniform sampler2D u_skin;
4545

4646
varying vec2 v_texCoord;
4747

48+
const float minAlpha = 1.0 / 255.0;
49+
4850
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
4951
// Branchless color conversions based on code from:
5052
// http://www.chilliant.com/rgb2hsv.html by Ian Taylor
@@ -155,9 +157,14 @@ void main()
155157

156158
gl_FragColor = texture2D(u_skin, texcoord0);
157159

158-
#ifdef ENABLE_ghost
159-
gl_FragColor.a *= u_ghost;
160-
#endif // ENABLE_ghost
160+
if (gl_FragColor.a < minAlpha)
161+
{
162+
discard;
163+
}
164+
165+
#ifdef ENABLE_ghost
166+
gl_FragColor.a *= u_ghost;
167+
#endif // ENABLE_ghost
161168

162169
#ifdef DRAW_MODE_silhouette
163170
// switch to u_silhouetteColor only AFTER the alpha test

test/integration/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
3030

3131
document.getElementById('file').addEventListener('click', e => {
32-
document.body.removeChild(document.getElementById('loaded'));
32+
const loaded = document.getElementById('loaded');
33+
if (loaded) {
34+
document.body.removeChild(loaded);
35+
}
3336
});
3437

3538
document.getElementById('file').addEventListener('change', e => {

0 commit comments

Comments
 (0)