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