diff --git a/playground/index.html b/playground/index.html
index 73407d4..19d0bac 100644
--- a/playground/index.html
+++ b/playground/index.html
@@ -5,7 +5,6 @@
Scratch WebGL rendering demo
@@ -129,7 +128,8 @@
function drawStep() {
renderer.draw();
- renderer.getBounds(drawableID2);
+ //renderer.getBounds(drawableID2);
+ renderer.isTouchingColor(drawableID2, [255,255,255]);
requestAnimationFrame(drawStep);
}
drawStep();
diff --git a/src/Drawable.js b/src/Drawable.js
index 5d07567..b32daf1 100644
--- a/src/Drawable.js
+++ b/src/Drawable.js
@@ -2,6 +2,7 @@ var twgl = require('twgl.js');
var svgToImage = require('svg-to-image');
var xhr = require('xhr');
+var Rectangle = require('./Rectangle');
var ShaderManager = require('./ShaderManager');
class Drawable {
@@ -461,7 +462,7 @@ Drawable.prototype.setConvexHullPoints = function (points) {
* This function applies the transform matrix to the known convex hull,
* and then finds the minimum box along the axes.
* Before calling this, ensure the renderer has updated convex hull points.
- * @return {Object} Bounds for a tight box around the Drawable.
+ * @return {!Rectangle} Bounds for a tight box around the Drawable.
*/
Drawable.prototype.getBounds = function () {
if (this.needsConvexHullPoints()) {
@@ -488,31 +489,48 @@ Drawable.prototype.getBounds = function () {
transformedHullPoints.push(glPoint);
}
// Search through transformed points to generate box on axes.
- let bounds = {
- left: Infinity,
- right: -Infinity,
- top: -Infinity,
- bottom: Infinity
- };
- for (let i = 0; i < transformedHullPoints.length; i++) {
- let x = transformedHullPoints[i][0];
- let y = transformedHullPoints[i][1];
- if (x < bounds.left) {
- bounds.left = x;
- }
- if (x > bounds.right) {
- bounds.right = x;
- }
- if (y > bounds.top) {
- bounds.top = y;
- }
- if (y < bounds.bottom) {
- bounds.bottom = y;
- }
- }
+ let bounds = new Rectangle();
+ bounds.initFromPointsAABB(transformedHullPoints);
return bounds;
};
+/**
+ * Get the rough axis-aligned bounding box for the Drawable.
+ * Calculated by transforming the skin's bounds.
+ * Note that this is less precise than the box returned by `getBounds`,
+ * which is tightly snapped to account for a Drawable's transparent regions.
+ * `getAABB` returns a much less accurate bounding box, but will be much
+ * faster to calculate so may be desired for quick checks/optimizations.
+ * @return {!Rectangle} Rough axis-aligned bounding box for Drawable.
+ */
+Drawable.prototype.getAABB = function () {
+ if (this._transformDirty) {
+ this._calculateTransform();
+ }
+ const tm = this._uniforms.u_modelMatrix;
+ const bounds = new Rectangle();
+ bounds.initFromPointsAABB([
+ twgl.m4.transformPoint(tm, [-0.5, -0.5, 0]),
+ twgl.m4.transformPoint(tm, [0.5, -0.5, 0]),
+ twgl.m4.transformPoint(tm, [-0.5, 0.5, 0]),
+ twgl.m4.transformPoint(tm, [0.5, 0.5, 0])
+ ]);
+ return bounds;
+};
+
+/**
+ * Return the best Drawable bounds possible without performing graphics queries.
+ * I.e., returns the tight bounding box when the convex hull points are already
+ * known, but otherwise return the rough AABB of the Drawable.
+ * @return {!Rectangle} Bounds for the Drawable.
+ */
+Drawable.prototype.getFastBounds = function () {
+ if (!this.needsConvexHullPoints()) {
+ return this.getBounds();
+ }
+ return this.getAABB();
+};
+
/**
* 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 Drawable.NONE.
diff --git a/src/Rectangle.js b/src/Rectangle.js
new file mode 100644
index 0000000..9abcc6b
--- /dev/null
+++ b/src/Rectangle.js
@@ -0,0 +1,138 @@
+/**
+ * @fileoverview
+ * A utility for creating and comparing axis-aligned rectangles.
+ */
+
+class Rectangle {
+ /**
+ * Rectangles are always initialized to the "largest possible rectangle";
+ * use one of the init* methods below to set up a particular rectangle.
+ * @constructor
+ */
+ constructor () {
+ this.left = -Infinity;
+ this.right = Infinity;
+ this.bottom = -Infinity;
+ this.top = Infinity;
+ }
+
+ /**
+ * Initialize a Rectangle from given Scratch-coordinate bounds.
+ * @param {number} left Left bound of the rectangle.
+ * @param {number} right Right bound of the rectangle.
+ * @param {number} bottom Bottom bound of the rectangle.
+ * @param {number} top Top bound of the rectangle.
+ */
+ initFromBounds (left, right, bottom, top) {
+ this.left = left;
+ this.right = right;
+ this.bottom = bottom;
+ 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++) {
+ let x = points[i][0];
+ let 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;
+ }
+ }
+ }
+
+ /**
+ * Determine if this Rectangle intersects some other.
+ * Note that this is a comparison assuming the Rectangle was
+ * initialized with Scratch-space bounds or points.
+ * @param {!Rectangle} other Rectangle to check if intersecting.
+ * @return {Boolean} True if this Rectangle intersects other.
+ */
+ intersects (other) {
+ return (
+ this.left <= other.right &&
+ other.left <= this.right &&
+ this.top >= other.bottom &&
+ other.top >= this.bottom
+ );
+ }
+
+ /**
+ * Determine if this Rectangle fully contains some other.
+ * Note that this is a comparison assuming the Rectangle was
+ * initialized with Scratch-space bounds or points.
+ * @param {!Rectangle} other Rectangle to check if fully contained.
+ * @return {Boolean} True if this Rectangle fully contains other.
+ */
+ contains (other) {
+ return (
+ other.left > this.left &&
+ other.right < this.right &&
+ other.top < this.top &&
+ other.bottom > this.bottom
+ );
+ }
+
+ /**
+ * Clamp a Rectangle to bounds.
+ * @param {number} left Left clamp.
+ * @param {number} right Right clamp.
+ * @param {number} bottom Bottom clamp.
+ * @param {number} top Top clamp.
+ */
+ clamp (left, right, bottom, top) {
+ this.left = Math.max(this.left, left);
+ this.right = Math.min(this.right, right);
+ this.bottom = Math.max(this.bottom, bottom);
+ this.top = Math.min(this.top, top);
+ // Ensure rectangle coordinates in order.
+ this.left = Math.min(this.left, this.right);
+ this.right = Math.max(this.right, this.left);
+ this.bottom = Math.min(this.bottom, this.top);
+ this.top = Math.max(this.top, this.bottom);
+ }
+
+ /**
+ * Push out the Rectangle to integer bounds.
+ */
+ snapToInt() {
+ this.left = Math.floor(this.left);
+ this.right = Math.ceil(this.right);
+ this.bottom = Math.floor(this.bottom);
+ this.top = Math.ceil(this.top);
+ }
+
+ /**
+ * Width of the Rectangle.
+ * @return {number} Width of rectangle.
+ */
+ get width () {
+ return Math.abs(this.left - this.right);
+ }
+
+ /**
+ * Height of the Rectangle.
+ * @return {number} Height of rectangle.
+ */
+ get height () {
+ return Math.abs(this.top - this.bottom);
+ }
+}
+
+module.exports = Rectangle;
diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js
index 865ea89..ba70e76 100644
--- a/src/RenderWebGL.js
+++ b/src/RenderWebGL.js
@@ -243,16 +243,25 @@ RenderWebGL.prototype.getSkinSize = function (drawableID) {
* @returns {Boolean} True iff the Drawable is touching the color.
*/
RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
-
- var gl = this._gl;
-
+ const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
- // TODO: restrict to only the area overlapped by the target Drawable
- // - limit size of viewport to the AABB around the target Drawable
- // - draw only the Drawables which could overlap the target Drawable
- // - read only the pixels in the AABB around the target Drawable
- gl.viewport(0, 0, this._nativeSize[0], this._nativeSize[1]);
+ let bounds = this._touchingBounds(drawableID);
+ if (!bounds) {
+ return;
+ }
+ let candidateIDs = this._filterCandidatesTouching(
+ drawableID, this._drawables, bounds);
+ if (!candidateIDs) {
+ return;
+ }
+
+
+ // 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.bottom, bounds.top, -1, 1);
gl.clearColor.apply(gl, this._backgroundColor);
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
@@ -275,7 +284,7 @@ RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
mask3b ?
ShaderManager.DRAW_MODE.colorMask :
ShaderManager.DRAW_MODE.silhouette,
- this._projection,
+ projection,
undefined,
extraUniforms);
@@ -283,10 +292,8 @@ RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
- // TODO: only draw items which could possibly overlap target Drawable
- // It might work to use the filter function for that
this._drawThese(
- this._drawables, ShaderManager.DRAW_MODE.default, this._projection,
+ candidateIDs, ShaderManager.DRAW_MODE.default, projection,
function (testID) {
return testID != drawableID;
});
@@ -296,17 +303,17 @@ RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
gl.disable(gl.STENCIL_TEST);
}
- var pixels = new Buffer(this._nativeSize[0] * this._nativeSize[1] * 4);
+ var pixels = new Buffer(bounds.width * bounds.height * 4);
gl.readPixels(
- 0, 0, this._nativeSize[0], this._nativeSize[1],
+ 0, 0, bounds.width, bounds.height,
gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
- this._debugCanvas.width = this._nativeSize[0];
- this._debugCanvas.height = this._nativeSize[1];
+ this._debugCanvas.width = bounds.width;
+ this._debugCanvas.height = bounds.height;
var context = this._debugCanvas.getContext('2d');
var imageData = context.getImageData(
- 0, 0, this._nativeSize[0], this._nativeSize[1]);
+ 0, 0, bounds.width, bounds.height);
for (var i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
@@ -341,11 +348,21 @@ RenderWebGL.prototype.isTouchingDrawables = function(drawableID, candidateIDs) {
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
- // TODO: restrict to only the area overlapped by the target Drawable
- // - limit size of viewport to the AABB around the target Drawable
- // - draw only the Drawables which could overlap the target Drawable
- // - read only the pixels in the AABB around the target Drawable
- gl.viewport(0, 0, this._nativeSize[0], this._nativeSize[1]);
+ let bounds = this._touchingBounds(drawableID);
+ if (!bounds) {
+ return;
+ }
+ candidateIDs = this._filterCandidatesTouching(
+ drawableID, candidateIDs, bounds);
+ if (!candidateIDs) {
+ return;
+ }
+
+ // 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.bottom, bounds.top, -1, 1);
const noneColor = Drawable.color4fFromID(Drawable.NONE);
gl.clearColor.apply(gl, noneColor);
@@ -357,34 +374,35 @@ RenderWebGL.prototype.isTouchingDrawables = function(drawableID, candidateIDs) {
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
this._drawThese(
- [drawableID], ShaderManager.DRAW_MODE.silhouette, this._projection
+ [drawableID], ShaderManager.DRAW_MODE.silhouette, projection
);
gl.stencilFunc(gl.EQUAL, 1, 1);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
- // TODO: only draw items which could possibly overlap target Drawable
- // It might work to use the filter function for that
this._drawThese(
- candidateIDs, ShaderManager.DRAW_MODE.silhouette, this._projection
+ candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection,
+ function (testID) {
+ return testID != drawableID;
+ }
);
} finally {
gl.colorMask(true, true, true, true);
gl.disable(gl.STENCIL_TEST);
}
- let pixels = new Buffer(this._nativeSize[0] * this._nativeSize[1] * 4);
+ let pixels = new Buffer(bounds.width * bounds.height * 4);
gl.readPixels(
- 0, 0, this._nativeSize[0], this._nativeSize[1],
+ 0, 0, bounds.width, bounds.height,
gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
- this._debugCanvas.width = this._nativeSize[0];
- this._debugCanvas.height = this._nativeSize[1];
+ this._debugCanvas.width = bounds.width;
+ this._debugCanvas.height = bounds.height;
const context = this._debugCanvas.getContext('2d');
let imageData = context.getImageData(
- 0, 0, this._nativeSize[0], this._nativeSize[1]);
+ 0, 0, bounds.width, bounds.height);
for (let i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
@@ -500,6 +518,56 @@ RenderWebGL.prototype.pick = function (
return hit | 0;
};
+/**
+ * Get the candidate bounding box for a touching query.
+ * @param {int} drawableID ID for drawable of query.
+ * @return {?Rectangle} Rectangle bounds for touching query, or null.
+ */
+RenderWebGL.prototype._touchingBounds = function (drawableID) {
+ const drawable = Drawable.getDrawableByID(drawableID);
+ const bounds = drawable.getFastBounds();
+
+ // Limit queries to the stage size.
+ bounds.clamp(this._xLeft, this._xRight, this._yBottom, this._yTop);
+
+ // Use integer coordinates for queries - weird things happen
+ // when you provide float width/heights to gl.viewport and projection.
+ bounds.snapToInt();
+
+ if (bounds.width == 0 || bounds.height == 0) {
+ // No space to query.
+ return null;
+ }
+ return 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.
+ * @param {Rectangle} Bounds to limit candidates to.
+ * @return {?Array.} Filtered candidateIDs, or null if none.
+ */
+RenderWebGL.prototype._filterCandidatesTouching = function (
+ drawableID, candidateIDs, bounds) {
+ // Filter candidates by rough bounding box intersection.
+ // Do this before _drawThese, so we can prevent any GL operations
+ // and readback by returning early.
+ candidateIDs = candidateIDs.filter(function (testID) {
+ if (testID == drawableID) return false;
+ // Only draw items which could possibly overlap target Drawable.
+ let candidate = Drawable.getDrawableByID(testID);
+ let candidateBounds = candidate.getFastBounds();
+ return bounds.intersects(candidateBounds);
+ });
+ if (candidateIDs.length == 0) {
+ // No possible intersections based on bounding boxes.
+ return null;
+ }
+ return candidateIDs;
+};
+
/**
* Update the position, direction, scale, or effect properties of this Drawable.
* @param {int} drawableID The ID of the Drawable to update.