From 6cadd0b31a955339fa3382371c1d9fedeef39e1a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 20 Mar 2019 22:58:32 -0700 Subject: [PATCH 01/14] test: make touching-color test more robust against GPU imprecision Previously, the `color-touching-tests.sb2` test "touches a color that doesn't actually exist right now" would use a sprite with ghost 50, blended against another sprite, to create the color that "doesn't actually exist" when the query sprite is skipped. Unfortunately the blend result was near a bit-boundary and, depending on the specific hardware used, that test could fail on the GPU. When the renderer uses the CPU path this test works fine, though, so the existing problem went unnoticed. To fix the problem I changed the project to use ghost 30 instead, which results in a color that is less near a bit boundary and is therefore less likely to fail on specific hardware. As an example of what was happening: the `touching color` block was checking for `RGB(127,101,216)` with a mask of `RGB(0xF8,0xF8,0xF0)`. On the CPU it would find `RGB(120,99,215)`, which is in range, but on some GPUs the closest color it could find was `RGB(119,98,215)` which mismatches on all four of the least significant bits -- one of which is enabled in the mask. --- src/RenderWebGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index c65e67470..3d5befd35 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -831,7 +831,7 @@ class RenderWebGL extends EventEmitter { debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1); } // ...and the target color is drawn at this pixel - if (colorMatches(color, color3b, 0)) { + if (colorMatches(color3b, color, 0)) { return true; } } From 35c9b7a74a32f623b3f406ca0ca80678715577fc Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 4 Feb 2019 11:06:27 -0800 Subject: [PATCH 02/14] fix: adjust CPU isTouchingColor to match GPU results Fix direction for Y iteration on CPU path For some reason the JavaScript engine insists on running the code instead of doing what the comment says. I guess they should match. Fix (x,y) => point[] conversion comments --- src/Drawable.js | 5 ++--- src/RenderWebGL.js | 56 ++++++++++++++++++++++++++++------------------ src/Silhouette.js | 12 +++++----- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 7a2813d88..512a1e95a 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -26,12 +26,11 @@ const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6; * @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix */ const getLocalPosition = (drawable, vec) => { - // Transfrom from world coordinates to Drawable coordinates. + // Transform from world coordinates to Drawable coordinates. const localPosition = __isTouchingPosition; const v0 = vec[0]; const v1 = vec[1]; const m = drawable._inverseMatrix; - // var v2 = v[2]; const d = (v0 * m[3]) + (v1 * m[7]) + m[15]; // The RenderWebGL quad flips the texture's X axis. So rendered bottom // left is 1, 0 and the top right is 0, 1. Flip the X axis so @@ -404,7 +403,7 @@ class Drawable { // Drawable configures a 3D matrix for drawing in WebGL, but most values // will never be set because the inputs are on the X and Y position axis // and the Z rotation axis. Drawable can bring the work inside - // _calculateTransform and greatly reduce the ammount of math and array + // _calculateTransform and greatly reduce the amount of math and array // assignments needed. const scale0 = this._skinScale[0]; diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 3d5befd35..a6d60be18 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -309,8 +309,7 @@ class RenderWebGL extends EventEmitter { this._yBottom = yBottom; this._yTop = yTop; - // swap yBottom & yTop to fit Scratch convention of +y=up - this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1); + this._projection = this._makeOrthoProjection(xLeft, xRight, yBottom, yTop); this._setNativeSize(Math.abs(xRight - xLeft), Math.abs(yBottom - yTop)); } @@ -334,6 +333,20 @@ class RenderWebGL extends EventEmitter { this.emit(RenderConstants.Events.NativeSizeChanged, {newSize: this._nativeSize}); } + /** + * Build a projection matrix for Scratch coordinates. For example, `_makeOrthoProjection(-240,240,-180,180)` will + * mean the lower-left pixel is at (-240,-179) and the upper right pixel is at (239,180), matching Scratch 2.0. + * @param {number} xLeft - the left edge of the projection volume (-240) + * @param {number} xRight - the right edge of the projection volume (240) + * @param {number} yBottom - the bottom edge of the projection volume (-180) + * @param {number} yTop - the top edge of the projection volume (180) + * @returns {module:twgl/m4.Mat4} - a projection matrix containing [xLeft,xRight) and (yBottom,yTop] + */ + _makeOrthoProjection (xLeft, xRight, yBottom, yTop) { + // swap yBottom & yTop to fit Scratch convention of +y=up + return twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1); + } + /** * Create a new bitmap skin from a snapshot of the provided bitmap data. * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin. @@ -578,7 +591,7 @@ class RenderWebGL extends EventEmitter { * Returns the position of the given drawableID in the draw list. This is * the absolute position irrespective of layer group. * @param {number} drawableID The drawable ID to find. - * @return {number} The postion of the given drawable ID. + * @return {number} The position of the given drawable ID. */ getDrawableOrder (drawableID) { return this._drawList.indexOf(drawableID); @@ -592,7 +605,7 @@ class RenderWebGL extends EventEmitter { * "go to back": setDrawableOrder(id, 1); (assuming stage at 0). * "go to front": setDrawableOrder(id, Infinity); * @param {int} drawableID ID of Drawable to reorder. - * @param {number} order New absolute order or relative order adjusment. + * @param {number} order New absolute order or relative order adjustment. * @param {string=} group Name of layer group drawable belongs to. * Reordering will not take place if drawable cannot be found within the bounds * of the layer group. @@ -766,7 +779,7 @@ class RenderWebGL extends EventEmitter { /** * Check if a particular Drawable is touching a particular color. - * Unlike touching drawable, if the "tester" is invisble, we will still test. + * Unlike touching drawable, if the "tester" is invisible, we will still test. * @param {int} drawableID The ID of the Drawable to check. * @param {Array} color3b Test if the Drawable is touching this color. * @param {Array} [mask3b] Optionally mask the check to this part of Drawable. @@ -814,13 +827,13 @@ class RenderWebGL extends EventEmitter { const effectMask = ~ShaderManager.EFFECT_INFO.ghost.mask; // Scratch Space - +y is top - for (let y = bounds.bottom; y <= bounds.top; y++) { - if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) { - return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom); + for (let y = 0; y < bounds.height; ++y) { + if (bounds.width * y * (candidates.length + 1) >= __cpuTouchingColorPixelCount) { + return this._isTouchingColorGpuFin(bounds, color3b, y); } - for (let x = bounds.left; x <= bounds.right; x++) { - point[1] = y; - point[0] = x; + for (let x = 0; x < bounds.width; ++x) { + point[0] = bounds.left + x; // bounds.left <= point[0] < bounds.right + point[1] = bounds.top - y; // bounds.bottom < point[1] <= bounds.top ("flipped") // if we use a mask, check our sample color... if (hasMask ? maskMatches(Drawable.sampleColor4b(point, drawable, color, effectMask), mask3b) : @@ -828,7 +841,7 @@ class RenderWebGL extends EventEmitter { RenderWebGL.sampleColor3b(point, candidates, color); if (debugCanvasContext) { debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`; - debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1); + debugCanvasContext.fillRect(x, y, 1, 1); } // ...and the target color is drawn at this pixel if (colorMatches(color3b, color, 0)) { @@ -874,7 +887,7 @@ class RenderWebGL extends EventEmitter { // Limit size of viewport to the bounds around the target Drawable, // and create the projection matrix for the draw. gl.viewport(0, 0, bounds.width, bounds.height); - const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1); + const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom); // Clear the query buffer to fully transparent. This will be the color of pixels that fail the stencil test. gl.clearColor(0, 0, 0, 0); @@ -968,7 +981,7 @@ class RenderWebGL extends EventEmitter { const candidates = this._candidatesTouching(drawableID, // even if passed an invisible drawable, we will NEVER touch it! candidateIDs.filter(id => this._allDrawables[id]._visible)); - // if we are invisble we don't touch anything. + // if we are invisible we don't touch anything. if (candidates.length === 0 || !this._allDrawables[drawableID]._visible) { return false; } @@ -1003,7 +1016,7 @@ class RenderWebGL extends EventEmitter { /** * Convert a client based x/y position on the canvas to a Scratch 3 world space - * Rectangle. This creates recangles with a radius to cover selecting multiple + * Rectangle. This creates rectangles with a radius to cover selecting multiple * scratch pixels with touch / small render areas. * * @param {int} centerX The client x coordinate of the picking location. @@ -1114,7 +1127,7 @@ class RenderWebGL extends EventEmitter { for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) { // Check candidates in the reverse order they would have been - // drawn. This will determine what candiate's silhouette pixel + // drawn. This will determine what candidate's silhouette pixel // would have been drawn at the point. for (let d = candidateIDs.length - 1; d >= 0; d--) { const id = candidateIDs[d]; @@ -1207,12 +1220,11 @@ class RenderWebGL extends EventEmitter { // Limit size of viewport to the bounds around the target Drawable, // and create the projection matrix for the draw. gl.viewport(0, 0, clampedWidth, clampedHeight); - const projection = twgl.m4.ortho( + const projection = this._makeOrthoProjection( scratchBounds.left, scratchBounds.right, scratchBounds.top, - scratchBounds.bottom, - -1, 1 + scratchBounds.bottom ); gl.clearColor(0, 0, 0, 0); @@ -1283,7 +1295,7 @@ class RenderWebGL extends EventEmitter { const pickY = bounds.top - scratchY; gl.viewport(0, 0, bounds.width, bounds.height); - const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1); + const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom); gl.clearColor(...this._backgroundColor4f); gl.clear(gl.COLOR_BUFFER_BIT); @@ -1619,7 +1631,7 @@ class RenderWebGL extends EventEmitter { bounds.width, bounds.height ); - const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1); + const projection = this._makeOrthoProjection(bounds.left, bounds.right, bounds.top, bounds.bottom); // Draw the stamped sprite onto the PenSkin's framebuffer. this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection, {ignoreVisibility: true}); @@ -1700,7 +1712,7 @@ class RenderWebGL extends EventEmitter { * can skip superfluous extra state calls when it is already in that * region. Since one region may be entered from within another a exit * handle can also be registered that is called when a new region is about - * to be entered to restore a common inbetween state. + * to be entered to restore a common in-between state. * * @param {any} regionId - id of the region to enter * @param {function} enter - handle to call when first entering a region diff --git a/src/Silhouette.js b/src/Silhouette.js index b96348a81..7412c1af0 100644 --- a/src/Silhouette.js +++ b/src/Silhouette.js @@ -16,12 +16,12 @@ const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31)); const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31)); /** - * Internal helper function (in hopes that compiler can inline). Get a pixel - * from silhouette data, or 0 if outside it's bounds. + * Internal helper function (in hopes that compiler can inline). Get the alpha value for a texel in the silhouette + * data, or 0 if outside it's bounds. * @private - * @param {Silhouette} silhouette - has data width and height - * @param {number} x - x - * @param {number} y - y + * @param {Silhouette} $0 - has data, width, and height + * @param {number} x - X position in texels (0..width). + * @param {number} y - Y position in texels (0..height). * @return {number} Alpha value for x/y position */ const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => { @@ -157,7 +157,7 @@ class Silhouette { } this._colorData = imageData.data; - // delete our custom overriden "uninitalized" color functions + // delete our custom overridden "uninitialized" color functions // let the prototype work for itself delete this.colorAtNearest; delete this.colorAtLinear; From 73864f3598e28d63f23a4fd0a10f8133b1f354ed Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 27 Feb 2019 11:52:01 -0800 Subject: [PATCH 03/14] fix: iterate drawables in the same order on CPU & GPU --- src/RenderWebGL.js | 15 ++++++--------- test/integration/cpu-render.html | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index a6d60be18..addb893fb 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -813,7 +813,7 @@ class RenderWebGL extends EventEmitter { // if there are just too many pixels to CPU render efficiently, we need to let readPixels happen if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) { - this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b); + this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id), bounds, color3b, mask3b); } const drawable = this._allDrawables[drawableID]; @@ -828,7 +828,7 @@ class RenderWebGL extends EventEmitter { // Scratch Space - +y is top for (let y = 0; y < bounds.height; ++y) { - if (bounds.width * y * (candidates.length + 1) >= __cpuTouchingColorPixelCount) { + if (bounds.width * y * (candidates.length + 1) >= maxPixelsForCPU) { return this._isTouchingColorGpuFin(bounds, color3b, y); } for (let x = 0; x < bounds.width; ++x) { @@ -1362,8 +1362,7 @@ class RenderWebGL extends EventEmitter { } /** - * Filter a list of candidates for a touching query into only those that - * could possibly intersect the given bounds. + * Filter a list of candidates for a touching query into only those that could possibly intersect the given bounds. * @param {int} drawableID - ID for drawable of query. * @param {Array} candidateIDs - Candidates for touching query. * @return {?Array< {id, drawable, intersection} >} Filtered candidates with useful data. @@ -1374,8 +1373,7 @@ class RenderWebGL extends EventEmitter { if (bounds === null) { return result; } - // iterate through the drawables list BACKWARDS - we want the top most item to be the first we check - for (let index = candidateIDs.length - 1; index >= 0; index--) { + for (let index = 0; index < candidateIDs.length; ++index) { const id = candidateIDs[index]; if (id !== drawableID) { const drawable = this._allDrawables[id]; @@ -1970,8 +1968,7 @@ class RenderWebGL extends EventEmitter { * Sample a "final" color from an array of drawables at a given scratch space. * Will blend any alpha values with the drawables "below" it. * @param {twgl.v3} vec Scratch Vector Space to sample - * @param {Array} drawables A list of drawables with the "top most" - * drawable at index 0 + * @param {Array} drawables A list of drawables with the "bottom most" drawable at index 0 * @param {Uint8ClampedArray} dst The color3b space to store the answer in. * @return {Uint8ClampedArray} The dst vector with everything blended down. */ @@ -1979,7 +1976,7 @@ class RenderWebGL extends EventEmitter { dst = dst || new Uint8ClampedArray(3); dst.fill(0); let blendAlpha = 1; - for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) { + for (let index = drawables.length - 1; blendAlpha !== 0 && index >= 0; --index) { /* if (left > vec[0] || right < vec[0] || bottom > vec[1] || top < vec[0]) { diff --git a/test/integration/cpu-render.html b/test/integration/cpu-render.html index 7eec0a612..634bb76a9 100644 --- a/test/integration/cpu-render.html +++ b/test/integration/cpu-render.html @@ -44,7 +44,7 @@ } drawable.updateCPURenderAttributes(); return { id, drawable }; - }).reverse().filter(Boolean); + }).filter(Boolean); const color = new Uint8ClampedArray(3); for (let x = -239; x <= 240; x++) { for (let y = -180; y< 180; y++) { From 9514d3b003e262197e1ab059125365b609a3866b Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 27 Feb 2019 18:24:28 -0800 Subject: [PATCH 04/14] test: add touching-color test to verify stencil use --- .../scratch-tests/stencil-touching-circle.sb2 | Bin 0 -> 8332 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/integration/scratch-tests/stencil-touching-circle.sb2 diff --git a/test/integration/scratch-tests/stencil-touching-circle.sb2 b/test/integration/scratch-tests/stencil-touching-circle.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..b6e75c1aa902d08a25d325d63ecbe263087c2ffc GIT binary patch literal 8332 zcmeHM30M=?79Q3{1VKTR?PUyNk!3QI4H6R(1Qbx#f)!;6AwXCXNFrOMs4Pm^1S)Fl zQbYwsEl2^23b;NNi(A=56c9uyNCBZ*m3Be`D4+WLzW4fluN89daJPTXx#!+<=G=1~ z?PO#X006)M+Y?vXJ2fp!8j=Bky4e7rh_;6Ed4X(}5DFCVxZQ58+*VVSnDB~i?Jl!@ zJu=Jl95|;#QkS$?sRqwG^W{QY-3=#=6x8|`Z|J(eySyb0zoJZTlpTU8tiR^y?^mBP z@+x4(u9wkrg2SK2dur{$F-pCUs9QO&4}SS70Dko3i!$e3B`(V8^~Tli-UlK}Ry|sw z7H{5p^_GK<#*(*9-7tAl~RU@0?8_t^H z`_JD8>Ws`ceY2aG?~&+v$v3+@sr&S)Cr90E_iL!dEMV>5b@cJ2J;f|%uLnrX9wzd8 z3gTy1U}#Z{BMN=i3Xi9hFN>lT@HRhCN}WPmkZYIEQpKHt9kXR z^u^Ut!iBig!L7T-vpBbnN9Wx!{x_YCBwYC%A3_`^@ls!{Ie~xv(gQ&H^^l4W?W9?+K^q6>5CEw zhp9=`o3~O@7ZmAz)^MQNl3AklW1ei<0v)5R&x+<@+`KB?ao_-EWuVyRWwSm)ClzX;s(@My>ei`4#x}ZG3x)2+PO>Rpt8hep? z?T-;_?!AQ5WZQ!6Pw_vy)}=!p2bEjP(xYW7-5;)P3TT(#H@vgt=);nS={{LH9-rkq zUe8v&cLB4TutIRW23kbf-rd zBzaQe=`~V-AOme%C`z!mC{=BdLK7e(FG}K$kGW4Ql();}@81OcyD{tB6fP6*Xoi608tiMLnqTn^{3BP@8444g3nqKQaE6Ap|F z3FZnIxB#Irl!nJgL_|OlM2N@t#}i-}#-lHn4538^bGSio8;DS;RQz}^Zn**o(ia&U zgI1BDJid_a3;INXE-XG%$O-_BK?xCoxB&{`2qCubCOl|tJPr(H3IjlnF9T;|2}1-j zg+Qeu6e0m3nNkQuG7j|P1P3#4x-d+%wzkHB0wJFl#Ae{QJT4mtqOi3ffk=g@CS(wy zkRX^0gDXJ-i3AZT)X5eC$rK`@JqU#inG#7Lfs8-|R1^rs6f!|fL>`Z zhS541qN4v16EZ|LMP93mX^9Y$5vF2VlW#B;)0(6~71I)NCE;J79P>Cd%N;IXQ%LY>@WK?6t zY9QYA6FQ)4ZH=lcJfRfE5)(j}&pH)1>@D^q~m9QTh9H6$h%p58{|LCY8vE21Th7fDt!o*aVdTVwhs?r8UE>Nu2?u@ zBd32N{(gIP#zxNA$anfRFhCAC>TkE3@>&)G({6d7F3fdus{^lA2 z0L~3P69X!MFH^{rMaMJ%GBW2s;VTWychRBie!I96EKnE7yAFEY@xGi|%sG~s-(9ro zih0MdbpMi@t8AGuJl#+QX1!&vj=`{UO<>-u487DRd#ELk+By~Y(=5G1!%LzL1vFdv zFR<+jE{V>(w>2m+>|k2;qL-a;jK}?`u2_?>V{1#bkM%g5Ny(N;;$%3S8fn?62+$q9 z26ziNZuSa`!-m#h@y{*Iiro9`bike5S!tA@bxZvYd7ZN!!B!8+)Q+62KZGqHTpsML zQY)b9I}N&#Z0rqXWQ|%Y%KP%?9J_fgrR|(N9ok~2w*FIR59$e3?cT5}Cr>WTD=*f% zQ8;?!U(sgOPPI-!W--{F=eHZTR$xnAD^4_=8!=e8jW}W+zrqNwsJd?GU_-OG7&9#s1Ok zr)?um&#`}`$?nE1Q1}YtD$5*mw3Cun0N#Clj&7gV{$9UKpF+OQXmnhu$+T2qi-ZHb~e+R2H& Q=72o Date: Thu, 9 Jan 2020 15:24:54 -0500 Subject: [PATCH 05/14] fix: fix silhouette sampling math to match GPU --- src/Drawable.js | 25 ++++++++--------------- src/RenderWebGL.js | 6 +++--- src/Silhouette.js | 51 ++++++++++++++++++++++++++++++---------------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 512a1e95a..e691f735d 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -13,7 +13,6 @@ const log = require('./util/log'); * @type {twgl.v3} */ const __isTouchingPosition = twgl.v3.create(); -const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6; /** * Convert a scratch space location into a texture space float. Uses the @@ -28,8 +27,14 @@ const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6; const getLocalPosition = (drawable, vec) => { // Transform from world coordinates to Drawable coordinates. const localPosition = __isTouchingPosition; - const v0 = vec[0]; - const v1 = vec[1]; + // World coordinates/screen-space coordinates refer to pixels by integer coordinates. + // The GL rasterizer considers a pixel to be an area sample. + // Without multisampling, it samples once from the pixel center, + // which is offset by (0.5, 0.5) from the pixel's integer coordinate. + // If you think of it as a pixel grid, the coordinates we're given are grid lines, but we want grid boxes. + // That's why we offset by 0.5 (-0.5 in the Y direction because it's flipped). + const v0 = vec[0] + 0.5; + const v1 = vec[1] - 0.5; const m = drawable._inverseMatrix; const d = (v0 * m[3]) + (v1 * m[7]) + m[15]; // The RenderWebGL quad flips the texture's X axis. So rendered bottom @@ -37,19 +42,7 @@ const getLocalPosition = (drawable, vec) => { // localPosition matches that transformation. localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d); localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5; - // Fix floating point issues near 0. Filed https://github.com/LLK/scratch-render/issues/688 that - // they're happening in the first place. - // TODO: Check if this can be removed after render pull 479 is merged - if (Math.abs(localPosition[0]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[0] = 0; - if (Math.abs(localPosition[1]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[1] = 0; - // Apply texture effect transform if the localPosition is within the drawable's space, - // and any effects are currently active. - if (drawable.enabledEffects !== 0 && - (localPosition[0] >= 0 && localPosition[0] < 1) && - (localPosition[1] >= 0 && localPosition[1] < 1)) { - - EffectTransform.transformPoint(drawable, localPosition, localPosition); - } + if (drawable.enabledEffects !== 0) EffectTransform.transformPoint(drawable, localPosition, localPosition); return localPosition; }; diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index addb893fb..0972b6efe 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -1888,12 +1888,12 @@ class RenderWebGL extends EventEmitter { // *Not* Scratch Space-- +y is bottom // Loop over all rows of pixels, starting at the top for (let y = 0; y < height; y++) { - _pixelPos[1] = y / height; + _pixelPos[1] = (y + 0.5) / height; // We start at the leftmost point, then go rightwards until we hit an opaque pixel let x = 0; for (; x < width; x++) { - _pixelPos[0] = x / width; + _pixelPos[0] = (x + 0.5) / width; EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); if (drawable.skin.isTouchingLinear(_effectPos)) { currentPoint = [x, y]; @@ -1930,7 +1930,7 @@ class RenderWebGL extends EventEmitter { // Now we repeat the process for the right side, looking leftwards for a pixel. for (x = width - 1; x >= 0; x--) { - _pixelPos[0] = x / width; + _pixelPos[0] = (x + 0.5) / width; EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); if (drawable.skin.isTouchingLinear(_effectPos)) { currentPoint = [x, y]; diff --git a/src/Silhouette.js b/src/Silhouette.js index 7412c1af0..17705241c 100644 --- a/src/Silhouette.js +++ b/src/Silhouette.js @@ -16,8 +16,8 @@ const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31)); const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31)); /** - * Internal helper function (in hopes that compiler can inline). Get the alpha value for a texel in the silhouette - * data, or 0 if outside it's bounds. + * Internal helper function (in hopes that compiler can inline). Get a pixel's alpha + * from silhouette data, matching texture sampling rules. * @private * @param {Silhouette} $0 - has data, width, and height * @param {number} x - X position in texels (0..width). @@ -25,10 +25,10 @@ const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31)); * @return {number} Alpha value for x/y position */ const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => { - // 0 if outside bounds, otherwise read from data. - if (x >= width || y >= height || x < 0 || y < 0) { - return 0; - } + // Clamp coords to edge, matching GL_CLAMP_TO_EDGE. + x = intMax(0, intMin(x, width - 1)); + y = intMax(0, intMin(y, height - 1)); + return data[(((y * width) + x) * 4) + 3]; }; @@ -57,10 +57,6 @@ const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, ds x = intMax(0, intMin(x, width - 1)); y = intMax(0, intMin(y, height - 1)); - // 0 if outside bounds, otherwise read from data. - if (x >= width || y >= height || x < 0 || y < 0) { - return dst.fill(0); - } const offset = ((y * width) + x) * 4; // premultiply alpha const alpha = data[offset + 3] / 255; @@ -173,8 +169,8 @@ class Silhouette { colorAtNearest (vec, dst) { return this._getColor( this, - Math.floor(vec[0] * (this._width - 1)), - Math.floor(vec[1] * (this._height - 1)), + Math.floor(vec[0] * this._width), + Math.floor(vec[1] * this._height), dst ); } @@ -187,8 +183,13 @@ class Silhouette { * @returns {Uint8ClampedArray} dst */ colorAtLinear (vec, dst) { - const x = vec[0] * (this._width - 1); - const y = vec[1] * (this._height - 1); + // In texture space, pixel centers are at integer coords. Here, the *corners* are at integers. + // We cannot skip the "add 0.5 in Drawable.getLocalPosition -> subtract 0.5 here" roundtrip + // because the two spaces are different--we add 0.5 in Drawable.getLocalPosition in "Scratch space" + // (-240,240 & -180,180), but subtract 0.5 in silhouette space (0, width or height). + // See https://web.archive.org/web/20190125211252/http://hacksoflife.blogspot.com/2009/12/texture-coordinate-system-for-opengl.html + const x = (vec[0] * (this._width)) - 0.5; + const y = (vec[1] * (this._height)) - 0.5; const x1D = x % 1; const y1D = y % 1; @@ -218,10 +219,17 @@ class Silhouette { */ isTouchingNearest (vec) { if (!this._colorData) return; + + // Never touching if the coord falls outside the texture space. + if (vec[0] < 0 || vec[0] > 1 || + vec[1] < 0 || vec[1] > 1) { + return false; + } + return getPoint( this, - Math.floor(vec[0] * (this._width - 1)), - Math.floor(vec[1] * (this._height - 1)) + Math.floor(vec[0] * this._width), + Math.floor(vec[1] * this._height) ) > 0; } @@ -233,8 +241,15 @@ class Silhouette { */ isTouchingLinear (vec) { if (!this._colorData) return; - const x = Math.floor(vec[0] * (this._width - 1)); - const y = Math.floor(vec[1] * (this._height - 1)); + + // Never touching if the coord falls outside the texture space. + if (vec[0] < 0 || vec[0] > 1 || + vec[1] < 0 || vec[1] > 1) { + return false; + } + + const x = Math.floor((vec[0] * this._width) - 0.5); + const y = Math.floor((vec[1] * this._height) - 0.5); return getPoint(this, x, y) > 0 || getPoint(this, x + 1, y) > 0 || getPoint(this, x, y + 1) > 0 || From f1918b4ba2034a2df340e03a71a3c7de30c17ec1 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Jan 2020 15:25:03 -0500 Subject: [PATCH 06/14] test: add collision test --- .../scratch-tests/collision-bounds.sb3 | Bin 0 -> 57382 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/integration/scratch-tests/collision-bounds.sb3 diff --git a/test/integration/scratch-tests/collision-bounds.sb3 b/test/integration/scratch-tests/collision-bounds.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..6b707cb9e1f5284aaa25b89a1584fca367734d3a GIT binary patch literal 57382 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 @zVtf}~2QQ8}k)EmZ(V2m-t}M$m{V8oNy~1+O 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 Zli;nP{1Ef6UbuG;@J`ts&;IrG{{U-B`3nF5 literal 0 HcmV?d00001 From b0ef5e99fe81d13c05bfef3ec67f32b0cd6f0b1e Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Jan 2020 15:25:23 -0500 Subject: [PATCH 07/14] test: add border to CPU render test canvases --- test/integration/cpu-render.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/cpu-render.html b/test/integration/cpu-render.html index 634bb76a9..513654d59 100644 --- a/test/integration/cpu-render.html +++ b/test/integration/cpu-render.html @@ -1,3 +1,9 @@ + + + + From be15f209b61b31240ebcd1b2c91aa84dee150e45 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 7 May 2020 14:05:17 -0400 Subject: [PATCH 08/14] fix: re-add effect transform check --- src/Drawable.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Drawable.js b/src/Drawable.js index e691f735d..b7fc46e0c 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -42,7 +42,14 @@ const getLocalPosition = (drawable, vec) => { // localPosition matches that transformation. localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d); localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5; - if (drawable.enabledEffects !== 0) EffectTransform.transformPoint(drawable, localPosition, localPosition); + // Apply texture effect transform if the localPosition is within the drawable's space, + // and any effects are currently active. + if (drawable.enabledEffects !== 0 && + (localPosition[0] >= 0 && localPosition[0] < 1) && + (localPosition[1] >= 0 && localPosition[1] < 1)) { + + EffectTransform.transformPoint(drawable, localPosition, localPosition); + } return localPosition; }; From 0554ed7f31cc09b49e16ff63c5d8ed1ddb1b0b36 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 7 May 2020 19:14:42 -0400 Subject: [PATCH 09/14] fix: fix cpu-render.html coordinate space This should ensure that CPU/GPU diffs are actually useful, and not improperly shifted. --- test/integration/cpu-render.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/integration/cpu-render.html b/test/integration/cpu-render.html index 513654d59..00af88be8 100644 --- a/test/integration/cpu-render.html +++ b/test/integration/cpu-render.html @@ -52,10 +52,15 @@ return { id, drawable }; }).filter(Boolean); const color = new Uint8ClampedArray(3); - for (let x = -239; x <= 240; x++) { - for (let y = -180; y< 180; y++) { - render.constructor.sampleColor3b([x, y], drawBits, color); - const offset = (((179-y) * 480) + 239 + x) * 4 + const vec = [0, 0]; + for (let x = 0; x < 480; x++) { + // leftmost pixel is -240, rightmost is 239 + vec[0] = x - 240; + for (let y = 0; y < 360; y++) { + // bottommost pixel is -179, topmost is 180 + vec[1] = 180 - y; + render.constructor.sampleColor3b(vec, drawBits, color); + const offset = ((y * 480) + x) * 4; cpuImageData.data.set(color, offset); } } From 37a0fa026994740cb921142e5f347a500f893597 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 6 Jul 2020 16:51:43 -0400 Subject: [PATCH 10/14] docs: document what's going on with convex hull points --- src/RenderWebGL.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 0972b6efe..f15ca8784 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -1888,6 +1888,10 @@ class RenderWebGL extends EventEmitter { // *Not* Scratch Space-- +y is bottom // Loop over all rows of pixels, starting at the top for (let y = 0; y < height; y++) { + // See comment in Drawable.getLocalPosition for why we're adding 0.5 here. + // Essentially, _pixelPos is supposed to be in "texture space", and "texture space" positions are offset + // by 0.5. Notice that we're calling drawable.skin.isTouchingLinear (operates in texture space) + // and not drawable.isTouching (operates in Scratch space). _pixelPos[1] = (y + 0.5) / height; // We start at the leftmost point, then go rightwards until we hit an opaque pixel From 4a24cd8547d97c21bf300aa4cb41342360ef4dfa Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 9 Nov 2020 17:08:08 -0500 Subject: [PATCH 11/14] test: draw convex hull on debug canvas in getBounds --- src/RenderWebGL.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index f15ca8784..3bb93a26f 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -695,21 +695,43 @@ class RenderWebGL extends EventEmitter { drawable.setConvexHullPoints(points); } const bounds = drawable.getFastBounds(); - // In debug mode, draw the bounds. + // In debug mode, draw the bounds and convex hull. if (this._debugCanvas) { const gl = this._gl; this._debugCanvas.width = gl.canvas.width; this._debugCanvas.height = gl.canvas.height; const context = this._debugCanvas.getContext('2d'); context.drawImage(gl.canvas, 0, 0); - context.strokeStyle = '#FF0000'; + + // Prepare the coordinate space. + context.save(); const pr = window.devicePixelRatio; + context.translate(pr * this._nativeSize[0] / 2, pr * this._nativeSize[1] / 2); + context.scale(pr, pr * -1); + + // Draw the bounds. + context.strokeStyle = 'red'; context.strokeRect( - pr * (bounds.left + (this._nativeSize[0] / 2)), - pr * (-bounds.top + (this._nativeSize[1] / 2)), - pr * (bounds.right - bounds.left), - pr * (-bounds.bottom + bounds.top) + bounds.left, + bounds.top, + bounds.right - bounds.left, + bounds.bottom - bounds.top ); + + // Draw the convex hull. + context.beginPath(); + const points = drawable._getTransformedHullPoints(); + if (points.length > 0) { + context.moveTo(points[0][0], points[0][1]); + for (let i = 0; i < points.length; i++) { + const point = points[(i + 1) % points.length]; + context.lineTo(point[0], point[1]); + } + context.lineWidth = 1; + context.strokeStyle = 'blue'; + context.stroke(); + } + context.restore(); } return bounds; } From bcdae72d5596c2a4f6c265807829fd74ad609683 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 9 Nov 2020 17:51:17 -0500 Subject: [PATCH 12/14] fix: calculate convex hulls in texture space --- src/Drawable.js | 7 ++----- src/RenderWebGL.js | 16 +++++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index b7fc46e0c..2bf80330e 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -596,16 +596,13 @@ class Drawable { } const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1); - const skinSize = this.skin.size; - const halfXPixel = 1 / skinSize[0] / 2; - const halfYPixel = 1 / skinSize[1] / 2; const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection); for (let i = 0; i < this._convexHullPoints.length; i++) { const point = this._convexHullPoints[i]; const dstPoint = this._transformedHullPoints[i]; - dstPoint[0] = 0.5 + (-point[0] / skinSize[0]) - halfXPixel; - dstPoint[1] = (point[1] / skinSize[1]) - 0.5 + halfYPixel; + dstPoint[0] = 0.5 - point[0]; + dstPoint[1] = point[1] - 0.5; twgl.m4.transformPoint(tm, dstPoint, dstPoint); } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 3bb93a26f..bcdba5089 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -1861,14 +1861,16 @@ class RenderWebGL extends EventEmitter { _getConvexHullPointsForDrawable (drawableID) { const drawable = this._allDrawables[drawableID]; - const [width, height] = drawable.skin.size; + drawable.updateCPURenderAttributes(); + + const silhouette = drawable.skin._silhouette; + const width = silhouette._width; + const height = silhouette._height; // No points in the hull if invisible or size is 0. if (!drawable.getVisible() || width === 0 || height === 0) { return []; } - drawable.updateCPURenderAttributes(); - /** * Return the determinant of two vectors, the vector from A to B and the vector from A to C. * @@ -1921,8 +1923,8 @@ class RenderWebGL extends EventEmitter { for (; x < width; x++) { _pixelPos[0] = (x + 0.5) / width; EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); - if (drawable.skin.isTouchingLinear(_effectPos)) { - currentPoint = [x, y]; + if (drawable.skin.isTouchingNearest(_effectPos)) { + currentPoint = [_pixelPos[0], _pixelPos[1]]; break; } } @@ -1958,8 +1960,8 @@ class RenderWebGL extends EventEmitter { for (x = width - 1; x >= 0; x--) { _pixelPos[0] = (x + 0.5) / width; EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); - if (drawable.skin.isTouchingLinear(_effectPos)) { - currentPoint = [x, y]; + if (drawable.skin.isTouchingNearest(_effectPos)) { + currentPoint = [_pixelPos[0], _pixelPos[1]]; break; } } From 799fd18f8ac327d0a893935c69becfb663f93705 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 9 Nov 2020 17:54:28 -0500 Subject: [PATCH 13/14] fix: expand convex hull bounds by half a pixel per side --- src/Drawable.js | 36 ++++++++++++++++++++++++++++++++++-- src/Rectangle.js | 30 +----------------------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 2bf80330e..907654571 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -503,6 +503,38 @@ class Drawable { return this.skin.isTouchingLinear(getLocalPosition(this, vec)); } + /** + * Initialize a bounding rectangle with a set of convex hull points, taking into account that the points refer to + * pixel centers and not pixel edges. + * @param {Rectangle} rect The bounding rectangle to initialize + * @param {Array} points The convex hull points + */ + _initRectangleFromConvexHullPoints (rect, points) { + rect.left = Infinity; + rect.right = -Infinity; + rect.top = -Infinity; + rect.bottom = Infinity; + + // Each convex hull point is the center of a pixel. However, said pixels each have area. We must take into + // account the size of the pixels when calculating the bounds. The pixel dimensions depend on the scale and + // rotation (as we're treating pixels as squares, which change dimensions when rotated). + + // The "Scratch-space" size of one texture pixel at the drawable's current size. + const pixelScale = (this.scale[0] / 100) * (this.skin.size[0] / this.skin._silhouette._width); + // Half the size of a rotated pixel, if we assume pixels are shaped like squares. + // At 0 degrees of rotation, this will be 0.5. At 45 degrees, it'll be 0.707 (half the square root of 2), etc. + const halfPixel = (Math.abs(this._rotationMatrix[0]) + Math.abs(this._rotationMatrix[1])) * 0.5 * pixelScale; + + for (let i = 0; i < points.length; i++) { + const x = points[i][0]; + const y = points[i][1]; + if ((x - halfPixel) < rect.left) rect.left = x - halfPixel; + if ((x + halfPixel) > rect.right) rect.right = x + halfPixel; + if ((y + halfPixel) > rect.top) rect.top = y + halfPixel; + if ((y - halfPixel) < rect.bottom) rect.bottom = y - halfPixel; + } + } + /** * Get the precise bounds for a Drawable. * This function applies the transform matrix to the known convex hull, @@ -521,7 +553,7 @@ class Drawable { const transformedHullPoints = this._getTransformedHullPoints(); // Search through transformed points to generate box on axes. result = result || new Rectangle(); - result.initFromPointsAABB(transformedHullPoints); + this._initRectangleFromConvexHullPoints(result, transformedHullPoints); return result; } @@ -545,7 +577,7 @@ class Drawable { const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice); // Search through filtered points to generate box on axes. result = result || new Rectangle(); - result.initFromPointsAABB(filteredHullPoints); + this._initRectangleFromConvexHullPoints(result, filteredHullPoints); return result; } diff --git a/src/Rectangle.js b/src/Rectangle.js index c998651b0..438f0a3d8 100644 --- a/src/Rectangle.js +++ b/src/Rectangle.js @@ -26,34 +26,6 @@ class Rectangle { this.top = top; } - /** - * Initialize a Rectangle to the minimum AABB around a set of points. - * @param {Array>} points Array of [x, y] points. - */ - initFromPointsAABB (points) { - this.left = Infinity; - this.right = -Infinity; - this.top = -Infinity; - this.bottom = Infinity; - - for (let i = 0; i < points.length; i++) { - const x = points[i][0]; - const y = points[i][1]; - if (x < this.left) { - this.left = x; - } - if (x > this.right) { - this.right = x; - } - if (y > this.top) { - this.top = y; - } - if (y < this.bottom) { - this.bottom = y; - } - } - } - /** * Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed * by a model matrix. @@ -123,7 +95,7 @@ class Rectangle { this.right = Math.min(this.right, right); this.bottom = Math.max(this.bottom, bottom); this.top = Math.min(this.top, top); - + this.left = Math.min(this.left, right); this.right = Math.max(this.right, left); this.bottom = Math.min(this.bottom, top); From b911304f0528110ea27cc0ad4acd185f69a3ede3 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 9 Nov 2020 17:57:19 -0500 Subject: [PATCH 14/14] fix: set convex hull dirty when silhouettes update --- src/Drawable.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Drawable.js b/src/Drawable.js index 907654571..70a117ca0 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -123,6 +123,7 @@ class Drawable { this._transformedHullDirty = true; this._skinWasAltered = this._skinWasAltered.bind(this); + this._silhouetteWasUpdated = this._silhouetteWasUpdated.bind(this); this.isTouching = this._isTouchingNever; } @@ -166,10 +167,12 @@ class Drawable { if (this._skin !== newSkin) { if (this._skin) { this._skin.removeListener(Skin.Events.WasAltered, this._skinWasAltered); + this._skin.removeListener(Skin.Events.SilhouetteUpdated, this._silhouetteWasUpdated); } this._skin = newSkin; if (this._skin) { this._skin.addListener(Skin.Events.WasAltered, this._skinWasAltered); + this._skin.addListener(Skin.Events.SilhouetteUpdated, this._silhouetteWasUpdated); } this._skinWasAltered(); } @@ -696,6 +699,14 @@ class Drawable { this.setTransformDirty(); } + /** + * Respond to an internal change in the current Skin's silhouette. + * @private + */ + _silhouetteWasUpdated () { + this.setConvexHullDirty(); + } + /** * Calculate a color to represent the given ID number. At least one component of * the resulting color will be non-zero if the ID is not RenderConstants.ID_NONE.