Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimized WebAssembly "touching" routines #3

Draft
wants to merge 14 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist/*
node_modules/*
playground/*
tap-snapshots/*
swrender/build/*
18 changes: 11 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@
"main": "./dist/node/scratch-render.js",
"browser": "./src/index.js",
"scripts": {
"build": "webpack --progress --colors",
"build": "npm run build:swrender && webpack --progress --colors",
"build:swrender": "cargo build --release --target wasm32-unknown-unknown --manifest-path=\"swrender/Cargo.toml\" && wasm-bindgen --target bundler swrender/target/wasm32-unknown-unknown/release/swrender.wasm --out-dir swrender/build",
"docs": "jsdoc -c .jsdoc.json",
"lint": "eslint .",
"prepublish": "npm run build",
"prepublish-watch": "npm run watch",
"start": "webpack-dev-server",
"start": "npm run build:swrender && webpack-dev-server",
"tap": "tap test/unit test/integration",
"test": "npm run lint && npm run docs && npm run build && npm run tap",
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"",
"watch": "webpack --progress --colors --watch --watch-poll"
},
"devDependencies": {
"babel-core": "^6.23.1",
"@babel/core": "^7.8.7",
"@babel/plugin-syntax-import-meta": "^7.8.3",
"@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.8.7",
"@wasm-tool/wasm-pack-plugin": "^1.2.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^7.1.4",
"babel-polyfill": "^6.22.0",
"babel-preset-env": "^1.6.1",
"babel-loader": "^8.0.6",
"babel-plugin-bundled-import-meta": "^0.3.2",
"copy-webpack-plugin": "^4.5.1",
"docdash": "^0.4.0",
"eslint": "^7.13.0",
Expand All @@ -41,13 +45,13 @@
"tap": "^11.0.0",
"travis-after-all": "^1.4.4",
"uglifyjs-webpack-plugin": "^1.2.5",
"webassembly-loader": "^1.1.0",
"webpack": "^4.8.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.4"
},
"dependencies": {
"grapheme-breaker": "0.3.2",
"hull.js": "0.2.10",
"ify-loader": "1.0.4",
"linebreak": "0.3.0",
"minilog": "3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/BitmapSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class BitmapSkin extends Skin {
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
*/
constructor (id, renderer) {
super(id);
super(id, renderer);

/** @type {!int} */
this._costumeResolution = 1;
Expand Down
185 changes: 53 additions & 132 deletions src/Drawable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,21 @@ const Rectangle = require('./Rectangle');
const RenderConstants = require('./RenderConstants');
const ShaderManager = require('./ShaderManager');
const Skin = require('./Skin');
const EffectTransform = require('./EffectTransform');
const log = require('./util/log');

/**
* 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();
const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6;

/**
* 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;
// 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);
}
return localPosition;
};

class Drawable {
/**
* An object which can be drawn by the renderer.
* @todo double-buffer all rendering state (position, skin, effects, etc.)
* @param {!int} id - This Drawable's unique ID.
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
* @constructor
*/
constructor (id) {
constructor (id, renderer) {
/** @type {!int} */
this._id = id;

this._renderer = renderer;

/**
* The uniforms to be used by the vertex and pixel shaders.
* Some of these are used by other parts of the renderer as well.
Expand Down Expand Up @@ -113,6 +67,8 @@ class Drawable {
* @type {int} */
this.enabledEffects = 0;

this._effectsDirty = true;

/** @todo move convex hull functionality, maybe bounds functionality overall, to Skin classes */
this._convexHullPoints = null;
this._convexHullDirty = true;
Expand All @@ -124,8 +80,7 @@ class Drawable {
this._transformedHullDirty = true;

this._skinWasAltered = this._skinWasAltered.bind(this);

this.isTouching = this._isTouchingNever;
this._silhouetteWasUpdated = this._silhouetteWasUpdated.bind(this);
}

/**
Expand All @@ -134,6 +89,7 @@ class Drawable {
dispose () {
// Use the setter: disconnect events
this.skin = null;
this._renderer.softwareRenderer.remove_drawable(this.id);
}

/**
Expand Down Expand Up @@ -167,10 +123,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();
}
Expand Down Expand Up @@ -251,6 +209,10 @@ class Drawable {
}
}

setEffectsDirty () {
this._effectsDirty = true;
}

/**
* Update an effect. Marks the convex hull as dirty if the effect changes shape.
* @param {string} effectName The name of the effect.
Expand All @@ -268,6 +230,7 @@ class Drawable {
if (effectInfo.shapeChanges) {
this.setConvexHullDirty();
}
this.setEffectsDirty();
}

/**
Expand Down Expand Up @@ -474,36 +437,6 @@ class Drawable {
this._transformedHullDirty = true;
}

/**
* @function
* @name isTouching
* Check if the world position touches the skin.
* The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date.
* @see updateCPURenderAttributes
* @param {twgl.v3} vec World coordinate vector.
* @return {boolean} True if the world position touches the skin.
*/

// `updateCPURenderAttributes` sets this Drawable instance's `isTouching` method
// to one of the following three functions:
// If this drawable has no skin, set it to `_isTouchingNever`.
// Otherwise, if this drawable uses nearest-neighbor scaling at its current scale, set it to `_isTouchingNearest`.
// Otherwise, set it to `_isTouchingLinear`.
// This allows several checks to be moved from the `isTouching` function to `updateCPURenderAttributes`.

// eslint-disable-next-line no-unused-vars
_isTouchingNever (vec) {
return false;
}

_isTouchingNearest (vec) {
return this.skin.isTouchingNearest(getLocalPosition(this, vec));
}

_isTouchingLinear (vec) {
return this.skin.isTouchingLinear(getLocalPosition(this, vec));
}

/**
* Get the precise bounds for a Drawable.
* This function applies the transform matrix to the known convex hull,
Expand All @@ -523,6 +456,14 @@ class Drawable {
// Search through transformed points to generate box on axes.
result = result || new Rectangle();
result.initFromPointsAABB(transformedHullPoints);

// Expand bounds by half a pixel per side because convex hull points lie in the centers of pixels
const silhouetteHalfPixel = (this.scale[0] / 200) * (this.skin.size[0] / this.skin.silhouetteSize[0]);
result.left -= silhouetteHalfPixel;
result.right += silhouetteHalfPixel;
result.bottom -= silhouetteHalfPixel;
result.top += silhouetteHalfPixel;

return result;
}

Expand Down Expand Up @@ -597,16 +538,12 @@ 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);
}

Expand Down Expand Up @@ -638,23 +575,30 @@ class Drawable {

/**
* Update everything necessary to render this drawable on the CPU.
* @param {int} [effectMask] An optional bitmask of effects that will be applied to this drawable on the CPU.
*/
updateCPURenderAttributes () {
updateCPURenderAttributes (effectMask) {
this.updateMatrix();
// CPU rendering always occurs at the "native" size, so no need to scale up this._scale
if (this.skin) {
this.skin.updateSilhouette(this._scale);

if (this.skin.useNearest(this._scale, this)) {
this.isTouching = this._isTouchingNearest;
} else {
this.isTouching = this._isTouchingLinear;
}
} else {
log.warn(`Could not find skin for drawable with id: ${this._id}`);

this.isTouching = this._isTouchingNever;
if (this.skin) this.skin.updateSilhouette(this._scale);

let effects = null;
if (this._effectsDirty) {
effects = this._uniforms;
this._effectsDirty = false;
}

let {enabledEffects} = this;
if (effectMask) enabledEffects &= effectMask;

this._renderer.softwareRenderer.set_drawable(
this.id,
this._uniforms.u_modelMatrix,
this.skin.id,
effects,
enabledEffects,
this.skin.useNearest(this._scale, this)
);
}

/**
Expand All @@ -668,6 +612,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.
Expand Down Expand Up @@ -698,37 +650,6 @@ class Drawable {
id |= (b & 255) << 16;
return id + RenderConstants.ID_NONE;
}

/**
* Sample a color from a drawable's texture.
* The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date.
* @see updateCPURenderAttributes
* @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.
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} The dst object filled with the color4b
*/
static sampleColor4b (vec, drawable, dst, effectMask) {
const localPosition = getLocalPosition(drawable, vec);
if (localPosition[0] < 0 || localPosition[1] < 0 ||
localPosition[0] > 1 || localPosition[1] > 1) {
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
dst[3] = 0;
return dst;
}

const textColor =
// commenting out to only use nearest for now
// drawable.skin.useNearest(drawable._scale, drawable) ?
drawable.skin._silhouette.colorAtNearest(localPosition, dst);
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);

if (drawable.enabledEffects === 0) return textColor;
return EffectTransform.transformColor(drawable, textColor, effectMask);
}
}

module.exports = Drawable;
Loading