@ikijs/editor 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.d.mts +27 -7
- package/dist/index.d.ts +27 -7
- package/dist/index.js +1190 -650
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1190 -650
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1664,6 +1664,21 @@ function createDefaultWarpDeformer(model) {
|
|
|
1664
1664
|
|
|
1665
1665
|
// src/auto-rig.ts
|
|
1666
1666
|
var import_format3 = require("@ikijs/format");
|
|
1667
|
+
var TURN_GROUP_IDS = [
|
|
1668
|
+
"faceWarp",
|
|
1669
|
+
"eyeWarp_L",
|
|
1670
|
+
"eyeWarp_R",
|
|
1671
|
+
"mouthWarp",
|
|
1672
|
+
"noseWarp",
|
|
1673
|
+
"browWarp_L",
|
|
1674
|
+
"browWarp_R",
|
|
1675
|
+
"blushWarp_L",
|
|
1676
|
+
"blushWarp_R"
|
|
1677
|
+
];
|
|
1678
|
+
var TURN_GROUP_ID_SET = new Set(TURN_GROUP_IDS);
|
|
1679
|
+
function isTurnGroup(id) {
|
|
1680
|
+
return TURN_GROUP_ID_SET.has(id);
|
|
1681
|
+
}
|
|
1667
1682
|
var ROLE_TABLE = {
|
|
1668
1683
|
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1669
1684
|
// whose grid it would stretch across the shoulders) as a MESH, so its ends
|
|
@@ -1677,34 +1692,43 @@ var ROLE_TABLE = {
|
|
|
1677
1692
|
// reading as a floating head. Drawn over the back hair so long hair falls
|
|
1678
1693
|
// behind the shoulders.
|
|
1679
1694
|
body: { deformer: "bodyDeformer", order: 5, mesh: false },
|
|
1695
|
+
// The face plate and everything painted on it ride per-group turn warps
|
|
1696
|
+
// under the head (TurnGroupId): the plate its own, each eye stack its
|
|
1697
|
+
// side's, both mouth drawings one, the nose, each brow and each blush their
|
|
1698
|
+
// own. A group's grid is sized to its members alone and baked from the one
|
|
1699
|
+
// solved surface with the family's turn depth in its keyforms, so a feature
|
|
1700
|
+
// slides across the plate on its own grid instead of a translate binding
|
|
1701
|
+
// (see generateIkiFromLayerSet).
|
|
1680
1702
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1681
|
-
nose: { deformer: "
|
|
1682
|
-
blush_L: { deformer: "
|
|
1683
|
-
blush_R: { deformer: "
|
|
1684
|
-
mouth: { deformer: "
|
|
1703
|
+
nose: { deformer: "noseWarp", order: 15, mesh: true },
|
|
1704
|
+
blush_L: { deformer: "blushWarp_L", order: 20, mesh: true },
|
|
1705
|
+
blush_R: { deformer: "blushWarp_R", order: 20, mesh: true },
|
|
1706
|
+
mouth: { deformer: "mouthWarp", order: 25, mesh: true },
|
|
1685
1707
|
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1686
1708
|
// MouthOpen instead of the closed one being stretched, which is the
|
|
1687
1709
|
// difference between a portrait rig and one that can lip-sync.
|
|
1688
|
-
mouth_open: { deformer: "
|
|
1689
|
-
eye_L: { deformer: "
|
|
1690
|
-
eye_R: { deformer: "
|
|
1691
|
-
iris_L: { deformer: "
|
|
1692
|
-
iris_R: { deformer: "
|
|
1693
|
-
pupil_L: { deformer: "
|
|
1694
|
-
pupil_R: { deformer: "
|
|
1695
|
-
highlight_L: { deformer: "
|
|
1696
|
-
highlight_R: { deformer: "
|
|
1710
|
+
mouth_open: { deformer: "mouthWarp", order: 26, mesh: true },
|
|
1711
|
+
eye_L: { deformer: "eyeWarp_L", order: 30, mesh: true, eyeSide: "L" },
|
|
1712
|
+
eye_R: { deformer: "eyeWarp_R", order: 30, mesh: true, eyeSide: "R" },
|
|
1713
|
+
iris_L: { deformer: "eyeWarp_L", order: 31, mesh: true, eyeSide: "L" },
|
|
1714
|
+
iris_R: { deformer: "eyeWarp_R", order: 31, mesh: true, eyeSide: "R" },
|
|
1715
|
+
pupil_L: { deformer: "eyeWarp_L", order: 32, mesh: true, eyeSide: "L" },
|
|
1716
|
+
pupil_R: { deformer: "eyeWarp_R", order: 32, mesh: true, eyeSide: "R" },
|
|
1717
|
+
highlight_L: { deformer: "eyeWarp_L", order: 33, mesh: true, eyeSide: "L" },
|
|
1718
|
+
highlight_R: { deformer: "eyeWarp_R", order: 33, mesh: true, eyeSide: "R" },
|
|
1697
1719
|
// Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
|
|
1698
1720
|
// the closed-eye seam (the same crease the white folds to), covering the cut
|
|
1699
1721
|
// eyeball cleanly. When absent, the white's own fold is the only closed line.
|
|
1700
|
-
lash_L: { deformer: "
|
|
1701
|
-
lash_R: { deformer: "
|
|
1702
|
-
brow_L: { deformer: "
|
|
1703
|
-
brow_R: { deformer: "
|
|
1704
|
-
//
|
|
1705
|
-
//
|
|
1706
|
-
//
|
|
1707
|
-
|
|
1722
|
+
lash_L: { deformer: "eyeWarp_L", order: 34, mesh: true, eyeSide: "L" },
|
|
1723
|
+
lash_R: { deformer: "eyeWarp_R", order: 34, mesh: true, eyeSide: "R" },
|
|
1724
|
+
brow_L: { deformer: "browWarp_L", order: 40, mesh: true },
|
|
1725
|
+
brow_R: { deformer: "browWarp_R", order: 40, mesh: true },
|
|
1726
|
+
// The bangs ride the rigid head, like the back hair, and bind to no grid:
|
|
1727
|
+
// every motion of their own — the silhouette hold, the turn lead, the nod
|
|
1728
|
+
// and the sway — is a per-vertex warp attached in generateIkiFromLayerSet,
|
|
1729
|
+
// so they draw the head's outline where the plate under them bends and
|
|
1730
|
+
// slides (see bakeHairFrontSilhouetteWarp).
|
|
1731
|
+
hair_front: { deformer: "headDeformer", order: 50, mesh: true }
|
|
1708
1732
|
};
|
|
1709
1733
|
var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
|
|
1710
1734
|
var ALIAS_MAP = {
|
|
@@ -1792,6 +1816,27 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1792
1816
|
`auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
|
|
1793
1817
|
);
|
|
1794
1818
|
}
|
|
1819
|
+
if (layer.rowHalfWidths !== void 0) {
|
|
1820
|
+
const rows = layer.rowHalfWidths;
|
|
1821
|
+
if (!Array.isArray(rows)) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths must be an array with one entry per crop row`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
if (rows.length !== cropH) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths has ${rows.length} entries, not one per crop row (cropH ${cropH})`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1832
|
+
const a = rows[i];
|
|
1833
|
+
if (typeof a !== "number" || !Number.isFinite(a) || a < 0 || a > cropW / 2) {
|
|
1834
|
+
throw new Error(
|
|
1835
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths[${i}] is ${String(a)}, not a finite number in [0, ${cropW / 2}] (half the crop's width)`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1795
1840
|
}
|
|
1796
1841
|
}
|
|
1797
1842
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1847,20 +1892,16 @@ function meshCellsFor(w, h) {
|
|
|
1847
1892
|
);
|
|
1848
1893
|
return { cols: cells(w), rows: cells(h) };
|
|
1849
1894
|
}
|
|
1850
|
-
var
|
|
1895
|
+
var FACE_PLATE_CELLS = 10;
|
|
1896
|
+
var FEATURE_GRID_CELLS = 4;
|
|
1897
|
+
var TURN_LATTICE_CELL_PX = 16;
|
|
1898
|
+
var GRID_MARGIN = 0.12;
|
|
1851
1899
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1852
1900
|
var HEAD_TURN_MAX_DEG = 30;
|
|
1853
1901
|
var HEAD_TURN_STOPS = [-30, -15, 0, 15, 30];
|
|
1854
1902
|
function headTurnParallaxUnit(radius) {
|
|
1855
1903
|
return radius * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1856
1904
|
}
|
|
1857
|
-
function gridReach(grid, axis, center) {
|
|
1858
|
-
let reach = 0;
|
|
1859
|
-
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1860
|
-
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1861
|
-
}
|
|
1862
|
-
return reach;
|
|
1863
|
-
}
|
|
1864
1905
|
function pinnedCylinderBend(local, radius, theta) {
|
|
1865
1906
|
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1866
1907
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
@@ -1873,26 +1914,101 @@ function boundedCylinderBend(local, radius, theta) {
|
|
|
1873
1914
|
function turnSlide(travel, angleX) {
|
|
1874
1915
|
return travel * angleX / HEAD_TURN_MAX_DEG;
|
|
1875
1916
|
}
|
|
1876
|
-
|
|
1917
|
+
var FACE_ROW_MIN_FRACTION = 0.15;
|
|
1918
|
+
var FACE_ROW_SMOOTHING = 0.05;
|
|
1919
|
+
var CHIN_SWING = 0.08;
|
|
1920
|
+
function faceRowProfile(face, faceCenterY) {
|
|
1921
|
+
const raw = face.rowHalfWidths;
|
|
1922
|
+
if (raw === void 0) return void 0;
|
|
1923
|
+
const n = raw.length;
|
|
1924
|
+
let aMax = 0;
|
|
1925
|
+
let widest = -1;
|
|
1926
|
+
for (let i = 0; i < n; i++) {
|
|
1927
|
+
if (raw[i] > 0 && raw[i] >= aMax) {
|
|
1928
|
+
aMax = raw[i];
|
|
1929
|
+
widest = i;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (widest < 0) return void 0;
|
|
1933
|
+
const filled = [];
|
|
1934
|
+
for (let i = 0; i < n; i++) {
|
|
1935
|
+
filled.push(i < widest ? aMax : raw[i] > 0 ? raw[i] : filled[i - 1]);
|
|
1936
|
+
}
|
|
1937
|
+
const half = Math.max(1, Math.round(FACE_ROW_SMOOTHING * n / 2));
|
|
1938
|
+
const floor = FACE_ROW_MIN_FRACTION * aMax;
|
|
1939
|
+
const rows = [];
|
|
1940
|
+
for (let i = 0; i < n; i++) {
|
|
1941
|
+
if (i < widest) {
|
|
1942
|
+
rows.push(aMax);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
let sum = 0;
|
|
1946
|
+
for (let k = i - half; k <= i + half; k++) {
|
|
1947
|
+
sum += filled[Math.max(0, Math.min(n - 1, k))];
|
|
1948
|
+
}
|
|
1949
|
+
rows.push(Math.max(floor, sum / (2 * half + 1)));
|
|
1950
|
+
}
|
|
1951
|
+
const top = faceCenterY + face.cropH / 2;
|
|
1952
|
+
const rowAt = (y) => Math.max(0, Math.min(n - 1, top - y - 0.5));
|
|
1953
|
+
return {
|
|
1954
|
+
at: (y) => {
|
|
1955
|
+
const r = rowAt(y);
|
|
1956
|
+
const i = Math.floor(r);
|
|
1957
|
+
if (i + 1 >= n) return rows[n - 1];
|
|
1958
|
+
return rows[i] + (rows[i + 1] - rows[i]) * (r - i);
|
|
1959
|
+
},
|
|
1960
|
+
aMax,
|
|
1961
|
+
widestY: top - widest - 0.5,
|
|
1962
|
+
faceHalfWidth: face.cropW / 2
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function turnSurface(spec) {
|
|
1966
|
+
const { faceCenterX, faceCenterY, radius, travel, nodRadius, lattice } = spec;
|
|
1967
|
+
const { profile } = spec;
|
|
1968
|
+
let radiusAt = (_y) => radius;
|
|
1969
|
+
let swingAt = (_y) => 0;
|
|
1970
|
+
if (profile !== void 0) {
|
|
1971
|
+
const aEye = profile.at(spec.eyeRowY ?? faceCenterY);
|
|
1972
|
+
radiusAt = (y) => radius * (profile.at(y) / aEye);
|
|
1973
|
+
swingAt = (y) => y < profile.widestY ? -CHIN_SWING * profile.faceHalfWidth * Math.max(0, 1 - profile.at(y) / profile.aMax) : 0;
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
faceCenterX,
|
|
1977
|
+
faceCenterY,
|
|
1978
|
+
radius,
|
|
1979
|
+
travel,
|
|
1980
|
+
nodRadius,
|
|
1981
|
+
lattice,
|
|
1982
|
+
radiusAt,
|
|
1983
|
+
swingAt,
|
|
1984
|
+
mapAt: (deg, y) => turnColumnMap(
|
|
1985
|
+
lattice,
|
|
1986
|
+
faceCenterX,
|
|
1987
|
+
radiusAt(y),
|
|
1988
|
+
deg,
|
|
1989
|
+
travel + swingAt(y)
|
|
1990
|
+
),
|
|
1991
|
+
nodBendAt: (y, angleY) => pinnedCylinderBend(
|
|
1992
|
+
y - faceCenterY,
|
|
1993
|
+
nodRadius,
|
|
1994
|
+
angleY * NOD_BEND * (Math.PI / 180)
|
|
1995
|
+
)
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt) {
|
|
1877
1999
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1878
|
-
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1879
2000
|
const pointCount = grid.points.length / 2;
|
|
1880
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
1881
2001
|
const keyforms2d = [];
|
|
1882
2002
|
for (const angleY of STOPS) {
|
|
1883
|
-
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1884
2003
|
for (const angleX of STOPS) {
|
|
1885
|
-
const
|
|
1886
|
-
const slide = turnSlide(travel, angleX);
|
|
2004
|
+
const shift = shiftAt(angleX);
|
|
1887
2005
|
const offsets = [];
|
|
1888
2006
|
for (let i = 0; i < pointCount; i++) {
|
|
2007
|
+
const x = grid.points[i * 2];
|
|
2008
|
+
const y = grid.points[i * 2 + 1];
|
|
1889
2009
|
offsets.push(
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
grid.points[i * 2 + 1] - centerY,
|
|
1893
|
-
RADIUS_Y,
|
|
1894
|
-
thetaY
|
|
1895
|
-
)
|
|
2010
|
+
angleX === 0 ? 0 : surface.mapAt(angleX, y).mapX(x + shift) - x,
|
|
2011
|
+
angleY === 0 ? 0 : surface.nodBendAt(y, angleY)
|
|
1896
2012
|
);
|
|
1897
2013
|
}
|
|
1898
2014
|
keyforms2d.push({ offsets });
|
|
@@ -1924,30 +2040,147 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
1924
2040
|
);
|
|
1925
2041
|
}
|
|
1926
2042
|
const cellFor = (v, edges) => {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2043
|
+
let lo = 0;
|
|
2044
|
+
let hi = grid.cols - 1;
|
|
2045
|
+
while (lo < hi) {
|
|
2046
|
+
const mid = lo + hi >> 1;
|
|
2047
|
+
if (v < edges[mid + 1]) hi = mid;
|
|
2048
|
+
else lo = mid + 1;
|
|
2049
|
+
}
|
|
2050
|
+
return lo;
|
|
2051
|
+
};
|
|
2052
|
+
const sample = (edges, values, v) => {
|
|
2053
|
+
const c = cellFor(v, edges);
|
|
2054
|
+
const s = Math.max(
|
|
2055
|
+
0,
|
|
2056
|
+
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2057
|
+
);
|
|
2058
|
+
if (s === 0) return values[c];
|
|
2059
|
+
if (s === 1) return values[c + 1];
|
|
2060
|
+
return values[c] + (values[c + 1] - values[c]) * s;
|
|
1931
2061
|
};
|
|
1932
2062
|
return {
|
|
1933
2063
|
restX,
|
|
1934
2064
|
warpedX,
|
|
1935
|
-
mapX(x)
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2065
|
+
mapX: (x) => sample(restX, warpedX, x),
|
|
2066
|
+
invertX: (X) => sample(warpedX, restX, X)
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
function renderedLandingX(part, vertexLanding, px, py) {
|
|
2070
|
+
const { cols, rows } = part;
|
|
2071
|
+
const u = (px - part.x + part.cropW / 2) / part.cropW * cols;
|
|
2072
|
+
const v = (part.y + part.cropH / 2 - py) / part.cropH * rows;
|
|
2073
|
+
const col = Math.max(0, Math.min(cols - 1, Math.floor(u)));
|
|
2074
|
+
const row = Math.max(0, Math.min(rows - 1, Math.floor(v)));
|
|
2075
|
+
const fx = Math.max(0, Math.min(1, u - col));
|
|
2076
|
+
const fy = Math.max(0, Math.min(1, v - row));
|
|
2077
|
+
const cos = Math.cos(part.rotation * (Math.PI / 180));
|
|
2078
|
+
const sin = Math.sin(part.rotation * (Math.PI / 180));
|
|
2079
|
+
const landingAt = (c, r) => {
|
|
2080
|
+
const lx = (-part.cropW / 2 + c / cols * part.cropW) * part.scaleX;
|
|
2081
|
+
const ly = (part.cropH / 2 - r / rows * part.cropH) * part.scaleY;
|
|
2082
|
+
return vertexLanding(
|
|
2083
|
+
part.x + cos * lx - sin * ly,
|
|
2084
|
+
part.y + sin * lx + cos * ly
|
|
2085
|
+
);
|
|
2086
|
+
};
|
|
2087
|
+
const tl = landingAt(col, row);
|
|
2088
|
+
const br = landingAt(col + 1, row + 1);
|
|
2089
|
+
if (fx <= fy) {
|
|
2090
|
+
const bl = landingAt(col, row + 1);
|
|
2091
|
+
return tl + fy * (bl - tl) + fx * (br - bl);
|
|
2092
|
+
}
|
|
2093
|
+
const tr = landingAt(col + 1, row);
|
|
2094
|
+
return tl + fx * (tr - tl) + fy * (br - tr);
|
|
2095
|
+
}
|
|
2096
|
+
function gridChildLandingX(grid, nodeLanding, x, y) {
|
|
2097
|
+
const { cols, rows, points } = grid;
|
|
2098
|
+
const stride = cols + 1;
|
|
2099
|
+
let col = cols - 1;
|
|
2100
|
+
for (let c = 0; c < cols; c++) {
|
|
2101
|
+
if (x < points[(c + 1) * 2]) {
|
|
2102
|
+
col = c;
|
|
2103
|
+
break;
|
|
1950
2104
|
}
|
|
2105
|
+
}
|
|
2106
|
+
let row = rows - 1;
|
|
2107
|
+
for (let r = 0; r < rows; r++) {
|
|
2108
|
+
if (y > points[(r + 1) * stride * 2 + 1]) {
|
|
2109
|
+
row = r;
|
|
2110
|
+
break;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
const xLeft = points[col * 2];
|
|
2114
|
+
const xRight = points[(col + 1) * 2];
|
|
2115
|
+
const yTop = points[row * stride * 2 + 1];
|
|
2116
|
+
const yBottom = points[(row + 1) * stride * 2 + 1];
|
|
2117
|
+
const s = Math.max(0, Math.min(1, (x - xLeft) / (xRight - xLeft)));
|
|
2118
|
+
const t = Math.max(0, Math.min(1, (yTop - y) / (yTop - yBottom)));
|
|
2119
|
+
const at = (c, r) => nodeLanding(points[(r * stride + c) * 2], points[(r * stride + c) * 2 + 1]);
|
|
2120
|
+
const top = at(col, row) + (at(col + 1, row) - at(col, row)) * s;
|
|
2121
|
+
const bot = at(col, row + 1) + (at(col + 1, row + 1) - at(col, row + 1)) * s;
|
|
2122
|
+
return top + (bot - top) * t;
|
|
2123
|
+
}
|
|
2124
|
+
function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
2125
|
+
return renderedLandingX(
|
|
2126
|
+
carrier.part,
|
|
2127
|
+
(vx, vy) => gridChildLandingX(carrier.grid, nodeLanding, vx, vy),
|
|
2128
|
+
px,
|
|
2129
|
+
py
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
function rowMapsOf(surface) {
|
|
2133
|
+
const maps = /* @__PURE__ */ new Map();
|
|
2134
|
+
return (deg, nodeY) => {
|
|
2135
|
+
let byRow = maps.get(deg);
|
|
2136
|
+
if (byRow === void 0) {
|
|
2137
|
+
byRow = /* @__PURE__ */ new Map();
|
|
2138
|
+
maps.set(deg, byRow);
|
|
2139
|
+
}
|
|
2140
|
+
let map = byRow.get(nodeY);
|
|
2141
|
+
if (map === void 0) {
|
|
2142
|
+
map = surface.mapAt(deg, nodeY);
|
|
2143
|
+
byRow.set(nodeY, map);
|
|
2144
|
+
}
|
|
2145
|
+
return map;
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function plateLandingOn(plate, mapAt) {
|
|
2149
|
+
return (deg, x, y) => carrierLandingX(
|
|
2150
|
+
plate,
|
|
2151
|
+
(nodeX, nodeY) => mapAt(deg, nodeY).mapX(nodeX),
|
|
2152
|
+
x,
|
|
2153
|
+
y
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
function plateGuardRowsFor(face, hairFront) {
|
|
2157
|
+
const rowsOf = (part, y) => {
|
|
2158
|
+
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2159
|
+
return Array.from(
|
|
2160
|
+
{ length: rows + 1 },
|
|
2161
|
+
(_, r) => y + (part.cropH / 2 - r / rows * part.cropH)
|
|
2162
|
+
);
|
|
2163
|
+
};
|
|
2164
|
+
const faceRows = rowsOf(face, face.y);
|
|
2165
|
+
if (hairFront === void 0) return faceRows;
|
|
2166
|
+
const top = face.y + face.cropH / 2;
|
|
2167
|
+
const bottom = face.y - face.cropH / 2;
|
|
2168
|
+
return [
|
|
2169
|
+
...faceRows,
|
|
2170
|
+
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2171
|
+
(y) => Math.max(bottom, Math.min(top, y))
|
|
2172
|
+
)
|
|
2173
|
+
];
|
|
2174
|
+
}
|
|
2175
|
+
function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
2176
|
+
const plate = carriers.get("face");
|
|
2177
|
+
if (plate === void 0) {
|
|
2178
|
+
throw new Error("auto-rig: plateGuardsOf: the carriers have no face");
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
plate,
|
|
2182
|
+
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2183
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
1951
2184
|
};
|
|
1952
2185
|
}
|
|
1953
2186
|
var TurnTargetError = class extends Error {
|
|
@@ -2018,12 +2251,15 @@ function resolveTurnTargets(targets = {}) {
|
|
|
2018
2251
|
defaulted
|
|
2019
2252
|
};
|
|
2020
2253
|
}
|
|
2021
|
-
function turnLandmarks(layers) {
|
|
2254
|
+
function turnLandmarks(layers, carriers) {
|
|
2022
2255
|
const markOf = (role) => {
|
|
2023
2256
|
const layer = layers.find((l) => l.role === role);
|
|
2024
2257
|
if (!layer) return void 0;
|
|
2025
2258
|
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
2026
|
-
|
|
2259
|
+
const mark = { x: t.x, y: t.y, w: layer.cropW };
|
|
2260
|
+
const carrier = carriers?.get(role);
|
|
2261
|
+
if (carrier !== void 0) mark.carrier = carrier;
|
|
2262
|
+
return mark;
|
|
2027
2263
|
};
|
|
2028
2264
|
const pairOf = (left, right) => {
|
|
2029
2265
|
const l = markOf(left);
|
|
@@ -2040,16 +2276,34 @@ function turnLandmarks(layers) {
|
|
|
2040
2276
|
mouth: one("mouth")
|
|
2041
2277
|
};
|
|
2042
2278
|
}
|
|
2279
|
+
function eyeRowOf(landmarks, fallback) {
|
|
2280
|
+
const [l, r] = landmarks.eye;
|
|
2281
|
+
return landmarks.eye.length === 2 && l.y !== void 0 && r.y !== void 0 ? (l.y + r.y) / 2 : fallback;
|
|
2282
|
+
}
|
|
2043
2283
|
var TURN_BISECT_STEPS = 40;
|
|
2044
2284
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2045
|
-
function
|
|
2285
|
+
function landmarkLandingX(landmark, mapAt, shift, px, py) {
|
|
2286
|
+
if (landmark.carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2287
|
+
return carrierLandingX(
|
|
2288
|
+
landmark.carrier,
|
|
2289
|
+
(nodeX, nodeY) => mapAt(nodeY).mapX(nodeX + shift),
|
|
2290
|
+
px,
|
|
2291
|
+
py
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
function familyReachPx(landmarks, plateEdgeX) {
|
|
2295
|
+
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2296
|
+
return Math.max(0, farEdge - plateEdgeX);
|
|
2297
|
+
}
|
|
2298
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX) {
|
|
2046
2299
|
if (landmarks.length === 0) {
|
|
2047
2300
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2048
2301
|
}
|
|
2049
|
-
const
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2302
|
+
const cap = familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2303
|
+
const achieved = (d) => landmarks.reduce(
|
|
2304
|
+
(sum, l) => sum + (landmarkLandingX(l, mapAt, -d * unit, l.x, l.y ?? 0) - l.x),
|
|
2305
|
+
0
|
|
2306
|
+
) / landmarks.length;
|
|
2053
2307
|
const atCap = achieved(cap);
|
|
2054
2308
|
const atRest = achieved(0);
|
|
2055
2309
|
const attainable = [atCap, atRest];
|
|
@@ -2072,68 +2326,52 @@ var TURN_SWEEP_MAX_FACTOR = 8;
|
|
|
2072
2326
|
var TURN_SWEEP_SAMPLES = 32;
|
|
2073
2327
|
var HOLD_CLEARANCE = 1;
|
|
2074
2328
|
var TURN_HOLD_SAMPLES = 16;
|
|
2075
|
-
var
|
|
2076
|
-
function hairFrontHoldTarget(x, faceCenterX,
|
|
2329
|
+
var TURN_SETTLE_PASSES = 24;
|
|
2330
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2077
2331
|
const local = x - faceCenterX;
|
|
2078
2332
|
const dist = Math.abs(local);
|
|
2079
2333
|
const side = Math.sign(local);
|
|
2080
|
-
const
|
|
2081
|
-
if (dist <=
|
|
2334
|
+
const painted = edgeAt(y);
|
|
2335
|
+
if (dist <= painted) return plateLandingAt(deg, x, y);
|
|
2082
2336
|
if (dist <= holdBase) {
|
|
2083
|
-
const inner =
|
|
2084
|
-
const u = (dist -
|
|
2085
|
-
return inner + (faceCenterX + side * holdEdge
|
|
2337
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, y);
|
|
2338
|
+
const u = (dist - painted) / (holdBase - painted);
|
|
2339
|
+
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2086
2340
|
}
|
|
2087
|
-
return x + side * (holdEdge
|
|
2341
|
+
return x + side * (holdEdge - holdBase);
|
|
2088
2342
|
}
|
|
2089
|
-
function hairFrontLandingAt(x,
|
|
2343
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
2090
2344
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
lo,
|
|
2108
|
-
hi: Math.max(0, Math.min(cells, lo + 1)),
|
|
2109
|
-
t: Math.max(0, Math.min(1, v - lo))
|
|
2110
|
-
};
|
|
2111
|
-
};
|
|
2112
|
-
const col = bracket(
|
|
2113
|
-
(x - hairFront.x + hairFront.cropW / 2) / hairFront.cropW * cols,
|
|
2114
|
-
cols
|
|
2345
|
+
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2346
|
+
return renderedLandingX(
|
|
2347
|
+
{
|
|
2348
|
+
x: hairFront.x,
|
|
2349
|
+
y: hairFront.centerY,
|
|
2350
|
+
cropW: hairFront.cropW,
|
|
2351
|
+
cropH: hairFront.cropH,
|
|
2352
|
+
cols,
|
|
2353
|
+
rows,
|
|
2354
|
+
scaleX: 1,
|
|
2355
|
+
scaleY: 1,
|
|
2356
|
+
rotation: 0
|
|
2357
|
+
},
|
|
2358
|
+
(vx, vy) => holdTargetAt(vx, vy) - leadTipShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2359
|
+
x,
|
|
2360
|
+
y
|
|
2115
2361
|
);
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
const tl = landingAt(col.lo, row.lo);
|
|
2119
|
-
const br = landingAt(col.hi, row.hi);
|
|
2120
|
-
if (col.t <= row.t) {
|
|
2121
|
-
const bl = landingAt(col.lo, row.hi);
|
|
2122
|
-
return tl + row.t * (bl - tl) + col.t * (br - bl);
|
|
2123
|
-
}
|
|
2124
|
-
const tr = landingAt(col.hi, row.lo);
|
|
2125
|
-
return tl + col.t * (tr - tl) + row.t * (br - tr);
|
|
2126
|
-
}
|
|
2127
|
-
function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt, holdEdgeAt) {
|
|
2362
|
+
}
|
|
2363
|
+
function shellTravelCap(faceCenterX, edgeAt, plateGuardRows, askedTravel, bendOnlyLandingAt, holdEdgeAt) {
|
|
2128
2364
|
let cap = Infinity;
|
|
2129
2365
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2130
2366
|
if (deg === 0) continue;
|
|
2131
|
-
const map = bendOnlyMapAt(deg);
|
|
2132
2367
|
const slideSign = turnSlide(askedTravel, deg) < 0 ? -1 : 1;
|
|
2133
|
-
const towardReach =
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2368
|
+
const towardReach = plateSideReachAt(
|
|
2369
|
+
bendOnlyLandingAt,
|
|
2370
|
+
faceCenterX,
|
|
2371
|
+
edgeAt,
|
|
2372
|
+
plateGuardRows,
|
|
2373
|
+
deg,
|
|
2374
|
+
slideSign
|
|
2137
2375
|
);
|
|
2138
2376
|
cap = Math.min(
|
|
2139
2377
|
cap,
|
|
@@ -2142,8 +2380,26 @@ function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt,
|
|
|
2142
2380
|
}
|
|
2143
2381
|
return cap;
|
|
2144
2382
|
}
|
|
2383
|
+
function aitken(x0, x1, x2, cap) {
|
|
2384
|
+
const d1 = x1 - x0;
|
|
2385
|
+
const d2 = x2 - x1;
|
|
2386
|
+
const denominator = d2 - d1;
|
|
2387
|
+
if (denominator === 0) return x2;
|
|
2388
|
+
const x = x2 - d2 * d2 / denominator;
|
|
2389
|
+
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2390
|
+
}
|
|
2145
2391
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2146
|
-
const
|
|
2392
|
+
const surfaceAt = (travel) => turnSurface({
|
|
2393
|
+
faceCenterX: ctx.faceCenterX,
|
|
2394
|
+
faceCenterY: ctx.faceCenterY,
|
|
2395
|
+
radius,
|
|
2396
|
+
travel,
|
|
2397
|
+
nodRadius: ctx.nodRadius,
|
|
2398
|
+
lattice: ctx.lattice,
|
|
2399
|
+
profile: ctx.profile,
|
|
2400
|
+
eyeRowY: ctx.eyeRowY
|
|
2401
|
+
});
|
|
2402
|
+
const bendOnlyLandingAt = plateLandingOn(ctx.plate, rowMapsOf(surfaceAt(0)));
|
|
2147
2403
|
const measuredEdgeAt = (side) => {
|
|
2148
2404
|
const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;
|
|
2149
2405
|
if (candidates === void 0 || candidates.length === 0) return void 0;
|
|
@@ -2155,66 +2411,71 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2155
2411
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2156
2412
|
};
|
|
2157
2413
|
const unit = headTurnParallaxUnit(radius);
|
|
2158
|
-
const leadTipShift = ctx.
|
|
2159
|
-
const
|
|
2160
|
-
const evaluateHold = (ratio) => {
|
|
2414
|
+
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2415
|
+
const evaluateHold = (ratio, depths2) => {
|
|
2161
2416
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2162
|
-
const
|
|
2417
|
+
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
2163
2418
|
ctx.travel,
|
|
2164
2419
|
shellTravelCap(
|
|
2165
2420
|
ctx.faceCenterX,
|
|
2166
|
-
ctx.
|
|
2421
|
+
ctx.edgeAt,
|
|
2422
|
+
ctx.plateGuardRows,
|
|
2167
2423
|
ctx.travel,
|
|
2168
|
-
|
|
2424
|
+
bendOnlyLandingAt,
|
|
2169
2425
|
(deg, side) => {
|
|
2170
2426
|
const rest = shellHalfWidthAt(side);
|
|
2171
2427
|
return Math.min(rest, holdEdgeFrom(rest)(deg));
|
|
2172
2428
|
}
|
|
2173
2429
|
)
|
|
2174
2430
|
);
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2177
|
-
const
|
|
2431
|
+
const surface = surfaceAt(travel);
|
|
2432
|
+
const rowMaps = rowMapsOf(surface);
|
|
2433
|
+
const mapAt = (y) => rowMaps(-HEAD_TURN_MAX_DEG, y);
|
|
2434
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2435
|
+
const holdBase = ctx.headHalfWidth ?? plateReach(
|
|
2436
|
+
plateLandingAt,
|
|
2437
|
+
ctx.faceCenterX,
|
|
2438
|
+
ctx.edgeAt,
|
|
2439
|
+
ctx.plateGuardRows
|
|
2440
|
+
) + HOLD_CLEARANCE;
|
|
2441
|
+
const holdEdgeAt = holdEdgeFrom(holdBase);
|
|
2178
2442
|
let foldLowerBound = -Infinity;
|
|
2179
2443
|
let folds = false;
|
|
2180
2444
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2181
2445
|
if (deg === 0) continue;
|
|
2182
2446
|
const reach = plateReachAt(
|
|
2183
|
-
|
|
2447
|
+
plateLandingAt,
|
|
2184
2448
|
ctx.faceCenterX,
|
|
2185
|
-
ctx.
|
|
2449
|
+
ctx.edgeAt,
|
|
2450
|
+
ctx.plateGuardRows,
|
|
2451
|
+
deg
|
|
2186
2452
|
);
|
|
2187
2453
|
foldLowerBound = Math.max(
|
|
2188
2454
|
foldLowerBound,
|
|
2189
|
-
1 + ((reach + HOLD_CLEARANCE) /
|
|
2455
|
+
1 + ((reach + HOLD_CLEARANCE) / holdBase - 1) * (HEAD_TURN_MAX_DEG / Math.abs(deg))
|
|
2190
2456
|
);
|
|
2191
|
-
if (
|
|
2457
|
+
if (holdEdgeAt(deg) <= reach) folds = true;
|
|
2192
2458
|
}
|
|
2193
|
-
const map2 = columnMapAt(-HEAD_TURN_MAX_DEG);
|
|
2194
|
-
const requestedHoldEdge = holdEdgeAt2(HEAD_TURN_MAX_DEG);
|
|
2195
|
-
const gridReach2 = [
|
|
2196
|
-
Math.abs(map2.warpedX[0] - ctx.faceCenterX),
|
|
2197
|
-
Math.abs(map2.warpedX[map2.warpedX.length - 1] - ctx.faceCenterX)
|
|
2198
|
-
];
|
|
2199
|
-
const cappedHoldEdge = [
|
|
2200
|
-
Math.min(requestedHoldEdge, gridReach2[0] - HOLD_CLEARANCE),
|
|
2201
|
-
Math.min(requestedHoldEdge, gridReach2[1] - HOLD_CLEARANCE)
|
|
2202
|
-
];
|
|
2203
2459
|
const restAt = (side) => {
|
|
2204
2460
|
const edge = measuredEdgeAt(side);
|
|
2205
2461
|
if (edge !== void 0) return edge;
|
|
2206
|
-
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side *
|
|
2462
|
+
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2207
2463
|
};
|
|
2208
2464
|
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : hairFrontLandingAt(
|
|
2209
2465
|
x,
|
|
2466
|
+
ctx.eyeRowY,
|
|
2210
2467
|
ctx.hairFrontSilhouette,
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2468
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2469
|
+
vx,
|
|
2470
|
+
vy,
|
|
2471
|
+
-HEAD_TURN_MAX_DEG,
|
|
2472
|
+
ctx.faceCenterX,
|
|
2473
|
+
plateLandingAt,
|
|
2474
|
+
ctx.edgeAt,
|
|
2475
|
+
holdBase,
|
|
2476
|
+
holdEdgeAt(-HEAD_TURN_MAX_DEG)
|
|
2477
|
+
),
|
|
2478
|
+
leadTipShift
|
|
2218
2479
|
);
|
|
2219
2480
|
const landingOfRole = (role, x) => {
|
|
2220
2481
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2222,10 +2483,22 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2222
2483
|
return x;
|
|
2223
2484
|
}
|
|
2224
2485
|
if (role === "body") {
|
|
2225
|
-
return x - BODY_TURN_FOLLOW *
|
|
2486
|
+
return x - BODY_TURN_FOLLOW * travel;
|
|
2226
2487
|
}
|
|
2227
2488
|
if (role === "face" || FEATURE_NOD_DEPTH[roleFamily(role)] !== void 0) {
|
|
2228
|
-
|
|
2489
|
+
const carrier = ctx.carriers.get(role);
|
|
2490
|
+
if (carrier === void 0) {
|
|
2491
|
+
throw new Error(
|
|
2492
|
+
`auto-rig: evaluateTurnCandidate: headEdges names "${role}", which this layer set has no layer for`
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2496
|
+
return carrierLandingX(
|
|
2497
|
+
carrier,
|
|
2498
|
+
(nodeX, nodeY) => mapAt(nodeY).mapX(nodeX + shift),
|
|
2499
|
+
x,
|
|
2500
|
+
ctx.eyeRowY
|
|
2501
|
+
);
|
|
2229
2502
|
}
|
|
2230
2503
|
throw new Error(
|
|
2231
2504
|
`auto-rig: evaluateTurnCandidate: unrecognised role "${role}"`
|
|
@@ -2241,13 +2514,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2241
2514
|
};
|
|
2242
2515
|
return {
|
|
2243
2516
|
ratio,
|
|
2244
|
-
travel
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2517
|
+
travel,
|
|
2518
|
+
mapAt,
|
|
2519
|
+
surface,
|
|
2520
|
+
holdBase,
|
|
2521
|
+
holdEdgeAt,
|
|
2248
2522
|
folds,
|
|
2249
2523
|
foldLowerBound,
|
|
2250
|
-
gridReach: gridReach2,
|
|
2251
2524
|
// How far the silhouette's own centre drifts off the face centre at full
|
|
2252
2525
|
// turn, against how far it already sat off centre at rest (zero unless
|
|
2253
2526
|
// hair_front's crop is itself off-centre) — the held shell keeps its own
|
|
@@ -2264,24 +2537,18 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2264
2537
|
restSpan: restAt(1) - restAt(-1)
|
|
2265
2538
|
};
|
|
2266
2539
|
};
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
for (let i = 0; i < TURN_HOLD_SATURATION_PASSES; i++) {
|
|
2270
|
-
const needed = (Math.max(hold2.gridReach[0], hold2.gridReach[1]) - HOLD_CLEARANCE) / hold2.holdBase;
|
|
2271
|
-
if (needed <= hold2.ratio) break;
|
|
2272
|
-
hold2 = evaluateHold(needed);
|
|
2273
|
-
}
|
|
2274
|
-
return hold2;
|
|
2275
|
-
};
|
|
2276
|
-
const holdRange = () => {
|
|
2277
|
-
const ceiling = saturatedHold();
|
|
2540
|
+
const holdRange = (depths2) => {
|
|
2541
|
+
const ceiling = evaluateHold(TURN_RATIO_MAX, depths2);
|
|
2278
2542
|
if (ceiling.folds) return void 0;
|
|
2279
|
-
let floor = evaluateHold(
|
|
2543
|
+
let floor = evaluateHold(
|
|
2544
|
+
Math.min(ceiling.foldLowerBound, ceiling.ratio),
|
|
2545
|
+
depths2
|
|
2546
|
+
);
|
|
2280
2547
|
if (floor.folds) {
|
|
2281
2548
|
let lo = floor;
|
|
2282
2549
|
let hi = ceiling;
|
|
2283
2550
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2284
|
-
const mid = evaluateHold((lo.ratio + hi.ratio) / 2);
|
|
2551
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2285
2552
|
if (mid.folds) lo = mid;
|
|
2286
2553
|
else hi = mid;
|
|
2287
2554
|
}
|
|
@@ -2290,7 +2557,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2290
2557
|
const samples = [floor];
|
|
2291
2558
|
for (let i = 1; i < TURN_HOLD_SAMPLES; i++) {
|
|
2292
2559
|
const sample = evaluateHold(
|
|
2293
|
-
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES
|
|
2560
|
+
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES,
|
|
2561
|
+
depths2
|
|
2294
2562
|
);
|
|
2295
2563
|
if (!sample.folds) samples.push(sample);
|
|
2296
2564
|
}
|
|
@@ -2303,15 +2571,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2303
2571
|
return lower > upper ? { blocked: "silhouetteRatio", lower: Infinity, upper: -Infinity } : { blocked: "silhouetteRatio", lower, upper };
|
|
2304
2572
|
};
|
|
2305
2573
|
const asked = ctx.silhouetteRatio;
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (samples === void 0) return silhouetteMiss();
|
|
2574
|
+
const fitHold = (depths2) => {
|
|
2575
|
+
const natural = evaluateHold(asked, depths2);
|
|
2576
|
+
const eps = TURN_DEPTH_EPS / natural.restSpan;
|
|
2577
|
+
if (!natural.folds && Math.abs(natural.renderedSilhouetteRatio - asked) <= eps) {
|
|
2578
|
+
return { hold: natural, silhouetteClamped: false };
|
|
2579
|
+
}
|
|
2580
|
+
const samples = holdRange(depths2);
|
|
2581
|
+
if (samples === void 0) return void 0;
|
|
2315
2582
|
const rendered = samples.map((h) => h.renderedSilhouetteRatio);
|
|
2316
2583
|
const span = [
|
|
2317
2584
|
Math.min(...rendered),
|
|
@@ -2326,91 +2593,145 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2326
2593
|
return ea <= eb ? a : b;
|
|
2327
2594
|
};
|
|
2328
2595
|
if (asked < span[0] - eps || asked > span[1] + eps) {
|
|
2329
|
-
if (!ctx.clampSilhouetteRatio) return silhouetteMiss(span);
|
|
2330
2596
|
const wanted = asked < span[0] ? span[0] : span[1];
|
|
2331
|
-
hold = samples.reduce(
|
|
2597
|
+
const hold = samples.reduce(
|
|
2332
2598
|
(a, b) => Math.abs(b.renderedSilhouetteRatio - wanted) < Math.abs(a.renderedSilhouetteRatio - wanted) ? b : a
|
|
2333
2599
|
);
|
|
2334
|
-
silhouetteClamped
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
}
|
|
2600
|
+
return { hold, silhouetteClamped: true, span };
|
|
2601
|
+
}
|
|
2602
|
+
let best = samples.reduce(nearer);
|
|
2603
|
+
let lo;
|
|
2604
|
+
let hi;
|
|
2605
|
+
for (let i = 0; i + 1 < samples.length; i++) {
|
|
2606
|
+
const a = samples[i].renderedSilhouetteRatio - asked;
|
|
2607
|
+
const b = samples[i + 1].renderedSilhouetteRatio - asked;
|
|
2608
|
+
if (a * b <= 0) {
|
|
2609
|
+
lo = samples[i];
|
|
2610
|
+
hi = samples[i + 1];
|
|
2611
|
+
break;
|
|
2347
2612
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2613
|
+
}
|
|
2614
|
+
for (let i = 0; lo !== void 0 && hi !== void 0; i++) {
|
|
2615
|
+
if (i >= TURN_BISECT_STEPS || Math.abs(best.renderedSilhouetteRatio - asked) <= eps) {
|
|
2616
|
+
break;
|
|
2617
|
+
}
|
|
2618
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2619
|
+
if (!mid.folds) best = nearer(best, mid);
|
|
2620
|
+
if ((mid.renderedSilhouetteRatio - asked) * (lo.renderedSilhouetteRatio - asked) > 0) {
|
|
2621
|
+
lo = mid;
|
|
2622
|
+
} else {
|
|
2623
|
+
hi = mid;
|
|
2359
2624
|
}
|
|
2360
|
-
hold = best;
|
|
2361
2625
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2626
|
+
return { hold: best, silhouetteClamped: false };
|
|
2627
|
+
};
|
|
2628
|
+
let depths = { eye: 0, nose: 0, mouth: 0 };
|
|
2629
|
+
let seed;
|
|
2630
|
+
for (let pass = 0; ; pass++) {
|
|
2631
|
+
const fitted = fitHold(depths);
|
|
2632
|
+
if (fitted === void 0) return silhouetteMiss();
|
|
2633
|
+
const { hold, silhouetteClamped, span } = fitted;
|
|
2634
|
+
if (hold.folds) {
|
|
2635
|
+
throw new Error(
|
|
2636
|
+
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
const eye = solveTurnDepthSigned(
|
|
2640
|
+
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2641
|
+
ctx.landmarks.eye,
|
|
2642
|
+
unit,
|
|
2643
|
+
hold.mapAt,
|
|
2644
|
+
ctx.plateEdgeX
|
|
2366
2645
|
);
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2646
|
+
const features = solveFeatureDepths(
|
|
2647
|
+
ctx,
|
|
2648
|
+
(-eye.achieved + hold.silhouetteCenterShift) / ctx.hh,
|
|
2649
|
+
unit,
|
|
2650
|
+
hold.mapAt,
|
|
2651
|
+
hold.silhouetteCenterShift
|
|
2652
|
+
);
|
|
2653
|
+
const solved = {
|
|
2654
|
+
eye: eye.depth,
|
|
2655
|
+
nose: features.nose.depth,
|
|
2656
|
+
mouth: features.mouth.depth
|
|
2657
|
+
};
|
|
2658
|
+
const settled = evaluateHold(hold.ratio, solved);
|
|
2659
|
+
if (Math.abs(settled.silhouetteCenterShift - hold.silhouetteCenterShift) > TURN_DEPTH_EPS || Math.abs(settled.renderedSilhouetteRatio - hold.renderedSilhouetteRatio) * hold.restSpan > TURN_DEPTH_EPS) {
|
|
2660
|
+
if (pass + 1 >= TURN_SETTLE_PASSES) return { blocked: "settle" };
|
|
2661
|
+
if (seed === void 0) {
|
|
2662
|
+
seed = depths;
|
|
2663
|
+
depths = solved;
|
|
2664
|
+
} else {
|
|
2665
|
+
const capOf = (marks) => familyReachPx(marks, ctx.plateEdgeX) / unit;
|
|
2666
|
+
depths = {
|
|
2667
|
+
eye: aitken(
|
|
2668
|
+
seed.eye,
|
|
2669
|
+
depths.eye,
|
|
2670
|
+
solved.eye,
|
|
2671
|
+
capOf(ctx.landmarks.eye)
|
|
2672
|
+
),
|
|
2673
|
+
nose: aitken(
|
|
2674
|
+
seed.nose,
|
|
2675
|
+
depths.nose,
|
|
2676
|
+
solved.nose,
|
|
2677
|
+
capOf(ctx.landmarks.nose)
|
|
2678
|
+
),
|
|
2679
|
+
mouth: aitken(
|
|
2680
|
+
seed.mouth,
|
|
2681
|
+
depths.mouth,
|
|
2682
|
+
solved.mouth,
|
|
2683
|
+
capOf(ctx.landmarks.mouth)
|
|
2684
|
+
)
|
|
2685
|
+
};
|
|
2686
|
+
seed = void 0;
|
|
2687
|
+
}
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
const {
|
|
2691
|
+
travel,
|
|
2692
|
+
mapAt,
|
|
2693
|
+
surface,
|
|
2694
|
+
holdBase,
|
|
2695
|
+
holdEdgeAt,
|
|
2696
|
+
silhouetteCenterShift,
|
|
2697
|
+
renderedSilhouetteRatio
|
|
2698
|
+
} = settled;
|
|
2699
|
+
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2700
|
+
return silhouetteMiss(span);
|
|
2701
|
+
}
|
|
2702
|
+
if (!eye.reached && !ctx.clampEyeShift) {
|
|
2703
|
+
return {
|
|
2704
|
+
blocked: "eyeShift",
|
|
2705
|
+
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
2706
|
+
// a negative lower bound here would advertise a value the field's own
|
|
2707
|
+
// contract already rules out; 0 is the true floor.
|
|
2708
|
+
offeredShift: [
|
|
2709
|
+
Math.max(0, (-eye.attainable[1] + silhouetteCenterShift) / ctx.hh),
|
|
2710
|
+
(-eye.attainable[0] + silhouetteCenterShift) / ctx.hh
|
|
2711
|
+
]
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
const eyeShiftPx = -eye.depth * unit;
|
|
2715
|
+
const scaleOf = (l) => (landmarkLandingX(l, mapAt, eyeShiftPx, l.x + l.w / 2, l.y ?? 0) - landmarkLandingX(l, mapAt, eyeShiftPx, l.x - l.w / 2, l.y ?? 0)) / l.w;
|
|
2716
|
+
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2717
|
+
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2386
2718
|
return {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2719
|
+
radius,
|
|
2720
|
+
ratio: scaleOf(far) / scaleOf(near),
|
|
2721
|
+
unit,
|
|
2722
|
+
travel,
|
|
2723
|
+
mapAt,
|
|
2724
|
+
surface,
|
|
2725
|
+
holdBase,
|
|
2726
|
+
holdEdgeAt,
|
|
2727
|
+
eye,
|
|
2728
|
+
nose: features.nose,
|
|
2729
|
+
mouth: features.mouth,
|
|
2730
|
+
silhouetteClamped,
|
|
2731
|
+
silhouetteCenterShift,
|
|
2732
|
+
renderedSilhouetteRatio
|
|
2395
2733
|
};
|
|
2396
2734
|
}
|
|
2397
|
-
const scaleOf = (l) => (map.mapX(l.x + l.w / 2 - eye.depth * unit) - map.mapX(l.x - l.w / 2 - eye.depth * unit)) / l.w;
|
|
2398
|
-
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2399
|
-
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2400
|
-
return {
|
|
2401
|
-
radius,
|
|
2402
|
-
ratio: scaleOf(far) / scaleOf(near),
|
|
2403
|
-
unit,
|
|
2404
|
-
travel,
|
|
2405
|
-
map,
|
|
2406
|
-
holdBase,
|
|
2407
|
-
holdEdgeAt,
|
|
2408
|
-
holdEdgeX,
|
|
2409
|
-
eye,
|
|
2410
|
-
silhouetteClamped,
|
|
2411
|
-
silhouetteCenterShift,
|
|
2412
|
-
renderedSilhouetteRatio
|
|
2413
|
-
};
|
|
2414
2735
|
}
|
|
2415
2736
|
function sweepTurnRadii(ctx) {
|
|
2416
2737
|
const minRadius = ctx.faceHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
@@ -2419,6 +2740,7 @@ function sweepTurnRadii(ctx) {
|
|
|
2419
2740
|
let heldRatioLower = Infinity;
|
|
2420
2741
|
let heldRatioUpper = -Infinity;
|
|
2421
2742
|
let offeredShift;
|
|
2743
|
+
let unsettled = 0;
|
|
2422
2744
|
for (let i = 0; i < TURN_SWEEP_SAMPLES; i++) {
|
|
2423
2745
|
const radius = minRadius * Math.pow(maxRadius / minRadius, i / (TURN_SWEEP_SAMPLES - 1));
|
|
2424
2746
|
const result = evaluateTurnCandidate(ctx, radius);
|
|
@@ -2427,6 +2749,8 @@ function sweepTurnRadii(ctx) {
|
|
|
2427
2749
|
} else if (result.blocked === "silhouetteRatio") {
|
|
2428
2750
|
heldRatioLower = Math.min(heldRatioLower, result.lower);
|
|
2429
2751
|
heldRatioUpper = Math.max(heldRatioUpper, result.upper);
|
|
2752
|
+
} else if (result.blocked === "settle") {
|
|
2753
|
+
unsettled++;
|
|
2430
2754
|
} else {
|
|
2431
2755
|
offeredShift = offeredShift ? [
|
|
2432
2756
|
Math.min(offeredShift[0], result.offeredShift[0]),
|
|
@@ -2434,7 +2758,13 @@ function sweepTurnRadii(ctx) {
|
|
|
2434
2758
|
] : result.offeredShift;
|
|
2435
2759
|
}
|
|
2436
2760
|
}
|
|
2437
|
-
return {
|
|
2761
|
+
return {
|
|
2762
|
+
candidates,
|
|
2763
|
+
heldRatioLower,
|
|
2764
|
+
heldRatioUpper,
|
|
2765
|
+
offeredShift,
|
|
2766
|
+
unsettled
|
|
2767
|
+
};
|
|
2438
2768
|
}
|
|
2439
2769
|
function ratioClusters(candidates) {
|
|
2440
2770
|
const clusters = [];
|
|
@@ -2476,71 +2806,40 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2476
2806
|
}
|
|
2477
2807
|
return Math.abs(lo.ratio - target) <= Math.abs(hi.ratio - target) ? lo : hi;
|
|
2478
2808
|
}
|
|
2479
|
-
function solveFeatureDepths(ctx,
|
|
2480
|
-
const
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const m = Math.abs(
|
|
2488
|
-
targets.defaulted.has(field) ? share * eyeShift : targets[field]
|
|
2489
|
-
);
|
|
2490
|
-
const solution = solveTurnDepthSigned(
|
|
2491
|
-
best.silhouetteCenterShift - m * ctx.hh,
|
|
2492
|
-
marks,
|
|
2493
|
-
best.unit,
|
|
2494
|
-
best.map,
|
|
2495
|
-
best.holdEdgeX,
|
|
2496
|
-
ctx.plateEdgeX
|
|
2497
|
-
);
|
|
2498
|
-
if (!solution.reached) {
|
|
2499
|
-
if (!targets.defaulted.has(field)) {
|
|
2500
|
-
return {
|
|
2501
|
-
blocked: field,
|
|
2502
|
-
// noseShift/mouthShift are MAGNITUDES too (see the correction
|
|
2503
|
-
// above), so a negative lower bound here would advertise a value
|
|
2504
|
-
// the field's own contract already rules out; 0 is the true floor.
|
|
2505
|
-
attainable: [
|
|
2506
|
-
Math.max(
|
|
2507
|
-
0,
|
|
2508
|
-
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
2509
|
-
),
|
|
2510
|
-
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
2511
|
-
]
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
clamped.push(field);
|
|
2515
|
-
}
|
|
2516
|
-
solved[field] = solution.depth;
|
|
2517
|
-
}
|
|
2809
|
+
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
2810
|
+
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
2811
|
+
silhouetteCenterShift - (measured ?? Math.abs(share * eyeShift)) * ctx.hh,
|
|
2812
|
+
marks,
|
|
2813
|
+
unit,
|
|
2814
|
+
mapAt,
|
|
2815
|
+
ctx.plateEdgeX
|
|
2816
|
+
);
|
|
2518
2817
|
return {
|
|
2519
|
-
nose:
|
|
2520
|
-
mouth:
|
|
2521
|
-
clamped
|
|
2818
|
+
nose: solve(ctx.noseShift, ctx.landmarks.nose, NOSE_SHIFT_SHARE),
|
|
2819
|
+
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2522
2820
|
};
|
|
2523
2821
|
}
|
|
2524
|
-
function solveTurnModel(targets, landmarks,
|
|
2822
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
2525
2823
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2526
2824
|
throw new TurnTargetError(
|
|
2527
2825
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2528
2826
|
);
|
|
2529
2827
|
}
|
|
2530
|
-
const eyeRowY =
|
|
2531
|
-
const hairFrontLeadFraction = hairFront === void 0 || eyeRowY === void 0 ? void 0 : Math.max(
|
|
2532
|
-
0,
|
|
2533
|
-
Math.min(
|
|
2534
|
-
1,
|
|
2535
|
-
(hairFront.centerY + hairFront.cropH / 2 - eyeRowY) / hairFront.cropH
|
|
2536
|
-
)
|
|
2537
|
-
);
|
|
2828
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
2538
2829
|
const ctx = {
|
|
2539
2830
|
landmarks,
|
|
2540
|
-
|
|
2831
|
+
lattice,
|
|
2541
2832
|
faceCenterX,
|
|
2833
|
+
faceCenterY,
|
|
2834
|
+
nodRadius,
|
|
2835
|
+
eyeRowY,
|
|
2542
2836
|
faceHalfWidth,
|
|
2837
|
+
// The slide's hard stop is the CROP's edge, profile or not: the far eye
|
|
2838
|
+
// may sit on the plate's transparent margin, as it does on the hero.
|
|
2543
2839
|
plateEdgeX: faceCenterX - faceHalfWidth,
|
|
2840
|
+
carriers,
|
|
2841
|
+
profile,
|
|
2842
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
2544
2843
|
// What this plate asks the turn for; each candidate caps it to its own
|
|
2545
2844
|
// shell — see TurnSolveContext.travel.
|
|
2546
2845
|
travel: headTurnTravel(faceHalfWidth),
|
|
@@ -2553,9 +2852,10 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2553
2852
|
// every downstream use already has the magnitude.
|
|
2554
2853
|
eyeShift: Math.abs(targets.eyeShift),
|
|
2555
2854
|
clampEyeShift: targets.defaulted.has("eyeShift"),
|
|
2855
|
+
noseShift: targets.defaulted.has("noseShift") ? void 0 : Math.abs(targets.noseShift),
|
|
2856
|
+
mouthShift: targets.defaulted.has("mouthShift") ? void 0 : Math.abs(targets.mouthShift),
|
|
2556
2857
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2557
|
-
|
|
2558
|
-
hairFrontSilhouette: hairFront === void 0 ? void 0 : { x: hairFront.x, cropW: hairFront.cropW, cropH: hairFront.cropH },
|
|
2858
|
+
hairFrontSilhouette: hairFront,
|
|
2559
2859
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2560
2860
|
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
2561
2861
|
};
|
|
@@ -2563,6 +2863,12 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2563
2863
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2564
2864
|
const pass = sweepTurnRadii(ctx);
|
|
2565
2865
|
if (pass.candidates.length === 0) {
|
|
2866
|
+
if (pass.unsettled > 0) {
|
|
2867
|
+
const refused = TURN_SWEEP_SAMPLES - pass.unsettled;
|
|
2868
|
+
throw new TurnTargetError(
|
|
2869
|
+
`auto-rig: headEdges: no sampled radius carries the turn \u2014 the silhouette and the depths its headEdges owners ride on did not settle within ${TURN_SETTLE_PASSES} passes at ${pass.unsettled} of the ${TURN_SWEEP_SAMPLES} radii${refused > 0 ? `, and the targets refused the other ${refused}` : ""}`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2566
2872
|
return pass.offeredShift ? {
|
|
2567
2873
|
unreachable: true,
|
|
2568
2874
|
field: "eyeShift",
|
|
@@ -2624,14 +2930,29 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2624
2930
|
if (!best.eye.reached) clamped.push("eyeShift");
|
|
2625
2931
|
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
2626
2932
|
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2933
|
+
for (const [field, solution] of [
|
|
2934
|
+
["noseShift", best.nose],
|
|
2935
|
+
["mouthShift", best.mouth]
|
|
2936
|
+
]) {
|
|
2937
|
+
if (solution.reached) continue;
|
|
2938
|
+
if (!targets.defaulted.has(field)) {
|
|
2939
|
+
return {
|
|
2940
|
+
unreachable: true,
|
|
2941
|
+
field,
|
|
2942
|
+
value: targets[field],
|
|
2943
|
+
// noseShift/mouthShift are MAGNITUDES too (see solveFeatureDepths),
|
|
2944
|
+
// so a negative lower bound here would advertise a value the field's
|
|
2945
|
+
// own contract already rules out; 0 is the true floor.
|
|
2946
|
+
attainable: [
|
|
2947
|
+
Math.max(
|
|
2948
|
+
0,
|
|
2949
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
2950
|
+
),
|
|
2951
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
2952
|
+
]
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
clamped.push(field);
|
|
2635
2956
|
}
|
|
2636
2957
|
return {
|
|
2637
2958
|
unreachable: false,
|
|
@@ -2639,13 +2960,18 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2639
2960
|
holdBase: best.holdBase,
|
|
2640
2961
|
holdEdgeAt: best.holdEdgeAt,
|
|
2641
2962
|
travel: best.travel,
|
|
2642
|
-
|
|
2963
|
+
surface: best.surface,
|
|
2964
|
+
depths: {
|
|
2965
|
+
eye: best.eye.depth,
|
|
2966
|
+
nose: best.nose.depth,
|
|
2967
|
+
mouth: best.mouth.depth
|
|
2968
|
+
},
|
|
2643
2969
|
achieved: {
|
|
2644
2970
|
eyeShift,
|
|
2645
2971
|
farEyeRatio: best.ratio,
|
|
2646
2972
|
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2647
2973
|
},
|
|
2648
|
-
clamped
|
|
2974
|
+
clamped
|
|
2649
2975
|
};
|
|
2650
2976
|
}
|
|
2651
2977
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
@@ -2682,27 +3008,16 @@ function featureParallaxBindings(role, options) {
|
|
|
2682
3008
|
if (!options.hasNose) return [];
|
|
2683
3009
|
const nodDepth = FEATURE_NOD_DEPTH[roleFamily(role)];
|
|
2684
3010
|
if (nodDepth === void 0) return [];
|
|
2685
|
-
const bindings = [];
|
|
2686
|
-
const turnDepth = options.turnDepths?.[turnFamily(role)] ?? 0;
|
|
2687
|
-
const shiftX = turnDepth * (options.parallaxUnit ?? 0);
|
|
2688
|
-
if (shiftX !== 0) {
|
|
2689
|
-
bindings.push({
|
|
2690
|
-
parameter: import_format3.StandardParameter.AngleX,
|
|
2691
|
-
channel: "translateX",
|
|
2692
|
-
from: -shiftX,
|
|
2693
|
-
to: shiftX
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
3011
|
const shiftY = nodDepth * (options.parallaxUnitY ?? 0);
|
|
2697
|
-
if (shiftY
|
|
2698
|
-
|
|
3012
|
+
if (shiftY === 0) return [];
|
|
3013
|
+
return [
|
|
3014
|
+
{
|
|
2699
3015
|
parameter: import_format3.StandardParameter.AngleY,
|
|
2700
3016
|
channel: "translateY",
|
|
2701
3017
|
from: -shiftY,
|
|
2702
3018
|
to: shiftY
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
return bindings;
|
|
3019
|
+
}
|
|
3020
|
+
];
|
|
2706
3021
|
}
|
|
2707
3022
|
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
2708
3023
|
return [
|
|
@@ -2810,20 +3125,17 @@ function roleOwnBindings(spec, role, cropW, cropH, options) {
|
|
|
2810
3125
|
];
|
|
2811
3126
|
}
|
|
2812
3127
|
}
|
|
2813
|
-
if (role === "
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
if (shiftY !== 0) {
|
|
2819
|
-
parallax.push({
|
|
3128
|
+
if (role === "hair_back") {
|
|
3129
|
+
const shiftY = HAIR_BACK_NOD_DEPTH * (options.parallaxUnitY ?? 0);
|
|
3130
|
+
if (shiftY === 0) return [];
|
|
3131
|
+
return [
|
|
3132
|
+
{
|
|
2820
3133
|
parameter: import_format3.StandardParameter.AngleY,
|
|
2821
3134
|
channel: "translateY",
|
|
2822
3135
|
from: -shiftY,
|
|
2823
3136
|
to: shiftY
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
return parallax;
|
|
3137
|
+
}
|
|
3138
|
+
];
|
|
2827
3139
|
}
|
|
2828
3140
|
return [];
|
|
2829
3141
|
}
|
|
@@ -2846,64 +3158,98 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
2846
3158
|
]
|
|
2847
3159
|
};
|
|
2848
3160
|
}
|
|
2849
|
-
function
|
|
3161
|
+
function bakeNodWarp(mesh, partY, surface, nodTravel) {
|
|
3162
|
+
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
3163
|
+
const t = nodTravel * deg / HEAD_TURN_MAX_DEG;
|
|
3164
|
+
const offsets = [];
|
|
3165
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
3166
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3167
|
+
offsets.push(0, deg === 0 ? 0 : t + surface.nodBendAt(y + t, deg));
|
|
3168
|
+
}
|
|
3169
|
+
return { value: deg, offsets };
|
|
3170
|
+
});
|
|
3171
|
+
return { parameter: import_format3.StandardParameter.AngleY, keyforms };
|
|
3172
|
+
}
|
|
3173
|
+
function plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) {
|
|
2850
3174
|
let reach = 0;
|
|
2851
3175
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2852
3176
|
reach = Math.max(
|
|
2853
3177
|
reach,
|
|
2854
|
-
plateReachAt(
|
|
3178
|
+
plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
2855
3179
|
);
|
|
2856
3180
|
}
|
|
2857
3181
|
return reach;
|
|
2858
3182
|
}
|
|
2859
|
-
function
|
|
3183
|
+
function plateSideReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg, side) {
|
|
2860
3184
|
let reach = 0;
|
|
2861
|
-
for (const
|
|
2862
|
-
const dest =
|
|
3185
|
+
for (const y of plateGuardRows) {
|
|
3186
|
+
const dest = plateLandingAt(deg, faceCenterX + side * edgeAt(y), y);
|
|
2863
3187
|
reach = Math.max(reach, Math.abs(dest - faceCenterX));
|
|
2864
3188
|
}
|
|
2865
3189
|
return reach;
|
|
2866
3190
|
}
|
|
2867
|
-
function
|
|
3191
|
+
function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg) {
|
|
3192
|
+
return Math.max(
|
|
3193
|
+
plateSideReachAt(
|
|
3194
|
+
plateLandingAt,
|
|
3195
|
+
faceCenterX,
|
|
3196
|
+
edgeAt,
|
|
3197
|
+
plateGuardRows,
|
|
3198
|
+
deg,
|
|
3199
|
+
-1
|
|
3200
|
+
),
|
|
3201
|
+
plateSideReachAt(
|
|
3202
|
+
plateLandingAt,
|
|
3203
|
+
faceCenterX,
|
|
3204
|
+
edgeAt,
|
|
3205
|
+
plateGuardRows,
|
|
3206
|
+
deg,
|
|
3207
|
+
1
|
|
3208
|
+
)
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
2868
3212
|
if (holdEdgeAt(0) !== holdBase) {
|
|
2869
3213
|
throw new Error(
|
|
2870
3214
|
`auto-rig: bakeHairFrontSilhouetteWarp: holdEdgeAt(0) is ${holdEdgeAt(0)}, not the hold edge's own rest distance ${holdBase}, so the bangs would move in the rest pose`
|
|
2871
3215
|
);
|
|
2872
3216
|
}
|
|
2873
|
-
const SIDES = [-1, 1];
|
|
2874
3217
|
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
2875
|
-
const
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
3218
|
+
const holdEdge = holdEdgeAt(deg);
|
|
3219
|
+
const reach = [-1, 1].map(
|
|
3220
|
+
(side) => plateSideReachAt(
|
|
3221
|
+
plateLandingAt,
|
|
3222
|
+
faceCenterX,
|
|
3223
|
+
edgeAt,
|
|
3224
|
+
plateGuardRows,
|
|
3225
|
+
deg,
|
|
3226
|
+
side
|
|
3227
|
+
)
|
|
2879
3228
|
);
|
|
2880
|
-
const reached =
|
|
2881
|
-
if (
|
|
2882
|
-
const side =
|
|
3229
|
+
const reached = Math.max(reach[0], reach[1]);
|
|
3230
|
+
if (holdEdge <= reached) {
|
|
3231
|
+
const side = reach[1] > reach[0] ? 1 : -1;
|
|
2883
3232
|
throw new Error(
|
|
2884
|
-
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${
|
|
3233
|
+
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${holdEdge} from the face centre but the plate's edge maps to ${reached}, so the ramp between them would fold`
|
|
2885
3234
|
);
|
|
2886
3235
|
}
|
|
2887
|
-
const holdEdge = deg === 0 ? [requestedHoldEdge, requestedHoldEdge] : SIDES.map(
|
|
2888
|
-
(_side, idx) => Math.min(
|
|
2889
|
-
requestedHoldEdge,
|
|
2890
|
-
Math.abs(
|
|
2891
|
-
map.warpedX[idx === 0 ? 0 : map.warpedX.length - 1] - faceCenterX
|
|
2892
|
-
) - HOLD_CLEARANCE
|
|
2893
|
-
)
|
|
2894
|
-
);
|
|
2895
3236
|
const offsets = [];
|
|
2896
3237
|
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2897
3238
|
const x = partX + mesh.vertices[i];
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3239
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3240
|
+
offsets.push(
|
|
3241
|
+
deg === 0 ? 0 : hairFrontHoldTarget(
|
|
3242
|
+
x,
|
|
3243
|
+
y,
|
|
3244
|
+
deg,
|
|
3245
|
+
faceCenterX,
|
|
3246
|
+
plateLandingAt,
|
|
3247
|
+
edgeAt,
|
|
3248
|
+
holdBase,
|
|
3249
|
+
holdEdge
|
|
3250
|
+
) - x,
|
|
3251
|
+
0
|
|
2905
3252
|
);
|
|
2906
|
-
offsets.push(map.invertX(target) - x, 0);
|
|
2907
3253
|
}
|
|
2908
3254
|
return { value: deg, offsets };
|
|
2909
3255
|
});
|
|
@@ -2912,7 +3258,6 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, faceCenterX, faceHalfWidth, ho
|
|
|
2912
3258
|
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2913
3259
|
var HAIR_SWAY_CURL = 1.5;
|
|
2914
3260
|
var HAIR_SWAY_RANGE = 20;
|
|
2915
|
-
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2916
3261
|
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2917
3262
|
let top = -Infinity;
|
|
2918
3263
|
let bottom = Infinity;
|
|
@@ -2942,112 +3287,378 @@ function headTurnTravel(faceHalfWidth) {
|
|
|
2942
3287
|
var BODY_TURN_FOLLOW = 0.3;
|
|
2943
3288
|
var HEAD_BREATH_BOB = -12;
|
|
2944
3289
|
var BODY_BREATH_FOLLOW = 0.5;
|
|
3290
|
+
var STANDARD_PARAMETERS = [
|
|
3291
|
+
{
|
|
3292
|
+
id: import_format3.StandardParameter.MouthOpen,
|
|
3293
|
+
name: "Mouth Open",
|
|
3294
|
+
min: 0,
|
|
3295
|
+
max: 1,
|
|
3296
|
+
default: 0
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
id: import_format3.StandardParameter.MouthForm,
|
|
3300
|
+
name: "Mouth Form",
|
|
3301
|
+
min: -1,
|
|
3302
|
+
max: 1,
|
|
3303
|
+
default: 0
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
id: import_format3.StandardParameter.EyeOpenLeft,
|
|
3307
|
+
name: "Eye L",
|
|
3308
|
+
min: 0,
|
|
3309
|
+
max: 1,
|
|
3310
|
+
default: 1
|
|
3311
|
+
},
|
|
3312
|
+
{
|
|
3313
|
+
id: import_format3.StandardParameter.EyeOpenRight,
|
|
3314
|
+
name: "Eye R",
|
|
3315
|
+
min: 0,
|
|
3316
|
+
max: 1,
|
|
3317
|
+
default: 1
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
id: import_format3.StandardParameter.EyeballX,
|
|
3321
|
+
name: "Gaze X",
|
|
3322
|
+
min: -1,
|
|
3323
|
+
max: 1,
|
|
3324
|
+
default: 0
|
|
3325
|
+
},
|
|
3326
|
+
{
|
|
3327
|
+
id: import_format3.StandardParameter.EyeballY,
|
|
3328
|
+
name: "Gaze Y",
|
|
3329
|
+
min: -1,
|
|
3330
|
+
max: 1,
|
|
3331
|
+
default: 0
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
id: import_format3.StandardParameter.AngleX,
|
|
3335
|
+
name: "Head Angle",
|
|
3336
|
+
min: -30,
|
|
3337
|
+
max: 30,
|
|
3338
|
+
default: 0
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
id: import_format3.StandardParameter.AngleY,
|
|
3342
|
+
name: "Head Angle Y",
|
|
3343
|
+
min: -30,
|
|
3344
|
+
max: 30,
|
|
3345
|
+
default: 0
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
id: import_format3.StandardParameter.AngleZ,
|
|
3349
|
+
name: "Head Angle Z",
|
|
3350
|
+
min: -30,
|
|
3351
|
+
max: 30,
|
|
3352
|
+
default: 0
|
|
3353
|
+
},
|
|
3354
|
+
{
|
|
3355
|
+
id: import_format3.StandardParameter.Breath,
|
|
3356
|
+
name: "Breath",
|
|
3357
|
+
min: 0,
|
|
3358
|
+
max: 1,
|
|
3359
|
+
default: 0
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
id: import_format3.StandardParameter.BrowLeftY,
|
|
3363
|
+
name: "Brow L Y",
|
|
3364
|
+
min: -1,
|
|
3365
|
+
max: 1,
|
|
3366
|
+
default: 0
|
|
3367
|
+
},
|
|
3368
|
+
{
|
|
3369
|
+
id: import_format3.StandardParameter.BrowRightY,
|
|
3370
|
+
name: "Brow R Y",
|
|
3371
|
+
min: -1,
|
|
3372
|
+
max: 1,
|
|
3373
|
+
default: 0
|
|
3374
|
+
},
|
|
3375
|
+
{
|
|
3376
|
+
id: import_format3.StandardParameter.BrowLeftAngle,
|
|
3377
|
+
name: "Brow L Angle",
|
|
3378
|
+
min: -1,
|
|
3379
|
+
max: 1,
|
|
3380
|
+
default: 0
|
|
3381
|
+
},
|
|
3382
|
+
{
|
|
3383
|
+
id: import_format3.StandardParameter.BrowRightAngle,
|
|
3384
|
+
name: "Brow R Angle",
|
|
3385
|
+
min: -1,
|
|
3386
|
+
max: 1,
|
|
3387
|
+
default: 0
|
|
3388
|
+
}
|
|
3389
|
+
];
|
|
3390
|
+
function transformUnder(bindings, valueOf) {
|
|
3391
|
+
const trs = { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1 };
|
|
3392
|
+
bindings.forEach((binding, i) => {
|
|
3393
|
+
const value = valueOf(binding, i);
|
|
3394
|
+
switch (binding.channel) {
|
|
3395
|
+
case "translateX":
|
|
3396
|
+
trs.x += value;
|
|
3397
|
+
break;
|
|
3398
|
+
case "translateY":
|
|
3399
|
+
trs.y += value;
|
|
3400
|
+
break;
|
|
3401
|
+
case "rotate":
|
|
3402
|
+
trs.rotation += value;
|
|
3403
|
+
break;
|
|
3404
|
+
case "scaleX":
|
|
3405
|
+
trs.scaleX += value;
|
|
3406
|
+
break;
|
|
3407
|
+
case "scaleY":
|
|
3408
|
+
trs.scaleY += value;
|
|
3409
|
+
break;
|
|
3410
|
+
case "opacity":
|
|
3411
|
+
break;
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
return trs;
|
|
3415
|
+
}
|
|
3416
|
+
function defaultPoseOf(bindings) {
|
|
3417
|
+
return transformUnder(bindings, (binding) => {
|
|
3418
|
+
const p = STANDARD_PARAMETERS.find((p2) => p2.id === binding.parameter);
|
|
3419
|
+
if (p === void 0) {
|
|
3420
|
+
throw new Error(
|
|
3421
|
+
`auto-rig: defaultPoseOf: "${binding.parameter}" is not a standard parameter`
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
const t = (p.default - p.min) / (p.max - p.min);
|
|
3425
|
+
return binding.from + (binding.to - binding.from) * t;
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
3429
|
+
let minX = Infinity;
|
|
3430
|
+
let maxX = -Infinity;
|
|
3431
|
+
let minY = Infinity;
|
|
3432
|
+
let maxY = -Infinity;
|
|
3433
|
+
for (const member of members) {
|
|
3434
|
+
const rest = member.mesh.vertices;
|
|
3435
|
+
const shapes = [rest];
|
|
3436
|
+
for (const warp of member.warps) {
|
|
3437
|
+
for (const keyform of warp.keyforms) {
|
|
3438
|
+
shapes.push(rest.map((v, i) => v + keyform.offsets[i]));
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
const geometric = member.bindings.filter((b) => b.channel !== "opacity");
|
|
3442
|
+
for (let corner = 0; corner < 1 << geometric.length; corner++) {
|
|
3443
|
+
const trs = transformUnder(
|
|
3444
|
+
geometric,
|
|
3445
|
+
(b, i) => (corner & 1 << i) === 0 ? b.from : b.to
|
|
3446
|
+
);
|
|
3447
|
+
const cos = Math.cos(trs.rotation * (Math.PI / 180));
|
|
3448
|
+
const sin = Math.sin(trs.rotation * (Math.PI / 180));
|
|
3449
|
+
for (const shape of shapes) {
|
|
3450
|
+
for (let i = 0; i < shape.length; i += 2) {
|
|
3451
|
+
const sx = shape[i] * trs.scaleX;
|
|
3452
|
+
const sy = shape[i + 1] * trs.scaleY;
|
|
3453
|
+
const x = member.transform.x + trs.x + cos * sx - sin * sy;
|
|
3454
|
+
const y = member.transform.y + trs.y + sin * sx + cos * sy;
|
|
3455
|
+
minX = Math.min(minX, x);
|
|
3456
|
+
maxX = Math.max(maxX, x);
|
|
3457
|
+
minY = Math.min(minY, y);
|
|
3458
|
+
maxY = Math.max(maxY, y);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
minX -= turnShiftPx;
|
|
3464
|
+
maxX += turnShiftPx;
|
|
3465
|
+
minY -= nodShiftPx;
|
|
3466
|
+
maxY += nodShiftPx;
|
|
3467
|
+
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3468
|
+
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3469
|
+
return {
|
|
3470
|
+
cols: cells,
|
|
3471
|
+
rows: cells,
|
|
3472
|
+
points: generateGridPoints(
|
|
3473
|
+
cells,
|
|
3474
|
+
cells,
|
|
3475
|
+
minX - marginX,
|
|
3476
|
+
maxX + marginX,
|
|
3477
|
+
minY - marginY,
|
|
3478
|
+
maxY + marginY
|
|
3479
|
+
)
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3483
|
+
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3484
|
+
const isLash = layer.role.startsWith("lash_");
|
|
3485
|
+
if (side === void 0 || !(layer.role.startsWith("eye_") || isLash)) {
|
|
3486
|
+
return void 0;
|
|
3487
|
+
}
|
|
3488
|
+
const white = layers.find((l) => l.role === `eye_${side}`);
|
|
3489
|
+
const creaseWorldY = bboxToTransform(white.bbox, white.canvasW, white.canvasH, white.role).y - EYELID_FOLD_CREASE * white.cropH;
|
|
3490
|
+
const hasLash = layers.some((l) => l.role === `lash_${side}`);
|
|
3491
|
+
return bakeEyelidFoldWarp(
|
|
3492
|
+
mesh,
|
|
3493
|
+
side === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight,
|
|
3494
|
+
creaseWorldY - t.y,
|
|
3495
|
+
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
function turnSetup(layers) {
|
|
3499
|
+
const faceLayer = layers.find((l) => l.role === "face");
|
|
3500
|
+
const faceTransform = bboxToTransform(
|
|
3501
|
+
faceLayer.bbox,
|
|
3502
|
+
faceLayer.canvasW,
|
|
3503
|
+
faceLayer.canvasH,
|
|
3504
|
+
"face"
|
|
3505
|
+
);
|
|
3506
|
+
const faceCenterX = faceTransform.x;
|
|
3507
|
+
const faceCenterY = faceTransform.y;
|
|
3508
|
+
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3509
|
+
const plateEdgeX = faceCenterX - faceHalfWidth;
|
|
3510
|
+
const hasNose = layers.some((l) => l.role === "nose");
|
|
3511
|
+
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
3512
|
+
const turnLayers = layers.filter(
|
|
3513
|
+
(l) => isTurnGroup(ROLE_TABLE[l.role].deformer) || l.role === "hair_front"
|
|
3514
|
+
);
|
|
3515
|
+
const turnYs = turnLayers.map((l) => ({
|
|
3516
|
+
y: bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role).y,
|
|
3517
|
+
h: l.cropH
|
|
3518
|
+
}));
|
|
3519
|
+
let unionMinY = Math.min(...turnYs.map(({ y, h }) => y - h / 2));
|
|
3520
|
+
let unionMaxY = Math.max(...turnYs.map(({ y, h }) => y + h / 2));
|
|
3521
|
+
const spanY = unionMaxY - unionMinY;
|
|
3522
|
+
unionMinY -= spanY * GRID_MARGIN;
|
|
3523
|
+
unionMaxY += spanY * GRID_MARGIN;
|
|
3524
|
+
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3525
|
+
const nodRadius = halfH * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
3526
|
+
const parallaxUnitY = headNodParallaxUnit(halfH);
|
|
3527
|
+
const members = [];
|
|
3528
|
+
for (const layer of layers) {
|
|
3529
|
+
const spec = ROLE_TABLE[layer.role];
|
|
3530
|
+
if (!isTurnGroup(spec.deformer)) continue;
|
|
3531
|
+
const { role, cropW, cropH } = layer;
|
|
3532
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
3533
|
+
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3534
|
+
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3535
|
+
const bindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3536
|
+
hasMouthOpen
|
|
3537
|
+
});
|
|
3538
|
+
const fold = eyelidFoldFor(layers, layer, mesh, t);
|
|
3539
|
+
const pose = defaultPoseOf(bindings);
|
|
3540
|
+
members.push({
|
|
3541
|
+
role,
|
|
3542
|
+
group: spec.deformer,
|
|
3543
|
+
transform: t,
|
|
3544
|
+
part: {
|
|
3545
|
+
x: t.x + pose.x,
|
|
3546
|
+
y: t.y + pose.y,
|
|
3547
|
+
cropW,
|
|
3548
|
+
cropH,
|
|
3549
|
+
cols,
|
|
3550
|
+
rows,
|
|
3551
|
+
scaleX: pose.scaleX,
|
|
3552
|
+
scaleY: pose.scaleY,
|
|
3553
|
+
rotation: pose.rotation
|
|
3554
|
+
},
|
|
3555
|
+
mesh,
|
|
3556
|
+
bindings,
|
|
3557
|
+
warps: fold === void 0 ? [] : [fold]
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
const landmarks = turnLandmarks(layers);
|
|
3561
|
+
const turnShiftOf = (marks) => hasNose && marks.length > 0 ? familyReachPx(marks, plateEdgeX) : 0;
|
|
3562
|
+
const turnShift = {
|
|
3563
|
+
eye: turnShiftOf(landmarks.eye),
|
|
3564
|
+
nose: turnShiftOf(landmarks.nose),
|
|
3565
|
+
mouth: turnShiftOf(landmarks.mouth)
|
|
3566
|
+
};
|
|
3567
|
+
const groupGrids = /* @__PURE__ */ new Map();
|
|
3568
|
+
for (const { group, role } of members) {
|
|
3569
|
+
if (groupGrids.has(group)) continue;
|
|
3570
|
+
const own = members.filter((m) => m.group === group);
|
|
3571
|
+
const isPlate = group === "faceWarp";
|
|
3572
|
+
const nodShiftPx = hasNose ? Math.max(
|
|
3573
|
+
...own.map((m) => FEATURE_NOD_DEPTH[roleFamily(m.role)] ?? 0)
|
|
3574
|
+
) * parallaxUnitY : 0;
|
|
3575
|
+
groupGrids.set(
|
|
3576
|
+
group,
|
|
3577
|
+
groupGridFor(
|
|
3578
|
+
own,
|
|
3579
|
+
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3580
|
+
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3581
|
+
nodShiftPx
|
|
3582
|
+
)
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
const carriers = new Map(
|
|
3586
|
+
members.map((m) => [
|
|
3587
|
+
m.role,
|
|
3588
|
+
{ grid: groupGrids.get(m.group), part: m.part }
|
|
3589
|
+
])
|
|
3590
|
+
);
|
|
3591
|
+
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3592
|
+
let reach = 0;
|
|
3593
|
+
for (const grid of groupGrids.values()) {
|
|
3594
|
+
reach = Math.max(
|
|
3595
|
+
reach,
|
|
3596
|
+
faceCenterX - grid.points[0] + maxShift,
|
|
3597
|
+
grid.points[grid.cols * 2] - faceCenterX + maxShift
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
const halfCells = Math.floor(reach / TURN_LATTICE_CELL_PX) + 1;
|
|
3601
|
+
const points = [];
|
|
3602
|
+
for (const y of [unionMaxY, unionMinY]) {
|
|
3603
|
+
for (let k = -halfCells; k <= halfCells; k++) {
|
|
3604
|
+
points.push(faceCenterX + k * TURN_LATTICE_CELL_PX, y);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
const lattice = { cols: 2 * halfCells, rows: 1, points };
|
|
3608
|
+
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3609
|
+
const hairFront = hairFrontLayer && (() => {
|
|
3610
|
+
const t = bboxToTransform(
|
|
3611
|
+
hairFrontLayer.bbox,
|
|
3612
|
+
hairFrontLayer.canvasW,
|
|
3613
|
+
hairFrontLayer.canvasH,
|
|
3614
|
+
"hair_front"
|
|
3615
|
+
);
|
|
3616
|
+
return {
|
|
3617
|
+
x: t.x,
|
|
3618
|
+
centerY: t.y,
|
|
3619
|
+
cropW: hairFrontLayer.cropW,
|
|
3620
|
+
cropH: hairFrontLayer.cropH
|
|
3621
|
+
};
|
|
3622
|
+
})();
|
|
3623
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3624
|
+
const profile = faceRowProfile(faceLayer, faceCenterY);
|
|
3625
|
+
return {
|
|
3626
|
+
landmarks: turnLandmarks(layers, carriers),
|
|
3627
|
+
lattice,
|
|
3628
|
+
faceCenterX,
|
|
3629
|
+
faceHalfWidth,
|
|
3630
|
+
faceCenterY,
|
|
3631
|
+
nodRadius,
|
|
3632
|
+
carriers,
|
|
3633
|
+
profile,
|
|
3634
|
+
hairFront,
|
|
3635
|
+
eyeRowY,
|
|
3636
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
3637
|
+
members,
|
|
3638
|
+
groupGrids,
|
|
3639
|
+
parallaxUnitY,
|
|
3640
|
+
hasNose,
|
|
3641
|
+
hasMouthOpen
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function solveInputsOf(s) {
|
|
3645
|
+
return [
|
|
3646
|
+
s.landmarks,
|
|
3647
|
+
s.lattice,
|
|
3648
|
+
s.faceCenterX,
|
|
3649
|
+
s.faceHalfWidth,
|
|
3650
|
+
s.faceCenterY,
|
|
3651
|
+
s.nodRadius,
|
|
3652
|
+
s.carriers,
|
|
3653
|
+
s.profile,
|
|
3654
|
+
s.hairFront
|
|
3655
|
+
];
|
|
3656
|
+
}
|
|
2945
3657
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
2946
3658
|
validateLayerInputs(layers, canvas);
|
|
2947
3659
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2948
|
-
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2949
|
-
const hasNose = layers.some((l) => l.role === "nose");
|
|
2950
3660
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2951
|
-
const parameters = [
|
|
2952
|
-
{
|
|
2953
|
-
id: import_format3.StandardParameter.MouthOpen,
|
|
2954
|
-
name: "Mouth Open",
|
|
2955
|
-
min: 0,
|
|
2956
|
-
max: 1,
|
|
2957
|
-
default: 0
|
|
2958
|
-
},
|
|
2959
|
-
{
|
|
2960
|
-
id: import_format3.StandardParameter.MouthForm,
|
|
2961
|
-
name: "Mouth Form",
|
|
2962
|
-
min: -1,
|
|
2963
|
-
max: 1,
|
|
2964
|
-
default: 0
|
|
2965
|
-
},
|
|
2966
|
-
{
|
|
2967
|
-
id: import_format3.StandardParameter.EyeOpenLeft,
|
|
2968
|
-
name: "Eye L",
|
|
2969
|
-
min: 0,
|
|
2970
|
-
max: 1,
|
|
2971
|
-
default: 1
|
|
2972
|
-
},
|
|
2973
|
-
{
|
|
2974
|
-
id: import_format3.StandardParameter.EyeOpenRight,
|
|
2975
|
-
name: "Eye R",
|
|
2976
|
-
min: 0,
|
|
2977
|
-
max: 1,
|
|
2978
|
-
default: 1
|
|
2979
|
-
},
|
|
2980
|
-
{
|
|
2981
|
-
id: import_format3.StandardParameter.EyeballX,
|
|
2982
|
-
name: "Gaze X",
|
|
2983
|
-
min: -1,
|
|
2984
|
-
max: 1,
|
|
2985
|
-
default: 0
|
|
2986
|
-
},
|
|
2987
|
-
{
|
|
2988
|
-
id: import_format3.StandardParameter.EyeballY,
|
|
2989
|
-
name: "Gaze Y",
|
|
2990
|
-
min: -1,
|
|
2991
|
-
max: 1,
|
|
2992
|
-
default: 0
|
|
2993
|
-
},
|
|
2994
|
-
{
|
|
2995
|
-
id: import_format3.StandardParameter.AngleX,
|
|
2996
|
-
name: "Head Angle",
|
|
2997
|
-
min: -30,
|
|
2998
|
-
max: 30,
|
|
2999
|
-
default: 0
|
|
3000
|
-
},
|
|
3001
|
-
{
|
|
3002
|
-
id: import_format3.StandardParameter.AngleY,
|
|
3003
|
-
name: "Head Angle Y",
|
|
3004
|
-
min: -30,
|
|
3005
|
-
max: 30,
|
|
3006
|
-
default: 0
|
|
3007
|
-
},
|
|
3008
|
-
{
|
|
3009
|
-
id: import_format3.StandardParameter.AngleZ,
|
|
3010
|
-
name: "Head Angle Z",
|
|
3011
|
-
min: -30,
|
|
3012
|
-
max: 30,
|
|
3013
|
-
default: 0
|
|
3014
|
-
},
|
|
3015
|
-
{
|
|
3016
|
-
id: import_format3.StandardParameter.Breath,
|
|
3017
|
-
name: "Breath",
|
|
3018
|
-
min: 0,
|
|
3019
|
-
max: 1,
|
|
3020
|
-
default: 0
|
|
3021
|
-
},
|
|
3022
|
-
{
|
|
3023
|
-
id: import_format3.StandardParameter.BrowLeftY,
|
|
3024
|
-
name: "Brow L Y",
|
|
3025
|
-
min: -1,
|
|
3026
|
-
max: 1,
|
|
3027
|
-
default: 0
|
|
3028
|
-
},
|
|
3029
|
-
{
|
|
3030
|
-
id: import_format3.StandardParameter.BrowRightY,
|
|
3031
|
-
name: "Brow R Y",
|
|
3032
|
-
min: -1,
|
|
3033
|
-
max: 1,
|
|
3034
|
-
default: 0
|
|
3035
|
-
},
|
|
3036
|
-
{
|
|
3037
|
-
id: import_format3.StandardParameter.BrowLeftAngle,
|
|
3038
|
-
name: "Brow L Angle",
|
|
3039
|
-
min: -1,
|
|
3040
|
-
max: 1,
|
|
3041
|
-
default: 0
|
|
3042
|
-
},
|
|
3043
|
-
{
|
|
3044
|
-
id: import_format3.StandardParameter.BrowRightAngle,
|
|
3045
|
-
name: "Brow R Angle",
|
|
3046
|
-
min: -1,
|
|
3047
|
-
max: 1,
|
|
3048
|
-
default: 0
|
|
3049
|
-
}
|
|
3050
|
-
];
|
|
3661
|
+
const parameters = [...STANDARD_PARAMETERS];
|
|
3051
3662
|
if (hasHair) {
|
|
3052
3663
|
parameters.push(
|
|
3053
3664
|
{
|
|
@@ -3066,86 +3677,28 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3066
3677
|
}
|
|
3067
3678
|
);
|
|
3068
3679
|
}
|
|
3069
|
-
const
|
|
3070
|
-
const
|
|
3071
|
-
|
|
3072
|
-
faceLayer.bbox,
|
|
3073
|
-
faceLayer.canvasW,
|
|
3074
|
-
faceLayer.canvasH,
|
|
3075
|
-
"face"
|
|
3076
|
-
);
|
|
3077
|
-
const faceCenterX = faceTransform.x;
|
|
3078
|
-
const faceCropH = faceLayer.cropH;
|
|
3079
|
-
const faceWarpLayers = layers.filter(
|
|
3080
|
-
(l) => ROLE_TABLE[l.role].deformer === "faceWarp"
|
|
3081
|
-
);
|
|
3082
|
-
let unionMinX = -canvas.width / 2;
|
|
3083
|
-
let unionMaxX = canvas.width / 2;
|
|
3084
|
-
let unionMinY = -canvas.height / 2;
|
|
3085
|
-
let unionMaxY = canvas.height / 2;
|
|
3086
|
-
if (faceWarpLayers.length > 0) {
|
|
3087
|
-
const transforms = faceWarpLayers.map(
|
|
3088
|
-
(l) => bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role)
|
|
3089
|
-
);
|
|
3090
|
-
unionMinX = Math.min(
|
|
3091
|
-
...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2)
|
|
3092
|
-
);
|
|
3093
|
-
unionMaxX = Math.max(
|
|
3094
|
-
...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2)
|
|
3095
|
-
);
|
|
3096
|
-
unionMinY = Math.min(
|
|
3097
|
-
...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2)
|
|
3098
|
-
);
|
|
3099
|
-
unionMaxY = Math.max(
|
|
3100
|
-
...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2)
|
|
3101
|
-
);
|
|
3102
|
-
const spanX = unionMaxX - unionMinX;
|
|
3103
|
-
const spanY = unionMaxY - unionMinY;
|
|
3104
|
-
const MARGIN = 0.12;
|
|
3105
|
-
unionMinX -= spanX * MARGIN;
|
|
3106
|
-
unionMaxX += spanX * MARGIN;
|
|
3107
|
-
unionMinY -= spanY * MARGIN;
|
|
3108
|
-
unionMaxY += spanY * MARGIN;
|
|
3109
|
-
}
|
|
3110
|
-
const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);
|
|
3111
|
-
const faceGridMinX = faceCenterX - halfW;
|
|
3112
|
-
const faceGridMaxX = faceCenterX + halfW;
|
|
3113
|
-
const faceGrid = {
|
|
3114
|
-
cols: FACE_GRID_CELLS,
|
|
3115
|
-
rows: FACE_GRID_CELLS,
|
|
3116
|
-
points: generateGridPoints(
|
|
3117
|
-
FACE_GRID_CELLS,
|
|
3118
|
-
FACE_GRID_CELLS,
|
|
3119
|
-
faceGridMinX,
|
|
3120
|
-
faceGridMaxX,
|
|
3121
|
-
unionMinY,
|
|
3122
|
-
unionMaxY
|
|
3123
|
-
)
|
|
3124
|
-
};
|
|
3125
|
-
const faceCenterY = faceTransform.y;
|
|
3126
|
-
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3127
|
-
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3128
|
-
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3129
|
-
const turn = hasNose ? solveTurnModel(
|
|
3130
|
-
resolveTurnTargets(options.turnTargets),
|
|
3131
|
-
turnLandmarks(layers),
|
|
3132
|
-
faceGrid,
|
|
3680
|
+
const setup = turnSetup(layers);
|
|
3681
|
+
const {
|
|
3682
|
+
lattice,
|
|
3133
3683
|
faceCenterX,
|
|
3134
3684
|
faceHalfWidth,
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3685
|
+
faceCenterY,
|
|
3686
|
+
nodRadius,
|
|
3687
|
+
profile,
|
|
3688
|
+
eyeRowY,
|
|
3689
|
+
plate,
|
|
3690
|
+
edgeAt,
|
|
3691
|
+
plateGuardRows,
|
|
3692
|
+
members,
|
|
3693
|
+
groupGrids,
|
|
3694
|
+
parallaxUnitY,
|
|
3695
|
+
hasNose,
|
|
3696
|
+
hasMouthOpen
|
|
3697
|
+
} = setup;
|
|
3698
|
+
const faceCropH = plate.part.cropH;
|
|
3699
|
+
const turn = hasNose ? solveTurnModel(
|
|
3700
|
+
resolveTurnTargets(options.turnTargets),
|
|
3701
|
+
...solveInputsOf(setup),
|
|
3149
3702
|
options.headEdges
|
|
3150
3703
|
) : void 0;
|
|
3151
3704
|
if (turn?.unreachable) {
|
|
@@ -3167,25 +3720,28 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3167
3720
|
clamped: turn.clamped
|
|
3168
3721
|
});
|
|
3169
3722
|
}
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
const
|
|
3723
|
+
const latticeReach = Math.max(
|
|
3724
|
+
faceCenterX - lattice.points[0],
|
|
3725
|
+
lattice.points[lattice.cols * 2] - faceCenterX
|
|
3726
|
+
);
|
|
3727
|
+
const surface = turn?.surface ?? turnSurface({
|
|
3728
|
+
faceCenterX,
|
|
3729
|
+
faceCenterY,
|
|
3730
|
+
radius: latticeReach * HEAD_CYLINDER_RADIUS_FACTOR,
|
|
3731
|
+
travel: headTurnTravel(faceHalfWidth),
|
|
3732
|
+
nodRadius,
|
|
3733
|
+
lattice,
|
|
3734
|
+
profile,
|
|
3735
|
+
eyeRowY
|
|
3736
|
+
});
|
|
3737
|
+
const parallaxUnit = headTurnParallaxUnit(surface.radius);
|
|
3738
|
+
const plateLandingAt = plateLandingOn(plate, rowMapsOf(surface));
|
|
3739
|
+
const faceBottom = faceCenterY - faceCropH / 2;
|
|
3175
3740
|
const neckPivot = {
|
|
3176
3741
|
x: faceCenterX,
|
|
3177
3742
|
y: faceBottom - faceCropH * 0.15
|
|
3178
3743
|
// 15% below face bottom = neck
|
|
3179
3744
|
};
|
|
3180
|
-
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
3181
|
-
faceGrid,
|
|
3182
|
-
import_format3.StandardParameter.AngleX,
|
|
3183
|
-
import_format3.StandardParameter.AngleY,
|
|
3184
|
-
faceCenterX,
|
|
3185
|
-
faceCenterY,
|
|
3186
|
-
faceRadius,
|
|
3187
|
-
headTravel
|
|
3188
|
-
);
|
|
3189
3745
|
const deformers = [
|
|
3190
3746
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
3191
3747
|
// neck pivot. Same bindings as sample-model.ts apart from the turn, which
|
|
@@ -3195,14 +3751,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3195
3751
|
pivot: neckPivot,
|
|
3196
3752
|
bindings: [
|
|
3197
3753
|
// No AngleX binding: the turn moves NOTHING rigidly. Its sideways
|
|
3198
|
-
// travel is baked into
|
|
3199
|
-
//
|
|
3200
|
-
//
|
|
3201
|
-
//
|
|
3202
|
-
//
|
|
3203
|
-
//
|
|
3204
|
-
//
|
|
3205
|
-
//
|
|
3754
|
+
// travel is baked into every group grid (the surface's `travel`, see
|
|
3755
|
+
// bakeTurnGroupWarp2D) so the face slides inside a silhouette the
|
|
3756
|
+
// bangs hold; a translate here would take the hair shell with it. Nor
|
|
3757
|
+
// is the turn a roll — the sample model's ±6° "lean into the turn" was
|
|
3758
|
+
// tried here and dropped, since rotating about the neck pivot swings
|
|
3759
|
+
// the crown (~500px above it) far more than the chin, so the top of
|
|
3760
|
+
// the head appeared to lunge ahead of the face on every turn. Roll is
|
|
3761
|
+
// AngleZ's job, below.
|
|
3206
3762
|
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
3207
3763
|
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
3208
3764
|
// poses, collapsing pitch into roll.
|
|
@@ -3231,19 +3787,25 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3231
3787
|
to: HEAD_BREATH_BOB
|
|
3232
3788
|
}
|
|
3233
3789
|
]
|
|
3234
|
-
},
|
|
3235
|
-
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
3236
|
-
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
3237
|
-
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
3238
|
-
// `warps` or `warp2d`, never both).
|
|
3239
|
-
{
|
|
3240
|
-
kind: "warp",
|
|
3241
|
-
id: "faceWarp",
|
|
3242
|
-
parent: "headDeformer",
|
|
3243
|
-
grid: faceGrid,
|
|
3244
|
-
warp2d: faceWarp2d
|
|
3245
3790
|
}
|
|
3246
3791
|
];
|
|
3792
|
+
for (const [group, grid] of groupGrids) {
|
|
3793
|
+
const family = turnFamily(members.find((m) => m.group === group).role);
|
|
3794
|
+
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
3795
|
+
deformers.push({
|
|
3796
|
+
kind: "warp",
|
|
3797
|
+
id: group,
|
|
3798
|
+
parent: "headDeformer",
|
|
3799
|
+
grid,
|
|
3800
|
+
warp2d: bakeTurnGroupWarp2D(
|
|
3801
|
+
grid,
|
|
3802
|
+
import_format3.StandardParameter.AngleX,
|
|
3803
|
+
import_format3.StandardParameter.AngleY,
|
|
3804
|
+
surface,
|
|
3805
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG
|
|
3806
|
+
)
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3247
3809
|
if (bodyLayer) {
|
|
3248
3810
|
const bt = bboxToTransform(
|
|
3249
3811
|
bodyLayer.bbox,
|
|
@@ -3260,8 +3822,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3260
3822
|
{
|
|
3261
3823
|
parameter: import_format3.StandardParameter.AngleX,
|
|
3262
3824
|
channel: "translateX",
|
|
3263
|
-
from: -BODY_TURN_FOLLOW *
|
|
3264
|
-
to: BODY_TURN_FOLLOW *
|
|
3825
|
+
from: -BODY_TURN_FOLLOW * surface.travel,
|
|
3826
|
+
to: BODY_TURN_FOLLOW * surface.travel
|
|
3265
3827
|
},
|
|
3266
3828
|
// Breath: follow the head's bob, same direction, half amplitude.
|
|
3267
3829
|
{
|
|
@@ -3273,34 +3835,21 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3273
3835
|
]
|
|
3274
3836
|
});
|
|
3275
3837
|
}
|
|
3276
|
-
const
|
|
3277
|
-
for (const layer of layers) {
|
|
3278
|
-
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3279
|
-
if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
|
|
3280
|
-
const ey = bboxToTransform(
|
|
3281
|
-
layer.bbox,
|
|
3282
|
-
layer.canvasW,
|
|
3283
|
-
layer.canvasH,
|
|
3284
|
-
layer.role
|
|
3285
|
-
).y;
|
|
3286
|
-
eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3838
|
+
const memberByRole = new Map(members.map((m) => [m.role, m]));
|
|
3289
3839
|
const parts = layers.map((layer) => {
|
|
3290
3840
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
3291
3841
|
const spec = ROLE_TABLE[role];
|
|
3292
3842
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
3293
3843
|
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3294
3844
|
hasMouthOpen,
|
|
3295
|
-
parallaxUnit,
|
|
3296
3845
|
parallaxUnitY,
|
|
3297
|
-
hasNose
|
|
3298
|
-
turnDepths: turn?.depths
|
|
3846
|
+
hasNose
|
|
3299
3847
|
});
|
|
3300
3848
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
3301
3849
|
if (spec.mesh) {
|
|
3850
|
+
const member = memberByRole.get(role);
|
|
3302
3851
|
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3303
|
-
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3852
|
+
const mesh = member?.mesh ?? createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3304
3853
|
const part = {
|
|
3305
3854
|
id: role,
|
|
3306
3855
|
color: [1, 1, 1, 1],
|
|
@@ -3315,35 +3864,38 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3315
3864
|
part.bindings = roleBindings;
|
|
3316
3865
|
}
|
|
3317
3866
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
let silhouetteWarp;
|
|
3867
|
+
const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
3868
|
+
const ownWarps = [];
|
|
3321
3869
|
if (role === "hair_front") {
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
3870
|
+
ownWarps.push(
|
|
3871
|
+
bakeHairSwayWarp(
|
|
3872
|
+
mesh,
|
|
3873
|
+
import_format3.StandardParameter.AngleX,
|
|
3874
|
+
HAIR_FRONT_DEPTH * parallaxUnit,
|
|
3875
|
+
HEAD_TURN_MAX_DEG
|
|
3876
|
+
)
|
|
3330
3877
|
);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3878
|
+
const holdBase = turn?.holdBase ?? plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) + HOLD_CLEARANCE;
|
|
3879
|
+
ownWarps.push(
|
|
3880
|
+
bakeHairFrontSilhouetteWarp(
|
|
3881
|
+
mesh,
|
|
3882
|
+
t.x,
|
|
3883
|
+
t.y,
|
|
3884
|
+
faceCenterX,
|
|
3885
|
+
plateLandingAt,
|
|
3886
|
+
edgeAt,
|
|
3887
|
+
holdBase,
|
|
3888
|
+
turn?.holdEdgeAt ?? (() => holdBase),
|
|
3889
|
+
plateGuardRows
|
|
3890
|
+
)
|
|
3336
3891
|
);
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
holdBase,
|
|
3345
|
-
turn?.holdEdgeAt ?? (() => holdBase),
|
|
3346
|
-
columnMapAt
|
|
3892
|
+
ownWarps.push(
|
|
3893
|
+
bakeNodWarp(
|
|
3894
|
+
mesh,
|
|
3895
|
+
t.y,
|
|
3896
|
+
surface,
|
|
3897
|
+
hasNose ? HAIR_FRONT_NOD_DEPTH * parallaxUnitY : 0
|
|
3898
|
+
)
|
|
3347
3899
|
);
|
|
3348
3900
|
}
|
|
3349
3901
|
part.warps = [
|
|
@@ -3359,24 +3911,12 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3359
3911
|
tipShift,
|
|
3360
3912
|
HAIR_SWAY_RANGE
|
|
3361
3913
|
),
|
|
3362
|
-
...
|
|
3363
|
-
...silhouetteWarp ? [silhouetteWarp] : []
|
|
3914
|
+
...ownWarps
|
|
3364
3915
|
];
|
|
3365
3916
|
}
|
|
3366
3917
|
if (spec.eyeSide !== void 0) {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
const openParam = spec.eyeSide === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight;
|
|
3370
|
-
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
3371
|
-
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
3372
|
-
part.warps = [
|
|
3373
|
-
bakeEyelidFoldWarp(
|
|
3374
|
-
mesh,
|
|
3375
|
-
openParam,
|
|
3376
|
-
creaseWorldY - t.y,
|
|
3377
|
-
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3378
|
-
)
|
|
3379
|
-
];
|
|
3918
|
+
if (role.startsWith("eye_") || role.startsWith("lash_")) {
|
|
3919
|
+
part.warps = [...member.warps];
|
|
3380
3920
|
} else {
|
|
3381
3921
|
part.clip = { masks: [`eye_${spec.eyeSide}`] };
|
|
3382
3922
|
}
|