Skip to content

Commit b98b813

Browse files
committed
Fix silhouette sampling math to match GPU
1 parent 5b71f94 commit b98b813

File tree

3 files changed

+57
-34
lines changed

3 files changed

+57
-34
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
@@ -1804,12 +1804,12 @@ class RenderWebGL extends EventEmitter {
18041804
let rr = -1;
18051805
let Q;
18061806
for (let y = 0; y < height; y++) {
1807-
_pixelPos[1] = y / height;
1807+
_pixelPos[1] = (y + 0.5) / height;
18081808
// Scan from left to right, looking for a touchable spot in the
18091809
// skin.
18101810
let x = 0;
18111811
for (; x < width; x++) {
1812-
_pixelPos[0] = x / width;
1812+
_pixelPos[0] = (x + 0.5) / width;
18131813
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
18141814
if (drawable.skin.isTouchingLinear(_effectPos)) {
18151815
Q = [x, y];
@@ -1839,7 +1839,7 @@ class RenderWebGL extends EventEmitter {
18391839
// Scan from right to left, looking for a touchable spot in the
18401840
// skin.
18411841
for (x = width - 1; x >= 0; x--) {
1842-
_pixelPos[0] = x / width;
1842+
_pixelPos[0] = (x + 0.5) / width;
18431843
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
18441844
if (drawable.skin.isTouchingLinear(_effectPos)) {
18451845
Q = [x, y];

src/Silhouette.js

Lines changed: 44 additions & 20 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 bouds, 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

@@ -40,16 +45,16 @@ const __cornerWork = [
4045
/**
4146
* Get the color from a given silhouette at an x/y local texture position.
4247
* @param {Silhouette} The silhouette to sample.
43-
* @param {number} x X position of texture (0-1).
44-
* @param {number} y Y position of texture (0-1).
48+
* @param {number} x X position of texture [0, width).
49+
* @param {number} y Y position of texture [0, height).
4550
* @param {Uint8ClampedArray} dst A color 4b space.
4651
* @return {Uint8ClampedArray} The dst vector.
4752
*/
4853
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
49-
// 0 if outside bouds, otherwise read from data.
50-
if (x >= width || y >= height || x < 0 || y < 0) {
51-
return dst.fill(0);
52-
}
54+
// Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
55+
x = intMax(0, intMin(x, width - 1));
56+
y = intMax(0, intMin(y, height - 1));
57+
5358
const offset = ((y * width) + x) * 4;
5459
dst[0] = data[offset];
5560
dst[1] = data[offset + 1];
@@ -126,8 +131,8 @@ class Silhouette {
126131
colorAtNearest (vec, dst) {
127132
return getColor4b(
128133
this,
129-
Math.floor(vec[0] * (this._width - 1)),
130-
Math.floor(vec[1] * (this._height - 1)),
134+
Math.floor(vec[0] * this._width),
135+
Math.floor(vec[1] * this._height),
131136
dst
132137
);
133138
}
@@ -140,8 +145,13 @@ class Silhouette {
140145
* @returns {Uint8ClampedArray} dst
141146
*/
142147
colorAtLinear (vec, dst) {
143-
const x = vec[0] * (this._width - 1);
144-
const y = vec[1] * (this._height - 1);
148+
// In texture space, pixel centers are at integer coords. Here, the *corners* are at integers.
149+
// We cannot skip the "add 0.5 in Drawable.getLocalPosition -> subtract 0.5 here" roundtrip
150+
// because the two spaces are different--we add 0.5 in Drawable.getLocalPosition in "Scratch space"
151+
// (-240,240 & -180,180), but subtract 0.5 in silhouette space (0, width or height).
152+
// See https://web.archive.org/web/20190125211252/http://hacksoflife.blogspot.com/2009/12/texture-coordinate-system-for-opengl.html
153+
const x = (vec[0] * (this._width)) - 0.5;
154+
const y = (vec[1] * (this._height)) - 0.5;
145155

146156
const x1D = x % 1;
147157
const y1D = y % 1;
@@ -171,10 +181,17 @@ class Silhouette {
171181
*/
172182
isTouchingNearest (vec) {
173183
if (!this._colorData) return;
184+
185+
// Never touching if the coord falls outside the texture space.
186+
if (vec[0] < 0 || vec[0] > 1 ||
187+
vec[1] < 0 || vec[1] > 1) {
188+
return false;
189+
}
190+
174191
return getPoint(
175192
this,
176-
Math.floor(vec[0] * (this._width - 1)),
177-
Math.floor(vec[1] * (this._height - 1))
193+
Math.floor(vec[0] * this._width),
194+
Math.floor(vec[1] * this._height)
178195
) > 0;
179196
}
180197

@@ -186,8 +203,15 @@ class Silhouette {
186203
*/
187204
isTouchingLinear (vec) {
188205
if (!this._colorData) return;
189-
const x = Math.floor(vec[0] * (this._width - 1));
190-
const y = Math.floor(vec[1] * (this._height - 1));
206+
207+
// Never touching if the coord falls outside the texture space.
208+
if (vec[0] < 0 || vec[0] > 1 ||
209+
vec[1] < 0 || vec[1] > 1) {
210+
return;
211+
}
212+
213+
const x = Math.floor((vec[0] * this._width) - 0.5);
214+
const y = Math.floor((vec[1] * this._height) - 0.5);
191215
return getPoint(this, x, y) > 0 ||
192216
getPoint(this, x + 1, y) > 0 ||
193217
getPoint(this, x, y + 1) > 0 ||

0 commit comments

Comments
 (0)