mirror of
https://github.com/scratchfoundation/scratch-render.git
synced 2025-07-20 11:12:17 -04:00
Touching color implementation (#312)
* Touching color draft implementation * New constant for CPU/GPU render split determined, removing query param * Small fix for pick tests
This commit is contained in:
parent
62b4a1d5a2
commit
c54a928f0a
8 changed files with 574 additions and 88 deletions
109
src/Drawable.js
109
src/Drawable.js
|
@ -6,8 +6,41 @@ const ShaderManager = require('./ShaderManager');
|
|||
const Skin = require('./Skin');
|
||||
const EffectTransform = require('./EffectTransform');
|
||||
|
||||
/**
|
||||
* An internal workspace for calculating texture locations from world vectors
|
||||
* this is REUSED for memory conservation reasons
|
||||
* @type {twgl.v3}
|
||||
*/
|
||||
const __isTouchingPosition = twgl.v3.create();
|
||||
|
||||
/**
|
||||
* Convert a scratch space location into a texture space float. Uses the
|
||||
* internal __isTouchingPosition as a return value, so this should be copied
|
||||
* if you ever need to get two local positions and store both. Requires that
|
||||
* the drawable inverseMatrix is up to date.
|
||||
*
|
||||
* @param {Drawable} drawable The drawable to get the inverse matrix and uniforms from
|
||||
* @param {twgl.v3} vec [x,y] scratch space vector
|
||||
* @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.
|
||||
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
|
||||
// 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;
|
||||
// Apply texture effect transform.
|
||||
EffectTransform.transformPoint(drawable, localPosition, localPosition);
|
||||
return localPosition;
|
||||
};
|
||||
|
||||
class Drawable {
|
||||
/**
|
||||
* An object which can be drawn by the renderer.
|
||||
|
@ -381,36 +414,7 @@ class Drawable {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (this._transformDirty) {
|
||||
this._calculateTransform();
|
||||
}
|
||||
|
||||
// Get the inverse of the model matrix or update it.
|
||||
const inverse = this._inverseMatrix;
|
||||
if (this._inverseTransformDirty) {
|
||||
const model = twgl.m4.copy(this._uniforms.u_modelMatrix, inverse);
|
||||
// The normal matrix uses a z scaling of 0 causing model[10] to be
|
||||
// 0. Getting a 4x4 inverse is impossible without a scaling in x, y,
|
||||
// and z.
|
||||
model[10] = 1;
|
||||
twgl.m4.inverse(model, model);
|
||||
this._inverseTransformDirty = false;
|
||||
}
|
||||
|
||||
// Transfrom from world coordinates to Drawable coordinates.
|
||||
const localPosition = twgl.m4.transformPoint(inverse, vec, __isTouchingPosition);
|
||||
|
||||
// Transform into texture coordinates. 0, 0 is the bottom left. 1, 1 is
|
||||
// the top right.
|
||||
localPosition[0] += 0.5;
|
||||
localPosition[1] += 0.5;
|
||||
// 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
|
||||
// localPosition matches that transformation.
|
||||
localPosition[0] = 1 - localPosition[0];
|
||||
|
||||
// Apply texture effect transform.
|
||||
EffectTransform.transformPoint(this, localPosition, localPosition);
|
||||
const localPosition = getLocalPosition(this, vec);
|
||||
|
||||
if (this.useNearest) {
|
||||
return this.skin.isTouchingNearest(localPosition);
|
||||
|
@ -460,6 +464,7 @@ class Drawable {
|
|||
bounds.initFromPointsAABB(transformedHullPoints);
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the precise bounds for the upper 8px slice of the Drawable.
|
||||
* Used for calculating where to position a text bubble.
|
||||
|
@ -514,6 +519,7 @@ class Drawable {
|
|||
* @return {!Rectangle} Bounds for the Drawable.
|
||||
*/
|
||||
getFastBounds () {
|
||||
this.updateMatrix();
|
||||
if (!this.needsConvexHullPoints()) {
|
||||
return this.getBounds();
|
||||
}
|
||||
|
@ -545,6 +551,27 @@ class Drawable {
|
|||
return transformedHullPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the transform matrix and calculate it's inverse for collision
|
||||
* and local texture position purposes.
|
||||
*/
|
||||
updateMatrix () {
|
||||
if (this._transformDirty) {
|
||||
this._calculateTransform();
|
||||
}
|
||||
// Get the inverse of the model matrix or update it.
|
||||
if (this._inverseTransformDirty) {
|
||||
const inverse = this._inverseMatrix;
|
||||
twgl.m4.copy(this._uniforms.u_modelMatrix, inverse);
|
||||
// The normal matrix uses a z scaling of 0 causing model[10] to be
|
||||
// 0. Getting a 4x4 inverse is impossible without a scaling in x, y,
|
||||
// and z.
|
||||
inverse[10] = 1;
|
||||
twgl.m4.inverse(inverse, inverse);
|
||||
this._inverseTransformDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to an internal change in the current Skin.
|
||||
* @private
|
||||
|
@ -586,6 +613,28 @@ class Drawable {
|
|||
id |= (b & 255) << 16;
|
||||
return id + RenderConstants.ID_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample a color from a drawable's texture.
|
||||
* @param {twgl.v3} vec The scratch space [x,y] vector
|
||||
* @param {Drawable} drawable The drawable to sample the texture from
|
||||
* @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point.
|
||||
* @returns {Uint8ClampedArray} The dst object filled with the color4b
|
||||
*/
|
||||
static sampleColor4b (vec, drawable, dst) {
|
||||
const localPosition = getLocalPosition(drawable, vec);
|
||||
if (localPosition[0] < 0 || localPosition[1] < 0 ||
|
||||
localPosition[0] > 1 || localPosition[1] > 1) {
|
||||
dst[3] = 0;
|
||||
return dst;
|
||||
}
|
||||
const textColor =
|
||||
// commenting out to only use nearest for now
|
||||
// drawable.useNearest ?
|
||||
drawable.skin._silhouette.colorAtNearest(localPosition, dst);
|
||||
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
|
||||
return EffectTransform.transformColor(drawable, textColor, textColor);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Drawable;
|
||||
|
|
|
@ -20,7 +20,165 @@ const CENTER_X = 0.5;
|
|||
*/
|
||||
const CENTER_Y = 0.5;
|
||||
|
||||
// color conversions grabbed from https://gist.github.com/mjackson/5311256
|
||||
|
||||
/**
|
||||
* Converts an RGB color value to HSL. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes r, g, and b are contained in the set [0, 255] and
|
||||
* returns h, s, and l in the set [0, 1].
|
||||
*
|
||||
* @param {number} r The red color value
|
||||
* @param {number} g The green color value
|
||||
* @param {number} b The blue color value
|
||||
* @return {Array} The HSL representation
|
||||
*/
|
||||
const rgbToHsl = ([r, g, b]) => {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h;
|
||||
let s;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d) + (g < b ? 6 : 0); break;
|
||||
case g: h = ((b - r) / d) + 2; break;
|
||||
case b: h = ((r - g) / d) + 4; break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h, s, l];
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for hslToRgb is called with varying 't' values to get
|
||||
* red green and blue values from the p/q/t color space calculations
|
||||
* @param {number} p vector coordinates
|
||||
* @param {number} q vector coordinates
|
||||
* @param {number} t vector coordinates
|
||||
* @return {number} amount of r/g/b byte
|
||||
*/
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + ((q - p) * 6 * t);
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + ((q - p) * ((2 / 3) - t) * 6);
|
||||
return p;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255].
|
||||
*
|
||||
* @param {number} h The hue
|
||||
* @param {number} s The saturation
|
||||
* @param {number} l The lightness
|
||||
* @return {Array} The RGB representation
|
||||
*/
|
||||
const hslToRgb = ([h, s, l]) => {
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - (l * s);
|
||||
const p = (2 * l) - q;
|
||||
|
||||
r = hue2rgb(p, q, h + (1 / 3));
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - (1 / 3));
|
||||
}
|
||||
|
||||
return [r * 255, g * 255, b * 255];
|
||||
};
|
||||
|
||||
class EffectTransform {
|
||||
|
||||
/**
|
||||
* Transform a color given the drawables effect uniforms. Will apply
|
||||
* Ghost and Color and Brightness effects.
|
||||
* @param {Drawable} drawable The drawable to get uniforms from.
|
||||
* @param {Uint8ClampedArray} color4b The initial color.
|
||||
* @param {Uint8ClampedArary} [dst] Working space to save the color in (is returned)
|
||||
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
|
||||
* @returns {Uint8ClampedArray} dst filled with the transformed color
|
||||
*/
|
||||
static transformColor (drawable, color4b, dst, effectMask) {
|
||||
dst = dst || new Uint8ClampedArray(4);
|
||||
effectMask = effectMask || 0xffffffff;
|
||||
dst.set(color4b);
|
||||
if (dst[3] === 0) {
|
||||
return dst;
|
||||
}
|
||||
|
||||
const uniforms = drawable.getUniforms();
|
||||
const effects = drawable.getEnabledEffects() & effectMask;
|
||||
|
||||
if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
|
||||
// gl_FragColor.a *= u_ghost
|
||||
dst[3] *= uniforms.u_ghost;
|
||||
}
|
||||
|
||||
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;
|
||||
const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0;
|
||||
|
||||
if (enableColor || enableBrightness) {
|
||||
// vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
|
||||
const hsl = rgbToHsl(dst);
|
||||
|
||||
if (enableColor) {
|
||||
// this code forces grayscale values to be slightly saturated
|
||||
// so that some slight change of hue will be visible
|
||||
// const float minLightness = 0.11 / 2.0;
|
||||
const minL = 0.11 / 2.0;
|
||||
// const float minSaturation = 0.09;
|
||||
const minS = 0.09;
|
||||
// if (hsl.z < minLightness) hsl = vec3(0.0, 1.0, minLightness);
|
||||
if (hsl[2] < minL) {
|
||||
hsl[0] = 0;
|
||||
hsl[1] = 1;
|
||||
hsl[2] = minL;
|
||||
// else if (hsl.y < minSaturation) hsl = vec3(0.0, minSaturation, hsl.z);
|
||||
} else if (hsl[1] < minS) {
|
||||
hsl[0] = 0;
|
||||
hsl[1] = minS;
|
||||
}
|
||||
|
||||
// hsl.x = mod(hsl.x + u_color, 1.0);
|
||||
// if (hsl.x < 0.0) hsl.x += 1.0;
|
||||
hsl[0] = (uniforms.u_color + hsl[0] + 1) % 1;
|
||||
}
|
||||
|
||||
if (enableBrightness) {
|
||||
// hsl.z = clamp(hsl.z + u_brightness, 0.0, 1.0);
|
||||
hsl[2] = Math.min(1, hsl[2] + uniforms.u_brightness);
|
||||
}
|
||||
// gl_FragColor.rgb = convertHSL2RGB(hsl);
|
||||
dst.set(hslToRgb(hsl));
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a texture coordinate to one that would be select after applying shader effects.
|
||||
* @param {Drawable} drawable The drawable whose effects to emulate.
|
||||
|
@ -56,7 +214,7 @@ class EffectTransform {
|
|||
const offsetX = dst[0] - CENTER_X;
|
||||
const offsetY = dst[1] - CENTER_Y;
|
||||
// float offsetMagnitude = length(offset);
|
||||
const offsetMagnitude = twgl.v3.length(dst);
|
||||
const offsetMagnitude = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
|
||||
// float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0);
|
||||
const whirlFactor = Math.max(1.0 - (offsetMagnitude / RADIUS), 0.0);
|
||||
// float whirlActual = u_whirl * whirlFactor * whirlFactor;
|
||||
|
@ -69,14 +227,14 @@ class EffectTransform {
|
|||
// cosWhirl, -sinWhirl,
|
||||
// sinWhirl, cosWhirl
|
||||
// );
|
||||
const rot00 = cosWhirl;
|
||||
const rot10 = -sinWhirl;
|
||||
const rot01 = sinWhirl;
|
||||
const rot11 = cosWhirl;
|
||||
const rot1 = cosWhirl;
|
||||
const rot2 = -sinWhirl;
|
||||
const rot3 = sinWhirl;
|
||||
const rot4 = cosWhirl;
|
||||
|
||||
// texcoord0 = rotationMatrix * offset + kCenter;
|
||||
dst[0] = (rot00 * offsetX) + (rot10 * offsetY) + CENTER_X;
|
||||
dst[1] = (rot01 * offsetX) + (rot11 * offsetY) + CENTER_Y;
|
||||
dst[0] = (rot1 * offsetX) + (rot3 * offsetY) + CENTER_X;
|
||||
dst[1] = (rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y;
|
||||
}
|
||||
if ((effects & ShaderManager.EFFECT_INFO.fisheye.mask) !== 0) {
|
||||
// vec2 vec = (texcoord0 - kCenter) / kCenter;
|
||||
|
|
|
@ -16,6 +16,12 @@ const log = require('./util/log');
|
|||
|
||||
const __isTouchingDrawablesPoint = twgl.v3.create();
|
||||
const __candidatesBounds = new Rectangle();
|
||||
const __touchingColor = new Uint8ClampedArray(4);
|
||||
const __blendColor = new Uint8ClampedArray(4);
|
||||
|
||||
// More pixels than this and we give up to the GPU and take the cost of readPixels
|
||||
// Width * Height * Number of drawables at location
|
||||
const __cpuTouchingColorPixelCount = 4e4;
|
||||
|
||||
/**
|
||||
* @callback RenderWebGL#idFilterFunc
|
||||
|
@ -32,26 +38,40 @@ const __candidatesBounds = new Rectangle();
|
|||
const MAX_TOUCH_SIZE = [3, 3];
|
||||
|
||||
/**
|
||||
* "touching {color}?" or "{color} touching {color}?" tests will be true if the
|
||||
* target is touching a color whose components are each within this tolerance of
|
||||
* the corresponding component of the query color.
|
||||
* between 0 (exact matches only) and 255 (match anything).
|
||||
* @type {object.<string,int>}
|
||||
* @memberof RenderWebGL
|
||||
* Passed to the uniforms for mask in touching color
|
||||
*/
|
||||
const TOLERANCE_TOUCHING_COLOR = {
|
||||
R: 7,
|
||||
G: 7,
|
||||
B: 15,
|
||||
Mask: 2
|
||||
};
|
||||
const MASK_TOUCHING_COLOR_TOLERANCE = 2;
|
||||
|
||||
/**
|
||||
* Constant used for masking when detecting the color white
|
||||
* @type {Array<int>}
|
||||
* @memberof RenderWebGL
|
||||
* Determines if the mask color is "close enough" (only test the 6 top bits for
|
||||
* each color). These bit masks are what scratch 2 used to use, so we do the same.
|
||||
* @param {Uint8Array} a A color3b or color4b value.
|
||||
* @param {Uint8Array} b A color3b or color4b value.
|
||||
* @returns {boolean} If the colors match within the parameters.
|
||||
*/
|
||||
const COLOR_BLACK = [0, 0, 0, 1];
|
||||
const maskMatches = (a, b) => (
|
||||
// has some non-alpha component to test against
|
||||
a[3] > 0 &&
|
||||
(a[0] & 0b11111100) === (b[0] & 0b11111100) &&
|
||||
(a[1] & 0b11111100) === (b[1] & 0b11111100) &&
|
||||
(a[2] & 0b11111100) === (b[2] & 0b11111100)
|
||||
);
|
||||
|
||||
/**
|
||||
* Determines if the given color is "close enough" (only test the 5 top bits for
|
||||
* red and green, 4 bits for blue). These bit masks are what scratch 2 used to use,
|
||||
* so we do the same.
|
||||
* @param {Uint8Array} a A color3b or color4b value.
|
||||
* @param {Uint8Array} b A color3b or color4b value / or a larger array when used with offsets
|
||||
* @param {number} offset An offset into the `b` array, which lets you use a larger array to test
|
||||
* multiple values at the same time.
|
||||
* @returns {boolean} If the colors match within the parameters.
|
||||
*/
|
||||
const colorMatches = (a, b, offset) => (
|
||||
(a[0] & 0b11111000) === (b[offset + 0] & 0b11111000) &&
|
||||
(a[1] & 0b11111000) === (b[offset + 1] & 0b11111000) &&
|
||||
(a[2] & 0b11110000) === (b[offset + 2] & 0b11110000)
|
||||
);
|
||||
|
||||
/**
|
||||
* Sprite Fencing - The number of pixels a sprite is required to leave remaining
|
||||
|
@ -667,9 +687,6 @@ class RenderWebGL extends EventEmitter {
|
|||
* @returns {boolean} True iff the Drawable is touching the color.
|
||||
*/
|
||||
isTouchingColor (drawableID, color3b, mask3b) {
|
||||
const gl = this._gl;
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
const candidates = this._candidatesTouching(drawableID, this._visibleDrawList);
|
||||
if (candidates.length === 0) {
|
||||
return false;
|
||||
|
@ -677,7 +694,43 @@ class RenderWebGL extends EventEmitter {
|
|||
|
||||
const bounds = this._candidatesBounds(candidates);
|
||||
|
||||
const candidateIDs = candidates.map(({id}) => id);
|
||||
// if there are just too many pixels to CPU render efficently, we
|
||||
// need to let readPixels happen
|
||||
if (bounds.width * bounds.height * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
|
||||
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
|
||||
}
|
||||
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
const point = __isTouchingDrawablesPoint;
|
||||
const color = __touchingColor;
|
||||
const hasMask = Boolean(mask3b);
|
||||
|
||||
for (let y = bounds.bottom; y <= bounds.top; y++) {
|
||||
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
|
||||
return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom);
|
||||
}
|
||||
// Scratch Space - +y is top
|
||||
for (let x = bounds.left; x <= bounds.right; x++) {
|
||||
point[1] = y;
|
||||
point[0] = x;
|
||||
if (
|
||||
// if we use a mask, check our sample color
|
||||
(hasMask ?
|
||||
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
|
||||
drawable.isTouching(point)) &&
|
||||
// and the target color is drawn at this pixel
|
||||
colorMatches(RenderWebGL.sampleColor3b(point, candidates, color), color3b, 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) {
|
||||
const gl = this._gl;
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
// Limit size of viewport to the bounds around the target Drawable,
|
||||
// and create the projection matrix for the draw.
|
||||
|
@ -689,7 +742,7 @@ class RenderWebGL extends EventEmitter {
|
|||
// When using masking such that the background fill color will showing through, ensure we don't
|
||||
// fill using the same color that we are trying to detect!
|
||||
if (color3b[0] > 196 && color3b[1] > 196 && color3b[2] > 196) {
|
||||
fillBackgroundColor = COLOR_BLACK;
|
||||
fillBackgroundColor = [0, 0, 0, 255];
|
||||
}
|
||||
|
||||
gl.clearColor.apply(gl, fillBackgroundColor);
|
||||
|
@ -699,7 +752,7 @@ class RenderWebGL extends EventEmitter {
|
|||
if (mask3b) {
|
||||
extraUniforms = {
|
||||
u_colorMask: [mask3b[0] / 255, mask3b[1] / 255, mask3b[2] / 255],
|
||||
u_colorMaskTolerance: TOLERANCE_TOUCHING_COLOR.Mask / 255
|
||||
u_colorMaskTolerance: MASK_TOUCHING_COLOR_TOLERANCE / 255
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -730,27 +783,24 @@ class RenderWebGL extends EventEmitter {
|
|||
gl.colorMask(true, true, true, true);
|
||||
gl.disable(gl.STENCIL_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
const pixels = new Uint8Array(Math.floor(bounds.width * bounds.height * 4));
|
||||
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
_isTouchingColorGpuFin (bounds, color3b, stop) {
|
||||
const gl = this._gl;
|
||||
const pixels = new Uint8Array(Math.floor(bounds.width * (bounds.height - stop) * 4));
|
||||
gl.readPixels(0, 0, bounds.width, (bounds.height - stop), gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
|
||||
if (this._debugCanvas) {
|
||||
this._debugCanvas.width = bounds.width;
|
||||
this._debugCanvas.height = bounds.height;
|
||||
const context = this._debugCanvas.getContext('2d');
|
||||
const imageData = context.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const imageData = context.getImageData(0, 0, bounds.width, bounds.height - stop);
|
||||
imageData.data.set(pixels);
|
||||
context.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) {
|
||||
const pixelDistanceR = Math.abs(pixels[pixelBase] - color3b[0]);
|
||||
const pixelDistanceG = Math.abs(pixels[pixelBase + 1] - color3b[1]);
|
||||
const pixelDistanceB = Math.abs(pixels[pixelBase + 2] - color3b[2]);
|
||||
|
||||
if (pixelDistanceR <= TOLERANCE_TOUCHING_COLOR.R &&
|
||||
pixelDistanceG <= TOLERANCE_TOUCHING_COLOR.G &&
|
||||
pixelDistanceB <= TOLERANCE_TOUCHING_COLOR.B) {
|
||||
if (colorMatches(color3b, pixels, pixelBase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -883,7 +933,12 @@ class RenderWebGL extends EventEmitter {
|
|||
candidateIDs = (candidateIDs || this._drawList).filter(id => {
|
||||
const drawable = this._allDrawables[id];
|
||||
// default pick list ignores visible and ghosted sprites.
|
||||
return drawable.getVisible() && drawable.getUniforms().u_ghost !== 0;
|
||||
if (drawable.getVisible() && drawable.getUniforms().u_ghost !== 0) {
|
||||
drawable.updateMatrix();
|
||||
drawable.skin.updateSilhouette();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (candidateIDs.length === 0) {
|
||||
return false;
|
||||
|
@ -1085,6 +1140,8 @@ class RenderWebGL extends EventEmitter {
|
|||
/** @todo remove this once URL-based skin setting is removed. */
|
||||
if (!drawable.skin || !drawable.skin.getTexture([100, 100])) return null;
|
||||
|
||||
drawable.updateMatrix();
|
||||
drawable.skin.updateSilhouette();
|
||||
const bounds = drawable.getFastBounds();
|
||||
|
||||
// Limit queries to the stage size.
|
||||
|
@ -1110,24 +1167,31 @@ class RenderWebGL extends EventEmitter {
|
|||
*/
|
||||
_candidatesTouching (drawableID, candidateIDs) {
|
||||
const bounds = this._touchingBounds(drawableID);
|
||||
if (!bounds) {
|
||||
return [];
|
||||
const result = [];
|
||||
if (bounds === null) {
|
||||
return result;
|
||||
}
|
||||
return candidateIDs.reduce((result, id) => {
|
||||
// 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--) {
|
||||
const id = candidateIDs[index];
|
||||
if (id !== drawableID) {
|
||||
const drawable = this._allDrawables[id];
|
||||
const candidateBounds = drawable.getFastBounds();
|
||||
|
||||
if (bounds.intersects(candidateBounds)) {
|
||||
result.push({
|
||||
id,
|
||||
drawable,
|
||||
intersection: Rectangle.intersect(bounds, candidateBounds)
|
||||
});
|
||||
if (drawable.skin && drawable._visible) {
|
||||
// Update the CPU position data
|
||||
drawable.updateMatrix();
|
||||
drawable.skin.updateSilhouette();
|
||||
const candidateBounds = drawable.getFastBounds();
|
||||
if (bounds.intersects(candidateBounds)) {
|
||||
result.push({
|
||||
id,
|
||||
drawable,
|
||||
intersection: Rectangle.intersect(bounds, candidateBounds)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1556,6 +1620,43 @@ class RenderWebGL extends EventEmitter {
|
|||
// Simplify boundary points using convex hull.
|
||||
return hull(boundaryPoints, Infinity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>} drawables A list of drawables with the "top 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.
|
||||
*/
|
||||
static sampleColor3b (vec, drawables, dst) {
|
||||
dst = dst || new Uint8ClampedArray(3);
|
||||
dst.fill(0);
|
||||
let blendAlpha = 1;
|
||||
for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) {
|
||||
/*
|
||||
if (left > vec[0] || right < vec[0] ||
|
||||
bottom > vec[1] || top < vec[0]) {
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
|
||||
// if we are fully transparent, go to the next one "down"
|
||||
const sampleAlpha = __blendColor[3] / 255;
|
||||
// premultiply alpha
|
||||
dst[0] += __blendColor[0] * blendAlpha * sampleAlpha;
|
||||
dst[1] += __blendColor[1] * blendAlpha * sampleAlpha;
|
||||
dst[2] += __blendColor[2] * blendAlpha * sampleAlpha;
|
||||
blendAlpha *= (1 - sampleAlpha);
|
||||
}
|
||||
// Backdrop could be transparent, so we need to go to the "clear color" of the
|
||||
// draw scene (white) as a fallback if everything was alpha
|
||||
dst[0] += blendAlpha * 255;
|
||||
dst[1] += blendAlpha * 255;
|
||||
dst[2] += blendAlpha * 255;
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
|
||||
// :3
|
||||
|
|
|
@ -27,6 +27,37 @@ const getPoint = ({_width: width, _height: height, _data: data}, x, y) => {
|
|||
return data[(y * width) + x];
|
||||
};
|
||||
|
||||
/**
|
||||
* Memory buffers for doing 4 corner sampling for linear interpolation
|
||||
*/
|
||||
const __cornerWork = [
|
||||
new Uint8ClampedArray(4),
|
||||
new Uint8ClampedArray(4),
|
||||
new Uint8ClampedArray(4),
|
||||
new Uint8ClampedArray(4)
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the color from a given silhouette at an x/y local texture position.
|
||||
* @param {Silhouette} The silhouette to sample.
|
||||
* @param {number} x X position of texture (0-1).
|
||||
* @param {number} y Y position of texture (0-1).
|
||||
* @param {Uint8ClampedArray} dst A color 4b space.
|
||||
* @return {Uint8ClampedArray} The dst vector.
|
||||
*/
|
||||
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
|
||||
// 0 if outside bouds, otherwise read from data.
|
||||
if (x >= width || y >= height || x < 0 || y < 0) {
|
||||
return dst.fill(0);
|
||||
}
|
||||
const offset = ((y * width) + x) * 4;
|
||||
dst[0] = data[offset];
|
||||
dst[1] = data[offset + 1];
|
||||
dst[2] = data[offset + 2];
|
||||
dst[3] = data[offset + 3];
|
||||
return dst;
|
||||
};
|
||||
|
||||
class Silhouette {
|
||||
constructor () {
|
||||
/**
|
||||
|
@ -46,6 +77,9 @@ class Silhouette {
|
|||
* @type {Uint8ClampedArray}
|
||||
*/
|
||||
this._data = null;
|
||||
this._colorData = null;
|
||||
|
||||
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,12 +101,65 @@ class Silhouette {
|
|||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
|
||||
this._data = new Uint8ClampedArray(imageData.data.length / 4);
|
||||
this._colorData = imageData.data;
|
||||
// delete our custom overriden "uninitalized" color functions
|
||||
// let the prototype work for itself
|
||||
delete this.colorAtNearest;
|
||||
delete this.colorAtLinear;
|
||||
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
this._data[i / 4] = imageData.data[i + 3];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample a color from the silhouette at a given local position using
|
||||
* "nearest neighbor"
|
||||
* @param {twgl.v3} vec [x,y] texture space (0-1)
|
||||
* @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
|
||||
* @returns {Uint8ClampedArray} dst
|
||||
*/
|
||||
colorAtNearest (vec, dst) {
|
||||
return getColor4b(
|
||||
this,
|
||||
Math.floor(vec[0] * (this._width - 1)),
|
||||
Math.floor(vec[1] * (this._height - 1)),
|
||||
dst
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample a color from the silhouette at a given local position using
|
||||
* "linear interpolation"
|
||||
* @param {twgl.v3} vec [x,y] texture space (0-1)
|
||||
* @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
|
||||
* @returns {Uint8ClampedArray} dst
|
||||
*/
|
||||
colorAtLinear (vec, dst) {
|
||||
const x = vec[0] * (this._width - 1);
|
||||
const y = vec[1] * (this._height - 1);
|
||||
|
||||
const x1D = x % 1;
|
||||
const y1D = y % 1;
|
||||
const x0D = 1 - x1D;
|
||||
const y0D = 1 - y1D;
|
||||
|
||||
const xFloor = Math.floor(x);
|
||||
const yFloor = Math.floor(y);
|
||||
|
||||
const x0y0 = getColor4b(this, xFloor, yFloor, __cornerWork[0]);
|
||||
const x1y0 = getColor4b(this, xFloor + 1, yFloor, __cornerWork[1]);
|
||||
const x0y1 = getColor4b(this, xFloor, yFloor + 1, __cornerWork[2]);
|
||||
const x1y1 = getColor4b(this, xFloor + 1, yFloor + 1, __cornerWork[3]);
|
||||
|
||||
dst[0] = (x0y0[0] * x0D * y0D) + (x0y1[0] * x0D * y1D) + (x1y0[0] * x1D * y0D) + (x1y1[0] * x1D * y1D);
|
||||
dst[1] = (x0y0[1] * x0D * y0D) + (x0y1[1] * x0D * y1D) + (x1y0[1] * x1D * y0D) + (x1y1[1] * x1D * y1D);
|
||||
dst[2] = (x0y0[2] * x0D * y0D) + (x0y1[2] * x0D * y1D) + (x1y0[2] * x1D * y0D) + (x1y1[2] * x1D * y1D);
|
||||
dst[3] = (x0y0[3] * x0D * y0D) + (x0y1[3] * x0D * y1D) + (x1y0[3] * x1D * y0D) + (x1y1[3] * x1D * y1D);
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if texture coordinate touches the silhouette using nearest neighbor.
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
|
@ -82,8 +169,8 @@ class Silhouette {
|
|||
if (!this._data) return;
|
||||
return getPoint(
|
||||
this,
|
||||
Math.round(vec[0] * (this._width - 1)),
|
||||
Math.round(vec[1] * (this._height - 1))
|
||||
Math.floor(vec[0] * (this._width - 1)),
|
||||
Math.floor(vec[1] * (this._height - 1))
|
||||
) > 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,6 @@ class Skin extends EventEmitter {
|
|||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouchingNearest (vec) {
|
||||
this.updateSilhouette();
|
||||
return this._silhouette.isTouchingNearest(vec);
|
||||
}
|
||||
|
||||
|
@ -172,7 +171,6 @@ class Skin extends EventEmitter {
|
|||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouchingLinear (vec) {
|
||||
this.updateSilhouette();
|
||||
return this._silhouette.isTouchingLinear(vec);
|
||||
}
|
||||
|
||||
|
|
86
test/integration/cpu-render.html
Normal file
86
test/integration/cpu-render.html
Normal file
|
@ -0,0 +1,86 @@
|
|||
<body>
|
||||
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
|
||||
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
|
||||
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
|
||||
<script src="../../dist/web/scratch-render.js"></script>
|
||||
|
||||
<canvas id="test" width="480" height="360"></canvas>
|
||||
<canvas id="cpu" width="480" height="360"></canvas>
|
||||
<br/>
|
||||
<canvas id="merge" width="480" height="360"></canvas>
|
||||
<input type="file" id="file" name="file">
|
||||
|
||||
<script>
|
||||
// These variables are going to be available in the "window global" intentionally.
|
||||
// Allows you easy access to debug with `vm.greenFlag()` etc.
|
||||
window.devicePixelRatio = 1;
|
||||
const gpuCanvas = document.getElementById('test');
|
||||
var render = new ScratchRender(gpuCanvas);
|
||||
var vm = new VirtualMachine();
|
||||
var storage = new ScratchStorage();
|
||||
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(render);
|
||||
|
||||
document.getElementById('file').addEventListener('click', e => {
|
||||
document.body.removeChild(document.getElementById('loaded'));
|
||||
});
|
||||
|
||||
document.getElementById('file').addEventListener('change', e => {
|
||||
const reader = new FileReader();
|
||||
const thisFileInput = e.target;
|
||||
reader.onload = () => {
|
||||
vm.start();
|
||||
vm.loadProject(reader.result)
|
||||
.then(() => {
|
||||
// we add a `#loaded` div to our document, the integration suite
|
||||
// waits for that element to show up to assume the vm is ready
|
||||
// to play!
|
||||
const div = document.createElement('div');
|
||||
div.id='loaded';
|
||||
document.body.appendChild(div);
|
||||
vm.greenFlag();
|
||||
setTimeout(() => {
|
||||
renderCpu();
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(thisFileInput.files[0]);
|
||||
});
|
||||
|
||||
const cpuCanvas = document.getElementById('cpu');
|
||||
const cpuCtx = cpuCanvas.getContext('2d');
|
||||
const cpuImageData = cpuCtx.getImageData(0, 0, cpuCanvas.width, cpuCanvas.height);
|
||||
function renderCpu() {
|
||||
cpuImageData.data.fill(255);
|
||||
const drawBits = render._drawList.map(id => {
|
||||
const drawable = render._allDrawables[id];
|
||||
if (!(drawable._visible && drawable.skin)) {
|
||||
return;
|
||||
}
|
||||
drawable.updateMatrix();
|
||||
drawable.skin.updateSilhouette();
|
||||
return { id, drawable };
|
||||
}).reverse().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
|
||||
cpuImageData.data.set(color, offset);
|
||||
}
|
||||
}
|
||||
cpuCtx.putImageData(cpuImageData, 0, 0);
|
||||
|
||||
const merge = document.getElementById('merge');
|
||||
const ctx = merge.getContext('2d');
|
||||
ctx.drawImage(gpuCanvas, 0, 0);
|
||||
const gpuImageData = ctx.getImageData(0, 0, 480, 360);
|
||||
for (let x=0; x<gpuImageData.data.length; x++) {
|
||||
gpuImageData.data[x] = 255 - Math.abs(gpuImageData.data[x] - cpuImageData.data[x]);
|
||||
}
|
||||
|
||||
ctx.putImageData(gpuImageData, 0, 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
|
@ -11,6 +11,7 @@
|
|||
<script>
|
||||
// These variables are going to be available in the "window global" intentionally.
|
||||
// Allows you easy access to debug with `vm.greenFlag()` etc.
|
||||
window.devicePixelRatio = 1;
|
||||
|
||||
var canvas = document.getElementById('test');
|
||||
var render = new ScratchRender(canvas);
|
||||
|
|
|
@ -26,7 +26,13 @@ const runFile = (file, script) =>
|
|||
vm.greenFlag();
|
||||
const sendResults = [];
|
||||
|
||||
const idToTargetName = id => vm.runtime.targets.find(target => target.drawableID === id).sprite.name;
|
||||
const idToTargetName = id => {
|
||||
const target = vm.runtime.targets.find(tar => tar.drawableID === id);
|
||||
if (!target) {
|
||||
return `[Unknown drawableID: ${id}]`;
|
||||
}
|
||||
return target.sprite.name;
|
||||
};
|
||||
const sprite = vm.runtime.targets.find(target => target.sprite.name === 'Sprite1');
|
||||
|
||||
sendResults.push(['center', idToTargetName(render.pick(240, 180))]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue