Skip to content

Commit 2faccaa

Browse files
committed
Fix silhouette sampling math to match GPU
1 parent f63fc6a commit 2faccaa

File tree

3 files changed

+63
-40
lines changed

3 files changed

+63
-40
lines changed

src/Drawable.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ const __isTouchingPosition = twgl.v3.create();
2424
* @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix
2525
*/
2626
const getLocalPosition = (drawable, vec) => {
27-
// Transfrom from world coordinates to Drawable coordinates.
27+
// Transform from world coordinates to Drawable coordinates.
2828
const localPosition = __isTouchingPosition;
29-
const v0 = vec[0];
30-
const v1 = vec[1];
29+
// World coordinates/screen-space coordinates refer to pixels by integer coordinates.
30+
// The GL rasterizer considers a pixel to be an area sample.
31+
// Without multisampling, it samples once from the pixel center,
32+
// which is offset by (0.5, 0.5) from the pixel's integer coordinate.
33+
// If you think of it as a pixel grid, the coordinates we're given are grid lines, but we want grid boxes.
34+
// That's why we offset by 0.5 (-0.5 in the X direction because it's flipped).
35+
const v0 = vec[0] - 0.5;
36+
const v1 = vec[1] + 0.5;
3137
const m = drawable._inverseMatrix;
3238
// var v2 = v[2];
3339
const d = (v0 * m[3]) + (v1 * m[7]) + m[15];
@@ -36,14 +42,7 @@ const getLocalPosition = (drawable, vec) => {
3642
// localPosition matches that transformation.
3743
localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d);
3844
localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5;
39-
// Apply texture effect transform if the localPosition is within the drawable's space,
40-
// and any effects are currently active.
41-
if (drawable.enabledEffects !== 0 &&
42-
(localPosition[0] >= 0 && localPosition[0] < 1) &&
43-
(localPosition[1] >= 0 && localPosition[1] < 1)) {
44-
45-
EffectTransform.transformPoint(drawable, localPosition, localPosition);
46-
}
45+
if (drawable.enabledEffects !== 0) EffectTransform.transformPoint(drawable, localPosition, localPosition);
4746
return localPosition;
4847
};
4948

src/RenderWebGL.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,12 +1806,12 @@ class RenderWebGL extends EventEmitter {
18061806
let rr = -1;
18071807
let Q;
18081808
for (let y = 0; y < height; y++) {
1809-
_pixelPos[1] = y / height;
1809+
_pixelPos[1] = (y + 0.5) / height;
18101810
// Scan from left to right, looking for a touchable spot in the
18111811
// skin.
18121812
let x = 0;
18131813
for (; x < width; x++) {
1814-
_pixelPos[0] = x / width;
1814+
_pixelPos[0] = (x + 0.5) / width;
18151815
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
18161816
if (drawable.skin.isTouchingLinear(_effectPos)) {
18171817
Q = [x, y];
@@ -1841,7 +1841,7 @@ class RenderWebGL extends EventEmitter {
18411841
// Scan from right to left, looking for a touchable spot in the
18421842
// skin.
18431843
for (x = width - 1; x >= 0; x--) {
1844-
_pixelPos[0] = x / width;
1844+
_pixelPos[0] = (x + 0.5) / width;
18451845
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
18461846
if (drawable.skin.isTouchingLinear(_effectPos)) {
18471847
Q = [x, y];

src/Silhouette.js

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@
1010
*/
1111
let __SilhouetteUpdateCanvas;
1212

13+
// Optimized Math.min and Math.max for integers;
14+
// taken from https://web.archive.org/web/20190716181049/http://guihaire.com/code/?p=549
15+
const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31));
16+
const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31));
17+
1318
/**
14-
* Internal helper function (in hopes that compiler can inline). Get a pixel
15-
* from silhouette data, or 0 if outside it's bounds.
19+
* Internal helper function (in hopes that compiler can inline). Get a pixel's alpha
20+
* from silhouette data, matching texture sampling rules.
1621
* @private
1722
* @param {Silhouette} silhouette - has data width and height
1823
* @param {number} x - x
1924
* @param {number} y - y
2025
* @return {number} Alpha value for x/y position
2126
*/
2227
const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
23-
// 0 if outside bounds, otherwise read from data.
24-
if (x >= width || y >= height || x < 0 || y < 0) {
25-
return 0;
26-
}
28+
// Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
29+
x = intMax(0, intMin(x, width - 1));
30+
y = intMax(0, intMin(y, height - 1));
31+
2732
return data[(((y * width) + x) * 4) + 3];
2833
};
2934

@@ -41,16 +46,16 @@ const __cornerWork = [
4146
* Get the color from a given silhouette at an x/y local texture position.
4247
* Multiply color values by alpha for proper blending.
4348
* @param {Silhouette} The silhouette to sample.
44-
* @param {number} x X position of texture (0-1).
45-
* @param {number} y Y position of texture (0-1).
49+
* @param {number} x X position of texture [0, width).
50+
* @param {number} y Y position of texture [0, height).
4651
* @param {Uint8ClampedArray} dst A color 4b space.
4752
* @return {Uint8ClampedArray} The dst vector.
4853
*/
4954
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
50-
// 0 if outside bounds, otherwise read from data.
51-
if (x >= width || y >= height || x < 0 || y < 0) {
52-
return dst.fill(0);
53-
}
55+
// Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
56+
x = intMax(0, intMin(x, width - 1));
57+
y = intMax(0, intMin(y, height - 1));
58+
5459
const offset = ((y * width) + x) * 4;
5560
// premultiply alpha
5661
const alpha = data[offset + 3] / 255;
@@ -65,16 +70,16 @@ const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, ds
6570
* Get the color from a given silhouette at an x/y local texture position.
6671
* Do not multiply color values by alpha, as it has already been done.
6772
* @param {Silhouette} The silhouette to sample.
68-
* @param {number} x X position of texture (0-1).
69-
* @param {number} y Y position of texture (0-1).
73+
* @param {number} x X position of texture [0, width).
74+
* @param {number} y Y position of texture [0, height).
7075
* @param {Uint8ClampedArray} dst A color 4b space.
7176
* @return {Uint8ClampedArray} The dst vector.
7277
*/
7378
const getPremultipliedColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
74-
// 0 if outside bounds, otherwise read from data.
75-
if (x >= width || y >= height || x < 0 || y < 0) {
76-
return dst.fill(0);
77-
}
79+
// Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
80+
x = intMax(0, intMin(x, width - 1));
81+
y = intMax(0, intMin(y, height - 1));
82+
7883
const offset = ((y * width) + x) * 4;
7984
dst[0] = data[offset];
8085
dst[1] = data[offset + 1];
@@ -163,8 +168,8 @@ class Silhouette {
163168
colorAtNearest (vec, dst) {
164169
return this._getColor(
165170
this,
166-
Math.floor(vec[0] * (this._width - 1)),
167-
Math.floor(vec[1] * (this._height - 1)),
171+
Math.floor(vec[0] * this._width),
172+
Math.floor(vec[1] * this._height),
168173
dst
169174
);
170175
}
@@ -177,8 +182,13 @@ class Silhouette {
177182
* @returns {Uint8ClampedArray} dst
178183
*/
179184
colorAtLinear (vec, dst) {
180-
const x = vec[0] * (this._width - 1);
181-
const y = vec[1] * (this._height - 1);
185+
// In texture space, pixel centers are at integer coords. Here, the *corners* are at integers.
186+
// We cannot skip the "add 0.5 in Drawable.getLocalPosition -> subtract 0.5 here" roundtrip
187+
// because the two spaces are different--we add 0.5 in Drawable.getLocalPosition in "Scratch space"
188+
// (-240,240 & -180,180), but subtract 0.5 in silhouette space (0, width or height).
189+
// See https://web.archive.org/web/20190125211252/http://hacksoflife.blogspot.com/2009/12/texture-coordinate-system-for-opengl.html
190+
const x = (vec[0] * (this._width)) - 0.5;
191+
const y = (vec[1] * (this._height)) - 0.5;
182192

183193
const x1D = x % 1;
184194
const y1D = y % 1;
@@ -208,10 +218,17 @@ class Silhouette {
208218
*/
209219
isTouchingNearest (vec) {
210220
if (!this._colorData) return;
221+
222+
// Never touching if the coord falls outside the texture space.
223+
if (vec[0] < 0 || vec[0] > 1 ||
224+
vec[1] < 0 || vec[1] > 1) {
225+
return false;
226+
}
227+
211228
return getPoint(
212229
this,
213-
Math.floor(vec[0] * (this._width - 1)),
214-
Math.floor(vec[1] * (this._height - 1))
230+
Math.floor(vec[0] * this._width),
231+
Math.floor(vec[1] * this._height)
215232
) > 0;
216233
}
217234

@@ -223,8 +240,15 @@ class Silhouette {
223240
*/
224241
isTouchingLinear (vec) {
225242
if (!this._colorData) return;
226-
const x = Math.floor(vec[0] * (this._width - 1));
227-
const y = Math.floor(vec[1] * (this._height - 1));
243+
244+
// Never touching if the coord falls outside the texture space.
245+
if (vec[0] < 0 || vec[0] > 1 ||
246+
vec[1] < 0 || vec[1] > 1) {
247+
return false;
248+
}
249+
250+
const x = Math.floor((vec[0] * this._width) - 0.5);
251+
const y = Math.floor((vec[1] * this._height) - 0.5);
228252
return getPoint(this, x, y) > 0 ||
229253
getPoint(this, x + 1, y) > 0 ||
230254
getPoint(this, x, y + 1) > 0 ||

0 commit comments

Comments
 (0)