@ikijs/editor 0.5.0 → 0.7.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 +38 -7
- package/dist/index.d.mts +140 -13
- package/dist/index.d.ts +140 -13
- package/dist/index.js +1634 -711
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1634 -711
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1664,6 +1664,21 @@ function createDefaultWarpDeformer(model) {
|
|
|
1664
1664
|
|
|
1665
1665
|
// src/auto-rig.ts
|
|
1666
1666
|
var import_format3 = require("@ikijs/format");
|
|
1667
|
+
var TURN_GROUP_IDS = [
|
|
1668
|
+
"faceWarp",
|
|
1669
|
+
"eyeWarp_L",
|
|
1670
|
+
"eyeWarp_R",
|
|
1671
|
+
"mouthWarp",
|
|
1672
|
+
"noseWarp",
|
|
1673
|
+
"browWarp_L",
|
|
1674
|
+
"browWarp_R",
|
|
1675
|
+
"blushWarp_L",
|
|
1676
|
+
"blushWarp_R"
|
|
1677
|
+
];
|
|
1678
|
+
var TURN_GROUP_ID_SET = new Set(TURN_GROUP_IDS);
|
|
1679
|
+
function isTurnGroup(id) {
|
|
1680
|
+
return TURN_GROUP_ID_SET.has(id);
|
|
1681
|
+
}
|
|
1667
1682
|
var ROLE_TABLE = {
|
|
1668
1683
|
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1669
1684
|
// whose grid it would stretch across the shoulders) as a MESH, so its ends
|
|
@@ -1677,34 +1692,43 @@ var ROLE_TABLE = {
|
|
|
1677
1692
|
// reading as a floating head. Drawn over the back hair so long hair falls
|
|
1678
1693
|
// behind the shoulders.
|
|
1679
1694
|
body: { deformer: "bodyDeformer", order: 5, mesh: false },
|
|
1695
|
+
// The face plate and everything painted on it ride per-group turn warps
|
|
1696
|
+
// under the head (TurnGroupId): the plate its own, each eye stack its
|
|
1697
|
+
// side's, both mouth drawings one, the nose, each brow and each blush their
|
|
1698
|
+
// own. A group's grid is sized to its members alone and baked from the one
|
|
1699
|
+
// solved surface with the family's turn depth in its keyforms, so a feature
|
|
1700
|
+
// slides across the plate on its own grid instead of a translate binding
|
|
1701
|
+
// (see generateIkiFromLayerSet).
|
|
1680
1702
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1681
|
-
nose: { deformer: "
|
|
1682
|
-
blush_L: { deformer: "
|
|
1683
|
-
blush_R: { deformer: "
|
|
1684
|
-
mouth: { deformer: "
|
|
1703
|
+
nose: { deformer: "noseWarp", order: 15, mesh: true },
|
|
1704
|
+
blush_L: { deformer: "blushWarp_L", order: 20, mesh: true },
|
|
1705
|
+
blush_R: { deformer: "blushWarp_R", order: 20, mesh: true },
|
|
1706
|
+
mouth: { deformer: "mouthWarp", order: 25, mesh: true },
|
|
1685
1707
|
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1686
1708
|
// MouthOpen instead of the closed one being stretched, which is the
|
|
1687
1709
|
// difference between a portrait rig and one that can lip-sync.
|
|
1688
|
-
mouth_open: { deformer: "
|
|
1689
|
-
eye_L: { deformer: "
|
|
1690
|
-
eye_R: { deformer: "
|
|
1691
|
-
iris_L: { deformer: "
|
|
1692
|
-
iris_R: { deformer: "
|
|
1693
|
-
pupil_L: { deformer: "
|
|
1694
|
-
pupil_R: { deformer: "
|
|
1695
|
-
highlight_L: { deformer: "
|
|
1696
|
-
highlight_R: { deformer: "
|
|
1710
|
+
mouth_open: { deformer: "mouthWarp", order: 26, mesh: true },
|
|
1711
|
+
eye_L: { deformer: "eyeWarp_L", order: 30, mesh: true, eyeSide: "L" },
|
|
1712
|
+
eye_R: { deformer: "eyeWarp_R", order: 30, mesh: true, eyeSide: "R" },
|
|
1713
|
+
iris_L: { deformer: "eyeWarp_L", order: 31, mesh: true, eyeSide: "L" },
|
|
1714
|
+
iris_R: { deformer: "eyeWarp_R", order: 31, mesh: true, eyeSide: "R" },
|
|
1715
|
+
pupil_L: { deformer: "eyeWarp_L", order: 32, mesh: true, eyeSide: "L" },
|
|
1716
|
+
pupil_R: { deformer: "eyeWarp_R", order: 32, mesh: true, eyeSide: "R" },
|
|
1717
|
+
highlight_L: { deformer: "eyeWarp_L", order: 33, mesh: true, eyeSide: "L" },
|
|
1718
|
+
highlight_R: { deformer: "eyeWarp_R", order: 33, mesh: true, eyeSide: "R" },
|
|
1697
1719
|
// Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
|
|
1698
1720
|
// the closed-eye seam (the same crease the white folds to), covering the cut
|
|
1699
1721
|
// eyeball cleanly. When absent, the white's own fold is the only closed line.
|
|
1700
|
-
lash_L: { deformer: "
|
|
1701
|
-
lash_R: { deformer: "
|
|
1702
|
-
brow_L: { deformer: "
|
|
1703
|
-
brow_R: { deformer: "
|
|
1704
|
-
//
|
|
1705
|
-
//
|
|
1706
|
-
//
|
|
1707
|
-
|
|
1722
|
+
lash_L: { deformer: "eyeWarp_L", order: 34, mesh: true, eyeSide: "L" },
|
|
1723
|
+
lash_R: { deformer: "eyeWarp_R", order: 34, mesh: true, eyeSide: "R" },
|
|
1724
|
+
brow_L: { deformer: "browWarp_L", order: 40, mesh: true },
|
|
1725
|
+
brow_R: { deformer: "browWarp_R", order: 40, mesh: true },
|
|
1726
|
+
// The bangs ride the rigid head, like the back hair, and bind to no grid:
|
|
1727
|
+
// every motion of their own — the silhouette hold, the turn lead, the nod
|
|
1728
|
+
// and the sway — is a per-vertex warp attached in generateIkiFromLayerSet,
|
|
1729
|
+
// so they draw the head's outline where the plate under them bends and
|
|
1730
|
+
// slides (see bakeHairFrontSilhouetteWarp).
|
|
1731
|
+
hair_front: { deformer: "headDeformer", order: 50, mesh: true }
|
|
1708
1732
|
};
|
|
1709
1733
|
var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
|
|
1710
1734
|
var ALIAS_MAP = {
|
|
@@ -1792,6 +1816,146 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1792
1816
|
`auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
|
|
1793
1817
|
);
|
|
1794
1818
|
}
|
|
1819
|
+
if (layer.rowHalfWidths !== void 0) {
|
|
1820
|
+
const rows = layer.rowHalfWidths;
|
|
1821
|
+
if (!Array.isArray(rows)) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths must be an array with one entry per crop row`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
if (rows.length !== cropH) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths has ${rows.length} entries, not one per crop row (cropH ${cropH})`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1832
|
+
const a = rows[i];
|
|
1833
|
+
if (typeof a !== "number" || !Number.isFinite(a) || a < 0 || a > cropW / 2) {
|
|
1834
|
+
throw new Error(
|
|
1835
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths[${i}] is ${String(a)}, not a finite number in [0, ${cropW / 2}] (half the crop's width)`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
function validateStrandEdges(layers, strandEdges) {
|
|
1843
|
+
if (strandEdges === void 0) return;
|
|
1844
|
+
const where = "auto-rig: validateStrandEdges: strandEdges";
|
|
1845
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1846
|
+
if (!isPlainObject(strandEdges)) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`${where} must be a plain object with optional left / right sides`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
for (const key of ["left", "right"]) {
|
|
1852
|
+
const raw = strandEdges[key];
|
|
1853
|
+
if (raw === void 0) continue;
|
|
1854
|
+
const path = `${where}.${key}`;
|
|
1855
|
+
if (!isPlainObject(raw)) {
|
|
1856
|
+
throw new Error(`${path} must be a plain object`);
|
|
1857
|
+
}
|
|
1858
|
+
for (const field of ["y", "irisOuter", "irisInner", "runOuter"]) {
|
|
1859
|
+
const v = raw[field];
|
|
1860
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
`${path}.${field} is ${String(v)}, not a finite number`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const rawFace = raw.runFace;
|
|
1867
|
+
if (rawFace !== null && (typeof rawFace !== "number" || !Number.isFinite(rawFace))) {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
`${path}.runFace is ${String(rawFace)}, not a finite number or null`
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
const { y, irisOuter, irisInner, runOuter, runFace } = raw;
|
|
1873
|
+
const cropOf = (role) => {
|
|
1874
|
+
const layer = layers.find((l) => l.role === role);
|
|
1875
|
+
if (layer === void 0) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
`${path} needs a "${role}" layer to be measured on, and this layer set has none`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1881
|
+
return {
|
|
1882
|
+
role,
|
|
1883
|
+
x: t.x,
|
|
1884
|
+
left: t.x - layer.cropW / 2,
|
|
1885
|
+
right: t.x + layer.cropW / 2,
|
|
1886
|
+
bottom: t.y - layer.cropH / 2,
|
|
1887
|
+
top: t.y + layer.cropH / 2
|
|
1888
|
+
};
|
|
1889
|
+
};
|
|
1890
|
+
const hair = cropOf("hair_front");
|
|
1891
|
+
const [lowIris, highIris] = [cropOf("iris_L"), cropOf("iris_R")].sort(
|
|
1892
|
+
(a, b) => a.x - b.x
|
|
1893
|
+
);
|
|
1894
|
+
const sign = key === "left" ? -1 : 1;
|
|
1895
|
+
const iris = key === "left" ? lowIris : highIris;
|
|
1896
|
+
const other = key === "left" ? highIris : lowIris;
|
|
1897
|
+
const fail = (field, value, why) => new Error(`${path}.${field} (${value}) ${why}`);
|
|
1898
|
+
for (const crop of [iris, hair]) {
|
|
1899
|
+
if (y < crop.bottom || y > crop.top) {
|
|
1900
|
+
throw fail(
|
|
1901
|
+
"y",
|
|
1902
|
+
y,
|
|
1903
|
+
`lies outside the "${crop.role}" crop's rows (${crop.bottom}\u2026${crop.top})`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
for (const [field, x] of [
|
|
1908
|
+
["irisOuter", irisOuter],
|
|
1909
|
+
["irisInner", irisInner]
|
|
1910
|
+
]) {
|
|
1911
|
+
if (x < iris.left || x > iris.right) {
|
|
1912
|
+
throw fail(
|
|
1913
|
+
field,
|
|
1914
|
+
x,
|
|
1915
|
+
`lies outside the "${iris.role}" crop's columns (${iris.left}\u2026${iris.right})`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (sign * (irisOuter - irisInner) <= 0) {
|
|
1920
|
+
throw fail(
|
|
1921
|
+
"irisOuter",
|
|
1922
|
+
irisOuter,
|
|
1923
|
+
`must lie strictly outward of strandEdges.${key}.irisInner (${irisInner})`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
for (const [field, x] of [
|
|
1927
|
+
["runOuter", runOuter],
|
|
1928
|
+
["runFace", runFace]
|
|
1929
|
+
]) {
|
|
1930
|
+
if (x !== null && (x < hair.left || x > hair.right)) {
|
|
1931
|
+
throw fail(
|
|
1932
|
+
field,
|
|
1933
|
+
x,
|
|
1934
|
+
`lies outside the "hair_front" crop's columns (${hair.left}\u2026${hair.right})`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (sign * (runOuter - iris.x) <= 0) {
|
|
1939
|
+
throw fail(
|
|
1940
|
+
"runOuter",
|
|
1941
|
+
runOuter,
|
|
1942
|
+
`must lie strictly outward of the "${iris.role}" crop's centre (${iris.x})`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
1946
|
+
throw fail(
|
|
1947
|
+
"runOuter",
|
|
1948
|
+
runOuter,
|
|
1949
|
+
`must lie strictly outward of strandEdges.${key}.runFace (${runFace})`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
if (runFace !== null && sign * (runFace - other.x) <= 0) {
|
|
1953
|
+
throw fail(
|
|
1954
|
+
"runFace",
|
|
1955
|
+
runFace,
|
|
1956
|
+
`must lie strictly on this side of the "${other.role}" crop's centre (${other.x}); a run that reaches it has no face-side end here, which is runFace null`
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1795
1959
|
}
|
|
1796
1960
|
}
|
|
1797
1961
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1847,20 +2011,16 @@ function meshCellsFor(w, h) {
|
|
|
1847
2011
|
);
|
|
1848
2012
|
return { cols: cells(w), rows: cells(h) };
|
|
1849
2013
|
}
|
|
1850
|
-
var
|
|
2014
|
+
var FACE_PLATE_CELLS = 10;
|
|
2015
|
+
var FEATURE_GRID_CELLS = 4;
|
|
2016
|
+
var TURN_LATTICE_CELL_PX = 16;
|
|
2017
|
+
var GRID_MARGIN = 0.12;
|
|
1851
2018
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1852
2019
|
var HEAD_TURN_MAX_DEG = 30;
|
|
1853
2020
|
var HEAD_TURN_STOPS = [-30, -15, 0, 15, 30];
|
|
1854
2021
|
function headTurnParallaxUnit(radius) {
|
|
1855
2022
|
return radius * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1856
2023
|
}
|
|
1857
|
-
function gridReach(grid, axis, center) {
|
|
1858
|
-
let reach = 0;
|
|
1859
|
-
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1860
|
-
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1861
|
-
}
|
|
1862
|
-
return reach;
|
|
1863
|
-
}
|
|
1864
2024
|
function pinnedCylinderBend(local, radius, theta) {
|
|
1865
2025
|
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1866
2026
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
@@ -1873,27 +2033,107 @@ function boundedCylinderBend(local, radius, theta) {
|
|
|
1873
2033
|
function turnSlide(travel, angleX) {
|
|
1874
2034
|
return travel * angleX / HEAD_TURN_MAX_DEG;
|
|
1875
2035
|
}
|
|
1876
|
-
|
|
2036
|
+
var FACE_ROW_MIN_FRACTION = 0.15;
|
|
2037
|
+
var FACE_ROW_SMOOTHING = 0.05;
|
|
2038
|
+
var CHIN_SWING = 0.08;
|
|
2039
|
+
function faceRowProfile(face, faceCenterY) {
|
|
2040
|
+
const raw = face.rowHalfWidths;
|
|
2041
|
+
if (raw === void 0) return void 0;
|
|
2042
|
+
const n = raw.length;
|
|
2043
|
+
let aMax = 0;
|
|
2044
|
+
let widest = -1;
|
|
2045
|
+
for (let i = 0; i < n; i++) {
|
|
2046
|
+
if (raw[i] > 0 && raw[i] >= aMax) {
|
|
2047
|
+
aMax = raw[i];
|
|
2048
|
+
widest = i;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (widest < 0) return void 0;
|
|
2052
|
+
const filled = [];
|
|
2053
|
+
for (let i = 0; i < n; i++) {
|
|
2054
|
+
filled.push(i < widest ? aMax : raw[i] > 0 ? raw[i] : filled[i - 1]);
|
|
2055
|
+
}
|
|
2056
|
+
const half = Math.max(1, Math.round(FACE_ROW_SMOOTHING * n / 2));
|
|
2057
|
+
const floor = FACE_ROW_MIN_FRACTION * aMax;
|
|
2058
|
+
const rows = [];
|
|
2059
|
+
for (let i = 0; i < n; i++) {
|
|
2060
|
+
if (i < widest) {
|
|
2061
|
+
rows.push(aMax);
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
let sum = 0;
|
|
2065
|
+
for (let k = i - half; k <= i + half; k++) {
|
|
2066
|
+
sum += filled[Math.max(0, Math.min(n - 1, k))];
|
|
2067
|
+
}
|
|
2068
|
+
rows.push(Math.max(floor, sum / (2 * half + 1)));
|
|
2069
|
+
}
|
|
2070
|
+
const top = faceCenterY + face.cropH / 2;
|
|
2071
|
+
const rowAt = (y) => Math.max(0, Math.min(n - 1, top - y - 0.5));
|
|
2072
|
+
return {
|
|
2073
|
+
at: (y) => {
|
|
2074
|
+
const r = rowAt(y);
|
|
2075
|
+
const i = Math.floor(r);
|
|
2076
|
+
if (i + 1 >= n) return rows[n - 1];
|
|
2077
|
+
return rows[i] + (rows[i + 1] - rows[i]) * (r - i);
|
|
2078
|
+
},
|
|
2079
|
+
aMax,
|
|
2080
|
+
widestY: top - widest - 0.5,
|
|
2081
|
+
faceHalfWidth: face.cropW / 2
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function turnSurface(spec) {
|
|
2085
|
+
const { faceCenterX, faceCenterY, radius, travel, nodRadius, lattice } = spec;
|
|
2086
|
+
const { profile } = spec;
|
|
2087
|
+
let radiusAt = (_y) => radius;
|
|
2088
|
+
let swingAt = (_y) => 0;
|
|
2089
|
+
if (profile !== void 0) {
|
|
2090
|
+
const aEye = profile.at(spec.eyeRowY ?? faceCenterY);
|
|
2091
|
+
radiusAt = (y) => radius * (profile.at(y) / aEye);
|
|
2092
|
+
swingAt = (y) => y < profile.widestY ? -CHIN_SWING * profile.faceHalfWidth * Math.max(0, 1 - profile.at(y) / profile.aMax) : 0;
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
faceCenterX,
|
|
2096
|
+
faceCenterY,
|
|
2097
|
+
radius,
|
|
2098
|
+
travel,
|
|
2099
|
+
nodRadius,
|
|
2100
|
+
lattice,
|
|
2101
|
+
radiusAt,
|
|
2102
|
+
swingAt,
|
|
2103
|
+
mapAt: (deg, y) => turnColumnMap(
|
|
2104
|
+
lattice,
|
|
2105
|
+
faceCenterX,
|
|
2106
|
+
radiusAt(y),
|
|
2107
|
+
deg,
|
|
2108
|
+
travel + swingAt(y)
|
|
2109
|
+
),
|
|
2110
|
+
nodBendAt: (y, angleY) => pinnedCylinderBend(
|
|
2111
|
+
y - faceCenterY,
|
|
2112
|
+
nodRadius,
|
|
2113
|
+
angleY * NOD_BEND * (Math.PI / 180)
|
|
2114
|
+
)
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1877
2118
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1878
|
-
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1879
2119
|
const pointCount = grid.points.length / 2;
|
|
1880
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
1881
2120
|
const keyforms2d = [];
|
|
1882
2121
|
for (const angleY of STOPS) {
|
|
1883
|
-
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1884
2122
|
for (const angleX of STOPS) {
|
|
1885
|
-
const
|
|
1886
|
-
const
|
|
2123
|
+
const shift = shiftAt(angleX);
|
|
2124
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
1887
2125
|
const offsets = [];
|
|
1888
2126
|
for (let i = 0; i < pointCount; i++) {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
);
|
|
2127
|
+
const x = grid.points[i * 2];
|
|
2128
|
+
const y = grid.points[i * 2 + 1];
|
|
2129
|
+
const nodDy = angleY === 0 ? 0 : surface.nodBendAt(y, angleY);
|
|
2130
|
+
if (angleX === 0) {
|
|
2131
|
+
offsets.push(0, nodDy);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const landed = groupNodeLanding(anchor, mapAt, angleX, shift, x, y);
|
|
2135
|
+
const turnDy = landed.y - y;
|
|
2136
|
+
offsets.push(landed.x - x, angleY === 0 ? turnDy : turnDy + nodDy);
|
|
1897
2137
|
}
|
|
1898
2138
|
keyforms2d.push({ offsets });
|
|
1899
2139
|
}
|
|
@@ -1924,30 +2164,163 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
1924
2164
|
);
|
|
1925
2165
|
}
|
|
1926
2166
|
const cellFor = (v, edges) => {
|
|
1927
|
-
|
|
1928
|
-
|
|
2167
|
+
let lo = 0;
|
|
2168
|
+
let hi = grid.cols - 1;
|
|
2169
|
+
while (lo < hi) {
|
|
2170
|
+
const mid = lo + hi >> 1;
|
|
2171
|
+
if (v < edges[mid + 1]) hi = mid;
|
|
2172
|
+
else lo = mid + 1;
|
|
1929
2173
|
}
|
|
1930
|
-
return
|
|
2174
|
+
return lo;
|
|
2175
|
+
};
|
|
2176
|
+
const sample = (edges, values, v) => {
|
|
2177
|
+
const c = cellFor(v, edges);
|
|
2178
|
+
const s = Math.max(
|
|
2179
|
+
0,
|
|
2180
|
+
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2181
|
+
);
|
|
2182
|
+
if (s === 0) return values[c];
|
|
2183
|
+
if (s === 1) return values[c + 1];
|
|
2184
|
+
return values[c] + (values[c + 1] - values[c]) * s;
|
|
1931
2185
|
};
|
|
1932
2186
|
return {
|
|
1933
2187
|
restX,
|
|
1934
2188
|
warpedX,
|
|
1935
|
-
mapX(x)
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2189
|
+
mapX: (x) => sample(restX, warpedX, x),
|
|
2190
|
+
invertX: (X) => sample(warpedX, restX, X)
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
function renderedLandingX(part, vertexLanding, px, py) {
|
|
2194
|
+
const { cols, rows } = part;
|
|
2195
|
+
const u = (px - part.x + part.cropW / 2) / part.cropW * cols;
|
|
2196
|
+
const v = (part.y + part.cropH / 2 - py) / part.cropH * rows;
|
|
2197
|
+
const col = Math.max(0, Math.min(cols - 1, Math.floor(u)));
|
|
2198
|
+
const row = Math.max(0, Math.min(rows - 1, Math.floor(v)));
|
|
2199
|
+
const fx = Math.max(0, Math.min(1, u - col));
|
|
2200
|
+
const fy = Math.max(0, Math.min(1, v - row));
|
|
2201
|
+
const cos = Math.cos(part.rotation * (Math.PI / 180));
|
|
2202
|
+
const sin = Math.sin(part.rotation * (Math.PI / 180));
|
|
2203
|
+
const landingAt = (c, r) => {
|
|
2204
|
+
const lx = (-part.cropW / 2 + c / cols * part.cropW) * part.scaleX;
|
|
2205
|
+
const ly = (part.cropH / 2 - r / rows * part.cropH) * part.scaleY;
|
|
2206
|
+
return vertexLanding(
|
|
2207
|
+
part.x + cos * lx - sin * ly,
|
|
2208
|
+
part.y + sin * lx + cos * ly
|
|
2209
|
+
);
|
|
2210
|
+
};
|
|
2211
|
+
const tl = landingAt(col, row);
|
|
2212
|
+
const br = landingAt(col + 1, row + 1);
|
|
2213
|
+
if (fx <= fy) {
|
|
2214
|
+
const bl = landingAt(col, row + 1);
|
|
2215
|
+
return tl + fy * (bl - tl) + fx * (br - bl);
|
|
2216
|
+
}
|
|
2217
|
+
const tr = landingAt(col + 1, row);
|
|
2218
|
+
return tl + fx * (tr - tl) + fy * (br - tr);
|
|
2219
|
+
}
|
|
2220
|
+
function gridChildLandingX(grid, nodeLanding, x, y) {
|
|
2221
|
+
const { cols, rows, points } = grid;
|
|
2222
|
+
const stride = cols + 1;
|
|
2223
|
+
let col = cols - 1;
|
|
2224
|
+
for (let c = 0; c < cols; c++) {
|
|
2225
|
+
if (x < points[(c + 1) * 2]) {
|
|
2226
|
+
col = c;
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
let row = rows - 1;
|
|
2231
|
+
for (let r = 0; r < rows; r++) {
|
|
2232
|
+
if (y > points[(r + 1) * stride * 2 + 1]) {
|
|
2233
|
+
row = r;
|
|
2234
|
+
break;
|
|
1950
2235
|
}
|
|
2236
|
+
}
|
|
2237
|
+
const xLeft = points[col * 2];
|
|
2238
|
+
const xRight = points[(col + 1) * 2];
|
|
2239
|
+
const yTop = points[row * stride * 2 + 1];
|
|
2240
|
+
const yBottom = points[(row + 1) * stride * 2 + 1];
|
|
2241
|
+
const s = Math.max(0, Math.min(1, (x - xLeft) / (xRight - xLeft)));
|
|
2242
|
+
const t = Math.max(0, Math.min(1, (yTop - y) / (yTop - yBottom)));
|
|
2243
|
+
const at = (c, r) => nodeLanding(points[(r * stride + c) * 2], points[(r * stride + c) * 2 + 1]);
|
|
2244
|
+
const top = at(col, row) + (at(col + 1, row) - at(col, row)) * s;
|
|
2245
|
+
const bot = at(col, row + 1) + (at(col + 1, row + 1) - at(col, row + 1)) * s;
|
|
2246
|
+
return top + (bot - top) * t;
|
|
2247
|
+
}
|
|
2248
|
+
function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
2249
|
+
return renderedLandingX(
|
|
2250
|
+
carrier.part,
|
|
2251
|
+
(vx, vy) => gridChildLandingX(carrier.grid, nodeLanding, vx, vy),
|
|
2252
|
+
px,
|
|
2253
|
+
py
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
function groupNodeLanding(anchor, mapAt, deg, shift, nodeX, nodeY) {
|
|
2257
|
+
if (anchor === void 0) {
|
|
2258
|
+
return { x: mapAt(nodeY).mapX(nodeX + shift), y: nodeY };
|
|
2259
|
+
}
|
|
2260
|
+
const anchorRow = mapAt(anchor.y);
|
|
2261
|
+
const pivotX = anchorRow.mapX(anchor.x + shift);
|
|
2262
|
+
const dx = mapAt(nodeY).mapX(nodeX) - anchorRow.mapX(anchor.x);
|
|
2263
|
+
const dy = nodeY - anchor.y;
|
|
2264
|
+
const phi = anchor.tiltDeg * deg / HEAD_TURN_MAX_DEG * (Math.PI / 180);
|
|
2265
|
+
const cos = Math.cos(phi);
|
|
2266
|
+
const sin = Math.sin(phi);
|
|
2267
|
+
return {
|
|
2268
|
+
x: pivotX + cos * dx - sin * dy,
|
|
2269
|
+
y: anchor.y + sin * dx + cos * dy
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
function rowMapsOf(surface) {
|
|
2273
|
+
const maps = /* @__PURE__ */ new Map();
|
|
2274
|
+
return (deg, nodeY) => {
|
|
2275
|
+
let byRow = maps.get(deg);
|
|
2276
|
+
if (byRow === void 0) {
|
|
2277
|
+
byRow = /* @__PURE__ */ new Map();
|
|
2278
|
+
maps.set(deg, byRow);
|
|
2279
|
+
}
|
|
2280
|
+
let map = byRow.get(nodeY);
|
|
2281
|
+
if (map === void 0) {
|
|
2282
|
+
map = surface.mapAt(deg, nodeY);
|
|
2283
|
+
byRow.set(nodeY, map);
|
|
2284
|
+
}
|
|
2285
|
+
return map;
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function plateLandingOn(plate, mapAt) {
|
|
2289
|
+
return (deg, x, y) => carrierLandingX(
|
|
2290
|
+
plate,
|
|
2291
|
+
(nodeX, nodeY) => mapAt(deg, nodeY).mapX(nodeX),
|
|
2292
|
+
x,
|
|
2293
|
+
y
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
function plateGuardRowsFor(face, hairFront) {
|
|
2297
|
+
const rowsOf = (part, y) => {
|
|
2298
|
+
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2299
|
+
return Array.from(
|
|
2300
|
+
{ length: rows + 1 },
|
|
2301
|
+
(_, r) => y + (part.cropH / 2 - r / rows * part.cropH)
|
|
2302
|
+
);
|
|
2303
|
+
};
|
|
2304
|
+
const faceRows = rowsOf(face, face.y);
|
|
2305
|
+
if (hairFront === void 0) return faceRows;
|
|
2306
|
+
const top = face.y + face.cropH / 2;
|
|
2307
|
+
const bottom = face.y - face.cropH / 2;
|
|
2308
|
+
return [
|
|
2309
|
+
...faceRows,
|
|
2310
|
+
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2311
|
+
(y) => Math.max(bottom, Math.min(top, y))
|
|
2312
|
+
)
|
|
2313
|
+
];
|
|
2314
|
+
}
|
|
2315
|
+
function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
2316
|
+
const plate = carriers.get("face");
|
|
2317
|
+
if (plate === void 0) {
|
|
2318
|
+
throw new Error("auto-rig: plateGuardsOf: the carriers have no face");
|
|
2319
|
+
}
|
|
2320
|
+
return {
|
|
2321
|
+
plate,
|
|
2322
|
+
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2323
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
1951
2324
|
};
|
|
1952
2325
|
}
|
|
1953
2326
|
var TurnTargetError = class extends Error {
|
|
@@ -2018,12 +2391,15 @@ function resolveTurnTargets(targets = {}) {
|
|
|
2018
2391
|
defaulted
|
|
2019
2392
|
};
|
|
2020
2393
|
}
|
|
2021
|
-
function turnLandmarks(layers) {
|
|
2394
|
+
function turnLandmarks(layers, carriers) {
|
|
2022
2395
|
const markOf = (role) => {
|
|
2023
2396
|
const layer = layers.find((l) => l.role === role);
|
|
2024
2397
|
if (!layer) return void 0;
|
|
2025
2398
|
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
2026
|
-
|
|
2399
|
+
const mark = { x: t.x, y: t.y, w: layer.cropW };
|
|
2400
|
+
const carrier = carriers?.get(role);
|
|
2401
|
+
if (carrier !== void 0) mark.carrier = carrier;
|
|
2402
|
+
return mark;
|
|
2027
2403
|
};
|
|
2028
2404
|
const pairOf = (left, right) => {
|
|
2029
2405
|
const l = markOf(left);
|
|
@@ -2040,23 +2416,67 @@ function turnLandmarks(layers) {
|
|
|
2040
2416
|
mouth: one("mouth")
|
|
2041
2417
|
};
|
|
2042
2418
|
}
|
|
2419
|
+
function eyeRowOf(landmarks, fallback) {
|
|
2420
|
+
const [l, r] = landmarks.eye;
|
|
2421
|
+
return landmarks.eye.length === 2 && l.y !== void 0 && r.y !== void 0 ? (l.y + r.y) / 2 : fallback;
|
|
2422
|
+
}
|
|
2043
2423
|
var TURN_BISECT_STEPS = 40;
|
|
2044
2424
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2045
|
-
function
|
|
2425
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2426
|
+
const { carrier } = landmark;
|
|
2427
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2428
|
+
return carrierLandingX(
|
|
2429
|
+
carrier,
|
|
2430
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2431
|
+
px,
|
|
2432
|
+
py
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
function familyReachPx(landmarks, plateEdgeX) {
|
|
2436
|
+
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2437
|
+
return Math.max(0, farEdge - plateEdgeX);
|
|
2438
|
+
}
|
|
2439
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2440
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2441
|
+
}
|
|
2442
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
2046
2443
|
if (landmarks.length === 0) {
|
|
2047
2444
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2048
2445
|
}
|
|
2049
|
-
const
|
|
2050
|
-
const
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2446
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2447
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2448
|
+
const achieved = (d) => landmarks.reduce(
|
|
2449
|
+
(sum, l) => sum + (landmarkLandingX(
|
|
2450
|
+
l,
|
|
2451
|
+
mapAt,
|
|
2452
|
+
-HEAD_TURN_MAX_DEG,
|
|
2453
|
+
-d * unit,
|
|
2454
|
+
l.x,
|
|
2455
|
+
l.y ?? 0
|
|
2456
|
+
) - l.x),
|
|
2457
|
+
0
|
|
2458
|
+
) / landmarks.length;
|
|
2053
2459
|
const atCap = achieved(cap);
|
|
2054
2460
|
const atRest = achieved(0);
|
|
2055
2461
|
const attainable = [atCap, atRest];
|
|
2056
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
2057
|
-
return {
|
|
2058
|
-
|
|
2059
|
-
|
|
2462
|
+
if (target > atRest + TURN_DEPTH_EPS) {
|
|
2463
|
+
return {
|
|
2464
|
+
reached: false,
|
|
2465
|
+
depth: 0,
|
|
2466
|
+
achieved: atRest,
|
|
2467
|
+
attainable,
|
|
2468
|
+
limit: "floor"
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
if (target < atCap - TURN_DEPTH_EPS) {
|
|
2472
|
+
return {
|
|
2473
|
+
reached: false,
|
|
2474
|
+
depth: cap,
|
|
2475
|
+
achieved: atCap,
|
|
2476
|
+
attainable,
|
|
2477
|
+
limit: "cap"
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2060
2480
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2061
2481
|
let lo = 0;
|
|
2062
2482
|
let hi = cap;
|
|
@@ -2072,68 +2492,52 @@ var TURN_SWEEP_MAX_FACTOR = 8;
|
|
|
2072
2492
|
var TURN_SWEEP_SAMPLES = 32;
|
|
2073
2493
|
var HOLD_CLEARANCE = 1;
|
|
2074
2494
|
var TURN_HOLD_SAMPLES = 16;
|
|
2075
|
-
var
|
|
2076
|
-
function hairFrontHoldTarget(x, faceCenterX,
|
|
2495
|
+
var TURN_SETTLE_PASSES = 24;
|
|
2496
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2077
2497
|
const local = x - faceCenterX;
|
|
2078
2498
|
const dist = Math.abs(local);
|
|
2079
2499
|
const side = Math.sign(local);
|
|
2080
|
-
const
|
|
2081
|
-
if (dist <=
|
|
2500
|
+
const painted = edgeAt(y);
|
|
2501
|
+
if (dist <= painted) return plateLandingAt(deg, x, y);
|
|
2082
2502
|
if (dist <= holdBase) {
|
|
2083
|
-
const inner =
|
|
2084
|
-
const u = (dist -
|
|
2085
|
-
return inner + (faceCenterX + side * holdEdge
|
|
2503
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, y);
|
|
2504
|
+
const u = (dist - painted) / (holdBase - painted);
|
|
2505
|
+
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2086
2506
|
}
|
|
2087
|
-
return x + side * (holdEdge
|
|
2507
|
+
return x + side * (holdEdge - holdBase);
|
|
2088
2508
|
}
|
|
2089
|
-
function hairFrontLandingAt(x,
|
|
2509
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2090
2510
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
lo,
|
|
2108
|
-
hi: Math.max(0, Math.min(cells, lo + 1)),
|
|
2109
|
-
t: Math.max(0, Math.min(1, v - lo))
|
|
2110
|
-
};
|
|
2111
|
-
};
|
|
2112
|
-
const col = bracket(
|
|
2113
|
-
(x - hairFront.x + hairFront.cropW / 2) / hairFront.cropW * cols,
|
|
2114
|
-
cols
|
|
2511
|
+
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2512
|
+
return renderedLandingX(
|
|
2513
|
+
{
|
|
2514
|
+
x: hairFront.x,
|
|
2515
|
+
y: hairFront.centerY,
|
|
2516
|
+
cropW: hairFront.cropW,
|
|
2517
|
+
cropH: hairFront.cropH,
|
|
2518
|
+
cols,
|
|
2519
|
+
rows,
|
|
2520
|
+
scaleX: 1,
|
|
2521
|
+
scaleY: 1,
|
|
2522
|
+
rotation: 0
|
|
2523
|
+
},
|
|
2524
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2525
|
+
x,
|
|
2526
|
+
y
|
|
2115
2527
|
);
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
const tl = landingAt(col.lo, row.lo);
|
|
2119
|
-
const br = landingAt(col.hi, row.hi);
|
|
2120
|
-
if (col.t <= row.t) {
|
|
2121
|
-
const bl = landingAt(col.lo, row.hi);
|
|
2122
|
-
return tl + row.t * (bl - tl) + col.t * (br - bl);
|
|
2123
|
-
}
|
|
2124
|
-
const tr = landingAt(col.hi, row.lo);
|
|
2125
|
-
return tl + col.t * (tr - tl) + row.t * (br - tr);
|
|
2126
|
-
}
|
|
2127
|
-
function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt, holdEdgeAt) {
|
|
2528
|
+
}
|
|
2529
|
+
function shellTravelCap(faceCenterX, edgeAt, plateGuardRows, askedTravel, bendOnlyLandingAt, holdEdgeAt) {
|
|
2128
2530
|
let cap = Infinity;
|
|
2129
2531
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2130
2532
|
if (deg === 0) continue;
|
|
2131
|
-
const map = bendOnlyMapAt(deg);
|
|
2132
2533
|
const slideSign = turnSlide(askedTravel, deg) < 0 ? -1 : 1;
|
|
2133
|
-
const towardReach =
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2534
|
+
const towardReach = plateSideReachAt(
|
|
2535
|
+
bendOnlyLandingAt,
|
|
2536
|
+
faceCenterX,
|
|
2537
|
+
edgeAt,
|
|
2538
|
+
plateGuardRows,
|
|
2539
|
+
deg,
|
|
2540
|
+
slideSign
|
|
2137
2541
|
);
|
|
2138
2542
|
cap = Math.min(
|
|
2139
2543
|
cap,
|
|
@@ -2142,8 +2546,39 @@ function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt,
|
|
|
2142
2546
|
}
|
|
2143
2547
|
return cap;
|
|
2144
2548
|
}
|
|
2549
|
+
function aitken(x0, x1, x2, cap) {
|
|
2550
|
+
const d1 = x1 - x0;
|
|
2551
|
+
const d2 = x2 - x1;
|
|
2552
|
+
const denominator = d2 - d1;
|
|
2553
|
+
if (denominator === 0) return x2;
|
|
2554
|
+
const x = x2 - d2 * d2 / denominator;
|
|
2555
|
+
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2556
|
+
}
|
|
2557
|
+
function strandDepthCap(margins, plateCap) {
|
|
2558
|
+
const holds = (depth) => margins.every((m) => m(depth) >= 0);
|
|
2559
|
+
if (holds(plateCap)) return plateCap;
|
|
2560
|
+
if (!holds(0)) return void 0;
|
|
2561
|
+
let lo = 0;
|
|
2562
|
+
let hi = plateCap;
|
|
2563
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2564
|
+
const mid = (lo + hi) / 2;
|
|
2565
|
+
if (holds(mid)) lo = mid;
|
|
2566
|
+
else hi = mid;
|
|
2567
|
+
}
|
|
2568
|
+
return lo;
|
|
2569
|
+
}
|
|
2145
2570
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2146
|
-
const
|
|
2571
|
+
const surfaceAt = (travel) => turnSurface({
|
|
2572
|
+
faceCenterX: ctx.faceCenterX,
|
|
2573
|
+
faceCenterY: ctx.faceCenterY,
|
|
2574
|
+
radius,
|
|
2575
|
+
travel,
|
|
2576
|
+
nodRadius: ctx.nodRadius,
|
|
2577
|
+
lattice: ctx.lattice,
|
|
2578
|
+
profile: ctx.profile,
|
|
2579
|
+
eyeRowY: ctx.eyeRowY
|
|
2580
|
+
});
|
|
2581
|
+
const bendOnlyLandingAt = plateLandingOn(ctx.plate, rowMapsOf(surfaceAt(0)));
|
|
2147
2582
|
const measuredEdgeAt = (side) => {
|
|
2148
2583
|
const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;
|
|
2149
2584
|
if (candidates === void 0 || candidates.length === 0) return void 0;
|
|
@@ -2155,66 +2590,160 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2155
2590
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2156
2591
|
};
|
|
2157
2592
|
const unit = headTurnParallaxUnit(radius);
|
|
2158
|
-
const
|
|
2159
|
-
const
|
|
2160
|
-
const
|
|
2593
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2594
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
2595
|
+
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2596
|
+
const bangsLandingAt = (hold, plateLandingAt, deg, leadShift, x, y) => hairFrontLandingAt(
|
|
2597
|
+
x,
|
|
2598
|
+
y,
|
|
2599
|
+
ctx.hairFrontSilhouette,
|
|
2600
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2601
|
+
vx,
|
|
2602
|
+
vy,
|
|
2603
|
+
deg,
|
|
2604
|
+
ctx.faceCenterX,
|
|
2605
|
+
plateLandingAt,
|
|
2606
|
+
ctx.edgeAt,
|
|
2607
|
+
hold.holdBase,
|
|
2608
|
+
hold.holdEdgeAt(deg)
|
|
2609
|
+
),
|
|
2610
|
+
leadShift
|
|
2611
|
+
);
|
|
2612
|
+
const strandStopsOn = (hold) => {
|
|
2613
|
+
const rowMaps = rowMapsOf(hold.surface);
|
|
2614
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2615
|
+
return (ctx.strands ?? []).map((strand) => {
|
|
2616
|
+
const iris = {
|
|
2617
|
+
x: strand.carrier.part.x,
|
|
2618
|
+
w: strand.carrier.part.cropW,
|
|
2619
|
+
carrier: strand.carrier
|
|
2620
|
+
};
|
|
2621
|
+
const stops = HEAD_TURN_STOPS.filter(
|
|
2622
|
+
(deg) => Math.sign(deg) === strand.side
|
|
2623
|
+
).map((deg) => {
|
|
2624
|
+
const runAt = (x) => bangsLandingAt(
|
|
2625
|
+
hold,
|
|
2626
|
+
plateLandingAt,
|
|
2627
|
+
deg,
|
|
2628
|
+
leadTipShift * deg / HEAD_TURN_MAX_DEG,
|
|
2629
|
+
x,
|
|
2630
|
+
strand.y
|
|
2631
|
+
);
|
|
2632
|
+
return {
|
|
2633
|
+
deg,
|
|
2634
|
+
runOuter: runAt(strand.runOuter),
|
|
2635
|
+
runFace: strand.runFace === null ? null : runAt(strand.runFace),
|
|
2636
|
+
irisAt: (depth, x) => landmarkLandingX(
|
|
2637
|
+
iris,
|
|
2638
|
+
(y) => rowMaps(deg, y),
|
|
2639
|
+
deg,
|
|
2640
|
+
depth * unit * deg / HEAD_TURN_MAX_DEG,
|
|
2641
|
+
x,
|
|
2642
|
+
strand.y
|
|
2643
|
+
)
|
|
2644
|
+
};
|
|
2645
|
+
});
|
|
2646
|
+
const { runFace } = strand;
|
|
2647
|
+
if (runFace === null) return { strand, stops, margins: void 0 };
|
|
2648
|
+
const allowance = Math.min(0, strand.side * (runFace - strand.irisOuter));
|
|
2649
|
+
const margins = stops.map(
|
|
2650
|
+
(stop) => (depth) => strand.side * (stop.runFace - stop.irisAt(depth, strand.irisOuter)) - allowance
|
|
2651
|
+
);
|
|
2652
|
+
return { strand, stops, margins };
|
|
2653
|
+
});
|
|
2654
|
+
};
|
|
2655
|
+
const strandOverlapOf = (sides, depth) => {
|
|
2656
|
+
const overlap = {};
|
|
2657
|
+
for (const { strand, stops, margins } of sides) {
|
|
2658
|
+
const s = strand.side;
|
|
2659
|
+
const covered = (irisOuter, irisInner, runOuter, runFace) => Math.max(
|
|
2660
|
+
0,
|
|
2661
|
+
Math.min(s * irisOuter, s * runOuter) - Math.max(s * irisInner, runFace === null ? -Infinity : s * runFace)
|
|
2662
|
+
);
|
|
2663
|
+
let worst;
|
|
2664
|
+
for (const stop of stops) {
|
|
2665
|
+
const px = covered(
|
|
2666
|
+
stop.irisAt(depth, strand.irisOuter),
|
|
2667
|
+
stop.irisAt(depth, strand.irisInner),
|
|
2668
|
+
stop.runOuter,
|
|
2669
|
+
stop.runFace
|
|
2670
|
+
);
|
|
2671
|
+
if (px > 0 && (worst === void 0 || px > worst.px)) {
|
|
2672
|
+
worst = { deg: stop.deg, px };
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
if (worst === void 0) continue;
|
|
2676
|
+
overlap[s < 0 ? "left" : "right"] = {
|
|
2677
|
+
deg: worst.deg,
|
|
2678
|
+
px: worst.px,
|
|
2679
|
+
hh: worst.px / ctx.hh,
|
|
2680
|
+
restPx: covered(
|
|
2681
|
+
strand.irisOuter,
|
|
2682
|
+
strand.irisInner,
|
|
2683
|
+
strand.runOuter,
|
|
2684
|
+
strand.runFace
|
|
2685
|
+
),
|
|
2686
|
+
held: margins !== void 0 && margins.every((m) => m(depth) >= 0)
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
return overlap.left === void 0 && overlap.right === void 0 ? void 0 : overlap;
|
|
2690
|
+
};
|
|
2691
|
+
const evaluateHold = (ratio, depths2) => {
|
|
2161
2692
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2162
|
-
const
|
|
2693
|
+
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
2163
2694
|
ctx.travel,
|
|
2164
2695
|
shellTravelCap(
|
|
2165
2696
|
ctx.faceCenterX,
|
|
2166
|
-
ctx.
|
|
2697
|
+
ctx.edgeAt,
|
|
2698
|
+
ctx.plateGuardRows,
|
|
2167
2699
|
ctx.travel,
|
|
2168
|
-
|
|
2700
|
+
bendOnlyLandingAt,
|
|
2169
2701
|
(deg, side) => {
|
|
2170
2702
|
const rest = shellHalfWidthAt(side);
|
|
2171
2703
|
return Math.min(rest, holdEdgeFrom(rest)(deg));
|
|
2172
2704
|
}
|
|
2173
2705
|
)
|
|
2174
2706
|
);
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2177
|
-
const
|
|
2707
|
+
const surface = surfaceAt(travel);
|
|
2708
|
+
const rowMaps = rowMapsOf(surface);
|
|
2709
|
+
const mapAt = (y) => rowMaps(-HEAD_TURN_MAX_DEG, y);
|
|
2710
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2711
|
+
const holdBase = ctx.headHalfWidth ?? plateReach(
|
|
2712
|
+
plateLandingAt,
|
|
2713
|
+
ctx.faceCenterX,
|
|
2714
|
+
ctx.edgeAt,
|
|
2715
|
+
ctx.plateGuardRows
|
|
2716
|
+
) + HOLD_CLEARANCE;
|
|
2717
|
+
const holdEdgeAt = holdEdgeFrom(holdBase);
|
|
2178
2718
|
let foldLowerBound = -Infinity;
|
|
2179
2719
|
let folds = false;
|
|
2180
2720
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2181
2721
|
if (deg === 0) continue;
|
|
2182
2722
|
const reach = plateReachAt(
|
|
2183
|
-
|
|
2723
|
+
plateLandingAt,
|
|
2184
2724
|
ctx.faceCenterX,
|
|
2185
|
-
ctx.
|
|
2725
|
+
ctx.edgeAt,
|
|
2726
|
+
ctx.plateGuardRows,
|
|
2727
|
+
deg
|
|
2186
2728
|
);
|
|
2187
2729
|
foldLowerBound = Math.max(
|
|
2188
2730
|
foldLowerBound,
|
|
2189
|
-
1 + ((reach + HOLD_CLEARANCE) /
|
|
2731
|
+
1 + ((reach + HOLD_CLEARANCE) / holdBase - 1) * (HEAD_TURN_MAX_DEG / Math.abs(deg))
|
|
2190
2732
|
);
|
|
2191
|
-
if (
|
|
2733
|
+
if (holdEdgeAt(deg) <= reach) folds = true;
|
|
2192
2734
|
}
|
|
2193
|
-
const map2 = columnMapAt(-HEAD_TURN_MAX_DEG);
|
|
2194
|
-
const requestedHoldEdge = holdEdgeAt2(HEAD_TURN_MAX_DEG);
|
|
2195
|
-
const gridReach2 = [
|
|
2196
|
-
Math.abs(map2.warpedX[0] - ctx.faceCenterX),
|
|
2197
|
-
Math.abs(map2.warpedX[map2.warpedX.length - 1] - ctx.faceCenterX)
|
|
2198
|
-
];
|
|
2199
|
-
const cappedHoldEdge = [
|
|
2200
|
-
Math.min(requestedHoldEdge, gridReach2[0] - HOLD_CLEARANCE),
|
|
2201
|
-
Math.min(requestedHoldEdge, gridReach2[1] - HOLD_CLEARANCE)
|
|
2202
|
-
];
|
|
2203
2735
|
const restAt = (side) => {
|
|
2204
2736
|
const edge = measuredEdgeAt(side);
|
|
2205
2737
|
if (edge !== void 0) return edge;
|
|
2206
|
-
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side *
|
|
2738
|
+
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2207
2739
|
};
|
|
2208
|
-
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x :
|
|
2740
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : bangsLandingAt(
|
|
2741
|
+
{ holdBase, holdEdgeAt },
|
|
2742
|
+
plateLandingAt,
|
|
2743
|
+
-HEAD_TURN_MAX_DEG,
|
|
2744
|
+
-leadTipShift,
|
|
2209
2745
|
x,
|
|
2210
|
-
ctx.
|
|
2211
|
-
ctx.faceCenterX,
|
|
2212
|
-
ctx.faceHalfWidth,
|
|
2213
|
-
holdBase2,
|
|
2214
|
-
cappedHoldEdge,
|
|
2215
|
-
map2,
|
|
2216
|
-
leadTipShift,
|
|
2217
|
-
leadRow
|
|
2746
|
+
ctx.eyeRowY
|
|
2218
2747
|
);
|
|
2219
2748
|
const landingOfRole = (role, x) => {
|
|
2220
2749
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2222,10 +2751,29 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2222
2751
|
return x;
|
|
2223
2752
|
}
|
|
2224
2753
|
if (role === "body") {
|
|
2225
|
-
return x - BODY_TURN_FOLLOW *
|
|
2754
|
+
return x - BODY_TURN_FOLLOW * travel;
|
|
2226
2755
|
}
|
|
2227
2756
|
if (role === "face" || FEATURE_NOD_DEPTH[roleFamily(role)] !== void 0) {
|
|
2228
|
-
|
|
2757
|
+
const carrier = ctx.carriers.get(role);
|
|
2758
|
+
if (carrier === void 0) {
|
|
2759
|
+
throw new Error(
|
|
2760
|
+
`auto-rig: evaluateTurnCandidate: headEdges names "${role}", which this layer set has no layer for`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2764
|
+
return carrierLandingX(
|
|
2765
|
+
carrier,
|
|
2766
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2767
|
+
carrier.anchor,
|
|
2768
|
+
mapAt,
|
|
2769
|
+
-HEAD_TURN_MAX_DEG,
|
|
2770
|
+
shift,
|
|
2771
|
+
nodeX,
|
|
2772
|
+
nodeY
|
|
2773
|
+
).x,
|
|
2774
|
+
x,
|
|
2775
|
+
ctx.eyeRowY
|
|
2776
|
+
);
|
|
2229
2777
|
}
|
|
2230
2778
|
throw new Error(
|
|
2231
2779
|
`auto-rig: evaluateTurnCandidate: unrecognised role "${role}"`
|
|
@@ -2241,13 +2789,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2241
2789
|
};
|
|
2242
2790
|
return {
|
|
2243
2791
|
ratio,
|
|
2244
|
-
travel
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2792
|
+
travel,
|
|
2793
|
+
mapAt,
|
|
2794
|
+
surface,
|
|
2795
|
+
holdBase,
|
|
2796
|
+
holdEdgeAt,
|
|
2248
2797
|
folds,
|
|
2249
2798
|
foldLowerBound,
|
|
2250
|
-
gridReach: gridReach2,
|
|
2251
2799
|
// How far the silhouette's own centre drifts off the face centre at full
|
|
2252
2800
|
// turn, against how far it already sat off centre at rest (zero unless
|
|
2253
2801
|
// hair_front's crop is itself off-centre) — the held shell keeps its own
|
|
@@ -2264,24 +2812,18 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2264
2812
|
restSpan: restAt(1) - restAt(-1)
|
|
2265
2813
|
};
|
|
2266
2814
|
};
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
for (let i = 0; i < TURN_HOLD_SATURATION_PASSES; i++) {
|
|
2270
|
-
const needed = (Math.max(hold2.gridReach[0], hold2.gridReach[1]) - HOLD_CLEARANCE) / hold2.holdBase;
|
|
2271
|
-
if (needed <= hold2.ratio) break;
|
|
2272
|
-
hold2 = evaluateHold(needed);
|
|
2273
|
-
}
|
|
2274
|
-
return hold2;
|
|
2275
|
-
};
|
|
2276
|
-
const holdRange = () => {
|
|
2277
|
-
const ceiling = saturatedHold();
|
|
2815
|
+
const holdRange = (depths2) => {
|
|
2816
|
+
const ceiling = evaluateHold(TURN_RATIO_MAX, depths2);
|
|
2278
2817
|
if (ceiling.folds) return void 0;
|
|
2279
|
-
let floor = evaluateHold(
|
|
2818
|
+
let floor = evaluateHold(
|
|
2819
|
+
Math.min(ceiling.foldLowerBound, ceiling.ratio),
|
|
2820
|
+
depths2
|
|
2821
|
+
);
|
|
2280
2822
|
if (floor.folds) {
|
|
2281
2823
|
let lo = floor;
|
|
2282
2824
|
let hi = ceiling;
|
|
2283
2825
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2284
|
-
const mid = evaluateHold((lo.ratio + hi.ratio) / 2);
|
|
2826
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2285
2827
|
if (mid.folds) lo = mid;
|
|
2286
2828
|
else hi = mid;
|
|
2287
2829
|
}
|
|
@@ -2290,7 +2832,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2290
2832
|
const samples = [floor];
|
|
2291
2833
|
for (let i = 1; i < TURN_HOLD_SAMPLES; i++) {
|
|
2292
2834
|
const sample = evaluateHold(
|
|
2293
|
-
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES
|
|
2835
|
+
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES,
|
|
2836
|
+
depths2
|
|
2294
2837
|
);
|
|
2295
2838
|
if (!sample.folds) samples.push(sample);
|
|
2296
2839
|
}
|
|
@@ -2303,15 +2846,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2303
2846
|
return lower > upper ? { blocked: "silhouetteRatio", lower: Infinity, upper: -Infinity } : { blocked: "silhouetteRatio", lower, upper };
|
|
2304
2847
|
};
|
|
2305
2848
|
const asked = ctx.silhouetteRatio;
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (samples === void 0) return silhouetteMiss();
|
|
2849
|
+
const fitHold = (depths2) => {
|
|
2850
|
+
const natural = evaluateHold(asked, depths2);
|
|
2851
|
+
const eps = TURN_DEPTH_EPS / natural.restSpan;
|
|
2852
|
+
if (!natural.folds && Math.abs(natural.renderedSilhouetteRatio - asked) <= eps) {
|
|
2853
|
+
return { hold: natural, silhouetteClamped: false };
|
|
2854
|
+
}
|
|
2855
|
+
const samples = holdRange(depths2);
|
|
2856
|
+
if (samples === void 0) return void 0;
|
|
2315
2857
|
const rendered = samples.map((h) => h.renderedSilhouetteRatio);
|
|
2316
2858
|
const span = [
|
|
2317
2859
|
Math.min(...rendered),
|
|
@@ -2326,91 +2868,169 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2326
2868
|
return ea <= eb ? a : b;
|
|
2327
2869
|
};
|
|
2328
2870
|
if (asked < span[0] - eps || asked > span[1] + eps) {
|
|
2329
|
-
if (!ctx.clampSilhouetteRatio) return silhouetteMiss(span);
|
|
2330
2871
|
const wanted = asked < span[0] ? span[0] : span[1];
|
|
2331
|
-
hold = samples.reduce(
|
|
2872
|
+
const hold = samples.reduce(
|
|
2332
2873
|
(a, b) => Math.abs(b.renderedSilhouetteRatio - wanted) < Math.abs(a.renderedSilhouetteRatio - wanted) ? b : a
|
|
2333
2874
|
);
|
|
2334
|
-
silhouetteClamped
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
}
|
|
2875
|
+
return { hold, silhouetteClamped: true, span };
|
|
2876
|
+
}
|
|
2877
|
+
let best = samples.reduce(nearer);
|
|
2878
|
+
let lo;
|
|
2879
|
+
let hi;
|
|
2880
|
+
for (let i = 0; i + 1 < samples.length; i++) {
|
|
2881
|
+
const a = samples[i].renderedSilhouetteRatio - asked;
|
|
2882
|
+
const b = samples[i + 1].renderedSilhouetteRatio - asked;
|
|
2883
|
+
if (a * b <= 0) {
|
|
2884
|
+
lo = samples[i];
|
|
2885
|
+
hi = samples[i + 1];
|
|
2886
|
+
break;
|
|
2347
2887
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2888
|
+
}
|
|
2889
|
+
for (let i = 0; lo !== void 0 && hi !== void 0; i++) {
|
|
2890
|
+
if (i >= TURN_BISECT_STEPS || Math.abs(best.renderedSilhouetteRatio - asked) <= eps) {
|
|
2891
|
+
break;
|
|
2892
|
+
}
|
|
2893
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2894
|
+
if (!mid.folds) best = nearer(best, mid);
|
|
2895
|
+
if ((mid.renderedSilhouetteRatio - asked) * (lo.renderedSilhouetteRatio - asked) > 0) {
|
|
2896
|
+
lo = mid;
|
|
2897
|
+
} else {
|
|
2898
|
+
hi = mid;
|
|
2359
2899
|
}
|
|
2360
|
-
hold = best;
|
|
2361
2900
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2901
|
+
return { hold: best, silhouetteClamped: false };
|
|
2902
|
+
};
|
|
2903
|
+
let depths = { eye: 0, nose: 0, mouth: 0 };
|
|
2904
|
+
let seed;
|
|
2905
|
+
for (let pass = 0; ; pass++) {
|
|
2906
|
+
const fitted = fitHold(depths);
|
|
2907
|
+
if (fitted === void 0) return silhouetteMiss();
|
|
2908
|
+
const { hold, silhouetteClamped, span } = fitted;
|
|
2909
|
+
if (hold.folds) {
|
|
2910
|
+
throw new Error(
|
|
2911
|
+
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
const strandSides = ctx.strands === void 0 ? void 0 : strandStopsOn(hold);
|
|
2915
|
+
let strandFeasible = true;
|
|
2916
|
+
let maxDepth;
|
|
2917
|
+
if (strandSides !== void 0) {
|
|
2918
|
+
const margins = strandSides.map((s) => s.margins);
|
|
2919
|
+
const cap = margins.every((m) => m !== void 0) ? strandDepthCap(
|
|
2920
|
+
margins.flatMap((m) => m),
|
|
2921
|
+
eyePlateCap
|
|
2922
|
+
) : void 0;
|
|
2923
|
+
strandFeasible = cap !== void 0;
|
|
2924
|
+
maxDepth = cap ?? 0;
|
|
2925
|
+
}
|
|
2926
|
+
const eye = solveTurnDepthSigned(
|
|
2927
|
+
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2928
|
+
ctx.landmarks.eye,
|
|
2929
|
+
unit,
|
|
2930
|
+
hold.mapAt,
|
|
2931
|
+
ctx.plateEdgeX,
|
|
2932
|
+
maxDepth
|
|
2366
2933
|
);
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2934
|
+
const features = solveFeatureDepths(
|
|
2935
|
+
ctx,
|
|
2936
|
+
(-eye.achieved + hold.silhouetteCenterShift) / ctx.hh,
|
|
2937
|
+
unit,
|
|
2938
|
+
hold.mapAt,
|
|
2939
|
+
hold.silhouetteCenterShift
|
|
2940
|
+
);
|
|
2941
|
+
const solved = {
|
|
2942
|
+
eye: eye.depth,
|
|
2943
|
+
nose: features.nose.depth,
|
|
2944
|
+
mouth: features.mouth.depth
|
|
2945
|
+
};
|
|
2946
|
+
const settled = evaluateHold(hold.ratio, solved);
|
|
2947
|
+
if (Math.abs(settled.silhouetteCenterShift - hold.silhouetteCenterShift) > TURN_DEPTH_EPS || Math.abs(settled.renderedSilhouetteRatio - hold.renderedSilhouetteRatio) * hold.restSpan > TURN_DEPTH_EPS) {
|
|
2948
|
+
if (pass + 1 >= TURN_SETTLE_PASSES) return { blocked: "settle" };
|
|
2949
|
+
if (seed === void 0) {
|
|
2950
|
+
seed = depths;
|
|
2951
|
+
depths = solved;
|
|
2952
|
+
} else {
|
|
2953
|
+
depths = {
|
|
2954
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2955
|
+
nose: aitken(
|
|
2956
|
+
seed.nose,
|
|
2957
|
+
depths.nose,
|
|
2958
|
+
solved.nose,
|
|
2959
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2960
|
+
),
|
|
2961
|
+
mouth: aitken(
|
|
2962
|
+
seed.mouth,
|
|
2963
|
+
depths.mouth,
|
|
2964
|
+
solved.mouth,
|
|
2965
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2966
|
+
)
|
|
2967
|
+
};
|
|
2968
|
+
seed = void 0;
|
|
2969
|
+
}
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
const {
|
|
2973
|
+
travel,
|
|
2974
|
+
mapAt,
|
|
2975
|
+
surface,
|
|
2976
|
+
holdBase,
|
|
2977
|
+
holdEdgeAt,
|
|
2978
|
+
silhouetteCenterShift,
|
|
2979
|
+
renderedSilhouetteRatio
|
|
2980
|
+
} = settled;
|
|
2981
|
+
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2982
|
+
return silhouetteMiss(span);
|
|
2983
|
+
}
|
|
2984
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2985
|
+
return {
|
|
2986
|
+
blocked: "eyeShift",
|
|
2987
|
+
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
2988
|
+
// a negative lower bound here would advertise a value the field's own
|
|
2989
|
+
// contract already rules out; 0 is the true floor.
|
|
2990
|
+
offeredShift: [
|
|
2991
|
+
Math.max(0, (-eye.attainable[1] + silhouetteCenterShift) / ctx.hh),
|
|
2992
|
+
(-eye.attainable[0] + silhouetteCenterShift) / ctx.hh
|
|
2993
|
+
]
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
const eyeShiftPx = -eye.depth * unit;
|
|
2997
|
+
const scaleOf = (l) => (landmarkLandingX(
|
|
2998
|
+
l,
|
|
2999
|
+
mapAt,
|
|
3000
|
+
-HEAD_TURN_MAX_DEG,
|
|
3001
|
+
eyeShiftPx,
|
|
3002
|
+
l.x + l.w / 2,
|
|
3003
|
+
l.y ?? 0
|
|
3004
|
+
) - landmarkLandingX(
|
|
3005
|
+
l,
|
|
3006
|
+
mapAt,
|
|
3007
|
+
-HEAD_TURN_MAX_DEG,
|
|
3008
|
+
eyeShiftPx,
|
|
3009
|
+
l.x - l.w / 2,
|
|
3010
|
+
l.y ?? 0
|
|
3011
|
+
)) / l.w;
|
|
3012
|
+
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
3013
|
+
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
3014
|
+
const strandOverlap = strandSides === void 0 ? void 0 : strandOverlapOf(strandSides, eye.depth);
|
|
2386
3015
|
return {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
3016
|
+
radius,
|
|
3017
|
+
ratio: scaleOf(far) / scaleOf(near),
|
|
3018
|
+
unit,
|
|
3019
|
+
travel,
|
|
3020
|
+
mapAt,
|
|
3021
|
+
surface,
|
|
3022
|
+
holdBase,
|
|
3023
|
+
holdEdgeAt,
|
|
3024
|
+
eye,
|
|
3025
|
+
nose: features.nose,
|
|
3026
|
+
mouth: features.mouth,
|
|
3027
|
+
silhouetteClamped,
|
|
3028
|
+
silhouetteCenterShift,
|
|
3029
|
+
renderedSilhouetteRatio,
|
|
3030
|
+
strandFeasible,
|
|
3031
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2395
3032
|
};
|
|
2396
3033
|
}
|
|
2397
|
-
const scaleOf = (l) => (map.mapX(l.x + l.w / 2 - eye.depth * unit) - map.mapX(l.x - l.w / 2 - eye.depth * unit)) / l.w;
|
|
2398
|
-
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2399
|
-
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2400
|
-
return {
|
|
2401
|
-
radius,
|
|
2402
|
-
ratio: scaleOf(far) / scaleOf(near),
|
|
2403
|
-
unit,
|
|
2404
|
-
travel,
|
|
2405
|
-
map,
|
|
2406
|
-
holdBase,
|
|
2407
|
-
holdEdgeAt,
|
|
2408
|
-
holdEdgeX,
|
|
2409
|
-
eye,
|
|
2410
|
-
silhouetteClamped,
|
|
2411
|
-
silhouetteCenterShift,
|
|
2412
|
-
renderedSilhouetteRatio
|
|
2413
|
-
};
|
|
2414
3034
|
}
|
|
2415
3035
|
function sweepTurnRadii(ctx) {
|
|
2416
3036
|
const minRadius = ctx.faceHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
@@ -2419,6 +3039,7 @@ function sweepTurnRadii(ctx) {
|
|
|
2419
3039
|
let heldRatioLower = Infinity;
|
|
2420
3040
|
let heldRatioUpper = -Infinity;
|
|
2421
3041
|
let offeredShift;
|
|
3042
|
+
let unsettled = 0;
|
|
2422
3043
|
for (let i = 0; i < TURN_SWEEP_SAMPLES; i++) {
|
|
2423
3044
|
const radius = minRadius * Math.pow(maxRadius / minRadius, i / (TURN_SWEEP_SAMPLES - 1));
|
|
2424
3045
|
const result = evaluateTurnCandidate(ctx, radius);
|
|
@@ -2427,6 +3048,8 @@ function sweepTurnRadii(ctx) {
|
|
|
2427
3048
|
} else if (result.blocked === "silhouetteRatio") {
|
|
2428
3049
|
heldRatioLower = Math.min(heldRatioLower, result.lower);
|
|
2429
3050
|
heldRatioUpper = Math.max(heldRatioUpper, result.upper);
|
|
3051
|
+
} else if (result.blocked === "settle") {
|
|
3052
|
+
unsettled++;
|
|
2430
3053
|
} else {
|
|
2431
3054
|
offeredShift = offeredShift ? [
|
|
2432
3055
|
Math.min(offeredShift[0], result.offeredShift[0]),
|
|
@@ -2434,7 +3057,16 @@ function sweepTurnRadii(ctx) {
|
|
|
2434
3057
|
] : result.offeredShift;
|
|
2435
3058
|
}
|
|
2436
3059
|
}
|
|
2437
|
-
return {
|
|
3060
|
+
return {
|
|
3061
|
+
candidates,
|
|
3062
|
+
heldRatioLower,
|
|
3063
|
+
heldRatioUpper,
|
|
3064
|
+
offeredShift,
|
|
3065
|
+
unsettled
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
function runHolds(run, ratio) {
|
|
3069
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
2438
3070
|
}
|
|
2439
3071
|
function ratioClusters(candidates) {
|
|
2440
3072
|
const clusters = [];
|
|
@@ -2458,7 +3090,7 @@ function ratioClusters(candidates) {
|
|
|
2458
3090
|
close();
|
|
2459
3091
|
return clusters;
|
|
2460
3092
|
}
|
|
2461
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3093
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2462
3094
|
let lo = candidates[0];
|
|
2463
3095
|
let hi = candidates[candidates.length - 1];
|
|
2464
3096
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2468,79 +3100,102 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2468
3100
|
break;
|
|
2469
3101
|
}
|
|
2470
3102
|
}
|
|
3103
|
+
return bisectTurnRadius(
|
|
3104
|
+
lo,
|
|
3105
|
+
hi,
|
|
3106
|
+
target,
|
|
3107
|
+
(radius) => {
|
|
3108
|
+
const mid = evaluateTurnCandidate(ctx, radius);
|
|
3109
|
+
return "blocked" in mid ? void 0 : mid;
|
|
3110
|
+
},
|
|
3111
|
+
accept
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
function bisectTurnRadius(lo, hi, target, evaluate, accept) {
|
|
3115
|
+
if (!accept(lo) || !accept(hi)) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
"auto-rig: bisectTurnRadius: a bracket end is not accepted"
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const miss = (c) => Math.abs(c.ratio - target);
|
|
3121
|
+
let nearest = miss(lo) <= miss(hi) ? lo : hi;
|
|
2471
3122
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2472
|
-
const mid =
|
|
2473
|
-
if (
|
|
3123
|
+
const mid = evaluate(Math.sqrt(lo.radius * hi.radius));
|
|
3124
|
+
if (mid === void 0) break;
|
|
3125
|
+
if (accept(mid) && miss(mid) < miss(nearest)) nearest = mid;
|
|
2474
3126
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2475
3127
|
else lo = mid;
|
|
2476
3128
|
}
|
|
2477
|
-
|
|
3129
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3130
|
+
return nearest;
|
|
2478
3131
|
}
|
|
2479
|
-
function
|
|
2480
|
-
const
|
|
2481
|
-
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
)
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
best.holdEdgeX,
|
|
2496
|
-
ctx.plateEdgeX
|
|
2497
|
-
);
|
|
2498
|
-
if (!solution.reached) {
|
|
2499
|
-
if (!targets.defaulted.has(field)) {
|
|
2500
|
-
return {
|
|
2501
|
-
blocked: field,
|
|
2502
|
-
// noseShift/mouthShift are MAGNITUDES too (see the correction
|
|
2503
|
-
// above), so a negative lower bound here would advertise a value
|
|
2504
|
-
// the field's own contract already rules out; 0 is the true floor.
|
|
2505
|
-
attainable: [
|
|
2506
|
-
Math.max(
|
|
2507
|
-
0,
|
|
2508
|
-
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
2509
|
-
),
|
|
2510
|
-
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
2511
|
-
]
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
clamped.push(field);
|
|
2515
|
-
}
|
|
2516
|
-
solved[field] = solution.depth;
|
|
2517
|
-
}
|
|
3132
|
+
function strandPreferredCandidates(candidates, farEyeRatio) {
|
|
3133
|
+
const feasible = candidates.filter((c) => c.strandFeasible);
|
|
3134
|
+
if (feasible.length === candidates.length) return candidates;
|
|
3135
|
+
const holds = ratioClusters(feasible).some(
|
|
3136
|
+
(run) => runHolds(run, farEyeRatio)
|
|
3137
|
+
);
|
|
3138
|
+
return holds ? feasible : candidates;
|
|
3139
|
+
}
|
|
3140
|
+
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
3141
|
+
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
3142
|
+
silhouetteCenterShift - (measured ?? Math.abs(share * eyeShift)) * ctx.hh,
|
|
3143
|
+
marks,
|
|
3144
|
+
unit,
|
|
3145
|
+
mapAt,
|
|
3146
|
+
ctx.plateEdgeX
|
|
3147
|
+
);
|
|
2518
3148
|
return {
|
|
2519
|
-
nose:
|
|
2520
|
-
mouth:
|
|
2521
|
-
clamped
|
|
3149
|
+
nose: solve(ctx.noseShift, ctx.landmarks.nose, NOSE_SHIFT_SHARE),
|
|
3150
|
+
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2522
3151
|
};
|
|
2523
3152
|
}
|
|
2524
|
-
function solveTurnModel(targets, landmarks,
|
|
3153
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2525
3154
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2526
3155
|
throw new TurnTargetError(
|
|
2527
3156
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2528
3157
|
);
|
|
2529
3158
|
}
|
|
2530
|
-
const eyeRowY =
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
1,
|
|
2535
|
-
(hairFront.centerY + hairFront.cropH / 2 - eyeRowY) / hairFront.cropH
|
|
2536
|
-
)
|
|
3159
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3160
|
+
let strands;
|
|
3161
|
+
const strandSides = ["left", "right"].filter(
|
|
3162
|
+
(key) => strandEdges?.[key] !== void 0
|
|
2537
3163
|
);
|
|
3164
|
+
if (strandSides.length > 0) {
|
|
3165
|
+
if (hairFront === void 0) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
"auto-rig: solveTurnModel: strandEdges was given, but there is no hair_front to land its runs on"
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const irises = ["iris_L", "iris_R"].map((role) => carriers.get(role)).filter((c) => c !== void 0).sort((a, b) => a.part.x - b.part.x);
|
|
3171
|
+
strands = strandSides.map((key) => {
|
|
3172
|
+
const carrier = irises.length === 2 ? irises[key === "left" ? 0 : 1] : void 0;
|
|
3173
|
+
if (carrier === void 0) {
|
|
3174
|
+
throw new Error(
|
|
3175
|
+
`auto-rig: solveTurnModel: strandEdges.${key} has no iris carrier to land`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
return {
|
|
3179
|
+
...strandEdges[key],
|
|
3180
|
+
side: key === "left" ? -1 : 1,
|
|
3181
|
+
carrier
|
|
3182
|
+
};
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
2538
3185
|
const ctx = {
|
|
2539
3186
|
landmarks,
|
|
2540
|
-
|
|
3187
|
+
lattice,
|
|
2541
3188
|
faceCenterX,
|
|
3189
|
+
faceCenterY,
|
|
3190
|
+
nodRadius,
|
|
3191
|
+
eyeRowY,
|
|
2542
3192
|
faceHalfWidth,
|
|
3193
|
+
// The slide's hard stop is the CROP's edge, profile or not: the far eye
|
|
3194
|
+
// may sit on the plate's transparent margin, as it does on the hero.
|
|
2543
3195
|
plateEdgeX: faceCenterX - faceHalfWidth,
|
|
3196
|
+
carriers,
|
|
3197
|
+
profile,
|
|
3198
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
2544
3199
|
// What this plate asks the turn for; each candidate caps it to its own
|
|
2545
3200
|
// shell — see TurnSolveContext.travel.
|
|
2546
3201
|
travel: headTurnTravel(faceHalfWidth),
|
|
@@ -2553,16 +3208,23 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2553
3208
|
// every downstream use already has the magnitude.
|
|
2554
3209
|
eyeShift: Math.abs(targets.eyeShift),
|
|
2555
3210
|
clampEyeShift: targets.defaulted.has("eyeShift"),
|
|
3211
|
+
noseShift: targets.defaulted.has("noseShift") ? void 0 : Math.abs(targets.noseShift),
|
|
3212
|
+
mouthShift: targets.defaulted.has("mouthShift") ? void 0 : Math.abs(targets.mouthShift),
|
|
2556
3213
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2557
|
-
|
|
2558
|
-
hairFrontSilhouette: hairFront === void 0 ? void 0 : { x: hairFront.x, cropW: hairFront.cropW, cropH: hairFront.cropH },
|
|
3214
|
+
hairFrontSilhouette: hairFront,
|
|
2559
3215
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2560
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3216
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3217
|
+
strands
|
|
2561
3218
|
};
|
|
2562
|
-
const clamped = [];
|
|
2563
3219
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2564
3220
|
const pass = sweepTurnRadii(ctx);
|
|
2565
3221
|
if (pass.candidates.length === 0) {
|
|
3222
|
+
if (pass.unsettled > 0) {
|
|
3223
|
+
const refused = TURN_SWEEP_SAMPLES - pass.unsettled;
|
|
3224
|
+
throw new TurnTargetError(
|
|
3225
|
+
`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}` : ""}`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
2566
3228
|
return pass.offeredShift ? {
|
|
2567
3229
|
unreachable: true,
|
|
2568
3230
|
field: "eyeShift",
|
|
@@ -2584,72 +3246,107 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2584
3246
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2585
3247
|
};
|
|
2586
3248
|
}
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
let cluster = clusters.find(
|
|
2595
|
-
(c) => farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS && farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS
|
|
2596
|
-
);
|
|
2597
|
-
if (cluster === void 0) {
|
|
2598
|
-
cluster = clusters.reduce(
|
|
2599
|
-
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3249
|
+
const fitFrom = (pool, accept) => {
|
|
3250
|
+
const clamped = [];
|
|
3251
|
+
const clusters = ratioClusters(pool);
|
|
3252
|
+
const distanceTo = (c) => Math.max(
|
|
3253
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
3254
|
+
targets.farEyeRatio - c.ratios[1],
|
|
3255
|
+
0
|
|
2600
3256
|
);
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
3257
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
3258
|
+
let cluster = clusters.find((c) => runHolds(c, farEyeRatio));
|
|
3259
|
+
if (cluster === void 0) {
|
|
3260
|
+
cluster = clusters.reduce(
|
|
3261
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3262
|
+
);
|
|
3263
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3264
|
+
return {
|
|
3265
|
+
unreachable: true,
|
|
3266
|
+
field: "farEyeRatio",
|
|
3267
|
+
value: targets.farEyeRatio,
|
|
3268
|
+
attainable: cluster.ratios
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
3272
|
+
clamped.push("farEyeRatio");
|
|
2608
3273
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
3274
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio, accept);
|
|
3275
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
3276
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3277
|
+
return {
|
|
3278
|
+
unreachable: true,
|
|
3279
|
+
field: "farEyeRatio",
|
|
3280
|
+
value: targets.farEyeRatio,
|
|
3281
|
+
attainable: cluster.ratios
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
3285
|
+
}
|
|
3286
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
3287
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
3288
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
3289
|
+
for (const [field, solution] of [
|
|
3290
|
+
["noseShift", best.nose],
|
|
3291
|
+
["mouthShift", best.mouth]
|
|
3292
|
+
]) {
|
|
3293
|
+
if (solution.reached) continue;
|
|
3294
|
+
if (!targets.defaulted.has(field)) {
|
|
3295
|
+
return {
|
|
3296
|
+
unreachable: true,
|
|
3297
|
+
field,
|
|
3298
|
+
value: targets[field],
|
|
3299
|
+
// noseShift/mouthShift are MAGNITUDES too (see
|
|
3300
|
+
// solveFeatureDepths), so a negative lower bound here would
|
|
3301
|
+
// advertise a value the field's own contract already rules out; 0
|
|
3302
|
+
// is the true floor.
|
|
3303
|
+
attainable: [
|
|
3304
|
+
Math.max(
|
|
3305
|
+
0,
|
|
3306
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
3307
|
+
),
|
|
3308
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
3309
|
+
]
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
clamped.push(field);
|
|
2621
3313
|
}
|
|
2622
|
-
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
2623
|
-
}
|
|
2624
|
-
if (!best.eye.reached) clamped.push("eyeShift");
|
|
2625
|
-
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
2626
|
-
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
2627
|
-
const features = solveFeatureDepths(ctx, targets, best, eyeShift);
|
|
2628
|
-
if ("blocked" in features) {
|
|
2629
3314
|
return {
|
|
2630
|
-
unreachable:
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
3315
|
+
unreachable: false,
|
|
3316
|
+
radius: best.radius,
|
|
3317
|
+
holdBase: best.holdBase,
|
|
3318
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
3319
|
+
travel: best.travel,
|
|
3320
|
+
surface: best.surface,
|
|
3321
|
+
depths: {
|
|
3322
|
+
eye: best.eye.depth,
|
|
3323
|
+
nose: best.nose.depth,
|
|
3324
|
+
mouth: best.mouth.depth
|
|
3325
|
+
},
|
|
3326
|
+
achieved: {
|
|
3327
|
+
eyeShift,
|
|
3328
|
+
farEyeRatio: best.ratio,
|
|
3329
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
3330
|
+
},
|
|
3331
|
+
clamped,
|
|
3332
|
+
...best.strandOverlap === void 0 ? {} : { strandOverlap: best.strandOverlap }
|
|
2634
3333
|
};
|
|
2635
|
-
}
|
|
2636
|
-
return {
|
|
2637
|
-
unreachable: false,
|
|
2638
|
-
radius: best.radius,
|
|
2639
|
-
holdBase: best.holdBase,
|
|
2640
|
-
holdEdgeAt: best.holdEdgeAt,
|
|
2641
|
-
travel: best.travel,
|
|
2642
|
-
depths: { eye: best.eye.depth, nose: features.nose, mouth: features.mouth },
|
|
2643
|
-
achieved: {
|
|
2644
|
-
eyeShift,
|
|
2645
|
-
farEyeRatio: best.ratio,
|
|
2646
|
-
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2647
|
-
},
|
|
2648
|
-
clamped: [...clamped, ...features.clamped]
|
|
2649
3334
|
};
|
|
3335
|
+
const preferred = strandPreferredCandidates(
|
|
3336
|
+
pass.candidates,
|
|
3337
|
+
targets.farEyeRatio
|
|
3338
|
+
);
|
|
3339
|
+
if (preferred !== pass.candidates) {
|
|
3340
|
+
const fitted = fitFrom(preferred, (c) => c.strandFeasible);
|
|
3341
|
+
if (!fitted.unreachable && !fitted.clamped.includes("farEyeRatio")) {
|
|
3342
|
+
return fitted;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
return fitFrom(pass.candidates, () => true);
|
|
2650
3346
|
}
|
|
2651
3347
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2652
3348
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3349
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
2653
3350
|
var NOD_TRAVEL = 30;
|
|
2654
3351
|
var NOD_BEND = 0.5;
|
|
2655
3352
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -2682,27 +3379,16 @@ function featureParallaxBindings(role, options) {
|
|
|
2682
3379
|
if (!options.hasNose) return [];
|
|
2683
3380
|
const nodDepth = FEATURE_NOD_DEPTH[roleFamily(role)];
|
|
2684
3381
|
if (nodDepth === void 0) return [];
|
|
2685
|
-
const bindings = [];
|
|
2686
|
-
const turnDepth = options.turnDepths?.[turnFamily(role)] ?? 0;
|
|
2687
|
-
const shiftX = turnDepth * (options.parallaxUnit ?? 0);
|
|
2688
|
-
if (shiftX !== 0) {
|
|
2689
|
-
bindings.push({
|
|
2690
|
-
parameter: import_format3.StandardParameter.AngleX,
|
|
2691
|
-
channel: "translateX",
|
|
2692
|
-
from: -shiftX,
|
|
2693
|
-
to: shiftX
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
3382
|
const shiftY = nodDepth * (options.parallaxUnitY ?? 0);
|
|
2697
|
-
if (shiftY
|
|
2698
|
-
|
|
3383
|
+
if (shiftY === 0) return [];
|
|
3384
|
+
return [
|
|
3385
|
+
{
|
|
2699
3386
|
parameter: import_format3.StandardParameter.AngleY,
|
|
2700
3387
|
channel: "translateY",
|
|
2701
3388
|
from: -shiftY,
|
|
2702
3389
|
to: shiftY
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
return bindings;
|
|
3390
|
+
}
|
|
3391
|
+
];
|
|
2706
3392
|
}
|
|
2707
3393
|
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
2708
3394
|
return [
|
|
@@ -2810,20 +3496,17 @@ function roleOwnBindings(spec, role, cropW, cropH, options) {
|
|
|
2810
3496
|
];
|
|
2811
3497
|
}
|
|
2812
3498
|
}
|
|
2813
|
-
if (role === "
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
if (shiftY !== 0) {
|
|
2819
|
-
parallax.push({
|
|
3499
|
+
if (role === "hair_back") {
|
|
3500
|
+
const shiftY = HAIR_BACK_NOD_DEPTH * (options.parallaxUnitY ?? 0);
|
|
3501
|
+
if (shiftY === 0) return [];
|
|
3502
|
+
return [
|
|
3503
|
+
{
|
|
2820
3504
|
parameter: import_format3.StandardParameter.AngleY,
|
|
2821
3505
|
channel: "translateY",
|
|
2822
3506
|
from: -shiftY,
|
|
2823
3507
|
to: shiftY
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
return parallax;
|
|
3508
|
+
}
|
|
3509
|
+
];
|
|
2827
3510
|
}
|
|
2828
3511
|
return [];
|
|
2829
3512
|
}
|
|
@@ -2846,64 +3529,98 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
2846
3529
|
]
|
|
2847
3530
|
};
|
|
2848
3531
|
}
|
|
2849
|
-
function
|
|
3532
|
+
function bakeNodWarp(mesh, partY, surface, nodTravel) {
|
|
3533
|
+
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
3534
|
+
const t = nodTravel * deg / HEAD_TURN_MAX_DEG;
|
|
3535
|
+
const offsets = [];
|
|
3536
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
3537
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3538
|
+
offsets.push(0, deg === 0 ? 0 : t + surface.nodBendAt(y + t, deg));
|
|
3539
|
+
}
|
|
3540
|
+
return { value: deg, offsets };
|
|
3541
|
+
});
|
|
3542
|
+
return { parameter: import_format3.StandardParameter.AngleY, keyforms };
|
|
3543
|
+
}
|
|
3544
|
+
function plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) {
|
|
2850
3545
|
let reach = 0;
|
|
2851
3546
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2852
3547
|
reach = Math.max(
|
|
2853
3548
|
reach,
|
|
2854
|
-
plateReachAt(
|
|
3549
|
+
plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
2855
3550
|
);
|
|
2856
3551
|
}
|
|
2857
3552
|
return reach;
|
|
2858
3553
|
}
|
|
2859
|
-
function
|
|
3554
|
+
function plateSideReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg, side) {
|
|
2860
3555
|
let reach = 0;
|
|
2861
|
-
for (const
|
|
2862
|
-
const dest =
|
|
3556
|
+
for (const y of plateGuardRows) {
|
|
3557
|
+
const dest = plateLandingAt(deg, faceCenterX + side * edgeAt(y), y);
|
|
2863
3558
|
reach = Math.max(reach, Math.abs(dest - faceCenterX));
|
|
2864
3559
|
}
|
|
2865
3560
|
return reach;
|
|
2866
3561
|
}
|
|
2867
|
-
function
|
|
3562
|
+
function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg) {
|
|
3563
|
+
return Math.max(
|
|
3564
|
+
plateSideReachAt(
|
|
3565
|
+
plateLandingAt,
|
|
3566
|
+
faceCenterX,
|
|
3567
|
+
edgeAt,
|
|
3568
|
+
plateGuardRows,
|
|
3569
|
+
deg,
|
|
3570
|
+
-1
|
|
3571
|
+
),
|
|
3572
|
+
plateSideReachAt(
|
|
3573
|
+
plateLandingAt,
|
|
3574
|
+
faceCenterX,
|
|
3575
|
+
edgeAt,
|
|
3576
|
+
plateGuardRows,
|
|
3577
|
+
deg,
|
|
3578
|
+
1
|
|
3579
|
+
)
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
2868
3583
|
if (holdEdgeAt(0) !== holdBase) {
|
|
2869
3584
|
throw new Error(
|
|
2870
3585
|
`auto-rig: bakeHairFrontSilhouetteWarp: holdEdgeAt(0) is ${holdEdgeAt(0)}, not the hold edge's own rest distance ${holdBase}, so the bangs would move in the rest pose`
|
|
2871
3586
|
);
|
|
2872
3587
|
}
|
|
2873
|
-
const SIDES = [-1, 1];
|
|
2874
3588
|
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
2875
|
-
const
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
3589
|
+
const holdEdge = holdEdgeAt(deg);
|
|
3590
|
+
const reach = [-1, 1].map(
|
|
3591
|
+
(side) => plateSideReachAt(
|
|
3592
|
+
plateLandingAt,
|
|
3593
|
+
faceCenterX,
|
|
3594
|
+
edgeAt,
|
|
3595
|
+
plateGuardRows,
|
|
3596
|
+
deg,
|
|
3597
|
+
side
|
|
3598
|
+
)
|
|
2879
3599
|
);
|
|
2880
|
-
const reached =
|
|
2881
|
-
if (
|
|
2882
|
-
const side =
|
|
3600
|
+
const reached = Math.max(reach[0], reach[1]);
|
|
3601
|
+
if (holdEdge <= reached) {
|
|
3602
|
+
const side = reach[1] > reach[0] ? 1 : -1;
|
|
2883
3603
|
throw new Error(
|
|
2884
|
-
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${
|
|
3604
|
+
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${holdEdge} from the face centre but the plate's edge maps to ${reached}, so the ramp between them would fold`
|
|
2885
3605
|
);
|
|
2886
3606
|
}
|
|
2887
|
-
const holdEdge = deg === 0 ? [requestedHoldEdge, requestedHoldEdge] : SIDES.map(
|
|
2888
|
-
(_side, idx) => Math.min(
|
|
2889
|
-
requestedHoldEdge,
|
|
2890
|
-
Math.abs(
|
|
2891
|
-
map.warpedX[idx === 0 ? 0 : map.warpedX.length - 1] - faceCenterX
|
|
2892
|
-
) - HOLD_CLEARANCE
|
|
2893
|
-
)
|
|
2894
|
-
);
|
|
2895
3607
|
const offsets = [];
|
|
2896
3608
|
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2897
3609
|
const x = partX + mesh.vertices[i];
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3610
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3611
|
+
offsets.push(
|
|
3612
|
+
deg === 0 ? 0 : hairFrontHoldTarget(
|
|
3613
|
+
x,
|
|
3614
|
+
y,
|
|
3615
|
+
deg,
|
|
3616
|
+
faceCenterX,
|
|
3617
|
+
plateLandingAt,
|
|
3618
|
+
edgeAt,
|
|
3619
|
+
holdBase,
|
|
3620
|
+
holdEdge
|
|
3621
|
+
) - x,
|
|
3622
|
+
0
|
|
2905
3623
|
);
|
|
2906
|
-
offsets.push(map.invertX(target) - x, 0);
|
|
2907
3624
|
}
|
|
2908
3625
|
return { value: deg, offsets };
|
|
2909
3626
|
});
|
|
@@ -2912,7 +3629,6 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, faceCenterX, faceHalfWidth, ho
|
|
|
2912
3629
|
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2913
3630
|
var HAIR_SWAY_CURL = 1.5;
|
|
2914
3631
|
var HAIR_SWAY_RANGE = 20;
|
|
2915
|
-
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2916
3632
|
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2917
3633
|
let top = -Infinity;
|
|
2918
3634
|
let bottom = Infinity;
|
|
@@ -2942,112 +3658,385 @@ function headTurnTravel(faceHalfWidth) {
|
|
|
2942
3658
|
var BODY_TURN_FOLLOW = 0.3;
|
|
2943
3659
|
var HEAD_BREATH_BOB = -12;
|
|
2944
3660
|
var BODY_BREATH_FOLLOW = 0.5;
|
|
3661
|
+
var STANDARD_PARAMETERS = [
|
|
3662
|
+
{
|
|
3663
|
+
id: import_format3.StandardParameter.MouthOpen,
|
|
3664
|
+
name: "Mouth Open",
|
|
3665
|
+
min: 0,
|
|
3666
|
+
max: 1,
|
|
3667
|
+
default: 0
|
|
3668
|
+
},
|
|
3669
|
+
{
|
|
3670
|
+
id: import_format3.StandardParameter.MouthForm,
|
|
3671
|
+
name: "Mouth Form",
|
|
3672
|
+
min: -1,
|
|
3673
|
+
max: 1,
|
|
3674
|
+
default: 0
|
|
3675
|
+
},
|
|
3676
|
+
{
|
|
3677
|
+
id: import_format3.StandardParameter.EyeOpenLeft,
|
|
3678
|
+
name: "Eye L",
|
|
3679
|
+
min: 0,
|
|
3680
|
+
max: 1,
|
|
3681
|
+
default: 1
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
id: import_format3.StandardParameter.EyeOpenRight,
|
|
3685
|
+
name: "Eye R",
|
|
3686
|
+
min: 0,
|
|
3687
|
+
max: 1,
|
|
3688
|
+
default: 1
|
|
3689
|
+
},
|
|
3690
|
+
{
|
|
3691
|
+
id: import_format3.StandardParameter.EyeballX,
|
|
3692
|
+
name: "Gaze X",
|
|
3693
|
+
min: -1,
|
|
3694
|
+
max: 1,
|
|
3695
|
+
default: 0
|
|
3696
|
+
},
|
|
3697
|
+
{
|
|
3698
|
+
id: import_format3.StandardParameter.EyeballY,
|
|
3699
|
+
name: "Gaze Y",
|
|
3700
|
+
min: -1,
|
|
3701
|
+
max: 1,
|
|
3702
|
+
default: 0
|
|
3703
|
+
},
|
|
3704
|
+
{
|
|
3705
|
+
id: import_format3.StandardParameter.AngleX,
|
|
3706
|
+
name: "Head Angle",
|
|
3707
|
+
min: -30,
|
|
3708
|
+
max: 30,
|
|
3709
|
+
default: 0
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
id: import_format3.StandardParameter.AngleY,
|
|
3713
|
+
name: "Head Angle Y",
|
|
3714
|
+
min: -30,
|
|
3715
|
+
max: 30,
|
|
3716
|
+
default: 0
|
|
3717
|
+
},
|
|
3718
|
+
{
|
|
3719
|
+
id: import_format3.StandardParameter.AngleZ,
|
|
3720
|
+
name: "Head Angle Z",
|
|
3721
|
+
min: -30,
|
|
3722
|
+
max: 30,
|
|
3723
|
+
default: 0
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
id: import_format3.StandardParameter.Breath,
|
|
3727
|
+
name: "Breath",
|
|
3728
|
+
min: 0,
|
|
3729
|
+
max: 1,
|
|
3730
|
+
default: 0
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
id: import_format3.StandardParameter.BrowLeftY,
|
|
3734
|
+
name: "Brow L Y",
|
|
3735
|
+
min: -1,
|
|
3736
|
+
max: 1,
|
|
3737
|
+
default: 0
|
|
3738
|
+
},
|
|
3739
|
+
{
|
|
3740
|
+
id: import_format3.StandardParameter.BrowRightY,
|
|
3741
|
+
name: "Brow R Y",
|
|
3742
|
+
min: -1,
|
|
3743
|
+
max: 1,
|
|
3744
|
+
default: 0
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
id: import_format3.StandardParameter.BrowLeftAngle,
|
|
3748
|
+
name: "Brow L Angle",
|
|
3749
|
+
min: -1,
|
|
3750
|
+
max: 1,
|
|
3751
|
+
default: 0
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
id: import_format3.StandardParameter.BrowRightAngle,
|
|
3755
|
+
name: "Brow R Angle",
|
|
3756
|
+
min: -1,
|
|
3757
|
+
max: 1,
|
|
3758
|
+
default: 0
|
|
3759
|
+
}
|
|
3760
|
+
];
|
|
3761
|
+
function transformUnder(bindings, valueOf) {
|
|
3762
|
+
const trs = { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1 };
|
|
3763
|
+
bindings.forEach((binding, i) => {
|
|
3764
|
+
const value = valueOf(binding, i);
|
|
3765
|
+
switch (binding.channel) {
|
|
3766
|
+
case "translateX":
|
|
3767
|
+
trs.x += value;
|
|
3768
|
+
break;
|
|
3769
|
+
case "translateY":
|
|
3770
|
+
trs.y += value;
|
|
3771
|
+
break;
|
|
3772
|
+
case "rotate":
|
|
3773
|
+
trs.rotation += value;
|
|
3774
|
+
break;
|
|
3775
|
+
case "scaleX":
|
|
3776
|
+
trs.scaleX += value;
|
|
3777
|
+
break;
|
|
3778
|
+
case "scaleY":
|
|
3779
|
+
trs.scaleY += value;
|
|
3780
|
+
break;
|
|
3781
|
+
case "opacity":
|
|
3782
|
+
break;
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
return trs;
|
|
3786
|
+
}
|
|
3787
|
+
function defaultPoseOf(bindings) {
|
|
3788
|
+
return transformUnder(bindings, (binding) => {
|
|
3789
|
+
const p = STANDARD_PARAMETERS.find((p2) => p2.id === binding.parameter);
|
|
3790
|
+
if (p === void 0) {
|
|
3791
|
+
throw new Error(
|
|
3792
|
+
`auto-rig: defaultPoseOf: "${binding.parameter}" is not a standard parameter`
|
|
3793
|
+
);
|
|
3794
|
+
}
|
|
3795
|
+
const t = (p.default - p.min) / (p.max - p.min);
|
|
3796
|
+
return binding.from + (binding.to - binding.from) * t;
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
3800
|
+
let minX = Infinity;
|
|
3801
|
+
let maxX = -Infinity;
|
|
3802
|
+
let minY = Infinity;
|
|
3803
|
+
let maxY = -Infinity;
|
|
3804
|
+
for (const member of members) {
|
|
3805
|
+
const rest = member.mesh.vertices;
|
|
3806
|
+
const shapes = [rest];
|
|
3807
|
+
for (const warp of member.warps) {
|
|
3808
|
+
for (const keyform of warp.keyforms) {
|
|
3809
|
+
shapes.push(rest.map((v, i) => v + keyform.offsets[i]));
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
const geometric = member.bindings.filter((b) => b.channel !== "opacity");
|
|
3813
|
+
for (let corner = 0; corner < 1 << geometric.length; corner++) {
|
|
3814
|
+
const trs = transformUnder(
|
|
3815
|
+
geometric,
|
|
3816
|
+
(b, i) => (corner & 1 << i) === 0 ? b.from : b.to
|
|
3817
|
+
);
|
|
3818
|
+
const cos = Math.cos(trs.rotation * (Math.PI / 180));
|
|
3819
|
+
const sin = Math.sin(trs.rotation * (Math.PI / 180));
|
|
3820
|
+
for (const shape of shapes) {
|
|
3821
|
+
for (let i = 0; i < shape.length; i += 2) {
|
|
3822
|
+
const sx = shape[i] * trs.scaleX;
|
|
3823
|
+
const sy = shape[i + 1] * trs.scaleY;
|
|
3824
|
+
const x = member.transform.x + trs.x + cos * sx - sin * sy;
|
|
3825
|
+
const y = member.transform.y + trs.y + sin * sx + cos * sy;
|
|
3826
|
+
minX = Math.min(minX, x);
|
|
3827
|
+
maxX = Math.max(maxX, x);
|
|
3828
|
+
minY = Math.min(minY, y);
|
|
3829
|
+
maxY = Math.max(maxY, y);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
minX -= turnShiftPx;
|
|
3835
|
+
maxX += turnShiftPx;
|
|
3836
|
+
minY -= nodShiftPx;
|
|
3837
|
+
maxY += nodShiftPx;
|
|
3838
|
+
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3839
|
+
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3840
|
+
return {
|
|
3841
|
+
cols: cells,
|
|
3842
|
+
rows: cells,
|
|
3843
|
+
points: generateGridPoints(
|
|
3844
|
+
cells,
|
|
3845
|
+
cells,
|
|
3846
|
+
minX - marginX,
|
|
3847
|
+
maxX + marginX,
|
|
3848
|
+
minY - marginY,
|
|
3849
|
+
maxY + marginY
|
|
3850
|
+
)
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3854
|
+
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3855
|
+
const isLash = layer.role.startsWith("lash_");
|
|
3856
|
+
if (side === void 0 || !(layer.role.startsWith("eye_") || isLash)) {
|
|
3857
|
+
return void 0;
|
|
3858
|
+
}
|
|
3859
|
+
const white = layers.find((l) => l.role === `eye_${side}`);
|
|
3860
|
+
const creaseWorldY = bboxToTransform(white.bbox, white.canvasW, white.canvasH, white.role).y - EYELID_FOLD_CREASE * white.cropH;
|
|
3861
|
+
const hasLash = layers.some((l) => l.role === `lash_${side}`);
|
|
3862
|
+
return bakeEyelidFoldWarp(
|
|
3863
|
+
mesh,
|
|
3864
|
+
side === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight,
|
|
3865
|
+
creaseWorldY - t.y,
|
|
3866
|
+
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
function turnSetup(layers) {
|
|
3870
|
+
const faceLayer = layers.find((l) => l.role === "face");
|
|
3871
|
+
const faceTransform = bboxToTransform(
|
|
3872
|
+
faceLayer.bbox,
|
|
3873
|
+
faceLayer.canvasW,
|
|
3874
|
+
faceLayer.canvasH,
|
|
3875
|
+
"face"
|
|
3876
|
+
);
|
|
3877
|
+
const faceCenterX = faceTransform.x;
|
|
3878
|
+
const faceCenterY = faceTransform.y;
|
|
3879
|
+
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3880
|
+
const plateEdgeX = faceCenterX - faceHalfWidth;
|
|
3881
|
+
const hasNose = layers.some((l) => l.role === "nose");
|
|
3882
|
+
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
3883
|
+
const turnLayers = layers.filter(
|
|
3884
|
+
(l) => isTurnGroup(ROLE_TABLE[l.role].deformer) || l.role === "hair_front"
|
|
3885
|
+
);
|
|
3886
|
+
const turnYs = turnLayers.map((l) => ({
|
|
3887
|
+
y: bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role).y,
|
|
3888
|
+
h: l.cropH
|
|
3889
|
+
}));
|
|
3890
|
+
let unionMinY = Math.min(...turnYs.map(({ y, h }) => y - h / 2));
|
|
3891
|
+
let unionMaxY = Math.max(...turnYs.map(({ y, h }) => y + h / 2));
|
|
3892
|
+
const spanY = unionMaxY - unionMinY;
|
|
3893
|
+
unionMinY -= spanY * GRID_MARGIN;
|
|
3894
|
+
unionMaxY += spanY * GRID_MARGIN;
|
|
3895
|
+
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3896
|
+
const nodRadius = halfH * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
3897
|
+
const parallaxUnitY = headNodParallaxUnit(halfH);
|
|
3898
|
+
const members = [];
|
|
3899
|
+
for (const layer of layers) {
|
|
3900
|
+
const spec = ROLE_TABLE[layer.role];
|
|
3901
|
+
if (!isTurnGroup(spec.deformer)) continue;
|
|
3902
|
+
const { role, cropW, cropH } = layer;
|
|
3903
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
3904
|
+
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3905
|
+
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3906
|
+
const bindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3907
|
+
hasMouthOpen
|
|
3908
|
+
});
|
|
3909
|
+
const fold = eyelidFoldFor(layers, layer, mesh, t);
|
|
3910
|
+
const pose = defaultPoseOf(bindings);
|
|
3911
|
+
members.push({
|
|
3912
|
+
role,
|
|
3913
|
+
group: spec.deformer,
|
|
3914
|
+
transform: t,
|
|
3915
|
+
part: {
|
|
3916
|
+
x: t.x + pose.x,
|
|
3917
|
+
y: t.y + pose.y,
|
|
3918
|
+
cropW,
|
|
3919
|
+
cropH,
|
|
3920
|
+
cols,
|
|
3921
|
+
rows,
|
|
3922
|
+
scaleX: pose.scaleX,
|
|
3923
|
+
scaleY: pose.scaleY,
|
|
3924
|
+
rotation: pose.rotation
|
|
3925
|
+
},
|
|
3926
|
+
mesh,
|
|
3927
|
+
bindings,
|
|
3928
|
+
warps: fold === void 0 ? [] : [fold]
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
const landmarks = turnLandmarks(layers);
|
|
3932
|
+
const turnShiftOf = (marks) => hasNose && marks.length > 0 ? familyReachPx(marks, plateEdgeX) : 0;
|
|
3933
|
+
const turnShift = {
|
|
3934
|
+
eye: turnShiftOf(landmarks.eye),
|
|
3935
|
+
nose: turnShiftOf(landmarks.nose),
|
|
3936
|
+
mouth: turnShiftOf(landmarks.mouth)
|
|
3937
|
+
};
|
|
3938
|
+
const groupGrids = /* @__PURE__ */ new Map();
|
|
3939
|
+
for (const { group, role } of members) {
|
|
3940
|
+
if (groupGrids.has(group)) continue;
|
|
3941
|
+
const own = members.filter((m) => m.group === group);
|
|
3942
|
+
const isPlate = group === "faceWarp";
|
|
3943
|
+
const nodShiftPx = hasNose ? Math.max(
|
|
3944
|
+
...own.map((m) => FEATURE_NOD_DEPTH[roleFamily(m.role)] ?? 0)
|
|
3945
|
+
) * parallaxUnitY : 0;
|
|
3946
|
+
groupGrids.set(
|
|
3947
|
+
group,
|
|
3948
|
+
groupGridFor(
|
|
3949
|
+
own,
|
|
3950
|
+
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3951
|
+
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3952
|
+
nodShiftPx
|
|
3953
|
+
)
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
const mouthAt = members.find((m) => m.role === "mouth").transform;
|
|
3957
|
+
const mouthAnchor = {
|
|
3958
|
+
x: mouthAt.x,
|
|
3959
|
+
y: mouthAt.y,
|
|
3960
|
+
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
3961
|
+
};
|
|
3962
|
+
const carriers = new Map(
|
|
3963
|
+
members.map((m) => [
|
|
3964
|
+
m.role,
|
|
3965
|
+
m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
|
|
3966
|
+
])
|
|
3967
|
+
);
|
|
3968
|
+
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3969
|
+
let reach = 0;
|
|
3970
|
+
for (const grid of groupGrids.values()) {
|
|
3971
|
+
reach = Math.max(
|
|
3972
|
+
reach,
|
|
3973
|
+
faceCenterX - grid.points[0] + maxShift,
|
|
3974
|
+
grid.points[grid.cols * 2] - faceCenterX + maxShift
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
const halfCells = Math.floor(reach / TURN_LATTICE_CELL_PX) + 1;
|
|
3978
|
+
const points = [];
|
|
3979
|
+
for (const y of [unionMaxY, unionMinY]) {
|
|
3980
|
+
for (let k = -halfCells; k <= halfCells; k++) {
|
|
3981
|
+
points.push(faceCenterX + k * TURN_LATTICE_CELL_PX, y);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
const lattice = { cols: 2 * halfCells, rows: 1, points };
|
|
3985
|
+
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3986
|
+
const hairFront = hairFrontLayer && (() => {
|
|
3987
|
+
const t = bboxToTransform(
|
|
3988
|
+
hairFrontLayer.bbox,
|
|
3989
|
+
hairFrontLayer.canvasW,
|
|
3990
|
+
hairFrontLayer.canvasH,
|
|
3991
|
+
"hair_front"
|
|
3992
|
+
);
|
|
3993
|
+
return {
|
|
3994
|
+
x: t.x,
|
|
3995
|
+
centerY: t.y,
|
|
3996
|
+
cropW: hairFrontLayer.cropW,
|
|
3997
|
+
cropH: hairFrontLayer.cropH
|
|
3998
|
+
};
|
|
3999
|
+
})();
|
|
4000
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
4001
|
+
const profile = faceRowProfile(faceLayer, faceCenterY);
|
|
4002
|
+
return {
|
|
4003
|
+
landmarks: turnLandmarks(layers, carriers),
|
|
4004
|
+
lattice,
|
|
4005
|
+
faceCenterX,
|
|
4006
|
+
faceHalfWidth,
|
|
4007
|
+
faceCenterY,
|
|
4008
|
+
nodRadius,
|
|
4009
|
+
carriers,
|
|
4010
|
+
profile,
|
|
4011
|
+
hairFront,
|
|
4012
|
+
eyeRowY,
|
|
4013
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
4014
|
+
members,
|
|
4015
|
+
groupGrids,
|
|
4016
|
+
parallaxUnitY,
|
|
4017
|
+
hasNose,
|
|
4018
|
+
hasMouthOpen
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
function solveInputsOf(s) {
|
|
4022
|
+
return [
|
|
4023
|
+
s.landmarks,
|
|
4024
|
+
s.lattice,
|
|
4025
|
+
s.faceCenterX,
|
|
4026
|
+
s.faceHalfWidth,
|
|
4027
|
+
s.faceCenterY,
|
|
4028
|
+
s.nodRadius,
|
|
4029
|
+
s.carriers,
|
|
4030
|
+
s.profile,
|
|
4031
|
+
s.hairFront
|
|
4032
|
+
];
|
|
4033
|
+
}
|
|
2945
4034
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
2946
4035
|
validateLayerInputs(layers, canvas);
|
|
4036
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
2947
4037
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2948
|
-
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2949
|
-
const hasNose = layers.some((l) => l.role === "nose");
|
|
2950
4038
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2951
|
-
const parameters = [
|
|
2952
|
-
{
|
|
2953
|
-
id: import_format3.StandardParameter.MouthOpen,
|
|
2954
|
-
name: "Mouth Open",
|
|
2955
|
-
min: 0,
|
|
2956
|
-
max: 1,
|
|
2957
|
-
default: 0
|
|
2958
|
-
},
|
|
2959
|
-
{
|
|
2960
|
-
id: import_format3.StandardParameter.MouthForm,
|
|
2961
|
-
name: "Mouth Form",
|
|
2962
|
-
min: -1,
|
|
2963
|
-
max: 1,
|
|
2964
|
-
default: 0
|
|
2965
|
-
},
|
|
2966
|
-
{
|
|
2967
|
-
id: import_format3.StandardParameter.EyeOpenLeft,
|
|
2968
|
-
name: "Eye L",
|
|
2969
|
-
min: 0,
|
|
2970
|
-
max: 1,
|
|
2971
|
-
default: 1
|
|
2972
|
-
},
|
|
2973
|
-
{
|
|
2974
|
-
id: import_format3.StandardParameter.EyeOpenRight,
|
|
2975
|
-
name: "Eye R",
|
|
2976
|
-
min: 0,
|
|
2977
|
-
max: 1,
|
|
2978
|
-
default: 1
|
|
2979
|
-
},
|
|
2980
|
-
{
|
|
2981
|
-
id: import_format3.StandardParameter.EyeballX,
|
|
2982
|
-
name: "Gaze X",
|
|
2983
|
-
min: -1,
|
|
2984
|
-
max: 1,
|
|
2985
|
-
default: 0
|
|
2986
|
-
},
|
|
2987
|
-
{
|
|
2988
|
-
id: import_format3.StandardParameter.EyeballY,
|
|
2989
|
-
name: "Gaze Y",
|
|
2990
|
-
min: -1,
|
|
2991
|
-
max: 1,
|
|
2992
|
-
default: 0
|
|
2993
|
-
},
|
|
2994
|
-
{
|
|
2995
|
-
id: import_format3.StandardParameter.AngleX,
|
|
2996
|
-
name: "Head Angle",
|
|
2997
|
-
min: -30,
|
|
2998
|
-
max: 30,
|
|
2999
|
-
default: 0
|
|
3000
|
-
},
|
|
3001
|
-
{
|
|
3002
|
-
id: import_format3.StandardParameter.AngleY,
|
|
3003
|
-
name: "Head Angle Y",
|
|
3004
|
-
min: -30,
|
|
3005
|
-
max: 30,
|
|
3006
|
-
default: 0
|
|
3007
|
-
},
|
|
3008
|
-
{
|
|
3009
|
-
id: import_format3.StandardParameter.AngleZ,
|
|
3010
|
-
name: "Head Angle Z",
|
|
3011
|
-
min: -30,
|
|
3012
|
-
max: 30,
|
|
3013
|
-
default: 0
|
|
3014
|
-
},
|
|
3015
|
-
{
|
|
3016
|
-
id: import_format3.StandardParameter.Breath,
|
|
3017
|
-
name: "Breath",
|
|
3018
|
-
min: 0,
|
|
3019
|
-
max: 1,
|
|
3020
|
-
default: 0
|
|
3021
|
-
},
|
|
3022
|
-
{
|
|
3023
|
-
id: import_format3.StandardParameter.BrowLeftY,
|
|
3024
|
-
name: "Brow L Y",
|
|
3025
|
-
min: -1,
|
|
3026
|
-
max: 1,
|
|
3027
|
-
default: 0
|
|
3028
|
-
},
|
|
3029
|
-
{
|
|
3030
|
-
id: import_format3.StandardParameter.BrowRightY,
|
|
3031
|
-
name: "Brow R Y",
|
|
3032
|
-
min: -1,
|
|
3033
|
-
max: 1,
|
|
3034
|
-
default: 0
|
|
3035
|
-
},
|
|
3036
|
-
{
|
|
3037
|
-
id: import_format3.StandardParameter.BrowLeftAngle,
|
|
3038
|
-
name: "Brow L Angle",
|
|
3039
|
-
min: -1,
|
|
3040
|
-
max: 1,
|
|
3041
|
-
default: 0
|
|
3042
|
-
},
|
|
3043
|
-
{
|
|
3044
|
-
id: import_format3.StandardParameter.BrowRightAngle,
|
|
3045
|
-
name: "Brow R Angle",
|
|
3046
|
-
min: -1,
|
|
3047
|
-
max: 1,
|
|
3048
|
-
default: 0
|
|
3049
|
-
}
|
|
3050
|
-
];
|
|
4039
|
+
const parameters = [...STANDARD_PARAMETERS];
|
|
3051
4040
|
if (hasHair) {
|
|
3052
4041
|
parameters.push(
|
|
3053
4042
|
{
|
|
@@ -3066,87 +4055,31 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3066
4055
|
}
|
|
3067
4056
|
);
|
|
3068
4057
|
}
|
|
3069
|
-
const
|
|
3070
|
-
const
|
|
3071
|
-
|
|
3072
|
-
faceLayer.bbox,
|
|
3073
|
-
faceLayer.canvasW,
|
|
3074
|
-
faceLayer.canvasH,
|
|
3075
|
-
"face"
|
|
3076
|
-
);
|
|
3077
|
-
const faceCenterX = faceTransform.x;
|
|
3078
|
-
const faceCropH = faceLayer.cropH;
|
|
3079
|
-
const faceWarpLayers = layers.filter(
|
|
3080
|
-
(l) => ROLE_TABLE[l.role].deformer === "faceWarp"
|
|
3081
|
-
);
|
|
3082
|
-
let unionMinX = -canvas.width / 2;
|
|
3083
|
-
let unionMaxX = canvas.width / 2;
|
|
3084
|
-
let unionMinY = -canvas.height / 2;
|
|
3085
|
-
let unionMaxY = canvas.height / 2;
|
|
3086
|
-
if (faceWarpLayers.length > 0) {
|
|
3087
|
-
const transforms = faceWarpLayers.map(
|
|
3088
|
-
(l) => bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role)
|
|
3089
|
-
);
|
|
3090
|
-
unionMinX = Math.min(
|
|
3091
|
-
...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2)
|
|
3092
|
-
);
|
|
3093
|
-
unionMaxX = Math.max(
|
|
3094
|
-
...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2)
|
|
3095
|
-
);
|
|
3096
|
-
unionMinY = Math.min(
|
|
3097
|
-
...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2)
|
|
3098
|
-
);
|
|
3099
|
-
unionMaxY = Math.max(
|
|
3100
|
-
...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2)
|
|
3101
|
-
);
|
|
3102
|
-
const spanX = unionMaxX - unionMinX;
|
|
3103
|
-
const spanY = unionMaxY - unionMinY;
|
|
3104
|
-
const MARGIN = 0.12;
|
|
3105
|
-
unionMinX -= spanX * MARGIN;
|
|
3106
|
-
unionMaxX += spanX * MARGIN;
|
|
3107
|
-
unionMinY -= spanY * MARGIN;
|
|
3108
|
-
unionMaxY += spanY * MARGIN;
|
|
3109
|
-
}
|
|
3110
|
-
const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);
|
|
3111
|
-
const faceGridMinX = faceCenterX - halfW;
|
|
3112
|
-
const faceGridMaxX = faceCenterX + halfW;
|
|
3113
|
-
const faceGrid = {
|
|
3114
|
-
cols: FACE_GRID_CELLS,
|
|
3115
|
-
rows: FACE_GRID_CELLS,
|
|
3116
|
-
points: generateGridPoints(
|
|
3117
|
-
FACE_GRID_CELLS,
|
|
3118
|
-
FACE_GRID_CELLS,
|
|
3119
|
-
faceGridMinX,
|
|
3120
|
-
faceGridMaxX,
|
|
3121
|
-
unionMinY,
|
|
3122
|
-
unionMaxY
|
|
3123
|
-
)
|
|
3124
|
-
};
|
|
3125
|
-
const faceCenterY = faceTransform.y;
|
|
3126
|
-
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3127
|
-
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3128
|
-
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3129
|
-
const turn = hasNose ? solveTurnModel(
|
|
3130
|
-
resolveTurnTargets(options.turnTargets),
|
|
3131
|
-
turnLandmarks(layers),
|
|
3132
|
-
faceGrid,
|
|
4058
|
+
const setup = turnSetup(layers);
|
|
4059
|
+
const {
|
|
4060
|
+
lattice,
|
|
3133
4061
|
faceCenterX,
|
|
3134
4062
|
faceHalfWidth,
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
4063
|
+
faceCenterY,
|
|
4064
|
+
nodRadius,
|
|
4065
|
+
profile,
|
|
4066
|
+
eyeRowY,
|
|
4067
|
+
plate,
|
|
4068
|
+
edgeAt,
|
|
4069
|
+
plateGuardRows,
|
|
4070
|
+
members,
|
|
4071
|
+
groupGrids,
|
|
4072
|
+
carriers,
|
|
4073
|
+
parallaxUnitY,
|
|
4074
|
+
hasNose,
|
|
4075
|
+
hasMouthOpen
|
|
4076
|
+
} = setup;
|
|
4077
|
+
const faceCropH = plate.part.cropH;
|
|
4078
|
+
const turn = hasNose ? solveTurnModel(
|
|
4079
|
+
resolveTurnTargets(options.turnTargets),
|
|
4080
|
+
...solveInputsOf(setup),
|
|
4081
|
+
options.headEdges,
|
|
4082
|
+
options.strandEdges
|
|
3150
4083
|
) : void 0;
|
|
3151
4084
|
if (turn?.unreachable) {
|
|
3152
4085
|
throw new TurnTargetError(
|
|
@@ -3164,28 +4097,32 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3164
4097
|
holdBase: turn.holdBase,
|
|
3165
4098
|
depths: turn.depths,
|
|
3166
4099
|
achieved: turn.achieved,
|
|
3167
|
-
clamped: turn.clamped
|
|
4100
|
+
clamped: turn.clamped,
|
|
4101
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3168
4102
|
});
|
|
3169
4103
|
}
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
const
|
|
4104
|
+
const latticeReach = Math.max(
|
|
4105
|
+
faceCenterX - lattice.points[0],
|
|
4106
|
+
lattice.points[lattice.cols * 2] - faceCenterX
|
|
4107
|
+
);
|
|
4108
|
+
const surface = turn?.surface ?? turnSurface({
|
|
4109
|
+
faceCenterX,
|
|
4110
|
+
faceCenterY,
|
|
4111
|
+
radius: latticeReach * HEAD_CYLINDER_RADIUS_FACTOR,
|
|
4112
|
+
travel: headTurnTravel(faceHalfWidth),
|
|
4113
|
+
nodRadius,
|
|
4114
|
+
lattice,
|
|
4115
|
+
profile,
|
|
4116
|
+
eyeRowY
|
|
4117
|
+
});
|
|
4118
|
+
const parallaxUnit = headTurnParallaxUnit(surface.radius);
|
|
4119
|
+
const plateLandingAt = plateLandingOn(plate, rowMapsOf(surface));
|
|
4120
|
+
const faceBottom = faceCenterY - faceCropH / 2;
|
|
3175
4121
|
const neckPivot = {
|
|
3176
4122
|
x: faceCenterX,
|
|
3177
4123
|
y: faceBottom - faceCropH * 0.15
|
|
3178
4124
|
// 15% below face bottom = neck
|
|
3179
4125
|
};
|
|
3180
|
-
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
3181
|
-
faceGrid,
|
|
3182
|
-
import_format3.StandardParameter.AngleX,
|
|
3183
|
-
import_format3.StandardParameter.AngleY,
|
|
3184
|
-
faceCenterX,
|
|
3185
|
-
faceCenterY,
|
|
3186
|
-
faceRadius,
|
|
3187
|
-
headTravel
|
|
3188
|
-
);
|
|
3189
4126
|
const deformers = [
|
|
3190
4127
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
3191
4128
|
// neck pivot. Same bindings as sample-model.ts apart from the turn, which
|
|
@@ -3195,14 +4132,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3195
4132
|
pivot: neckPivot,
|
|
3196
4133
|
bindings: [
|
|
3197
4134
|
// No AngleX binding: the turn moves NOTHING rigidly. Its sideways
|
|
3198
|
-
// travel is baked into
|
|
3199
|
-
//
|
|
3200
|
-
//
|
|
3201
|
-
//
|
|
3202
|
-
//
|
|
3203
|
-
//
|
|
3204
|
-
//
|
|
3205
|
-
//
|
|
4135
|
+
// travel is baked into every group grid (the surface's `travel`, see
|
|
4136
|
+
// bakeTurnGroupWarp2D) so the face slides inside a silhouette the
|
|
4137
|
+
// bangs hold; a translate here would take the hair shell with it. Nor
|
|
4138
|
+
// is the turn a roll — the sample model's ±6° "lean into the turn" was
|
|
4139
|
+
// tried here and dropped, since rotating about the neck pivot swings
|
|
4140
|
+
// the crown (~500px above it) far more than the chin, so the top of
|
|
4141
|
+
// the head appeared to lunge ahead of the face on every turn. Roll is
|
|
4142
|
+
// AngleZ's job, below.
|
|
3206
4143
|
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
3207
4144
|
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
3208
4145
|
// poses, collapsing pitch into roll.
|
|
@@ -3231,19 +4168,27 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3231
4168
|
to: HEAD_BREATH_BOB
|
|
3232
4169
|
}
|
|
3233
4170
|
]
|
|
3234
|
-
},
|
|
3235
|
-
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
3236
|
-
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
3237
|
-
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
3238
|
-
// `warps` or `warp2d`, never both).
|
|
3239
|
-
{
|
|
3240
|
-
kind: "warp",
|
|
3241
|
-
id: "faceWarp",
|
|
3242
|
-
parent: "headDeformer",
|
|
3243
|
-
grid: faceGrid,
|
|
3244
|
-
warp2d: faceWarp2d
|
|
3245
4171
|
}
|
|
3246
4172
|
];
|
|
4173
|
+
for (const [group, grid] of groupGrids) {
|
|
4174
|
+
const { role } = members.find((m) => m.group === group);
|
|
4175
|
+
const family = turnFamily(role);
|
|
4176
|
+
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
4177
|
+
deformers.push({
|
|
4178
|
+
kind: "warp",
|
|
4179
|
+
id: group,
|
|
4180
|
+
parent: "headDeformer",
|
|
4181
|
+
grid,
|
|
4182
|
+
warp2d: bakeTurnGroupWarp2D(
|
|
4183
|
+
grid,
|
|
4184
|
+
import_format3.StandardParameter.AngleX,
|
|
4185
|
+
import_format3.StandardParameter.AngleY,
|
|
4186
|
+
surface,
|
|
4187
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4188
|
+
carriers.get(role).anchor
|
|
4189
|
+
)
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
3247
4192
|
if (bodyLayer) {
|
|
3248
4193
|
const bt = bboxToTransform(
|
|
3249
4194
|
bodyLayer.bbox,
|
|
@@ -3260,8 +4205,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3260
4205
|
{
|
|
3261
4206
|
parameter: import_format3.StandardParameter.AngleX,
|
|
3262
4207
|
channel: "translateX",
|
|
3263
|
-
from: -BODY_TURN_FOLLOW *
|
|
3264
|
-
to: BODY_TURN_FOLLOW *
|
|
4208
|
+
from: -BODY_TURN_FOLLOW * surface.travel,
|
|
4209
|
+
to: BODY_TURN_FOLLOW * surface.travel
|
|
3265
4210
|
},
|
|
3266
4211
|
// Breath: follow the head's bob, same direction, half amplitude.
|
|
3267
4212
|
{
|
|
@@ -3273,34 +4218,21 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3273
4218
|
]
|
|
3274
4219
|
});
|
|
3275
4220
|
}
|
|
3276
|
-
const
|
|
3277
|
-
for (const layer of layers) {
|
|
3278
|
-
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3279
|
-
if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
|
|
3280
|
-
const ey = bboxToTransform(
|
|
3281
|
-
layer.bbox,
|
|
3282
|
-
layer.canvasW,
|
|
3283
|
-
layer.canvasH,
|
|
3284
|
-
layer.role
|
|
3285
|
-
).y;
|
|
3286
|
-
eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
4221
|
+
const memberByRole = new Map(members.map((m) => [m.role, m]));
|
|
3289
4222
|
const parts = layers.map((layer) => {
|
|
3290
4223
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
3291
4224
|
const spec = ROLE_TABLE[role];
|
|
3292
4225
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
3293
4226
|
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3294
4227
|
hasMouthOpen,
|
|
3295
|
-
parallaxUnit,
|
|
3296
4228
|
parallaxUnitY,
|
|
3297
|
-
hasNose
|
|
3298
|
-
turnDepths: turn?.depths
|
|
4229
|
+
hasNose
|
|
3299
4230
|
});
|
|
3300
4231
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
3301
4232
|
if (spec.mesh) {
|
|
4233
|
+
const member = memberByRole.get(role);
|
|
3302
4234
|
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3303
|
-
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
4235
|
+
const mesh = member?.mesh ?? createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3304
4236
|
const part = {
|
|
3305
4237
|
id: role,
|
|
3306
4238
|
color: [1, 1, 1, 1],
|
|
@@ -3315,35 +4247,38 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3315
4247
|
part.bindings = roleBindings;
|
|
3316
4248
|
}
|
|
3317
4249
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
let silhouetteWarp;
|
|
4250
|
+
const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
4251
|
+
const ownWarps = [];
|
|
3321
4252
|
if (role === "hair_front") {
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
4253
|
+
ownWarps.push(
|
|
4254
|
+
bakeHairSwayWarp(
|
|
4255
|
+
mesh,
|
|
4256
|
+
import_format3.StandardParameter.AngleX,
|
|
4257
|
+
HAIR_FRONT_DEPTH * parallaxUnit,
|
|
4258
|
+
HEAD_TURN_MAX_DEG
|
|
4259
|
+
)
|
|
3330
4260
|
);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
4261
|
+
const holdBase = turn?.holdBase ?? plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) + HOLD_CLEARANCE;
|
|
4262
|
+
ownWarps.push(
|
|
4263
|
+
bakeHairFrontSilhouetteWarp(
|
|
4264
|
+
mesh,
|
|
4265
|
+
t.x,
|
|
4266
|
+
t.y,
|
|
4267
|
+
faceCenterX,
|
|
4268
|
+
plateLandingAt,
|
|
4269
|
+
edgeAt,
|
|
4270
|
+
holdBase,
|
|
4271
|
+
turn?.holdEdgeAt ?? (() => holdBase),
|
|
4272
|
+
plateGuardRows
|
|
4273
|
+
)
|
|
3336
4274
|
);
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
holdBase,
|
|
3345
|
-
turn?.holdEdgeAt ?? (() => holdBase),
|
|
3346
|
-
columnMapAt
|
|
4275
|
+
ownWarps.push(
|
|
4276
|
+
bakeNodWarp(
|
|
4277
|
+
mesh,
|
|
4278
|
+
t.y,
|
|
4279
|
+
surface,
|
|
4280
|
+
hasNose ? HAIR_FRONT_NOD_DEPTH * parallaxUnitY : 0
|
|
4281
|
+
)
|
|
3347
4282
|
);
|
|
3348
4283
|
}
|
|
3349
4284
|
part.warps = [
|
|
@@ -3359,24 +4294,12 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3359
4294
|
tipShift,
|
|
3360
4295
|
HAIR_SWAY_RANGE
|
|
3361
4296
|
),
|
|
3362
|
-
...
|
|
3363
|
-
...silhouetteWarp ? [silhouetteWarp] : []
|
|
4297
|
+
...ownWarps
|
|
3364
4298
|
];
|
|
3365
4299
|
}
|
|
3366
4300
|
if (spec.eyeSide !== void 0) {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
const openParam = spec.eyeSide === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight;
|
|
3370
|
-
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
3371
|
-
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
3372
|
-
part.warps = [
|
|
3373
|
-
bakeEyelidFoldWarp(
|
|
3374
|
-
mesh,
|
|
3375
|
-
openParam,
|
|
3376
|
-
creaseWorldY - t.y,
|
|
3377
|
-
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3378
|
-
)
|
|
3379
|
-
];
|
|
4301
|
+
if (role.startsWith("eye_") || role.startsWith("lash_")) {
|
|
4302
|
+
part.warps = [...member.warps];
|
|
3380
4303
|
} else {
|
|
3381
4304
|
part.clip = { masks: [`eye_${spec.eyeSide}`] };
|
|
3382
4305
|
}
|