@ikijs/editor 0.4.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 +34 -2
- package/dist/index.d.mts +141 -8
- package/dist/index.d.ts +141 -8
- package/dist/index.js +1739 -358
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1737 -358
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
AddPart: () => AddPart,
|
|
27
27
|
AddPhysicsRig: () => AddPhysicsRig,
|
|
28
28
|
CaptureGridKeyform: () => CaptureGridKeyform,
|
|
29
|
+
DEFAULT_TURN_TARGETS: () => DEFAULT_TURN_TARGETS,
|
|
29
30
|
DeleteDeformer: () => DeleteDeformer,
|
|
30
31
|
DeletePart: () => DeletePart,
|
|
31
32
|
DeletePhysicsRig: () => DeletePhysicsRig,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
SetPartTransform: () => SetPartTransform,
|
|
46
47
|
SetPartWidth: () => SetPartWidth,
|
|
47
48
|
SetPhysicsRig: () => SetPhysicsRig,
|
|
49
|
+
TurnTargetError: () => TurnTargetError,
|
|
48
50
|
UV_INSET_PX: () => UV_INSET_PX,
|
|
49
51
|
captureBindingEndpoint: () => captureBindingEndpoint,
|
|
50
52
|
computeGridOffsets: () => computeGridOffsets,
|
|
@@ -1662,11 +1664,27 @@ function createDefaultWarpDeformer(model) {
|
|
|
1662
1664
|
|
|
1663
1665
|
// src/auto-rig.ts
|
|
1664
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
|
+
}
|
|
1665
1682
|
var ROLE_TABLE = {
|
|
1666
1683
|
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1667
|
-
// whose grid it would stretch across the shoulders) as a MESH
|
|
1668
|
-
//
|
|
1669
|
-
//
|
|
1684
|
+
// whose grid it would stretch across the shoulders) as a MESH, so its ends
|
|
1685
|
+
// can swing on the hair-sway warps. It holds the head's outline STILL
|
|
1686
|
+
// through the turn — the head it hangs off no longer travels and it has no
|
|
1687
|
+
// turn warp of its own — while the face slides inside it.
|
|
1670
1688
|
hair_back: { deformer: "headDeformer", order: 0, mesh: true },
|
|
1671
1689
|
// The torso. It rides its own rigid deformer, not the head's: the head turns
|
|
1672
1690
|
// about the neck pivot while the shoulders follow at BODY_TURN_FOLLOW and
|
|
@@ -1674,34 +1692,43 @@ var ROLE_TABLE = {
|
|
|
1674
1692
|
// reading as a floating head. Drawn over the back hair so long hair falls
|
|
1675
1693
|
// behind the shoulders.
|
|
1676
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).
|
|
1677
1702
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1678
|
-
nose: { deformer: "
|
|
1679
|
-
blush_L: { deformer: "
|
|
1680
|
-
blush_R: { deformer: "
|
|
1681
|
-
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 },
|
|
1682
1707
|
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1683
1708
|
// MouthOpen instead of the closed one being stretched, which is the
|
|
1684
1709
|
// difference between a portrait rig and one that can lip-sync.
|
|
1685
|
-
mouth_open: { deformer: "
|
|
1686
|
-
eye_L: { deformer: "
|
|
1687
|
-
eye_R: { deformer: "
|
|
1688
|
-
iris_L: { deformer: "
|
|
1689
|
-
iris_R: { deformer: "
|
|
1690
|
-
pupil_L: { deformer: "
|
|
1691
|
-
pupil_R: { deformer: "
|
|
1692
|
-
highlight_L: { deformer: "
|
|
1693
|
-
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" },
|
|
1694
1719
|
// Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
|
|
1695
1720
|
// the closed-eye seam (the same crease the white folds to), covering the cut
|
|
1696
1721
|
// eyeball cleanly. When absent, the white's own fold is the only closed line.
|
|
1697
|
-
lash_L: { deformer: "
|
|
1698
|
-
lash_R: { deformer: "
|
|
1699
|
-
brow_L: { deformer: "
|
|
1700
|
-
brow_R: { deformer: "
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
//
|
|
1704
|
-
|
|
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 }
|
|
1705
1732
|
};
|
|
1706
1733
|
var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
|
|
1707
1734
|
var ALIAS_MAP = {
|
|
@@ -1789,6 +1816,27 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1789
1816
|
`auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
|
|
1790
1817
|
);
|
|
1791
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
|
+
}
|
|
1792
1840
|
}
|
|
1793
1841
|
}
|
|
1794
1842
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1844,44 +1892,123 @@ function meshCellsFor(w, h) {
|
|
|
1844
1892
|
);
|
|
1845
1893
|
return { cols: cells(w), rows: cells(h) };
|
|
1846
1894
|
}
|
|
1847
|
-
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;
|
|
1848
1899
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1849
1900
|
var HEAD_TURN_MAX_DEG = 30;
|
|
1850
1901
|
var HEAD_TURN_STOPS = [-30, -15, 0, 15, 30];
|
|
1851
|
-
function headTurnParallaxUnit(
|
|
1852
|
-
return
|
|
1853
|
-
}
|
|
1854
|
-
function gridReach(grid, axis, center) {
|
|
1855
|
-
let reach = 0;
|
|
1856
|
-
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1857
|
-
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1858
|
-
}
|
|
1859
|
-
return reach;
|
|
1902
|
+
function headTurnParallaxUnit(radius) {
|
|
1903
|
+
return radius * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1860
1904
|
}
|
|
1861
1905
|
function pinnedCylinderBend(local, radius, theta) {
|
|
1862
1906
|
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1863
1907
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
1864
1908
|
}
|
|
1865
|
-
function
|
|
1909
|
+
function boundedCylinderBend(local, radius, theta) {
|
|
1910
|
+
const bound = radius / HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1911
|
+
const onSurface = Math.max(-bound, Math.min(bound, local));
|
|
1912
|
+
return pinnedCylinderBend(onSurface, radius, theta);
|
|
1913
|
+
}
|
|
1914
|
+
function turnSlide(travel, angleX) {
|
|
1915
|
+
return travel * angleX / HEAD_TURN_MAX_DEG;
|
|
1916
|
+
}
|
|
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) {
|
|
1866
1999
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1867
|
-
const RADIUS_X = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1868
|
-
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1869
2000
|
const pointCount = grid.points.length / 2;
|
|
1870
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
1871
2001
|
const keyforms2d = [];
|
|
1872
2002
|
for (const angleY of STOPS) {
|
|
1873
|
-
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1874
2003
|
for (const angleX of STOPS) {
|
|
1875
|
-
const
|
|
2004
|
+
const shift = shiftAt(angleX);
|
|
1876
2005
|
const offsets = [];
|
|
1877
2006
|
for (let i = 0; i < pointCount; i++) {
|
|
2007
|
+
const x = grid.points[i * 2];
|
|
2008
|
+
const y = grid.points[i * 2 + 1];
|
|
1878
2009
|
offsets.push(
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
grid.points[i * 2 + 1] - centerY,
|
|
1882
|
-
RADIUS_Y,
|
|
1883
|
-
thetaY
|
|
1884
|
-
)
|
|
2010
|
+
angleX === 0 ? 0 : surface.mapAt(angleX, y).mapX(x + shift) - x,
|
|
2011
|
+
angleY === 0 ? 0 : surface.nodBendAt(y, angleY)
|
|
1885
2012
|
);
|
|
1886
2013
|
}
|
|
1887
2014
|
keyforms2d.push({ offsets });
|
|
@@ -1895,16 +2022,967 @@ function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, c
|
|
|
1895
2022
|
keyforms2d
|
|
1896
2023
|
};
|
|
1897
2024
|
}
|
|
2025
|
+
function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
2026
|
+
if (Math.abs(angleX) > HEAD_TURN_MAX_DEG) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`auto-rig: turnColumnMap: angleX ${angleX}\xB0 is outside the turn's \xB1${HEAD_TURN_MAX_DEG}\xB0 range`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const theta = angleX * (Math.PI / 180);
|
|
2032
|
+
const slide = turnSlide(travel, angleX);
|
|
2033
|
+
const restX = [];
|
|
2034
|
+
const warpedX = [];
|
|
2035
|
+
for (let col = 0; col <= grid.cols; col++) {
|
|
2036
|
+
const x = grid.points[col * 2];
|
|
2037
|
+
restX.push(x);
|
|
2038
|
+
warpedX.push(
|
|
2039
|
+
x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const cellFor = (v, edges) => {
|
|
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;
|
|
2061
|
+
};
|
|
2062
|
+
return {
|
|
2063
|
+
restX,
|
|
2064
|
+
warpedX,
|
|
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;
|
|
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)
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
var TurnTargetError = class extends Error {
|
|
2187
|
+
constructor(message) {
|
|
2188
|
+
super(message);
|
|
2189
|
+
this.name = "TurnTargetError";
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
var DEFAULT_TURN_TARGETS = {
|
|
2193
|
+
eyeShift: 0.22,
|
|
2194
|
+
farEyeRatio: 0.67,
|
|
2195
|
+
silhouetteRatio: 1
|
|
2196
|
+
};
|
|
2197
|
+
var NOSE_SHIFT_SHARE = 1.36;
|
|
2198
|
+
var MOUTH_SHIFT_SHARE = 1.18;
|
|
2199
|
+
var TURN_RATIO_MAX = 1.5;
|
|
2200
|
+
var SILHOUETTE_RATIO_MIN = 0.5;
|
|
2201
|
+
function resolveTurnTargets(targets = {}) {
|
|
2202
|
+
const eyeShift = targets.eyeShift ?? DEFAULT_TURN_TARGETS.eyeShift;
|
|
2203
|
+
const check = (field, value, ok, expected) => {
|
|
2204
|
+
if (value === void 0) return;
|
|
2205
|
+
if (!Number.isFinite(value) || !ok(value)) {
|
|
2206
|
+
const derived = (field === "noseShift" || field === "mouthShift") && targets[field] === void 0;
|
|
2207
|
+
const from = derived ? `, derived from turnTargets.eyeShift (${eyeShift}),` : "";
|
|
2208
|
+
throw new TurnTargetError(
|
|
2209
|
+
`auto-rig: turnTargets.${field} (${value})${from} must be ${expected}`
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
const farEyeRatio = targets.farEyeRatio ?? DEFAULT_TURN_TARGETS.farEyeRatio;
|
|
2214
|
+
const silhouetteRatio = targets.silhouetteRatio ?? DEFAULT_TURN_TARGETS.silhouetteRatio;
|
|
2215
|
+
const noseShift = targets.noseShift ?? NOSE_SHIFT_SHARE * eyeShift;
|
|
2216
|
+
const mouthShift = targets.mouthShift ?? MOUTH_SHIFT_SHARE * eyeShift;
|
|
2217
|
+
const isShift = (v) => Math.abs(v) <= 1;
|
|
2218
|
+
const shiftRange = "a fraction of the head half-width, |value| <= 1";
|
|
2219
|
+
check("eyeShift", eyeShift, isShift, shiftRange);
|
|
2220
|
+
check(
|
|
2221
|
+
"farEyeRatio",
|
|
2222
|
+
farEyeRatio,
|
|
2223
|
+
(v) => v > 0 && v <= TURN_RATIO_MAX,
|
|
2224
|
+
`in (0, ${TURN_RATIO_MAX}]`
|
|
2225
|
+
);
|
|
2226
|
+
check(
|
|
2227
|
+
"silhouetteRatio",
|
|
2228
|
+
silhouetteRatio,
|
|
2229
|
+
(v) => v >= SILHOUETTE_RATIO_MIN && v <= TURN_RATIO_MAX,
|
|
2230
|
+
`in [${SILHOUETTE_RATIO_MIN}, ${TURN_RATIO_MAX}]`
|
|
2231
|
+
);
|
|
2232
|
+
check("noseShift", noseShift, isShift, shiftRange);
|
|
2233
|
+
check("mouthShift", mouthShift, isShift, shiftRange);
|
|
2234
|
+
check("headHalfWidth", targets.headHalfWidth, (v) => v > 0, "positive");
|
|
2235
|
+
const defaulted = new Set(
|
|
2236
|
+
[
|
|
2237
|
+
"eyeShift",
|
|
2238
|
+
"farEyeRatio",
|
|
2239
|
+
"silhouetteRatio",
|
|
2240
|
+
"noseShift",
|
|
2241
|
+
"mouthShift"
|
|
2242
|
+
].filter((field) => targets[field] === void 0)
|
|
2243
|
+
);
|
|
2244
|
+
return {
|
|
2245
|
+
eyeShift,
|
|
2246
|
+
farEyeRatio,
|
|
2247
|
+
silhouetteRatio,
|
|
2248
|
+
noseShift,
|
|
2249
|
+
mouthShift,
|
|
2250
|
+
headHalfWidth: targets.headHalfWidth,
|
|
2251
|
+
defaulted
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
function turnLandmarks(layers, carriers) {
|
|
2255
|
+
const markOf = (role) => {
|
|
2256
|
+
const layer = layers.find((l) => l.role === role);
|
|
2257
|
+
if (!layer) return void 0;
|
|
2258
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
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;
|
|
2263
|
+
};
|
|
2264
|
+
const pairOf = (left, right) => {
|
|
2265
|
+
const l = markOf(left);
|
|
2266
|
+
const r = markOf(right);
|
|
2267
|
+
return l && r ? [l, r] : void 0;
|
|
2268
|
+
};
|
|
2269
|
+
const one = (role) => {
|
|
2270
|
+
const m = markOf(role);
|
|
2271
|
+
return m ? [m] : [];
|
|
2272
|
+
};
|
|
2273
|
+
return {
|
|
2274
|
+
eye: pairOf("eye_L", "eye_R") ?? [],
|
|
2275
|
+
nose: one("nose"),
|
|
2276
|
+
mouth: one("mouth")
|
|
2277
|
+
};
|
|
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
|
+
}
|
|
2283
|
+
var TURN_BISECT_STEPS = 40;
|
|
2284
|
+
var TURN_DEPTH_EPS = 1e-6;
|
|
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) {
|
|
2299
|
+
if (landmarks.length === 0) {
|
|
2300
|
+
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2301
|
+
}
|
|
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;
|
|
2307
|
+
const atCap = achieved(cap);
|
|
2308
|
+
const atRest = achieved(0);
|
|
2309
|
+
const attainable = [atCap, atRest];
|
|
2310
|
+
if (target > atRest + TURN_DEPTH_EPS)
|
|
2311
|
+
return { reached: false, depth: 0, achieved: atRest, attainable };
|
|
2312
|
+
if (target < atCap - TURN_DEPTH_EPS)
|
|
2313
|
+
return { reached: false, depth: cap, achieved: atCap, attainable };
|
|
2314
|
+
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2315
|
+
let lo = 0;
|
|
2316
|
+
let hi = cap;
|
|
2317
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2318
|
+
const mid = (lo + hi) / 2;
|
|
2319
|
+
if (achieved(mid) > clampedTarget) lo = mid;
|
|
2320
|
+
else hi = mid;
|
|
2321
|
+
}
|
|
2322
|
+
const depth = (lo + hi) / 2;
|
|
2323
|
+
return { reached: true, depth, achieved: achieved(depth), attainable };
|
|
2324
|
+
}
|
|
2325
|
+
var TURN_SWEEP_MAX_FACTOR = 8;
|
|
2326
|
+
var TURN_SWEEP_SAMPLES = 32;
|
|
2327
|
+
var HOLD_CLEARANCE = 1;
|
|
2328
|
+
var TURN_HOLD_SAMPLES = 16;
|
|
2329
|
+
var TURN_SETTLE_PASSES = 24;
|
|
2330
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2331
|
+
const local = x - faceCenterX;
|
|
2332
|
+
const dist = Math.abs(local);
|
|
2333
|
+
const side = Math.sign(local);
|
|
2334
|
+
const painted = edgeAt(y);
|
|
2335
|
+
if (dist <= painted) return plateLandingAt(deg, x, y);
|
|
2336
|
+
if (dist <= holdBase) {
|
|
2337
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, y);
|
|
2338
|
+
const u = (dist - painted) / (holdBase - painted);
|
|
2339
|
+
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2340
|
+
}
|
|
2341
|
+
return x + side * (holdEdge - holdBase);
|
|
2342
|
+
}
|
|
2343
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
2344
|
+
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
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
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
function shellTravelCap(faceCenterX, edgeAt, plateGuardRows, askedTravel, bendOnlyLandingAt, holdEdgeAt) {
|
|
2364
|
+
let cap = Infinity;
|
|
2365
|
+
for (const deg of HEAD_TURN_STOPS) {
|
|
2366
|
+
if (deg === 0) continue;
|
|
2367
|
+
const slideSign = turnSlide(askedTravel, deg) < 0 ? -1 : 1;
|
|
2368
|
+
const towardReach = plateSideReachAt(
|
|
2369
|
+
bendOnlyLandingAt,
|
|
2370
|
+
faceCenterX,
|
|
2371
|
+
edgeAt,
|
|
2372
|
+
plateGuardRows,
|
|
2373
|
+
deg,
|
|
2374
|
+
slideSign
|
|
2375
|
+
);
|
|
2376
|
+
cap = Math.min(
|
|
2377
|
+
cap,
|
|
2378
|
+
Math.max(0, holdEdgeAt(deg, slideSign) - HOLD_CLEARANCE - towardReach) * HEAD_TURN_MAX_DEG / Math.abs(deg)
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
return cap;
|
|
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
|
+
}
|
|
2391
|
+
function evaluateTurnCandidate(ctx, radius) {
|
|
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)));
|
|
2403
|
+
const measuredEdgeAt = (side) => {
|
|
2404
|
+
const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;
|
|
2405
|
+
if (candidates === void 0 || candidates.length === 0) return void 0;
|
|
2406
|
+
const xs = candidates.map((c) => c.x);
|
|
2407
|
+
return side < 0 ? Math.min(...xs) : Math.max(...xs);
|
|
2408
|
+
};
|
|
2409
|
+
const shellHalfWidthAt = (side) => {
|
|
2410
|
+
const edge = measuredEdgeAt(side);
|
|
2411
|
+
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2412
|
+
};
|
|
2413
|
+
const unit = headTurnParallaxUnit(radius);
|
|
2414
|
+
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2415
|
+
const evaluateHold = (ratio, depths2) => {
|
|
2416
|
+
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2417
|
+
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
2418
|
+
ctx.travel,
|
|
2419
|
+
shellTravelCap(
|
|
2420
|
+
ctx.faceCenterX,
|
|
2421
|
+
ctx.edgeAt,
|
|
2422
|
+
ctx.plateGuardRows,
|
|
2423
|
+
ctx.travel,
|
|
2424
|
+
bendOnlyLandingAt,
|
|
2425
|
+
(deg, side) => {
|
|
2426
|
+
const rest = shellHalfWidthAt(side);
|
|
2427
|
+
return Math.min(rest, holdEdgeFrom(rest)(deg));
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
2430
|
+
);
|
|
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);
|
|
2442
|
+
let foldLowerBound = -Infinity;
|
|
2443
|
+
let folds = false;
|
|
2444
|
+
for (const deg of HEAD_TURN_STOPS) {
|
|
2445
|
+
if (deg === 0) continue;
|
|
2446
|
+
const reach = plateReachAt(
|
|
2447
|
+
plateLandingAt,
|
|
2448
|
+
ctx.faceCenterX,
|
|
2449
|
+
ctx.edgeAt,
|
|
2450
|
+
ctx.plateGuardRows,
|
|
2451
|
+
deg
|
|
2452
|
+
);
|
|
2453
|
+
foldLowerBound = Math.max(
|
|
2454
|
+
foldLowerBound,
|
|
2455
|
+
1 + ((reach + HOLD_CLEARANCE) / holdBase - 1) * (HEAD_TURN_MAX_DEG / Math.abs(deg))
|
|
2456
|
+
);
|
|
2457
|
+
if (holdEdgeAt(deg) <= reach) folds = true;
|
|
2458
|
+
}
|
|
2459
|
+
const restAt = (side) => {
|
|
2460
|
+
const edge = measuredEdgeAt(side);
|
|
2461
|
+
if (edge !== void 0) return edge;
|
|
2462
|
+
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2463
|
+
};
|
|
2464
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : hairFrontLandingAt(
|
|
2465
|
+
x,
|
|
2466
|
+
ctx.eyeRowY,
|
|
2467
|
+
ctx.hairFrontSilhouette,
|
|
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
|
|
2479
|
+
);
|
|
2480
|
+
const landingOfRole = (role, x) => {
|
|
2481
|
+
if (role === "hair_front") return hairFrontAt(x);
|
|
2482
|
+
if (role === "hair_back") {
|
|
2483
|
+
return x;
|
|
2484
|
+
}
|
|
2485
|
+
if (role === "body") {
|
|
2486
|
+
return x - BODY_TURN_FOLLOW * travel;
|
|
2487
|
+
}
|
|
2488
|
+
if (role === "face" || FEATURE_NOD_DEPTH[roleFamily(role)] !== void 0) {
|
|
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
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
throw new Error(
|
|
2504
|
+
`auto-rig: evaluateTurnCandidate: unrecognised role "${role}"`
|
|
2505
|
+
);
|
|
2506
|
+
};
|
|
2507
|
+
const landingAt = (side) => {
|
|
2508
|
+
const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;
|
|
2509
|
+
if (candidates === void 0 || candidates.length === 0) {
|
|
2510
|
+
return hairFrontAt(restAt(side));
|
|
2511
|
+
}
|
|
2512
|
+
const landings = candidates.map((c) => landingOfRole(c.role, c.x));
|
|
2513
|
+
return side < 0 ? Math.min(...landings) : Math.max(...landings);
|
|
2514
|
+
};
|
|
2515
|
+
return {
|
|
2516
|
+
ratio,
|
|
2517
|
+
travel,
|
|
2518
|
+
mapAt,
|
|
2519
|
+
surface,
|
|
2520
|
+
holdBase,
|
|
2521
|
+
holdEdgeAt,
|
|
2522
|
+
folds,
|
|
2523
|
+
foldLowerBound,
|
|
2524
|
+
// How far the silhouette's own centre drifts off the face centre at full
|
|
2525
|
+
// turn, against how far it already sat off centre at rest (zero unless
|
|
2526
|
+
// hair_front's crop is itself off-centre) — the held shell keeps its own
|
|
2527
|
+
// place while the face slides inside it, so this is the drift the hold
|
|
2528
|
+
// and the bangs' lead leave behind, and the slide itself shows up in the
|
|
2529
|
+
// eye cue instead of cancelling out of it.
|
|
2530
|
+
// `measure_turn_reference` reads the eye pair against THIS moved centre,
|
|
2531
|
+
// not the face centre, so it is what the eye cue is corrected by.
|
|
2532
|
+
silhouetteCenterShift: (landingAt(1) + landingAt(-1)) / 2 - (restAt(1) + restAt(-1)) / 2,
|
|
2533
|
+
// What a render actually shows: the real landing span over the real rest
|
|
2534
|
+
// span, the same pair silhouetteCenterShift reads — the measure the
|
|
2535
|
+
// hold is FITTED against, see this function's own doc.
|
|
2536
|
+
renderedSilhouetteRatio: (landingAt(1) - landingAt(-1)) / (restAt(1) - restAt(-1)),
|
|
2537
|
+
restSpan: restAt(1) - restAt(-1)
|
|
2538
|
+
};
|
|
2539
|
+
};
|
|
2540
|
+
const holdRange = (depths2) => {
|
|
2541
|
+
const ceiling = evaluateHold(TURN_RATIO_MAX, depths2);
|
|
2542
|
+
if (ceiling.folds) return void 0;
|
|
2543
|
+
let floor = evaluateHold(
|
|
2544
|
+
Math.min(ceiling.foldLowerBound, ceiling.ratio),
|
|
2545
|
+
depths2
|
|
2546
|
+
);
|
|
2547
|
+
if (floor.folds) {
|
|
2548
|
+
let lo = floor;
|
|
2549
|
+
let hi = ceiling;
|
|
2550
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2551
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2552
|
+
if (mid.folds) lo = mid;
|
|
2553
|
+
else hi = mid;
|
|
2554
|
+
}
|
|
2555
|
+
floor = hi;
|
|
2556
|
+
}
|
|
2557
|
+
const samples = [floor];
|
|
2558
|
+
for (let i = 1; i < TURN_HOLD_SAMPLES; i++) {
|
|
2559
|
+
const sample = evaluateHold(
|
|
2560
|
+
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES,
|
|
2561
|
+
depths2
|
|
2562
|
+
);
|
|
2563
|
+
if (!sample.folds) samples.push(sample);
|
|
2564
|
+
}
|
|
2565
|
+
samples.push(ceiling);
|
|
2566
|
+
return samples;
|
|
2567
|
+
};
|
|
2568
|
+
const silhouetteMiss = (span) => {
|
|
2569
|
+
const lower = Math.max(span?.[0] ?? Infinity, SILHOUETTE_RATIO_MIN);
|
|
2570
|
+
const upper = Math.min(span?.[1] ?? -Infinity, TURN_RATIO_MAX);
|
|
2571
|
+
return lower > upper ? { blocked: "silhouetteRatio", lower: Infinity, upper: -Infinity } : { blocked: "silhouetteRatio", lower, upper };
|
|
2572
|
+
};
|
|
2573
|
+
const asked = ctx.silhouetteRatio;
|
|
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;
|
|
2582
|
+
const rendered = samples.map((h) => h.renderedSilhouetteRatio);
|
|
2583
|
+
const span = [
|
|
2584
|
+
Math.min(...rendered),
|
|
2585
|
+
Math.max(...rendered)
|
|
2586
|
+
];
|
|
2587
|
+
const nearer = (a, b) => {
|
|
2588
|
+
const ea = Math.abs(a.renderedSilhouetteRatio - asked);
|
|
2589
|
+
const eb = Math.abs(b.renderedSilhouetteRatio - asked);
|
|
2590
|
+
if (ea <= eps && eb <= eps) {
|
|
2591
|
+
return Math.abs(a.ratio - asked) <= Math.abs(b.ratio - asked) ? a : b;
|
|
2592
|
+
}
|
|
2593
|
+
return ea <= eb ? a : b;
|
|
2594
|
+
};
|
|
2595
|
+
if (asked < span[0] - eps || asked > span[1] + eps) {
|
|
2596
|
+
const wanted = asked < span[0] ? span[0] : span[1];
|
|
2597
|
+
const hold = samples.reduce(
|
|
2598
|
+
(a, b) => Math.abs(b.renderedSilhouetteRatio - wanted) < Math.abs(a.renderedSilhouetteRatio - wanted) ? b : a
|
|
2599
|
+
);
|
|
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;
|
|
2612
|
+
}
|
|
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;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
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
|
|
2645
|
+
);
|
|
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);
|
|
2718
|
+
return {
|
|
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
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
function sweepTurnRadii(ctx) {
|
|
2737
|
+
const minRadius = ctx.faceHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
2738
|
+
const maxRadius = ctx.faceHalfWidth * TURN_SWEEP_MAX_FACTOR;
|
|
2739
|
+
const candidates = [];
|
|
2740
|
+
let heldRatioLower = Infinity;
|
|
2741
|
+
let heldRatioUpper = -Infinity;
|
|
2742
|
+
let offeredShift;
|
|
2743
|
+
let unsettled = 0;
|
|
2744
|
+
for (let i = 0; i < TURN_SWEEP_SAMPLES; i++) {
|
|
2745
|
+
const radius = minRadius * Math.pow(maxRadius / minRadius, i / (TURN_SWEEP_SAMPLES - 1));
|
|
2746
|
+
const result = evaluateTurnCandidate(ctx, radius);
|
|
2747
|
+
if (!("blocked" in result)) {
|
|
2748
|
+
candidates.push({ ...result, sweepIndex: i });
|
|
2749
|
+
} else if (result.blocked === "silhouetteRatio") {
|
|
2750
|
+
heldRatioLower = Math.min(heldRatioLower, result.lower);
|
|
2751
|
+
heldRatioUpper = Math.max(heldRatioUpper, result.upper);
|
|
2752
|
+
} else if (result.blocked === "settle") {
|
|
2753
|
+
unsettled++;
|
|
2754
|
+
} else {
|
|
2755
|
+
offeredShift = offeredShift ? [
|
|
2756
|
+
Math.min(offeredShift[0], result.offeredShift[0]),
|
|
2757
|
+
Math.max(offeredShift[1], result.offeredShift[1])
|
|
2758
|
+
] : result.offeredShift;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
candidates,
|
|
2763
|
+
heldRatioLower,
|
|
2764
|
+
heldRatioUpper,
|
|
2765
|
+
offeredShift,
|
|
2766
|
+
unsettled
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
function ratioClusters(candidates) {
|
|
2770
|
+
const clusters = [];
|
|
2771
|
+
let run = [];
|
|
2772
|
+
const close = () => {
|
|
2773
|
+
if (run.length === 0) return;
|
|
2774
|
+
const ratios = run.map((c) => c.ratio);
|
|
2775
|
+
clusters.push({
|
|
2776
|
+
candidates: run,
|
|
2777
|
+
ratios: [Math.min(...ratios), Math.max(...ratios)]
|
|
2778
|
+
});
|
|
2779
|
+
run = [];
|
|
2780
|
+
};
|
|
2781
|
+
for (const candidate of candidates) {
|
|
2782
|
+
const previous = run[run.length - 1];
|
|
2783
|
+
if (previous !== void 0 && candidate.sweepIndex !== previous.sweepIndex + 1) {
|
|
2784
|
+
close();
|
|
2785
|
+
}
|
|
2786
|
+
run.push(candidate);
|
|
2787
|
+
}
|
|
2788
|
+
close();
|
|
2789
|
+
return clusters;
|
|
2790
|
+
}
|
|
2791
|
+
function fitTurnRadius(ctx, candidates, target) {
|
|
2792
|
+
let lo = candidates[0];
|
|
2793
|
+
let hi = candidates[candidates.length - 1];
|
|
2794
|
+
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
2795
|
+
if ((candidates[i].ratio - target) * (candidates[i + 1].ratio - target) <= 0) {
|
|
2796
|
+
lo = candidates[i];
|
|
2797
|
+
hi = candidates[i + 1];
|
|
2798
|
+
break;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2802
|
+
const mid = evaluateTurnCandidate(ctx, Math.sqrt(lo.radius * hi.radius));
|
|
2803
|
+
if ("blocked" in mid) break;
|
|
2804
|
+
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2805
|
+
else lo = mid;
|
|
2806
|
+
}
|
|
2807
|
+
return Math.abs(lo.ratio - target) <= Math.abs(hi.ratio - target) ? lo : hi;
|
|
2808
|
+
}
|
|
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
|
+
);
|
|
2817
|
+
return {
|
|
2818
|
+
nose: solve(ctx.noseShift, ctx.landmarks.nose, NOSE_SHIFT_SHARE),
|
|
2819
|
+
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
2823
|
+
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2824
|
+
throw new TurnTargetError(
|
|
2825
|
+
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
2829
|
+
const ctx = {
|
|
2830
|
+
landmarks,
|
|
2831
|
+
lattice,
|
|
2832
|
+
faceCenterX,
|
|
2833
|
+
faceCenterY,
|
|
2834
|
+
nodRadius,
|
|
2835
|
+
eyeRowY,
|
|
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.
|
|
2839
|
+
plateEdgeX: faceCenterX - faceHalfWidth,
|
|
2840
|
+
carriers,
|
|
2841
|
+
profile,
|
|
2842
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
2843
|
+
// What this plate asks the turn for; each candidate caps it to its own
|
|
2844
|
+
// shell — see TurnSolveContext.travel.
|
|
2845
|
+
travel: headTurnTravel(faceHalfWidth),
|
|
2846
|
+
headHalfWidth: targets.headHalfWidth,
|
|
2847
|
+
// The pixels the shift fractions are fractions of. Without a measured head
|
|
2848
|
+
// the face plate stands in for it — see TurnTargets.headHalfWidth.
|
|
2849
|
+
hh: targets.headHalfWidth ?? faceHalfWidth,
|
|
2850
|
+
silhouetteRatio: targets.silhouetteRatio,
|
|
2851
|
+
// The sign is ignored (see TurnTargets.eyeShift) — taken here, once, so
|
|
2852
|
+
// every downstream use already has the magnitude.
|
|
2853
|
+
eyeShift: Math.abs(targets.eyeShift),
|
|
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),
|
|
2857
|
+
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2858
|
+
hairFrontSilhouette: hairFront,
|
|
2859
|
+
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2860
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
2861
|
+
};
|
|
2862
|
+
const clamped = [];
|
|
2863
|
+
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2864
|
+
const pass = sweepTurnRadii(ctx);
|
|
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
|
+
}
|
|
2872
|
+
return pass.offeredShift ? {
|
|
2873
|
+
unreachable: true,
|
|
2874
|
+
field: "eyeShift",
|
|
2875
|
+
value: targets.eyeShift,
|
|
2876
|
+
attainable: pass.offeredShift
|
|
2877
|
+
} : {
|
|
2878
|
+
unreachable: true,
|
|
2879
|
+
field: "silhouetteRatio",
|
|
2880
|
+
value: targets.silhouetteRatio,
|
|
2881
|
+
// Two different bounds, not one number twice, and computed the
|
|
2882
|
+
// same way regardless of which ratio was actually rejected: both
|
|
2883
|
+
// are ratios a RENDER could have shown (the measure the caller's
|
|
2884
|
+
// own number came from), the narrowest and the widest any radius
|
|
2885
|
+
// could be fitted to, with every ratio between them one it can be
|
|
2886
|
+
// fitted to exactly; see evaluateTurnCandidate, which clips each
|
|
2887
|
+
// radius' own range to the domain `resolveTurnTargets` accepts
|
|
2888
|
+
// before it gets here. Absent when that left every radius with
|
|
2889
|
+
// nothing: no ratio a caller is allowed to ask for renders at all.
|
|
2890
|
+
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
const clusters = ratioClusters(pass.candidates);
|
|
2894
|
+
const distanceTo = (c) => Math.max(
|
|
2895
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
2896
|
+
targets.farEyeRatio - c.ratios[1],
|
|
2897
|
+
0
|
|
2898
|
+
);
|
|
2899
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
2900
|
+
let cluster = clusters.find(
|
|
2901
|
+
(c) => farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS && farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS
|
|
2902
|
+
);
|
|
2903
|
+
if (cluster === void 0) {
|
|
2904
|
+
cluster = clusters.reduce(
|
|
2905
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
2906
|
+
);
|
|
2907
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
2908
|
+
return {
|
|
2909
|
+
unreachable: true,
|
|
2910
|
+
field: "farEyeRatio",
|
|
2911
|
+
value: targets.farEyeRatio,
|
|
2912
|
+
attainable: cluster.ratios
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
2916
|
+
clamped.push("farEyeRatio");
|
|
2917
|
+
}
|
|
2918
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio);
|
|
2919
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
2920
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
2921
|
+
return {
|
|
2922
|
+
unreachable: true,
|
|
2923
|
+
field: "farEyeRatio",
|
|
2924
|
+
value: targets.farEyeRatio,
|
|
2925
|
+
attainable: cluster.ratios
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
2929
|
+
}
|
|
2930
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
2931
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
2932
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
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);
|
|
2956
|
+
}
|
|
2957
|
+
return {
|
|
2958
|
+
unreachable: false,
|
|
2959
|
+
radius: best.radius,
|
|
2960
|
+
holdBase: best.holdBase,
|
|
2961
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
2962
|
+
travel: best.travel,
|
|
2963
|
+
surface: best.surface,
|
|
2964
|
+
depths: {
|
|
2965
|
+
eye: best.eye.depth,
|
|
2966
|
+
nose: best.nose.depth,
|
|
2967
|
+
mouth: best.mouth.depth
|
|
2968
|
+
},
|
|
2969
|
+
achieved: {
|
|
2970
|
+
eyeShift,
|
|
2971
|
+
farEyeRatio: best.ratio,
|
|
2972
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2973
|
+
},
|
|
2974
|
+
clamped
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
1898
2977
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
1899
2978
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
1900
|
-
var HAIR_BACK_DEPTH = -0.08;
|
|
1901
2979
|
var NOD_TRAVEL = 30;
|
|
1902
2980
|
var NOD_BEND = 0.5;
|
|
1903
2981
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
1904
2982
|
function headNodParallaxUnit(gridHalfHeight) {
|
|
1905
2983
|
return gridHalfHeight * HEAD_CYLINDER_RADIUS_FACTOR * Math.sin(HEAD_TURN_MAX_DEG * NOD_BEND * (Math.PI / 180));
|
|
1906
2984
|
}
|
|
1907
|
-
var
|
|
2985
|
+
var FEATURE_NOD_DEPTH = {
|
|
1908
2986
|
eye: 0.04,
|
|
1909
2987
|
iris: 0.04,
|
|
1910
2988
|
pupil: 0.04,
|
|
@@ -1916,34 +2994,30 @@ var FEATURE_DEPTH = {
|
|
|
1916
2994
|
mouth_open: 0.04,
|
|
1917
2995
|
nose: 0.08
|
|
1918
2996
|
};
|
|
1919
|
-
var HAIR_FRONT_NOD_DEPTH =
|
|
2997
|
+
var HAIR_FRONT_NOD_DEPTH = FEATURE_NOD_DEPTH.brow;
|
|
1920
2998
|
function roleFamily(role) {
|
|
1921
2999
|
return role.replace(/_[LR]$/, "");
|
|
1922
3000
|
}
|
|
3001
|
+
function turnFamily(role) {
|
|
3002
|
+
const family = roleFamily(role);
|
|
3003
|
+
if (family === "nose") return "nose";
|
|
3004
|
+
if (family === "mouth" || family === "mouth_open") return "mouth";
|
|
3005
|
+
return "eye";
|
|
3006
|
+
}
|
|
1923
3007
|
function featureParallaxBindings(role, options) {
|
|
1924
3008
|
if (!options.hasNose) return [];
|
|
1925
|
-
const
|
|
1926
|
-
if (
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
parameter: import_format3.StandardParameter.AngleX,
|
|
1932
|
-
channel: "translateX",
|
|
1933
|
-
from: -shiftX,
|
|
1934
|
-
to: shiftX
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
const shiftY = depth * (options.parallaxUnitY ?? 0);
|
|
1938
|
-
if (shiftY !== 0) {
|
|
1939
|
-
bindings.push({
|
|
3009
|
+
const nodDepth = FEATURE_NOD_DEPTH[roleFamily(role)];
|
|
3010
|
+
if (nodDepth === void 0) return [];
|
|
3011
|
+
const shiftY = nodDepth * (options.parallaxUnitY ?? 0);
|
|
3012
|
+
if (shiftY === 0) return [];
|
|
3013
|
+
return [
|
|
3014
|
+
{
|
|
1940
3015
|
parameter: import_format3.StandardParameter.AngleY,
|
|
1941
3016
|
channel: "translateY",
|
|
1942
3017
|
from: -shiftY,
|
|
1943
3018
|
to: shiftY
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
return bindings;
|
|
3019
|
+
}
|
|
3020
|
+
];
|
|
1947
3021
|
}
|
|
1948
3022
|
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
1949
3023
|
return [
|
|
@@ -2051,31 +3125,17 @@ function roleOwnBindings(spec, role, cropW, cropH, options) {
|
|
|
2051
3125
|
];
|
|
2052
3126
|
}
|
|
2053
3127
|
}
|
|
2054
|
-
if (role === "
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
if (shiftX !== 0) {
|
|
2060
|
-
parallax.push({
|
|
2061
|
-
parameter: import_format3.StandardParameter.AngleX,
|
|
2062
|
-
channel: "translateX",
|
|
2063
|
-
from: -shiftX,
|
|
2064
|
-
to: shiftX
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
const frontDepth = options.hasNose ? HAIR_FRONT_NOD_DEPTH : 0;
|
|
2069
|
-
const shiftY = (isFront ? frontDepth : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
|
|
2070
|
-
if (shiftY !== 0) {
|
|
2071
|
-
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
|
+
{
|
|
2072
3133
|
parameter: import_format3.StandardParameter.AngleY,
|
|
2073
3134
|
channel: "translateY",
|
|
2074
3135
|
from: -shiftY,
|
|
2075
3136
|
to: shiftY
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
return parallax;
|
|
3137
|
+
}
|
|
3138
|
+
];
|
|
2079
3139
|
}
|
|
2080
3140
|
return [];
|
|
2081
3141
|
}
|
|
@@ -2098,40 +3158,106 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
2098
3158
|
]
|
|
2099
3159
|
};
|
|
2100
3160
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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) {
|
|
3174
|
+
let reach = 0;
|
|
3175
|
+
for (const deg of HEAD_TURN_STOPS) {
|
|
3176
|
+
reach = Math.max(
|
|
3177
|
+
reach,
|
|
3178
|
+
plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
return reach;
|
|
3182
|
+
}
|
|
3183
|
+
function plateSideReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg, side) {
|
|
3184
|
+
let reach = 0;
|
|
3185
|
+
for (const y of plateGuardRows) {
|
|
3186
|
+
const dest = plateLandingAt(deg, faceCenterX + side * edgeAt(y), y);
|
|
3187
|
+
reach = Math.max(reach, Math.abs(dest - faceCenterX));
|
|
3188
|
+
}
|
|
3189
|
+
return reach;
|
|
3190
|
+
}
|
|
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) {
|
|
3212
|
+
if (holdEdgeAt(0) !== holdBase) {
|
|
3213
|
+
throw new Error(
|
|
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`
|
|
3215
|
+
);
|
|
2109
3216
|
}
|
|
2110
|
-
const halfWidth = (right - left) / 2;
|
|
2111
|
-
const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;
|
|
2112
|
-
const bulge = HAIR_BACK_FAR_BULGE * halfWidth;
|
|
2113
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
2114
3217
|
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
+
)
|
|
3228
|
+
);
|
|
3229
|
+
const reached = Math.max(reach[0], reach[1]);
|
|
3230
|
+
if (holdEdge <= reached) {
|
|
3231
|
+
const side = reach[1] > reach[0] ? 1 : -1;
|
|
3232
|
+
throw new Error(
|
|
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`
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
2120
3236
|
const offsets = [];
|
|
2121
3237
|
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2122
|
-
const x = mesh.vertices[i];
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
3238
|
+
const x = partX + mesh.vertices[i];
|
|
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
|
|
3252
|
+
);
|
|
2126
3253
|
}
|
|
2127
3254
|
return { value: deg, offsets };
|
|
2128
3255
|
});
|
|
2129
|
-
return { parameter, keyforms };
|
|
3256
|
+
return { parameter: import_format3.StandardParameter.AngleX, keyforms };
|
|
2130
3257
|
}
|
|
2131
3258
|
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2132
3259
|
var HAIR_SWAY_CURL = 1.5;
|
|
2133
3260
|
var HAIR_SWAY_RANGE = 20;
|
|
2134
|
-
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2135
3261
|
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2136
3262
|
let top = -Infinity;
|
|
2137
3263
|
let bottom = Infinity;
|
|
@@ -2154,116 +3280,385 @@ function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
|
2154
3280
|
]
|
|
2155
3281
|
};
|
|
2156
3282
|
}
|
|
2157
|
-
var
|
|
3283
|
+
var HEAD_TURN_TRAVEL_RATIO = 0.25;
|
|
3284
|
+
function headTurnTravel(faceHalfWidth) {
|
|
3285
|
+
return HEAD_TURN_TRAVEL_RATIO * faceHalfWidth;
|
|
3286
|
+
}
|
|
2158
3287
|
var BODY_TURN_FOLLOW = 0.3;
|
|
2159
3288
|
var HEAD_BREATH_BOB = -12;
|
|
2160
3289
|
var BODY_BREATH_FOLLOW = 0.5;
|
|
2161
|
-
|
|
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
|
+
}
|
|
3657
|
+
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
2162
3658
|
validateLayerInputs(layers, canvas);
|
|
2163
3659
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2164
|
-
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2165
|
-
const hasNose = layers.some((l) => l.role === "nose");
|
|
2166
3660
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2167
|
-
const parameters = [
|
|
2168
|
-
{
|
|
2169
|
-
id: import_format3.StandardParameter.MouthOpen,
|
|
2170
|
-
name: "Mouth Open",
|
|
2171
|
-
min: 0,
|
|
2172
|
-
max: 1,
|
|
2173
|
-
default: 0
|
|
2174
|
-
},
|
|
2175
|
-
{
|
|
2176
|
-
id: import_format3.StandardParameter.MouthForm,
|
|
2177
|
-
name: "Mouth Form",
|
|
2178
|
-
min: -1,
|
|
2179
|
-
max: 1,
|
|
2180
|
-
default: 0
|
|
2181
|
-
},
|
|
2182
|
-
{
|
|
2183
|
-
id: import_format3.StandardParameter.EyeOpenLeft,
|
|
2184
|
-
name: "Eye L",
|
|
2185
|
-
min: 0,
|
|
2186
|
-
max: 1,
|
|
2187
|
-
default: 1
|
|
2188
|
-
},
|
|
2189
|
-
{
|
|
2190
|
-
id: import_format3.StandardParameter.EyeOpenRight,
|
|
2191
|
-
name: "Eye R",
|
|
2192
|
-
min: 0,
|
|
2193
|
-
max: 1,
|
|
2194
|
-
default: 1
|
|
2195
|
-
},
|
|
2196
|
-
{
|
|
2197
|
-
id: import_format3.StandardParameter.EyeballX,
|
|
2198
|
-
name: "Gaze X",
|
|
2199
|
-
min: -1,
|
|
2200
|
-
max: 1,
|
|
2201
|
-
default: 0
|
|
2202
|
-
},
|
|
2203
|
-
{
|
|
2204
|
-
id: import_format3.StandardParameter.EyeballY,
|
|
2205
|
-
name: "Gaze Y",
|
|
2206
|
-
min: -1,
|
|
2207
|
-
max: 1,
|
|
2208
|
-
default: 0
|
|
2209
|
-
},
|
|
2210
|
-
{
|
|
2211
|
-
id: import_format3.StandardParameter.AngleX,
|
|
2212
|
-
name: "Head Angle",
|
|
2213
|
-
min: -30,
|
|
2214
|
-
max: 30,
|
|
2215
|
-
default: 0
|
|
2216
|
-
},
|
|
2217
|
-
{
|
|
2218
|
-
id: import_format3.StandardParameter.AngleY,
|
|
2219
|
-
name: "Head Angle Y",
|
|
2220
|
-
min: -30,
|
|
2221
|
-
max: 30,
|
|
2222
|
-
default: 0
|
|
2223
|
-
},
|
|
2224
|
-
{
|
|
2225
|
-
id: import_format3.StandardParameter.AngleZ,
|
|
2226
|
-
name: "Head Angle Z",
|
|
2227
|
-
min: -30,
|
|
2228
|
-
max: 30,
|
|
2229
|
-
default: 0
|
|
2230
|
-
},
|
|
2231
|
-
{
|
|
2232
|
-
id: import_format3.StandardParameter.Breath,
|
|
2233
|
-
name: "Breath",
|
|
2234
|
-
min: 0,
|
|
2235
|
-
max: 1,
|
|
2236
|
-
default: 0
|
|
2237
|
-
},
|
|
2238
|
-
{
|
|
2239
|
-
id: import_format3.StandardParameter.BrowLeftY,
|
|
2240
|
-
name: "Brow L Y",
|
|
2241
|
-
min: -1,
|
|
2242
|
-
max: 1,
|
|
2243
|
-
default: 0
|
|
2244
|
-
},
|
|
2245
|
-
{
|
|
2246
|
-
id: import_format3.StandardParameter.BrowRightY,
|
|
2247
|
-
name: "Brow R Y",
|
|
2248
|
-
min: -1,
|
|
2249
|
-
max: 1,
|
|
2250
|
-
default: 0
|
|
2251
|
-
},
|
|
2252
|
-
{
|
|
2253
|
-
id: import_format3.StandardParameter.BrowLeftAngle,
|
|
2254
|
-
name: "Brow L Angle",
|
|
2255
|
-
min: -1,
|
|
2256
|
-
max: 1,
|
|
2257
|
-
default: 0
|
|
2258
|
-
},
|
|
2259
|
-
{
|
|
2260
|
-
id: import_format3.StandardParameter.BrowRightAngle,
|
|
2261
|
-
name: "Brow R Angle",
|
|
2262
|
-
min: -1,
|
|
2263
|
-
max: 1,
|
|
2264
|
-
default: 0
|
|
2265
|
-
}
|
|
2266
|
-
];
|
|
3661
|
+
const parameters = [...STANDARD_PARAMETERS];
|
|
2267
3662
|
if (hasHair) {
|
|
2268
3663
|
parameters.push(
|
|
2269
3664
|
{
|
|
@@ -2282,97 +3677,88 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2282
3677
|
}
|
|
2283
3678
|
);
|
|
2284
3679
|
}
|
|
2285
|
-
const
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
)
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3680
|
+
const setup = turnSetup(layers);
|
|
3681
|
+
const {
|
|
3682
|
+
lattice,
|
|
3683
|
+
faceCenterX,
|
|
3684
|
+
faceHalfWidth,
|
|
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),
|
|
3702
|
+
options.headEdges
|
|
3703
|
+
) : void 0;
|
|
3704
|
+
if (turn?.unreachable) {
|
|
3705
|
+
throw new TurnTargetError(
|
|
3706
|
+
turn.attainable === void 0 ? (
|
|
3707
|
+
// Only a silhouette can land here: nothing the field's own accepted
|
|
3708
|
+
// range allows renders on this layer set, so there is no interval to
|
|
3709
|
+
// offer instead — see `silhouetteMiss` in evaluateTurnCandidate.
|
|
3710
|
+
`auto-rig: turnTargets.${turn.field} ${turn.value} is unreachable for this layer set, and so is every ${turn.field} in [${SILHOUETTE_RATIO_MIN}, ${TURN_RATIO_MAX}]`
|
|
3711
|
+
) : `auto-rig: turnTargets.${turn.field} ${turn.value} is unreachable for this layer set (attainable ${turn.attainable[0]}\u2026${turn.attainable[1]})`
|
|
2317
3712
|
);
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
cols
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
const
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
const faceBottom = faceTransform.y - faceCropH / 2;
|
|
3713
|
+
}
|
|
3714
|
+
if (turn) {
|
|
3715
|
+
options.onTurnSolved?.({
|
|
3716
|
+
radius: turn.radius,
|
|
3717
|
+
holdBase: turn.holdBase,
|
|
3718
|
+
depths: turn.depths,
|
|
3719
|
+
achieved: turn.achieved,
|
|
3720
|
+
clamped: turn.clamped
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
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;
|
|
2346
3740
|
const neckPivot = {
|
|
2347
3741
|
x: faceCenterX,
|
|
2348
3742
|
y: faceBottom - faceCropH * 0.15
|
|
2349
3743
|
// 15% below face bottom = neck
|
|
2350
3744
|
};
|
|
2351
|
-
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
2352
|
-
faceGrid,
|
|
2353
|
-
import_format3.StandardParameter.AngleX,
|
|
2354
|
-
import_format3.StandardParameter.AngleY,
|
|
2355
|
-
faceCenterX,
|
|
2356
|
-
faceCenterY
|
|
2357
|
-
);
|
|
2358
3745
|
const deformers = [
|
|
2359
3746
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
2360
|
-
// neck pivot
|
|
3747
|
+
// neck pivot. Same bindings as sample-model.ts apart from the turn, which
|
|
3748
|
+
// moves the face inside the head rather than the head itself (see below).
|
|
2361
3749
|
{
|
|
2362
3750
|
id: "headDeformer",
|
|
2363
3751
|
pivot: neckPivot,
|
|
2364
3752
|
bindings: [
|
|
2365
|
-
//
|
|
2366
|
-
//
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
2369
|
-
//
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
to: HEAD_TURN_TRAVEL
|
|
2375
|
-
},
|
|
3753
|
+
// No AngleX binding: the turn moves NOTHING rigidly. Its sideways
|
|
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.
|
|
2376
3762
|
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
2377
3763
|
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
2378
3764
|
// poses, collapsing pitch into roll.
|
|
@@ -2401,19 +3787,25 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2401
3787
|
to: HEAD_BREATH_BOB
|
|
2402
3788
|
}
|
|
2403
3789
|
]
|
|
2404
|
-
},
|
|
2405
|
-
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
2406
|
-
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
2407
|
-
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
2408
|
-
// `warps` or `warp2d`, never both).
|
|
2409
|
-
{
|
|
2410
|
-
kind: "warp",
|
|
2411
|
-
id: "faceWarp",
|
|
2412
|
-
parent: "headDeformer",
|
|
2413
|
-
grid: faceGrid,
|
|
2414
|
-
warp2d: faceWarp2d
|
|
2415
3790
|
}
|
|
2416
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
|
+
}
|
|
2417
3809
|
if (bodyLayer) {
|
|
2418
3810
|
const bt = bboxToTransform(
|
|
2419
3811
|
bodyLayer.bbox,
|
|
@@ -2425,12 +3817,13 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2425
3817
|
id: "bodyDeformer",
|
|
2426
3818
|
pivot: { x: bt.x, y: bt.y - bodyLayer.cropH / 2 },
|
|
2427
3819
|
bindings: [
|
|
2428
|
-
// Low-weight follow of the turn, same direction as the
|
|
3820
|
+
// Low-weight follow of the turn, same direction as the face's own
|
|
3821
|
+
// slide — the only rigid part of the travel left.
|
|
2429
3822
|
{
|
|
2430
3823
|
parameter: import_format3.StandardParameter.AngleX,
|
|
2431
3824
|
channel: "translateX",
|
|
2432
|
-
from: -BODY_TURN_FOLLOW *
|
|
2433
|
-
to: BODY_TURN_FOLLOW *
|
|
3825
|
+
from: -BODY_TURN_FOLLOW * surface.travel,
|
|
3826
|
+
to: BODY_TURN_FOLLOW * surface.travel
|
|
2434
3827
|
},
|
|
2435
3828
|
// Breath: follow the head's bob, same direction, half amplitude.
|
|
2436
3829
|
{
|
|
@@ -2442,33 +3835,21 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2442
3835
|
]
|
|
2443
3836
|
});
|
|
2444
3837
|
}
|
|
2445
|
-
const
|
|
2446
|
-
for (const layer of layers) {
|
|
2447
|
-
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
2448
|
-
if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
|
|
2449
|
-
const ey = bboxToTransform(
|
|
2450
|
-
layer.bbox,
|
|
2451
|
-
layer.canvasW,
|
|
2452
|
-
layer.canvasH,
|
|
2453
|
-
layer.role
|
|
2454
|
-
).y;
|
|
2455
|
-
eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
3838
|
+
const memberByRole = new Map(members.map((m) => [m.role, m]));
|
|
2458
3839
|
const parts = layers.map((layer) => {
|
|
2459
3840
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
2460
3841
|
const spec = ROLE_TABLE[role];
|
|
2461
3842
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
2462
3843
|
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
2463
3844
|
hasMouthOpen,
|
|
2464
|
-
parallaxUnit,
|
|
2465
3845
|
parallaxUnitY,
|
|
2466
3846
|
hasNose
|
|
2467
3847
|
});
|
|
2468
3848
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
2469
3849
|
if (spec.mesh) {
|
|
3850
|
+
const member = memberByRole.get(role);
|
|
2470
3851
|
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
2471
|
-
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3852
|
+
const mesh = member?.mesh ?? createPixelGridMesh(cols, rows, cropW, cropH);
|
|
2472
3853
|
const part = {
|
|
2473
3854
|
id: role,
|
|
2474
3855
|
color: [1, 1, 1, 1],
|
|
@@ -2483,23 +3864,38 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2483
3864
|
part.bindings = roleBindings;
|
|
2484
3865
|
}
|
|
2485
3866
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
2486
|
-
|
|
2487
|
-
|
|
3867
|
+
const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
3868
|
+
const ownWarps = [];
|
|
2488
3869
|
if (role === "hair_front") {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
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
|
+
)
|
|
2497
3877
|
);
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
+
)
|
|
3891
|
+
);
|
|
3892
|
+
ownWarps.push(
|
|
3893
|
+
bakeNodWarp(
|
|
3894
|
+
mesh,
|
|
3895
|
+
t.y,
|
|
3896
|
+
surface,
|
|
3897
|
+
hasNose ? HAIR_FRONT_NOD_DEPTH * parallaxUnitY : 0
|
|
3898
|
+
)
|
|
2503
3899
|
);
|
|
2504
3900
|
}
|
|
2505
3901
|
part.warps = [
|
|
@@ -2515,29 +3911,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2515
3911
|
tipShift,
|
|
2516
3912
|
HAIR_SWAY_RANGE
|
|
2517
3913
|
),
|
|
2518
|
-
...
|
|
2519
|
-
];
|
|
2520
|
-
}
|
|
2521
|
-
if (role === "hair_back") {
|
|
2522
|
-
part.warps = [
|
|
2523
|
-
...part.warps ?? [],
|
|
2524
|
-
bakeHairBackTurnWarp(mesh, import_format3.StandardParameter.AngleX)
|
|
3914
|
+
...ownWarps
|
|
2525
3915
|
];
|
|
2526
3916
|
}
|
|
2527
3917
|
if (spec.eyeSide !== void 0) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const openParam = spec.eyeSide === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight;
|
|
2531
|
-
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
2532
|
-
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
2533
|
-
part.warps = [
|
|
2534
|
-
bakeEyelidFoldWarp(
|
|
2535
|
-
mesh,
|
|
2536
|
-
openParam,
|
|
2537
|
-
creaseWorldY - t.y,
|
|
2538
|
-
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
2539
|
-
)
|
|
2540
|
-
];
|
|
3918
|
+
if (role.startsWith("eye_") || role.startsWith("lash_")) {
|
|
3919
|
+
part.warps = [...member.warps];
|
|
2541
3920
|
} else {
|
|
2542
3921
|
part.clip = { masks: [`eye_${spec.eyeSide}`] };
|
|
2543
3922
|
}
|
|
@@ -2602,6 +3981,7 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2602
3981
|
AddPart,
|
|
2603
3982
|
AddPhysicsRig,
|
|
2604
3983
|
CaptureGridKeyform,
|
|
3984
|
+
DEFAULT_TURN_TARGETS,
|
|
2605
3985
|
DeleteDeformer,
|
|
2606
3986
|
DeletePart,
|
|
2607
3987
|
DeletePhysicsRig,
|
|
@@ -2621,6 +4001,7 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2621
4001
|
SetPartTransform,
|
|
2622
4002
|
SetPartWidth,
|
|
2623
4003
|
SetPhysicsRig,
|
|
4004
|
+
TurnTargetError,
|
|
2624
4005
|
UV_INSET_PX,
|
|
2625
4006
|
captureBindingEndpoint,
|
|
2626
4007
|
computeGridOffsets,
|