@ikijs/editor 0.16.1 → 0.17.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 +23 -17
- package/dist/index.d.mts +38 -18
- package/dist/index.d.ts +38 -18
- package/dist/index.js +860 -357
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +860 -355
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1559,6 +1559,14 @@ function facePlateHalfOf(layers) {
|
|
|
1559
1559
|
return face.cropW / 2;
|
|
1560
1560
|
}
|
|
1561
1561
|
var STRAND_MIN_RUN_FRACTION = 0.5;
|
|
1562
|
+
var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
|
|
1563
|
+
"face",
|
|
1564
|
+
"hair_front",
|
|
1565
|
+
"hair_back",
|
|
1566
|
+
"body",
|
|
1567
|
+
"arm_L",
|
|
1568
|
+
"arm_R"
|
|
1569
|
+
]);
|
|
1562
1570
|
function createLayerSetMeasurer(canvas) {
|
|
1563
1571
|
for (const field of ["width", "height"]) {
|
|
1564
1572
|
const value = canvas[field];
|
|
@@ -1588,7 +1596,7 @@ function createLayerSetMeasurer(canvas) {
|
|
|
1588
1596
|
if (bbox === null) return null;
|
|
1589
1597
|
const rowLeft = new Int32Array(canvasH).fill(canvasW);
|
|
1590
1598
|
const rowRight = new Int32Array(canvasH).fill(-1);
|
|
1591
|
-
const rowRuns = role
|
|
1599
|
+
const rowRuns = ROW_RUN_ROLES.has(role) ? [] : void 0;
|
|
1592
1600
|
for (let y = 0; y < canvasH; y++) {
|
|
1593
1601
|
let runs;
|
|
1594
1602
|
if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
|
|
@@ -1930,7 +1938,13 @@ var PROFILE_VALUES = {
|
|
|
1930
1938
|
brow: 0.15,
|
|
1931
1939
|
breath: 0.03,
|
|
1932
1940
|
sway: 0.08,
|
|
1933
|
-
blushRest: 0.4
|
|
1941
|
+
blushRest: 0.4,
|
|
1942
|
+
bodySlide: 0.1,
|
|
1943
|
+
bodyNarrow: 0.05,
|
|
1944
|
+
bodyBow: 0.06,
|
|
1945
|
+
bodyRoll: 4,
|
|
1946
|
+
bodyFollowX: 0.3,
|
|
1947
|
+
bodyFollowZ: 0.3
|
|
1934
1948
|
};
|
|
1935
1949
|
function deriveProfile(v) {
|
|
1936
1950
|
const farLead = v.lead * (1 - v.fore / 2);
|
|
@@ -2024,9 +2038,32 @@ function deriveProfile(v) {
|
|
|
2024
2038
|
* slide, to the next 0.05. */
|
|
2025
2039
|
hiddenNeck: Math.ceil(30 * (v.slide + v.lead) - 1e-9) / 20
|
|
2026
2040
|
};
|
|
2027
|
-
|
|
2041
|
+
const body = {
|
|
2042
|
+
/** BodyAngleX: the upper body's slide, hh. */
|
|
2043
|
+
slide: v.bodySlide,
|
|
2044
|
+
/** BodyAngleX: how much narrower the upper body gets, either way. */
|
|
2045
|
+
narrow: v.bodyNarrow,
|
|
2046
|
+
/** BodyAngleY: the upper body's rise (+) or bow (−), hh. */
|
|
2047
|
+
bow: v.bodyBow,
|
|
2048
|
+
/** BodyAngleZ: the upper body's roll about the hips, degrees. */
|
|
2049
|
+
rollDeg: v.bodyRoll,
|
|
2050
|
+
/** AngleX: the body's follow, as a share of BodyAngleX's motion. */
|
|
2051
|
+
followX: v.bodyFollowX,
|
|
2052
|
+
/** AngleZ: the body's follow, as a share of its roll. */
|
|
2053
|
+
followZ: v.bodyFollowZ,
|
|
2054
|
+
/** The roll the follow gives the body at AngleZ ±30, degrees: β, the
|
|
2055
|
+
* share of `ROLL_DEG` the head's own roll leaves to the body. */
|
|
2056
|
+
followRoll: v.bodyFollowZ * v.bodyRoll
|
|
2057
|
+
};
|
|
2058
|
+
return {
|
|
2059
|
+
TURN: turn,
|
|
2060
|
+
NOD: nod,
|
|
2061
|
+
ROLL_DEG: v.rollDeg,
|
|
2062
|
+
AMPLITUDE: amplitude,
|
|
2063
|
+
BODY: body
|
|
2064
|
+
};
|
|
2028
2065
|
}
|
|
2029
|
-
var { TURN, NOD, ROLL_DEG, AMPLITUDE } = deriveProfile(PROFILE_VALUES);
|
|
2066
|
+
var { TURN, NOD, ROLL_DEG, AMPLITUDE, BODY } = deriveProfile(PROFILE_VALUES);
|
|
2030
2067
|
var STYLE_KNOBS = [
|
|
2031
2068
|
"turn",
|
|
2032
2069
|
"featureLead",
|
|
@@ -2056,6 +2093,12 @@ var TurnTargetError = class extends Error {
|
|
|
2056
2093
|
this.name = "TurnTargetError";
|
|
2057
2094
|
}
|
|
2058
2095
|
};
|
|
2096
|
+
var LayerGeometryError = class extends Error {
|
|
2097
|
+
constructor(message) {
|
|
2098
|
+
super(message);
|
|
2099
|
+
this.name = "LayerGeometryError";
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2059
2102
|
var DEFAULT_TURN_TARGETS = Object.freeze({
|
|
2060
2103
|
eyeShift: round2((TURN.eyeFar + TURN.eyeNear) / 2 - TURN.hairBack),
|
|
2061
2104
|
farEyeRatio: round2(TURN.eyeFarScale / TURN.eyeNearScale),
|
|
@@ -2066,6 +2109,8 @@ var DEFAULT_TURN_TARGETS = Object.freeze({
|
|
|
2066
2109
|
var ROLE_TABLE = [
|
|
2067
2110
|
{ role: "hair_back", family: "hair_back" },
|
|
2068
2111
|
{ role: "body", family: "body" },
|
|
2112
|
+
{ role: "arm_L", family: "arm", parts: ["arm_L", "forearm_L"] },
|
|
2113
|
+
{ role: "arm_R", family: "arm", parts: ["arm_R", "forearm_R"] },
|
|
2069
2114
|
{ role: "face", family: "face", required: true },
|
|
2070
2115
|
{ role: "blush_L", family: "face" },
|
|
2071
2116
|
{ role: "blush_R", family: "face" },
|
|
@@ -2097,6 +2142,15 @@ function roleSpec(role) {
|
|
|
2097
2142
|
if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
|
|
2098
2143
|
return spec;
|
|
2099
2144
|
}
|
|
2145
|
+
function partIdsOfRole(role) {
|
|
2146
|
+
return roleSpec(role).parts ?? [role];
|
|
2147
|
+
}
|
|
2148
|
+
var ROLE_BY_PART = new Map(
|
|
2149
|
+
ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
|
|
2150
|
+
);
|
|
2151
|
+
function roleOfPart(partId) {
|
|
2152
|
+
return ROLE_BY_PART.get(partId) ?? partId;
|
|
2153
|
+
}
|
|
2100
2154
|
var ALIASES = {
|
|
2101
2155
|
eyebrow_L: "brow_L",
|
|
2102
2156
|
eyebrow_R: "brow_R",
|
|
@@ -2107,6 +2161,16 @@ function roleOf(fileName) {
|
|
|
2107
2161
|
const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
|
|
2108
2162
|
return ALIASES[role] ?? role;
|
|
2109
2163
|
}
|
|
2164
|
+
function checkArmsHaveBody(roles) {
|
|
2165
|
+
const has = new Set(roles);
|
|
2166
|
+
if (has.has("body")) return;
|
|
2167
|
+
const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
|
|
2168
|
+
if (arm !== void 0) {
|
|
2169
|
+
throw new Error(
|
|
2170
|
+
`auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2110
2174
|
function parseLayerRoles(fileNames) {
|
|
2111
2175
|
const seen = /* @__PURE__ */ new Map();
|
|
2112
2176
|
const out = [];
|
|
@@ -2132,13 +2196,14 @@ function parseLayerRoles(fileNames) {
|
|
|
2132
2196
|
`auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
|
|
2133
2197
|
);
|
|
2134
2198
|
}
|
|
2199
|
+
checkArmsHaveBody(seen.keys());
|
|
2135
2200
|
return out;
|
|
2136
2201
|
}
|
|
2137
2202
|
|
|
2138
2203
|
// src/auto-rig/generate.ts
|
|
2139
2204
|
import {
|
|
2140
2205
|
IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
|
|
2141
|
-
StandardParameter as
|
|
2206
|
+
StandardParameter as P6,
|
|
2142
2207
|
parseIkiModel as parseIkiModel3
|
|
2143
2208
|
} from "@ikijs/format";
|
|
2144
2209
|
|
|
@@ -2293,6 +2358,16 @@ var DEFAULT_MOTIONS = {
|
|
|
2293
2358
|
]
|
|
2294
2359
|
};
|
|
2295
2360
|
|
|
2361
|
+
// src/auto-rig/arms.ts
|
|
2362
|
+
import {
|
|
2363
|
+
StandardParameter as P4
|
|
2364
|
+
} from "@ikijs/format";
|
|
2365
|
+
|
|
2366
|
+
// src/auto-rig/body.ts
|
|
2367
|
+
import {
|
|
2368
|
+
StandardParameter as P3
|
|
2369
|
+
} from "@ikijs/format";
|
|
2370
|
+
|
|
2296
2371
|
// src/auto-rig/layout.ts
|
|
2297
2372
|
function boxOfLayer(layer) {
|
|
2298
2373
|
return boxOfPixels(layer.bbox, layer.canvasW, layer.canvasH);
|
|
@@ -2366,6 +2441,653 @@ function meshPoints(mesh, box) {
|
|
|
2366
2441
|
return out;
|
|
2367
2442
|
}
|
|
2368
2443
|
|
|
2444
|
+
// src/auto-rig/drivers.ts
|
|
2445
|
+
import {
|
|
2446
|
+
StandardParameter as P2
|
|
2447
|
+
} from "@ikijs/format";
|
|
2448
|
+
var SWAY_RANGE = 20;
|
|
2449
|
+
var SWAY_CURVE = 1.4;
|
|
2450
|
+
var HAIR_HANG = 0.35;
|
|
2451
|
+
var Z_STOPS = [-30, 0, 30];
|
|
2452
|
+
function localWarp(parameter, mesh, box, values, f) {
|
|
2453
|
+
const pts = meshPoints(mesh, box);
|
|
2454
|
+
const w = bw(box);
|
|
2455
|
+
const h = bh(box);
|
|
2456
|
+
return {
|
|
2457
|
+
parameter,
|
|
2458
|
+
keyforms: values.map((value) => ({
|
|
2459
|
+
value,
|
|
2460
|
+
offsets: pts.flatMap((p, i) => {
|
|
2461
|
+
const [dx, dy] = f(p, value, i);
|
|
2462
|
+
return [roundTo(dx / w, 1e-4), roundTo(dy / h, 1e-4)];
|
|
2463
|
+
})
|
|
2464
|
+
}))
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
var rollOf = (v, deg = ROLL_DEG) => -v / 30 * deg * DEG;
|
|
2468
|
+
function motionReach(warps, mesh, box, extra = [0, 0]) {
|
|
2469
|
+
const out = {
|
|
2470
|
+
left: extra[0],
|
|
2471
|
+
right: extra[0],
|
|
2472
|
+
bottom: extra[1],
|
|
2473
|
+
top: extra[1]
|
|
2474
|
+
};
|
|
2475
|
+
if (mesh === void 0) return out;
|
|
2476
|
+
const pts = meshPoints(mesh, box);
|
|
2477
|
+
for (const warp of warps ?? []) {
|
|
2478
|
+
let l = 0;
|
|
2479
|
+
let r = 0;
|
|
2480
|
+
let b = 0;
|
|
2481
|
+
let t = 0;
|
|
2482
|
+
for (const k of warp.keyforms) {
|
|
2483
|
+
pts.forEach(([x, y], i) => {
|
|
2484
|
+
const px = x + k.offsets[2 * i] * bw(box);
|
|
2485
|
+
const py = y + k.offsets[2 * i + 1] * bh(box);
|
|
2486
|
+
l = Math.max(l, box.x0 - px);
|
|
2487
|
+
r = Math.max(r, px - box.x1);
|
|
2488
|
+
b = Math.max(b, box.y0 - py);
|
|
2489
|
+
t = Math.max(t, py - box.y1);
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
out.left += l;
|
|
2493
|
+
out.right += r;
|
|
2494
|
+
out.bottom += b;
|
|
2495
|
+
out.top += t;
|
|
2496
|
+
}
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
function blinkFold(param, mesh, box, travel, eye = box) {
|
|
2500
|
+
const yc = eye.y1 - travel * bh(eye);
|
|
2501
|
+
return localWarp(
|
|
2502
|
+
param,
|
|
2503
|
+
mesh,
|
|
2504
|
+
box,
|
|
2505
|
+
[0, 1],
|
|
2506
|
+
([, y], v) => v === 1 ? [0, 0] : [0, yc - y]
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
function lashFold(param, mesh, lash, eye, travel) {
|
|
2510
|
+
const yc = eye.y1 - travel * bh(eye) - 0.04 * bh(eye);
|
|
2511
|
+
const c = cx(lash);
|
|
2512
|
+
const half = bw(lash) / 2;
|
|
2513
|
+
const sag = 0.3 * bh(lash);
|
|
2514
|
+
return localWarp(param, mesh, lash, [0, 1], ([x, y], v) => {
|
|
2515
|
+
if (v === 1) return [0, 0];
|
|
2516
|
+
const u = (x - c) / half;
|
|
2517
|
+
return [0, yc + (y - lash.y0) * 0.5 - sag * (1 - u * u) - y];
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
function gazeBindings(irisW, share = 1) {
|
|
2521
|
+
const gx = AMPLITUDE.gazeX * irisW * share;
|
|
2522
|
+
const gy = AMPLITUDE.gazeY * irisW * share;
|
|
2523
|
+
return [
|
|
2524
|
+
{
|
|
2525
|
+
parameter: P2.EyeballX,
|
|
2526
|
+
channel: "translateX",
|
|
2527
|
+
from: -round1(gx),
|
|
2528
|
+
to: round1(gx)
|
|
2529
|
+
},
|
|
2530
|
+
{
|
|
2531
|
+
parameter: P2.EyeballY,
|
|
2532
|
+
channel: "translateY",
|
|
2533
|
+
from: -round1(gy),
|
|
2534
|
+
to: round1(gy)
|
|
2535
|
+
}
|
|
2536
|
+
];
|
|
2537
|
+
}
|
|
2538
|
+
function gazeReach(irisW, share = 1) {
|
|
2539
|
+
return [AMPLITUDE.gazeX * irisW * share, AMPLITUDE.gazeY * irisW * share];
|
|
2540
|
+
}
|
|
2541
|
+
function browReach(hh) {
|
|
2542
|
+
return AMPLITUDE.brow * hh;
|
|
2543
|
+
}
|
|
2544
|
+
function browBindings(side, reach) {
|
|
2545
|
+
const y = side === "L" ? P2.BrowLeftY : P2.BrowRightY;
|
|
2546
|
+
const a = side === "L" ? P2.BrowLeftAngle : P2.BrowRightAngle;
|
|
2547
|
+
return [
|
|
2548
|
+
{
|
|
2549
|
+
parameter: y,
|
|
2550
|
+
channel: "translateY",
|
|
2551
|
+
from: -round1(reach),
|
|
2552
|
+
to: round1(reach)
|
|
2553
|
+
},
|
|
2554
|
+
{ parameter: a, channel: "rotate", from: -BROW_TILT, to: BROW_TILT }
|
|
2555
|
+
];
|
|
2556
|
+
}
|
|
2557
|
+
var BROW_TILT = 15;
|
|
2558
|
+
function mouthForm(mesh, box) {
|
|
2559
|
+
const a = 0.3 * bh(box);
|
|
2560
|
+
const c = cx(box);
|
|
2561
|
+
const half = bw(box) / 2;
|
|
2562
|
+
return localWarp(P2.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
|
|
2563
|
+
if (v === 0) return [0, 0];
|
|
2564
|
+
const u = (x - c) / half;
|
|
2565
|
+
const widen = v > 0 ? 0.05 : 0.03;
|
|
2566
|
+
return [v * widen * (x - c), v * a * (u * u - 0.3)];
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
function mouthOpenGrow(mesh, box) {
|
|
2570
|
+
const c = cx(box);
|
|
2571
|
+
return localWarp(
|
|
2572
|
+
P2.MouthOpen,
|
|
2573
|
+
mesh,
|
|
2574
|
+
box,
|
|
2575
|
+
[0, 1],
|
|
2576
|
+
([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
function mouthWiden(mesh, box) {
|
|
2580
|
+
const c = cx(box);
|
|
2581
|
+
return localWarp(P2.MouthOpen, mesh, box, [0, 1], ([x], v) => [
|
|
2582
|
+
v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
|
|
2583
|
+
0
|
|
2584
|
+
]);
|
|
2585
|
+
}
|
|
2586
|
+
function hairSway(param, mesh, box, amp) {
|
|
2587
|
+
return localWarp(
|
|
2588
|
+
param,
|
|
2589
|
+
mesh,
|
|
2590
|
+
box,
|
|
2591
|
+
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
2592
|
+
([, y], v) => {
|
|
2593
|
+
const t = Math.max(0, (box.y1 - y) / bh(box));
|
|
2594
|
+
return [v / SWAY_RANGE * amp * t ** SWAY_CURVE, 0];
|
|
2595
|
+
}
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
function tiltHang(mesh, box, pivot, weight, deg = ROLL_DEG) {
|
|
2599
|
+
return localWarp(P2.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
|
|
2600
|
+
const g = weight(y, i);
|
|
2601
|
+
if (g === 0 || v === 0) return [0, 0];
|
|
2602
|
+
const a = -g * rollOf(v, deg);
|
|
2603
|
+
const px = x - pivot[0];
|
|
2604
|
+
const py = y - pivot[1];
|
|
2605
|
+
return [
|
|
2606
|
+
pivot[0] + px * Math.cos(a) - py * Math.sin(a) - x,
|
|
2607
|
+
pivot[1] + px * Math.sin(a) + py * Math.cos(a) - y
|
|
2608
|
+
];
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
function hairHangWeight(pivotY, bottom) {
|
|
2612
|
+
if (bottom >= pivotY) return () => 0;
|
|
2613
|
+
return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
|
|
2614
|
+
}
|
|
2615
|
+
var round1 = (v) => roundTo(v, 0.1);
|
|
2616
|
+
|
|
2617
|
+
// src/auto-rig/grid.ts
|
|
2618
|
+
var X_STOPS = [-30, 0, 30];
|
|
2619
|
+
var Y_STOPS = [-30, 0, 30];
|
|
2620
|
+
function lattice(box, cell, min, max) {
|
|
2621
|
+
const cols = cellsFor(box.x1 - box.x0, cell, min, max);
|
|
2622
|
+
const rows = cellsFor(box.y1 - box.y0, cell, min, max);
|
|
2623
|
+
const points = [];
|
|
2624
|
+
for (let r = 0; r <= rows; r++) {
|
|
2625
|
+
const y = box.y1 - (box.y1 - box.y0) * r / rows;
|
|
2626
|
+
for (let c = 0; c <= cols; c++) {
|
|
2627
|
+
points.push(
|
|
2628
|
+
roundTo(box.x0 + (box.x1 - box.x0) * c / cols, 0.01),
|
|
2629
|
+
roundTo(y, 0.01)
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return { box, cols, rows, points };
|
|
2634
|
+
}
|
|
2635
|
+
function bake(l, field) {
|
|
2636
|
+
const out = [];
|
|
2637
|
+
for (const ay of Y_STOPS) {
|
|
2638
|
+
for (const ax of X_STOPS) {
|
|
2639
|
+
const offsets = [];
|
|
2640
|
+
for (let k = 0; k < l.points.length; k += 2) {
|
|
2641
|
+
if (ax === 0 && ay === 0) {
|
|
2642
|
+
offsets.push(0, 0);
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
const [dx, dy] = field(l.points[k], l.points[k + 1], ax, ay);
|
|
2646
|
+
offsets.push(roundTo(dx, 0.1), roundTo(dy, 0.1));
|
|
2647
|
+
}
|
|
2648
|
+
out.push(offsets);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return out;
|
|
2652
|
+
}
|
|
2653
|
+
function keyformAt(keyforms, ax) {
|
|
2654
|
+
const j = Y_STOPS.indexOf(0);
|
|
2655
|
+
const row = X_STOPS.map((_, i) => keyforms[j * X_STOPS.length + i]);
|
|
2656
|
+
return blendStops(X_STOPS, row, ax);
|
|
2657
|
+
}
|
|
2658
|
+
function blendStops(values, offsets, v) {
|
|
2659
|
+
const x = clamp(v, values[0], values[values.length - 1]);
|
|
2660
|
+
let i = 0;
|
|
2661
|
+
while (i < values.length - 2 && x > values[i + 1]) i++;
|
|
2662
|
+
const t = (x - values[i]) / (values[i + 1] - values[i]);
|
|
2663
|
+
return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
|
|
2664
|
+
}
|
|
2665
|
+
function land(l, offsets, x, y) {
|
|
2666
|
+
const { cols, rows, points } = l;
|
|
2667
|
+
const stride = cols + 1;
|
|
2668
|
+
let col = cols - 1;
|
|
2669
|
+
for (let c = 0; c < cols; c++) {
|
|
2670
|
+
if (x < points[(c + 1) * 2]) {
|
|
2671
|
+
col = c;
|
|
2672
|
+
break;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
let row = rows - 1;
|
|
2676
|
+
for (let r = 0; r < rows; r++) {
|
|
2677
|
+
if (y > points[(r + 1) * stride * 2 + 1]) {
|
|
2678
|
+
row = r;
|
|
2679
|
+
break;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
const x0 = points[col * 2];
|
|
2683
|
+
const x1 = points[(col + 1) * 2];
|
|
2684
|
+
const yT = points[row * stride * 2 + 1];
|
|
2685
|
+
const yB = points[(row + 1) * stride * 2 + 1];
|
|
2686
|
+
const s = clamp((x - x0) / (x1 - x0), 0, 1);
|
|
2687
|
+
const t = clamp((yT - y) / (yT - yB), 0, 1);
|
|
2688
|
+
const at = (r, c) => {
|
|
2689
|
+
const k = (r * stride + c) * 2;
|
|
2690
|
+
return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
|
|
2691
|
+
};
|
|
2692
|
+
const p00 = at(row, col);
|
|
2693
|
+
const p10 = at(row, col + 1);
|
|
2694
|
+
const p01 = at(row + 1, col);
|
|
2695
|
+
const p11 = at(row + 1, col + 1);
|
|
2696
|
+
const topX = p00[0] + (p10[0] - p00[0]) * s;
|
|
2697
|
+
const topY = p00[1] + (p10[1] - p00[1]) * s;
|
|
2698
|
+
const botX = p01[0] + (p11[0] - p01[0]) * s;
|
|
2699
|
+
const botY = p01[1] + (p11[1] - p01[1]) * s;
|
|
2700
|
+
return [topX + (botX - topX) * t, topY + (botY - topY) * t];
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/auto-rig/body.ts
|
|
2704
|
+
var BODY_WARP_ID = "bodyWarp";
|
|
2705
|
+
var BODY_GRID_PAD = 6;
|
|
2706
|
+
var CHEST_DROP = 0.8;
|
|
2707
|
+
var HIP_BAND = [0.35, 0.75];
|
|
2708
|
+
var LEG_MIN = 0.1;
|
|
2709
|
+
var HIP_FRACTION = 0.5;
|
|
2710
|
+
var BODY_MESH_PX = 48;
|
|
2711
|
+
var BODY_STOPS = [-10, 0, 10];
|
|
2712
|
+
var FOLD_MARGIN = 0.01;
|
|
2713
|
+
function hipLine(layer) {
|
|
2714
|
+
const { bbox, canvasH, rowRuns } = layer;
|
|
2715
|
+
const top = canvasH / 2 - bbox.y;
|
|
2716
|
+
if (rowRuns !== void 0) {
|
|
2717
|
+
const legMin = LEG_MIN * bbox.w;
|
|
2718
|
+
const last = Math.floor(HIP_BAND[1] * bbox.h);
|
|
2719
|
+
for (let k = Math.ceil(HIP_BAND[0] * bbox.h); k <= last; k++) {
|
|
2720
|
+
const runs = rowRuns[k];
|
|
2721
|
+
let legs = 0;
|
|
2722
|
+
for (let i = 0; i < runs.length; i += 2) {
|
|
2723
|
+
if (runs[i + 1] - runs[i] >= legMin) legs++;
|
|
2724
|
+
}
|
|
2725
|
+
if (legs >= 2) return roundTo(top - k, 0.01);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (bbox.y + bbox.h === canvasH) return roundTo(top - bbox.h, 0.01);
|
|
2729
|
+
return roundTo(top - HIP_FRACTION * bbox.h, 0.01);
|
|
2730
|
+
}
|
|
2731
|
+
function bodyLattice(body, chin, hh, pivots, anchor) {
|
|
2732
|
+
const b = boxOfLayer(body);
|
|
2733
|
+
const g = (v) => Math.round(roundTo(v, 0.01) * 100);
|
|
2734
|
+
const pad = BODY_GRID_PAD * 100;
|
|
2735
|
+
const all = [chin, ...pivots];
|
|
2736
|
+
const a = g(anchor);
|
|
2737
|
+
const p = Math.min(...all.map((q) => g(q.y)));
|
|
2738
|
+
if (p - a < 2) {
|
|
2739
|
+
throw new LayerGeometryError(
|
|
2740
|
+
`auto-rig: layer "${body.fileName}": its hips (y ${anchor}) leave no row line strictly between them and the lowest of the head's and shoulders' pivots (y ${p / 100}), so the body warp cannot plant the legs below the pivots`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
let full = Math.min(g(chin.y - CHEST_DROP * hh), p - pad);
|
|
2744
|
+
if (full - a < pad) {
|
|
2745
|
+
full = Math.min(Math.max(Math.round((a + p) / 2), a + 1), p - 1);
|
|
2746
|
+
}
|
|
2747
|
+
const top = Math.max(g(b.y1), ...all.map((q) => g(q.y))) + pad;
|
|
2748
|
+
const bottom = Math.min(a, g(b.y0)) - pad;
|
|
2749
|
+
const split = (from, to, cells) => {
|
|
2750
|
+
const n = Math.max(1, Math.min(cells, from - to));
|
|
2751
|
+
const out = [from];
|
|
2752
|
+
for (let i = 1; i < n; i++) {
|
|
2753
|
+
out.push(Math.round(from - (from - to) * i / n));
|
|
2754
|
+
}
|
|
2755
|
+
out.push(to);
|
|
2756
|
+
return out;
|
|
2757
|
+
};
|
|
2758
|
+
const bandA = split(top, full, cellsFor((top - full) / 100, 96, 1, 8));
|
|
2759
|
+
const ramp = split(full, a, cellsFor((full - a) / 100, 96, 2, 8));
|
|
2760
|
+
const rows = [...bandA, ...ramp.slice(1, -1)];
|
|
2761
|
+
const ys = [...rows.map((y) => y / 100), anchor, bottom / 100];
|
|
2762
|
+
const weights = [...rows.map((y) => smoothstep(a, full, y)), 0, 0];
|
|
2763
|
+
const x0 = Math.min(b.x0, ...all.map((q) => q.x)) - BODY_GRID_PAD;
|
|
2764
|
+
const x1 = Math.max(b.x1, ...all.map((q) => q.x)) + BODY_GRID_PAD;
|
|
2765
|
+
const cols = cellsFor(x1 - x0, 96, 2, 8);
|
|
2766
|
+
const xs = [];
|
|
2767
|
+
for (let c = 0; c <= cols; c++) {
|
|
2768
|
+
xs.push(roundTo(x0 + (x1 - x0) * c / cols, 0.01));
|
|
2769
|
+
}
|
|
2770
|
+
return { xs, ys, weights, yFull: full / 100 };
|
|
2771
|
+
}
|
|
2772
|
+
function bodyMesh(b, rows) {
|
|
2773
|
+
const lines = [b.y1, ...rows.filter((y) => y > b.y0 && y < b.y1), b.y0];
|
|
2774
|
+
const ys = [lines[0]];
|
|
2775
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2776
|
+
const [from, to] = [lines[i - 1], lines[i]];
|
|
2777
|
+
const n = Math.ceil((from - to) / BODY_MESH_PX);
|
|
2778
|
+
for (let k = 1; k < n; k++) ys.push(from - (from - to) * k / n);
|
|
2779
|
+
ys.push(to);
|
|
2780
|
+
}
|
|
2781
|
+
const cols = cellsFor(bw(b), BODY_MESH_PX, 2, 16);
|
|
2782
|
+
const vertices = [];
|
|
2783
|
+
const uvs = [];
|
|
2784
|
+
for (const y of ys) {
|
|
2785
|
+
const vy = roundTo((y - cy(b)) / bh(b), 1e-5);
|
|
2786
|
+
for (let c = 0; c <= cols; c++) {
|
|
2787
|
+
const vx = roundTo(c / cols - 0.5, 1e-5);
|
|
2788
|
+
vertices.push(vx, vy);
|
|
2789
|
+
uvs.push(roundTo(vx + 0.5, 1e-5), roundTo(0.5 - vy, 1e-5));
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
const indices = [];
|
|
2793
|
+
for (let r = 0; r + 1 < ys.length; r++) {
|
|
2794
|
+
for (let c = 0; c < cols; c++) {
|
|
2795
|
+
const tl = r * (cols + 1) + c;
|
|
2796
|
+
const tr = tl + 1;
|
|
2797
|
+
const bl = tl + cols + 1;
|
|
2798
|
+
const br = bl + 1;
|
|
2799
|
+
indices.push(bl, br, tl, tl, br, tr);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return { vertices, uvs, indices };
|
|
2803
|
+
}
|
|
2804
|
+
function buildBodyWarp({
|
|
2805
|
+
body,
|
|
2806
|
+
chin,
|
|
2807
|
+
hh,
|
|
2808
|
+
pivots
|
|
2809
|
+
}) {
|
|
2810
|
+
const b = boxOfLayer(body);
|
|
2811
|
+
const anchor = hipLine(body);
|
|
2812
|
+
const { xs, ys, weights } = bodyLattice(body, chin, hh, pivots, anchor);
|
|
2813
|
+
const hx = cx(b);
|
|
2814
|
+
const turn = (x, t) => t * BODY.slide * hh - Math.abs(t) * BODY.narrow * (x - hx);
|
|
2815
|
+
const roll = (x, y, deg) => {
|
|
2816
|
+
const c = Math.cos(deg * DEG) - 1;
|
|
2817
|
+
const s = Math.sin(deg * DEG);
|
|
2818
|
+
const [dx, dy] = [x - hx, y - anchor];
|
|
2819
|
+
return [c * dx - s * dy, s * dx + c * dy];
|
|
2820
|
+
};
|
|
2821
|
+
const warp = (parameter, values, field) => {
|
|
2822
|
+
const extreme = Math.max(...values.map(Math.abs));
|
|
2823
|
+
return {
|
|
2824
|
+
parameter,
|
|
2825
|
+
keyforms: values.map((value) => ({
|
|
2826
|
+
value,
|
|
2827
|
+
offsets: ys.flatMap(
|
|
2828
|
+
(y, r) => xs.flatMap((x) => {
|
|
2829
|
+
const [dx, dy] = field(x, y, value / extreme);
|
|
2830
|
+
return [
|
|
2831
|
+
roundTo(weights[r] * dx, 0.01),
|
|
2832
|
+
roundTo(weights[r] * dy, 0.01)
|
|
2833
|
+
];
|
|
2834
|
+
})
|
|
2835
|
+
)
|
|
2836
|
+
}))
|
|
2837
|
+
};
|
|
2838
|
+
};
|
|
2839
|
+
const deformer = {
|
|
2840
|
+
kind: "warp",
|
|
2841
|
+
id: BODY_WARP_ID,
|
|
2842
|
+
grid: {
|
|
2843
|
+
cols: xs.length - 1,
|
|
2844
|
+
rows: ys.length - 1,
|
|
2845
|
+
points: ys.flatMap((y) => xs.flatMap((x) => [x, y]))
|
|
2846
|
+
},
|
|
2847
|
+
warps: [
|
|
2848
|
+
warp(P3.BodyAngleX, BODY_STOPS, (x, _y, t) => [turn(x, t), 0]),
|
|
2849
|
+
warp(P3.BodyAngleY, BODY_STOPS, (_x, _y, t) => [0, t * BODY.bow * hh]),
|
|
2850
|
+
warp(
|
|
2851
|
+
P3.BodyAngleZ,
|
|
2852
|
+
BODY_STOPS,
|
|
2853
|
+
(x, y, t) => roll(x, y, -t * BODY.rollDeg)
|
|
2854
|
+
),
|
|
2855
|
+
warp(P3.AngleX, X_STOPS, (x, _y, t) => [BODY.followX * turn(x, t), 0]),
|
|
2856
|
+
warp(P3.AngleZ, Z_STOPS, (x, y, t) => roll(x, y, -t * BODY.followRoll)),
|
|
2857
|
+
warp(P3.Breath, [0, 1], (_x, _y, t) => [0, t * AMPLITUDE.breathBody * hh])
|
|
2858
|
+
]
|
|
2859
|
+
};
|
|
2860
|
+
refuseFold(body, deformer, anchor, [chin, ...pivots]);
|
|
2861
|
+
return { deformer, mesh: bodyMesh(b, ys) };
|
|
2862
|
+
}
|
|
2863
|
+
function refuseFold(body, d, anchor, pivots) {
|
|
2864
|
+
const { cols, rows, points } = d.grid;
|
|
2865
|
+
const warps = d.warps ?? [];
|
|
2866
|
+
const stride = cols + 1;
|
|
2867
|
+
for (let r = 0; r < rows; r++) {
|
|
2868
|
+
for (let c = 0; c < cols; c++) {
|
|
2869
|
+
const tl = r * stride + c;
|
|
2870
|
+
const [tr, bl, br] = [tl + 1, tl + stride, tl + stride + 1];
|
|
2871
|
+
const corners = [
|
|
2872
|
+
[bl, br, tl],
|
|
2873
|
+
[br, tr, bl],
|
|
2874
|
+
[tr, tl, br],
|
|
2875
|
+
[tl, bl, tr]
|
|
2876
|
+
];
|
|
2877
|
+
for (const [p, q, s] of corners) {
|
|
2878
|
+
const fold = leastArea(points, warps, p, q, s);
|
|
2879
|
+
if (fold.least >= FOLD_MARGIN * fold.rest) continue;
|
|
2880
|
+
const lowest = roundTo(Math.min(...pivots.map((v) => v.y)), 0.01);
|
|
2881
|
+
const pose = warps.map((w, i) => `${w.parameter} ${w.keyforms[fold.stops[i]].value}`).join(", ");
|
|
2882
|
+
throw new LayerGeometryError(
|
|
2883
|
+
`auto-rig: layer "${body.fileName}": the body is too short between its hips (y ${anchor}) and the lowest of the head's and shoulders' pivots (y ${lowest}) for its motion: its body warp's cell from x ${points[tl * 2]} to ${points[tr * 2]}, y ${points[tl * 2 + 1]} to ${points[bl * 2 + 1]}, would fold at or near ${pose}`
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function leastArea(points, warps, p, q, s) {
|
|
2890
|
+
const moves = warps.map(
|
|
2891
|
+
(w) => w.keyforms.map(({ offsets: o }) => [
|
|
2892
|
+
o[q * 2] - o[p * 2],
|
|
2893
|
+
o[q * 2 + 1] - o[p * 2 + 1],
|
|
2894
|
+
o[s * 2] - o[p * 2],
|
|
2895
|
+
o[s * 2 + 1] - o[p * 2 + 1]
|
|
2896
|
+
])
|
|
2897
|
+
);
|
|
2898
|
+
const cross = (e) => e[0] * e[3] - e[1] * e[2];
|
|
2899
|
+
const edges = [
|
|
2900
|
+
points[q * 2] - points[p * 2],
|
|
2901
|
+
points[q * 2 + 1] - points[p * 2 + 1],
|
|
2902
|
+
points[s * 2] - points[p * 2],
|
|
2903
|
+
points[s * 2 + 1] - points[p * 2 + 1]
|
|
2904
|
+
];
|
|
2905
|
+
let least = Infinity;
|
|
2906
|
+
let stops = [];
|
|
2907
|
+
const at = [];
|
|
2908
|
+
const walk = (w, ax, ay, bx, by) => {
|
|
2909
|
+
if (w === moves.length) {
|
|
2910
|
+
const area = ax * by - ay * bx;
|
|
2911
|
+
if (area < least) [least, stops] = [area, [...at]];
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
moves[w].forEach(([mx, my, nx, ny], k) => {
|
|
2915
|
+
at[w] = k;
|
|
2916
|
+
walk(w + 1, ax + mx, ay + my, bx + nx, by + ny);
|
|
2917
|
+
});
|
|
2918
|
+
};
|
|
2919
|
+
walk(0, edges[0], edges[1], edges[2], edges[3]);
|
|
2920
|
+
for (const m of moves) {
|
|
2921
|
+
let most = 0;
|
|
2922
|
+
for (let k = 1; k < m.length; k++) {
|
|
2923
|
+
most = Math.max(most, cross(m[k].map((v, i) => v - m[k - 1][i])));
|
|
2924
|
+
}
|
|
2925
|
+
least -= most / 4;
|
|
2926
|
+
}
|
|
2927
|
+
return { rest: cross(edges), least, stops };
|
|
2928
|
+
}
|
|
2929
|
+
function headOwnRoll(hasBody) {
|
|
2930
|
+
return hasBody ? ROLL_DEG - BODY.followRoll : ROLL_DEG;
|
|
2931
|
+
}
|
|
2932
|
+
function headOwnBreath(hasBody) {
|
|
2933
|
+
return hasBody ? AMPLITUDE.breathHead - AMPLITUDE.breathBody : AMPLITUDE.breathHead;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/auto-rig/arms.ts
|
|
2937
|
+
var ELBOW_AT = 0.42;
|
|
2938
|
+
var CAP_ROWS = [0.1, 0.3];
|
|
2939
|
+
var CAP_SEGMENTS = 12;
|
|
2940
|
+
var ARM_RANGE = [-30, 150];
|
|
2941
|
+
var ARM_PARAMS = {
|
|
2942
|
+
arm_L: { arm: P4.ArmLeft, elbow: P4.ElbowLeft },
|
|
2943
|
+
arm_R: { arm: P4.ArmRight, elbow: P4.ElbowRight }
|
|
2944
|
+
};
|
|
2945
|
+
var ARM_IDS = {
|
|
2946
|
+
arm_L: {
|
|
2947
|
+
forearm: "forearm_L",
|
|
2948
|
+
armDeformer: "armDeformer_L",
|
|
2949
|
+
forearmDeformer: "forearmDeformer_L"
|
|
2950
|
+
},
|
|
2951
|
+
arm_R: {
|
|
2952
|
+
forearm: "forearm_R",
|
|
2953
|
+
armDeformer: "armDeformer_R",
|
|
2954
|
+
forearmDeformer: "forearmDeformer_R"
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
function armGeometry(layer, bodyAxisX) {
|
|
2958
|
+
const { bbox, canvasW, canvasH, rowRuns } = layer;
|
|
2959
|
+
const b = boxOfLayer(layer);
|
|
2960
|
+
const span = (k) => {
|
|
2961
|
+
const runs = rowRuns?.[k] ?? [];
|
|
2962
|
+
if (runs.length === 0) return [bbox.x, bbox.x + bbox.w];
|
|
2963
|
+
let [s2, e2] = [runs[0], runs[1]];
|
|
2964
|
+
for (let i = 2; i < runs.length; i += 2) {
|
|
2965
|
+
if (runs[i + 1] - runs[i] > e2 - s2) [s2, e2] = [runs[i], runs[i + 1]];
|
|
2966
|
+
}
|
|
2967
|
+
return [s2, e2];
|
|
2968
|
+
};
|
|
2969
|
+
const centreX = (k) => {
|
|
2970
|
+
const [s2, e2] = span(k);
|
|
2971
|
+
return (s2 + e2) / 2 - canvasW / 2;
|
|
2972
|
+
};
|
|
2973
|
+
const rowY = (k) => canvasH / 2 - (bbox.y + k + 0.5);
|
|
2974
|
+
const painted = rowRuns?.flatMap((runs, k) => runs.length > 0 ? [k] : []) ?? [];
|
|
2975
|
+
const first = painted[0] ?? 0;
|
|
2976
|
+
const last = painted.at(-1) ?? bbox.h - 1;
|
|
2977
|
+
const lo = Math.ceil(CAP_ROWS[0] * bbox.h);
|
|
2978
|
+
const hi = Math.max(lo, Math.floor(CAP_ROWS[1] * bbox.h));
|
|
2979
|
+
const widths = [];
|
|
2980
|
+
for (let k = lo; k <= hi; k++) {
|
|
2981
|
+
const [s2, e2] = span(k);
|
|
2982
|
+
widths.push(e2 - s2);
|
|
2983
|
+
}
|
|
2984
|
+
widths.sort((p, q) => p - q);
|
|
2985
|
+
const ru = widths[widths.length >> 1] / 2;
|
|
2986
|
+
const ks = first + ru;
|
|
2987
|
+
const shoulder = {
|
|
2988
|
+
x: roundTo(centreX(Math.round(ks)), 0.01),
|
|
2989
|
+
y: roundTo(rowY(ks), 0.01)
|
|
2990
|
+
};
|
|
2991
|
+
const ke = Math.round(ks + ELBOW_AT * (last - ks));
|
|
2992
|
+
if (ke <= ks) {
|
|
2993
|
+
throw new LayerGeometryError(
|
|
2994
|
+
`auto-rig: layer "${layer.fileName}": the arm is too short for its width to place a shoulder above an elbow (draw the arm hanging, shoulder at the top)`
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
const elbow = { x: roundTo(centreX(ke), 0.01), y: roundTo(rowY(ke), 0.01) };
|
|
2998
|
+
const [s, e] = span(ke);
|
|
2999
|
+
const capRadius = Math.min(
|
|
3000
|
+
(e - s) / 2 + 1,
|
|
3001
|
+
elbow.x - b.x0,
|
|
3002
|
+
b.x1 - elbow.x,
|
|
3003
|
+
b.y1 - elbow.y
|
|
3004
|
+
);
|
|
3005
|
+
return {
|
|
3006
|
+
shoulder,
|
|
3007
|
+
shoulderRadius: ru,
|
|
3008
|
+
elbow,
|
|
3009
|
+
capRadius,
|
|
3010
|
+
side: shoulder.x > bodyAxisX ? 1 : -1
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
function meshOf(vertices, indices) {
|
|
3014
|
+
const uvs = vertices.map(
|
|
3015
|
+
(v, i) => roundTo(i % 2 === 0 ? v + 0.5 : 0.5 - v, 1e-5)
|
|
3016
|
+
);
|
|
3017
|
+
return { vertices, uvs, indices };
|
|
3018
|
+
}
|
|
3019
|
+
function armParts(role, layer, g, order) {
|
|
3020
|
+
const b = boxOfLayer(layer);
|
|
3021
|
+
const lx = (x) => roundTo((x - cx(b)) / bw(b), 1e-5);
|
|
3022
|
+
const ly = (y) => roundTo((y - cy(b)) / bh(b), 1e-5);
|
|
3023
|
+
const seam = ly(g.elbow.y);
|
|
3024
|
+
const band = (top, bottom) => [
|
|
3025
|
+
-0.5,
|
|
3026
|
+
top,
|
|
3027
|
+
0.5,
|
|
3028
|
+
top,
|
|
3029
|
+
-0.5,
|
|
3030
|
+
bottom,
|
|
3031
|
+
0.5,
|
|
3032
|
+
bottom
|
|
3033
|
+
];
|
|
3034
|
+
const quad = [2, 3, 0, 0, 3, 1];
|
|
3035
|
+
const cap = [lx(g.elbow.x), seam];
|
|
3036
|
+
const fan = [];
|
|
3037
|
+
for (let i = 0; i <= CAP_SEGMENTS; i++) {
|
|
3038
|
+
const a = Math.PI * i / CAP_SEGMENTS;
|
|
3039
|
+
cap.push(
|
|
3040
|
+
lx(g.elbow.x + g.capRadius * Math.cos(a)),
|
|
3041
|
+
ly(g.elbow.y + g.capRadius * Math.sin(a))
|
|
3042
|
+
);
|
|
3043
|
+
if (i > 0) fan.push(4, 4 + i, 5 + i);
|
|
3044
|
+
}
|
|
3045
|
+
const part = (id, deformer, at, mesh) => ({
|
|
3046
|
+
id,
|
|
3047
|
+
color: [1, 1, 1, 1],
|
|
3048
|
+
width: bw(b),
|
|
3049
|
+
height: bh(b),
|
|
3050
|
+
transform: { x: cx(b), y: cy(b) },
|
|
3051
|
+
order: at,
|
|
3052
|
+
deformer,
|
|
3053
|
+
mesh
|
|
3054
|
+
});
|
|
3055
|
+
const ids = ARM_IDS[role];
|
|
3056
|
+
return [
|
|
3057
|
+
part(role, ids.armDeformer, order, meshOf(band(0.5, seam), quad)),
|
|
3058
|
+
part(
|
|
3059
|
+
ids.forearm,
|
|
3060
|
+
ids.forearmDeformer,
|
|
3061
|
+
order + 1,
|
|
3062
|
+
meshOf([...band(seam, -0.5), ...cap], [...quad, ...fan])
|
|
3063
|
+
)
|
|
3064
|
+
];
|
|
3065
|
+
}
|
|
3066
|
+
function armDeformers(role, g) {
|
|
3067
|
+
const ids = ARM_IDS[role];
|
|
3068
|
+
const params = ARM_PARAMS[role];
|
|
3069
|
+
const rotate = (parameter) => ({
|
|
3070
|
+
parameter,
|
|
3071
|
+
channel: "rotate",
|
|
3072
|
+
from: g.side * ARM_RANGE[0],
|
|
3073
|
+
to: g.side * ARM_RANGE[1]
|
|
3074
|
+
});
|
|
3075
|
+
return [
|
|
3076
|
+
{
|
|
3077
|
+
id: ids.armDeformer,
|
|
3078
|
+
parent: BODY_WARP_ID,
|
|
3079
|
+
pivot: { ...g.shoulder },
|
|
3080
|
+
bindings: [rotate(params.arm)]
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
id: ids.forearmDeformer,
|
|
3084
|
+
parent: ids.armDeformer,
|
|
3085
|
+
pivot: { ...g.elbow },
|
|
3086
|
+
bindings: [rotate(params.elbow)]
|
|
3087
|
+
}
|
|
3088
|
+
];
|
|
3089
|
+
}
|
|
3090
|
+
|
|
2369
3091
|
// src/auto-rig/checks.ts
|
|
2370
3092
|
var finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
2371
3093
|
var isObject = (v) => typeof v === "object" && v !== null;
|
|
@@ -2469,6 +3191,7 @@ function checkLayers(layers, canvas) {
|
|
|
2469
3191
|
if (missing.length > 0) {
|
|
2470
3192
|
throw new Error(`auto-rig: missing required role(s) ${missing.join(", ")}`);
|
|
2471
3193
|
}
|
|
3194
|
+
checkArmsHaveBody(seen);
|
|
2472
3195
|
}
|
|
2473
3196
|
function checkHeadEdges(edges, byRole) {
|
|
2474
3197
|
if (edges === void 0) return;
|
|
@@ -2551,129 +3274,43 @@ function checkStrandEdges(strand, irises, hair) {
|
|
|
2551
3274
|
["irisInner", inner]
|
|
2552
3275
|
]) {
|
|
2553
3276
|
if (x < iris.x0 || x > iris.x1) {
|
|
2554
|
-
throw new TurnTargetError(`${at}.${k} ${x} is off its iris's crop`);
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
if (sign * (outer - inner) <= 0) {
|
|
2558
|
-
throw new TurnTargetError(
|
|
2559
|
-
`${at}.irisOuter ${outer} is not strictly outward of irisInner ${inner}`
|
|
2560
|
-
);
|
|
2561
|
-
}
|
|
2562
|
-
for (const [k, x] of [
|
|
2563
|
-
["runOuter", runOuter],
|
|
2564
|
-
["runFace", runFace]
|
|
2565
|
-
]) {
|
|
2566
|
-
if (x !== null && (x < hair.x0 || x > hair.x1)) {
|
|
2567
|
-
throw new TurnTargetError(`${at}.${k} ${x} is off hair_front's crop`);
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
if (sign * (runOuter - cx(iris)) <= 0) {
|
|
2571
|
-
throw new TurnTargetError(
|
|
2572
|
-
`${at}.runOuter ${runOuter} is not strictly outward of its iris's centre`
|
|
2573
|
-
);
|
|
2574
|
-
}
|
|
2575
|
-
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
2576
|
-
throw new TurnTargetError(
|
|
2577
|
-
`${at}.runOuter ${runOuter} is not strictly outward of runFace ${runFace}`
|
|
2578
|
-
);
|
|
2579
|
-
}
|
|
2580
|
-
if (runFace !== null && sign * (runFace - cx(other)) <= 0) {
|
|
2581
|
-
throw new TurnTargetError(
|
|
2582
|
-
`${at}.runFace ${runFace} reaches the other iris's centre; a run that does is a fringe, runFace null`
|
|
2583
|
-
);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
// src/auto-rig/context.ts
|
|
2589
|
-
import { StandardParameter as P2 } from "@ikijs/format";
|
|
2590
|
-
|
|
2591
|
-
// src/auto-rig/grid.ts
|
|
2592
|
-
var X_STOPS = [-30, 0, 30];
|
|
2593
|
-
var Y_STOPS = [-30, 0, 30];
|
|
2594
|
-
function lattice(box, cell, min, max) {
|
|
2595
|
-
const cols = cellsFor(box.x1 - box.x0, cell, min, max);
|
|
2596
|
-
const rows = cellsFor(box.y1 - box.y0, cell, min, max);
|
|
2597
|
-
const points = [];
|
|
2598
|
-
for (let r = 0; r <= rows; r++) {
|
|
2599
|
-
const y = box.y1 - (box.y1 - box.y0) * r / rows;
|
|
2600
|
-
for (let c = 0; c <= cols; c++) {
|
|
2601
|
-
points.push(
|
|
2602
|
-
roundTo(box.x0 + (box.x1 - box.x0) * c / cols, 0.01),
|
|
2603
|
-
roundTo(y, 0.01)
|
|
2604
|
-
);
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
return { box, cols, rows, points };
|
|
2608
|
-
}
|
|
2609
|
-
function bake(l, field) {
|
|
2610
|
-
const out = [];
|
|
2611
|
-
for (const ay of Y_STOPS) {
|
|
2612
|
-
for (const ax of X_STOPS) {
|
|
2613
|
-
const offsets = [];
|
|
2614
|
-
for (let k = 0; k < l.points.length; k += 2) {
|
|
2615
|
-
if (ax === 0 && ay === 0) {
|
|
2616
|
-
offsets.push(0, 0);
|
|
2617
|
-
continue;
|
|
2618
|
-
}
|
|
2619
|
-
const [dx, dy] = field(l.points[k], l.points[k + 1], ax, ay);
|
|
2620
|
-
offsets.push(roundTo(dx, 0.1), roundTo(dy, 0.1));
|
|
3277
|
+
throw new TurnTargetError(`${at}.${k} ${x} is off its iris's crop`);
|
|
2621
3278
|
}
|
|
2622
|
-
out.push(offsets);
|
|
2623
3279
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
const j = Y_STOPS.indexOf(0);
|
|
2629
|
-
const row = X_STOPS.map((_, i) => keyforms[j * X_STOPS.length + i]);
|
|
2630
|
-
return blendStops(X_STOPS, row, ax);
|
|
2631
|
-
}
|
|
2632
|
-
function blendStops(values, offsets, v) {
|
|
2633
|
-
const x = clamp(v, values[0], values[values.length - 1]);
|
|
2634
|
-
let i = 0;
|
|
2635
|
-
while (i < values.length - 2 && x > values[i + 1]) i++;
|
|
2636
|
-
const t = (x - values[i]) / (values[i + 1] - values[i]);
|
|
2637
|
-
return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
|
|
2638
|
-
}
|
|
2639
|
-
function land(l, offsets, x, y) {
|
|
2640
|
-
const { cols, rows, points } = l;
|
|
2641
|
-
const stride = cols + 1;
|
|
2642
|
-
let col = cols - 1;
|
|
2643
|
-
for (let c = 0; c < cols; c++) {
|
|
2644
|
-
if (x < points[(c + 1) * 2]) {
|
|
2645
|
-
col = c;
|
|
2646
|
-
break;
|
|
3280
|
+
if (sign * (outer - inner) <= 0) {
|
|
3281
|
+
throw new TurnTargetError(
|
|
3282
|
+
`${at}.irisOuter ${outer} is not strictly outward of irisInner ${inner}`
|
|
3283
|
+
);
|
|
2647
3284
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
3285
|
+
for (const [k, x] of [
|
|
3286
|
+
["runOuter", runOuter],
|
|
3287
|
+
["runFace", runFace]
|
|
3288
|
+
]) {
|
|
3289
|
+
if (x !== null && (x < hair.x0 || x > hair.x1)) {
|
|
3290
|
+
throw new TurnTargetError(`${at}.${k} ${x} is off hair_front's crop`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (sign * (runOuter - cx(iris)) <= 0) {
|
|
3294
|
+
throw new TurnTargetError(
|
|
3295
|
+
`${at}.runOuter ${runOuter} is not strictly outward of its iris's centre`
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
3298
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
3299
|
+
throw new TurnTargetError(
|
|
3300
|
+
`${at}.runOuter ${runOuter} is not strictly outward of runFace ${runFace}`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
if (runFace !== null && sign * (runFace - cx(other)) <= 0) {
|
|
3304
|
+
throw new TurnTargetError(
|
|
3305
|
+
`${at}.runFace ${runFace} reaches the other iris's centre; a run that does is a fringe, runFace null`
|
|
3306
|
+
);
|
|
2654
3307
|
}
|
|
2655
3308
|
}
|
|
2656
|
-
const x0 = points[col * 2];
|
|
2657
|
-
const x1 = points[(col + 1) * 2];
|
|
2658
|
-
const yT = points[row * stride * 2 + 1];
|
|
2659
|
-
const yB = points[(row + 1) * stride * 2 + 1];
|
|
2660
|
-
const s = clamp((x - x0) / (x1 - x0), 0, 1);
|
|
2661
|
-
const t = clamp((yT - y) / (yT - yB), 0, 1);
|
|
2662
|
-
const at = (r, c) => {
|
|
2663
|
-
const k = (r * stride + c) * 2;
|
|
2664
|
-
return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
|
|
2665
|
-
};
|
|
2666
|
-
const p00 = at(row, col);
|
|
2667
|
-
const p10 = at(row, col + 1);
|
|
2668
|
-
const p01 = at(row + 1, col);
|
|
2669
|
-
const p11 = at(row + 1, col + 1);
|
|
2670
|
-
const topX = p00[0] + (p10[0] - p00[0]) * s;
|
|
2671
|
-
const topY = p00[1] + (p10[1] - p00[1]) * s;
|
|
2672
|
-
const botX = p01[0] + (p11[0] - p01[0]) * s;
|
|
2673
|
-
const botY = p01[1] + (p11[1] - p01[1]) * s;
|
|
2674
|
-
return [topX + (botX - topX) * t, topY + (botY - topY) * t];
|
|
2675
3309
|
}
|
|
2676
3310
|
|
|
3311
|
+
// src/auto-rig/context.ts
|
|
3312
|
+
import { StandardParameter as P5 } from "@ikijs/format";
|
|
3313
|
+
|
|
2677
3314
|
// src/auto-rig/head.ts
|
|
2678
3315
|
function outerEdge(runs, axisX, side) {
|
|
2679
3316
|
if (runs.length === 0) return void 0;
|
|
@@ -3703,6 +4340,7 @@ function familyField(m, family) {
|
|
|
3703
4340
|
case "hair_back":
|
|
3704
4341
|
return hairField(m, m.hairBack, NOD.hairBack);
|
|
3705
4342
|
case "body":
|
|
4343
|
+
case "arm":
|
|
3706
4344
|
return () => [0, 0];
|
|
3707
4345
|
}
|
|
3708
4346
|
}
|
|
@@ -3802,7 +4440,7 @@ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
|
|
|
3802
4440
|
const family = roleSpec(role).family;
|
|
3803
4441
|
const part = parts.find((p) => p.id === role);
|
|
3804
4442
|
const g = grids.get(family);
|
|
3805
|
-
const warp = part?.warps?.find((w) => w.parameter ===
|
|
4443
|
+
const warp = part?.warps?.find((w) => w.parameter === P5.AngleX);
|
|
3806
4444
|
if (part === void 0 || part.mesh === void 0 || !g && !warp) {
|
|
3807
4445
|
return x + field(role)(x, y, ax, 0)[0];
|
|
3808
4446
|
}
|
|
@@ -4511,179 +5149,6 @@ function buildTurn(frame, landmarks, q) {
|
|
|
4511
5149
|
};
|
|
4512
5150
|
}
|
|
4513
5151
|
|
|
4514
|
-
// src/auto-rig/drivers.ts
|
|
4515
|
-
import {
|
|
4516
|
-
StandardParameter as P3
|
|
4517
|
-
} from "@ikijs/format";
|
|
4518
|
-
var SWAY_RANGE = 20;
|
|
4519
|
-
var SWAY_CURVE = 1.4;
|
|
4520
|
-
var HAIR_HANG = 0.35;
|
|
4521
|
-
var Z_STOPS = [-30, 0, 30];
|
|
4522
|
-
function localWarp(parameter, mesh, box, values, f) {
|
|
4523
|
-
const pts = meshPoints(mesh, box);
|
|
4524
|
-
const w = bw(box);
|
|
4525
|
-
const h = bh(box);
|
|
4526
|
-
return {
|
|
4527
|
-
parameter,
|
|
4528
|
-
keyforms: values.map((value) => ({
|
|
4529
|
-
value,
|
|
4530
|
-
offsets: pts.flatMap((p, i) => {
|
|
4531
|
-
const [dx, dy] = f(p, value, i);
|
|
4532
|
-
return [roundTo(dx / w, 1e-4), roundTo(dy / h, 1e-4)];
|
|
4533
|
-
})
|
|
4534
|
-
}))
|
|
4535
|
-
};
|
|
4536
|
-
}
|
|
4537
|
-
var rollOf = (v) => -v / 30 * ROLL_DEG * DEG;
|
|
4538
|
-
function motionReach(warps, mesh, box, extra = [0, 0]) {
|
|
4539
|
-
const out = {
|
|
4540
|
-
left: extra[0],
|
|
4541
|
-
right: extra[0],
|
|
4542
|
-
bottom: extra[1],
|
|
4543
|
-
top: extra[1]
|
|
4544
|
-
};
|
|
4545
|
-
if (mesh === void 0) return out;
|
|
4546
|
-
const pts = meshPoints(mesh, box);
|
|
4547
|
-
for (const warp of warps ?? []) {
|
|
4548
|
-
let l = 0;
|
|
4549
|
-
let r = 0;
|
|
4550
|
-
let b = 0;
|
|
4551
|
-
let t = 0;
|
|
4552
|
-
for (const k of warp.keyforms) {
|
|
4553
|
-
pts.forEach(([x, y], i) => {
|
|
4554
|
-
const px = x + k.offsets[2 * i] * bw(box);
|
|
4555
|
-
const py = y + k.offsets[2 * i + 1] * bh(box);
|
|
4556
|
-
l = Math.max(l, box.x0 - px);
|
|
4557
|
-
r = Math.max(r, px - box.x1);
|
|
4558
|
-
b = Math.max(b, box.y0 - py);
|
|
4559
|
-
t = Math.max(t, py - box.y1);
|
|
4560
|
-
});
|
|
4561
|
-
}
|
|
4562
|
-
out.left += l;
|
|
4563
|
-
out.right += r;
|
|
4564
|
-
out.bottom += b;
|
|
4565
|
-
out.top += t;
|
|
4566
|
-
}
|
|
4567
|
-
return out;
|
|
4568
|
-
}
|
|
4569
|
-
function blinkFold(param, mesh, box, travel, eye = box) {
|
|
4570
|
-
const yc = eye.y1 - travel * bh(eye);
|
|
4571
|
-
return localWarp(
|
|
4572
|
-
param,
|
|
4573
|
-
mesh,
|
|
4574
|
-
box,
|
|
4575
|
-
[0, 1],
|
|
4576
|
-
([, y], v) => v === 1 ? [0, 0] : [0, yc - y]
|
|
4577
|
-
);
|
|
4578
|
-
}
|
|
4579
|
-
function lashFold(param, mesh, lash, eye, travel) {
|
|
4580
|
-
const yc = eye.y1 - travel * bh(eye) - 0.04 * bh(eye);
|
|
4581
|
-
const c = cx(lash);
|
|
4582
|
-
const half = bw(lash) / 2;
|
|
4583
|
-
const sag = 0.3 * bh(lash);
|
|
4584
|
-
return localWarp(param, mesh, lash, [0, 1], ([x, y], v) => {
|
|
4585
|
-
if (v === 1) return [0, 0];
|
|
4586
|
-
const u = (x - c) / half;
|
|
4587
|
-
return [0, yc + (y - lash.y0) * 0.5 - sag * (1 - u * u) - y];
|
|
4588
|
-
});
|
|
4589
|
-
}
|
|
4590
|
-
function gazeBindings(irisW, share = 1) {
|
|
4591
|
-
const gx = AMPLITUDE.gazeX * irisW * share;
|
|
4592
|
-
const gy = AMPLITUDE.gazeY * irisW * share;
|
|
4593
|
-
return [
|
|
4594
|
-
{
|
|
4595
|
-
parameter: P3.EyeballX,
|
|
4596
|
-
channel: "translateX",
|
|
4597
|
-
from: -round1(gx),
|
|
4598
|
-
to: round1(gx)
|
|
4599
|
-
},
|
|
4600
|
-
{
|
|
4601
|
-
parameter: P3.EyeballY,
|
|
4602
|
-
channel: "translateY",
|
|
4603
|
-
from: -round1(gy),
|
|
4604
|
-
to: round1(gy)
|
|
4605
|
-
}
|
|
4606
|
-
];
|
|
4607
|
-
}
|
|
4608
|
-
function gazeReach(irisW, share = 1) {
|
|
4609
|
-
return [AMPLITUDE.gazeX * irisW * share, AMPLITUDE.gazeY * irisW * share];
|
|
4610
|
-
}
|
|
4611
|
-
function browReach(hh) {
|
|
4612
|
-
return AMPLITUDE.brow * hh;
|
|
4613
|
-
}
|
|
4614
|
-
function browBindings(side, reach) {
|
|
4615
|
-
const y = side === "L" ? P3.BrowLeftY : P3.BrowRightY;
|
|
4616
|
-
const a = side === "L" ? P3.BrowLeftAngle : P3.BrowRightAngle;
|
|
4617
|
-
return [
|
|
4618
|
-
{
|
|
4619
|
-
parameter: y,
|
|
4620
|
-
channel: "translateY",
|
|
4621
|
-
from: -round1(reach),
|
|
4622
|
-
to: round1(reach)
|
|
4623
|
-
},
|
|
4624
|
-
{ parameter: a, channel: "rotate", from: -BROW_TILT, to: BROW_TILT }
|
|
4625
|
-
];
|
|
4626
|
-
}
|
|
4627
|
-
var BROW_TILT = 15;
|
|
4628
|
-
function mouthForm(mesh, box) {
|
|
4629
|
-
const a = 0.3 * bh(box);
|
|
4630
|
-
const c = cx(box);
|
|
4631
|
-
const half = bw(box) / 2;
|
|
4632
|
-
return localWarp(P3.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
|
|
4633
|
-
if (v === 0) return [0, 0];
|
|
4634
|
-
const u = (x - c) / half;
|
|
4635
|
-
const widen = v > 0 ? 0.05 : 0.03;
|
|
4636
|
-
return [v * widen * (x - c), v * a * (u * u - 0.3)];
|
|
4637
|
-
});
|
|
4638
|
-
}
|
|
4639
|
-
function mouthOpenGrow(mesh, box) {
|
|
4640
|
-
const c = cx(box);
|
|
4641
|
-
return localWarp(
|
|
4642
|
-
P3.MouthOpen,
|
|
4643
|
-
mesh,
|
|
4644
|
-
box,
|
|
4645
|
-
[0, 1],
|
|
4646
|
-
([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
|
|
4647
|
-
);
|
|
4648
|
-
}
|
|
4649
|
-
function mouthWiden(mesh, box) {
|
|
4650
|
-
const c = cx(box);
|
|
4651
|
-
return localWarp(P3.MouthOpen, mesh, box, [0, 1], ([x], v) => [
|
|
4652
|
-
v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
|
|
4653
|
-
0
|
|
4654
|
-
]);
|
|
4655
|
-
}
|
|
4656
|
-
function hairSway(param, mesh, box, amp) {
|
|
4657
|
-
return localWarp(
|
|
4658
|
-
param,
|
|
4659
|
-
mesh,
|
|
4660
|
-
box,
|
|
4661
|
-
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
4662
|
-
([, y], v) => {
|
|
4663
|
-
const t = Math.max(0, (box.y1 - y) / bh(box));
|
|
4664
|
-
return [v / SWAY_RANGE * amp * t ** SWAY_CURVE, 0];
|
|
4665
|
-
}
|
|
4666
|
-
);
|
|
4667
|
-
}
|
|
4668
|
-
function tiltHang(mesh, box, pivot, weight) {
|
|
4669
|
-
return localWarp(P3.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
|
|
4670
|
-
const g = weight(y, i);
|
|
4671
|
-
if (g === 0 || v === 0) return [0, 0];
|
|
4672
|
-
const a = -g * rollOf(v);
|
|
4673
|
-
const px = x - pivot[0];
|
|
4674
|
-
const py = y - pivot[1];
|
|
4675
|
-
return [
|
|
4676
|
-
pivot[0] + px * Math.cos(a) - py * Math.sin(a) - x,
|
|
4677
|
-
pivot[1] + px * Math.sin(a) + py * Math.cos(a) - y
|
|
4678
|
-
];
|
|
4679
|
-
});
|
|
4680
|
-
}
|
|
4681
|
-
function hairHangWeight(pivotY, bottom) {
|
|
4682
|
-
if (bottom >= pivotY) return () => 0;
|
|
4683
|
-
return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
|
|
4684
|
-
}
|
|
4685
|
-
var round1 = (v) => roundTo(v, 0.1);
|
|
4686
|
-
|
|
4687
5152
|
// src/auto-rig/generate.ts
|
|
4688
5153
|
var MESH_CELLS = {
|
|
4689
5154
|
face: { px: 28, min: 4, max: 24 },
|
|
@@ -4710,6 +5175,7 @@ var WARP_ID = {
|
|
|
4710
5175
|
mouth: "mouthWarp"
|
|
4711
5176
|
};
|
|
4712
5177
|
var isGridFamily = (f) => f in WARP_ID;
|
|
5178
|
+
var isArm = (spec) => spec.family === "arm";
|
|
4713
5179
|
var GRID_PAD = 6;
|
|
4714
5180
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
4715
5181
|
checkLayers(layers, canvas);
|
|
@@ -4758,6 +5224,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4758
5224
|
chinSlide: chinSlide(fit.model)
|
|
4759
5225
|
});
|
|
4760
5226
|
const turn = { ...fit.model, frame };
|
|
5227
|
+
const arms = /* @__PURE__ */ new Map();
|
|
5228
|
+
for (const spec of ROLE_TABLE) {
|
|
5229
|
+
const layer = byRole.get(spec.role);
|
|
5230
|
+
if (layer === void 0 || !isArm(spec)) continue;
|
|
5231
|
+
arms.set(spec.role, armGeometry(layer, cx(box("body"))));
|
|
5232
|
+
}
|
|
5233
|
+
const bodyWarp = has("body") ? buildBodyWarp({
|
|
5234
|
+
body: byRole.get("body"),
|
|
5235
|
+
chin: { x: frame.axisX, y: frame.chinY },
|
|
5236
|
+
hh: frame.hh,
|
|
5237
|
+
pivots: [...arms.values()].map((g) => g.shoulder)
|
|
5238
|
+
}) : void 0;
|
|
4761
5239
|
const parts = [];
|
|
4762
5240
|
const reach = /* @__PURE__ */ new Map();
|
|
4763
5241
|
let regionOf = [];
|
|
@@ -4765,6 +5243,12 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4765
5243
|
for (const spec of ROLE_TABLE) {
|
|
4766
5244
|
const layer = byRole.get(spec.role);
|
|
4767
5245
|
if (layer === void 0) continue;
|
|
5246
|
+
if (isArm(spec)) {
|
|
5247
|
+
parts.push(
|
|
5248
|
+
...armParts(spec.role, layer, arms.get(spec.role), parts.length)
|
|
5249
|
+
);
|
|
5250
|
+
continue;
|
|
5251
|
+
}
|
|
4768
5252
|
const built = buildPart(
|
|
4769
5253
|
spec,
|
|
4770
5254
|
boxOfLayer(layer),
|
|
@@ -4783,7 +5267,9 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4783
5267
|
}
|
|
4784
5268
|
const grids = /* @__PURE__ */ new Map();
|
|
4785
5269
|
for (const family of Object.keys(WARP_ID)) {
|
|
4786
|
-
const members = parts.filter(
|
|
5270
|
+
const members = parts.filter(
|
|
5271
|
+
(p) => roleSpec(roleOfPart(p.id)).family === family
|
|
5272
|
+
);
|
|
4787
5273
|
if (members.length === 0) continue;
|
|
4788
5274
|
const area = unionBoxes(
|
|
4789
5275
|
members.map((p) => {
|
|
@@ -4805,13 +5291,19 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4805
5291
|
});
|
|
4806
5292
|
}
|
|
4807
5293
|
for (const part of parts) {
|
|
4808
|
-
const family = roleSpec(part.id).family;
|
|
5294
|
+
const family = roleSpec(roleOfPart(part.id)).family;
|
|
4809
5295
|
if (isGridFamily(family)) {
|
|
4810
5296
|
part.deformer = WARP_ID[family];
|
|
4811
5297
|
continue;
|
|
4812
5298
|
}
|
|
4813
|
-
|
|
4814
|
-
|
|
5299
|
+
if (family === "body") {
|
|
5300
|
+
part.deformer = BODY_WARP_ID;
|
|
5301
|
+
part.mesh = bodyWarp.mesh;
|
|
5302
|
+
continue;
|
|
5303
|
+
}
|
|
5304
|
+
if (family === "arm") continue;
|
|
5305
|
+
part.deformer = "headDeformer";
|
|
5306
|
+
if (part.mesh === void 0) continue;
|
|
4815
5307
|
const own = familyField(turn, family);
|
|
4816
5308
|
const fieldOf = part.id === "face" ? plateFields(turn, regionOf, own) : () => own;
|
|
4817
5309
|
part.warps = [
|
|
@@ -4826,7 +5318,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4826
5318
|
canvas: { width: canvas.width, height: canvas.height },
|
|
4827
5319
|
parameters,
|
|
4828
5320
|
parts,
|
|
4829
|
-
deformers: deformers(frame, grids,
|
|
5321
|
+
deformers: deformers(frame, grids, bodyWarp?.deformer, arms),
|
|
4830
5322
|
...has("hair_front") ? { physics: hairPhysics() } : {},
|
|
4831
5323
|
expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
|
|
4832
5324
|
motions: DEFAULT_MOTIONS
|
|
@@ -4846,14 +5338,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4846
5338
|
function turnWarps(mesh, b, fieldOf) {
|
|
4847
5339
|
return [
|
|
4848
5340
|
localWarp(
|
|
4849
|
-
|
|
5341
|
+
P6.AngleX,
|
|
4850
5342
|
mesh,
|
|
4851
5343
|
b,
|
|
4852
5344
|
X_STOPS,
|
|
4853
5345
|
([x, y], v, i) => v === 0 ? [0, 0] : [fieldOf(i)(x, y, v, 0)[0], 0]
|
|
4854
5346
|
),
|
|
4855
5347
|
localWarp(
|
|
4856
|
-
|
|
5348
|
+
P6.AngleY,
|
|
4857
5349
|
mesh,
|
|
4858
5350
|
b,
|
|
4859
5351
|
Y_STOPS,
|
|
@@ -4901,16 +5393,22 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
4901
5393
|
}
|
|
4902
5394
|
if (mesh !== void 0) part.mesh = mesh;
|
|
4903
5395
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
4904
|
-
const eyeOpen = side === "L" ?
|
|
5396
|
+
const eyeOpen = side === "L" ? P6.EyeOpenLeft : P6.EyeOpenRight;
|
|
4905
5397
|
const hh = frame.hh;
|
|
4906
5398
|
switch (spec.role) {
|
|
4907
5399
|
case "face":
|
|
4908
5400
|
if (region?.some((r) => r === "neck")) {
|
|
4909
5401
|
const isNeck = region.map((r) => r === "neck");
|
|
4910
5402
|
warps.push(
|
|
4911
|
-
tiltHang(
|
|
5403
|
+
tiltHang(
|
|
5404
|
+
mesh,
|
|
5405
|
+
b,
|
|
5406
|
+
pivot,
|
|
5407
|
+
(_y, i) => isNeck[i] ? 1 : 0,
|
|
5408
|
+
headOwnRoll(byRole.has("body"))
|
|
5409
|
+
),
|
|
4912
5410
|
localWarp(
|
|
4913
|
-
|
|
5411
|
+
P6.Breath,
|
|
4914
5412
|
mesh,
|
|
4915
5413
|
b,
|
|
4916
5414
|
[0, 1],
|
|
@@ -4957,8 +5455,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
4957
5455
|
if (byRole.has("mouth_open")) {
|
|
4958
5456
|
warps.push(mouthWiden(mesh, b));
|
|
4959
5457
|
bindings.push(
|
|
4960
|
-
{ parameter:
|
|
4961
|
-
{ parameter:
|
|
5458
|
+
{ parameter: P6.MouthOpen, channel: "opacity", from: 1, to: 0 },
|
|
5459
|
+
{ parameter: P6.MouthOpen, channel: "opacity", from: 1, to: 0 }
|
|
4962
5460
|
);
|
|
4963
5461
|
extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
|
|
4964
5462
|
} else {
|
|
@@ -4968,19 +5466,19 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
4968
5466
|
);
|
|
4969
5467
|
bindings.push(
|
|
4970
5468
|
{
|
|
4971
|
-
parameter:
|
|
5469
|
+
parameter: P6.MouthOpen,
|
|
4972
5470
|
channel: "scaleY",
|
|
4973
5471
|
from: 0,
|
|
4974
5472
|
to: roundTo(grow, 0.01)
|
|
4975
5473
|
},
|
|
4976
5474
|
{
|
|
4977
|
-
parameter:
|
|
5475
|
+
parameter: P6.MouthOpen,
|
|
4978
5476
|
channel: "scaleX",
|
|
4979
5477
|
from: 0,
|
|
4980
5478
|
to: roundTo(AMPLITUDE.mouthOpenWidth - 1, 0.01)
|
|
4981
5479
|
},
|
|
4982
5480
|
{
|
|
4983
|
-
parameter:
|
|
5481
|
+
parameter: P6.MouthOpen,
|
|
4984
5482
|
channel: "translateY",
|
|
4985
5483
|
from: 0,
|
|
4986
5484
|
to: -roundTo(grow * bh(b) / 2, 0.01)
|
|
@@ -4992,7 +5490,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
4992
5490
|
case "mouth_open":
|
|
4993
5491
|
warps.push(mouthForm(mesh, b), mouthOpenGrow(mesh, b));
|
|
4994
5492
|
bindings.push({
|
|
4995
|
-
parameter:
|
|
5493
|
+
parameter: P6.MouthOpen,
|
|
4996
5494
|
channel: "opacity",
|
|
4997
5495
|
from: 0,
|
|
4998
5496
|
to: 1
|
|
@@ -5002,7 +5500,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
5002
5500
|
case "blush_L":
|
|
5003
5501
|
case "blush_R":
|
|
5004
5502
|
bindings.push({
|
|
5005
|
-
parameter:
|
|
5503
|
+
parameter: P6.Cheek,
|
|
5006
5504
|
channel: "opacity",
|
|
5007
5505
|
from: AMPLITUDE.blushRest,
|
|
5008
5506
|
to: 1
|
|
@@ -5013,8 +5511,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
|
|
|
5013
5511
|
if (byRole.has("hair_front")) {
|
|
5014
5512
|
const amp = style.sway * AMPLITUDE.sway * hh;
|
|
5015
5513
|
warps.push(
|
|
5016
|
-
hairSway(
|
|
5017
|
-
hairSway(
|
|
5514
|
+
hairSway(P6.HairSwayX, mesh, b, amp),
|
|
5515
|
+
hairSway(P6.HairSwayZ, mesh, b, amp)
|
|
5018
5516
|
);
|
|
5019
5517
|
}
|
|
5020
5518
|
warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
|
|
@@ -5087,33 +5585,24 @@ function meshScale(role, frame) {
|
|
|
5087
5585
|
return Infinity;
|
|
5088
5586
|
}
|
|
5089
5587
|
}
|
|
5090
|
-
function deformers(frame, grids,
|
|
5588
|
+
function deformers(frame, grids, bodyWarp, arms) {
|
|
5091
5589
|
const out = [];
|
|
5092
|
-
if (
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
bindings: [
|
|
5097
|
-
{
|
|
5098
|
-
parameter: P4.Breath,
|
|
5099
|
-
channel: "translateY",
|
|
5100
|
-
from: 0,
|
|
5101
|
-
to: roundTo(AMPLITUDE.breathBody * frame.hh, 0.01)
|
|
5102
|
-
}
|
|
5103
|
-
]
|
|
5104
|
-
});
|
|
5105
|
-
}
|
|
5590
|
+
if (bodyWarp !== void 0) out.push(bodyWarp);
|
|
5591
|
+
for (const [role, g] of arms) out.push(...armDeformers(role, g));
|
|
5592
|
+
const hasBody = bodyWarp !== void 0;
|
|
5593
|
+
const roll = headOwnRoll(hasBody);
|
|
5106
5594
|
out.push({
|
|
5107
5595
|
id: "headDeformer",
|
|
5596
|
+
...hasBody ? { parent: BODY_WARP_ID } : {},
|
|
5108
5597
|
pivot: { x: roundTo(frame.axisX, 0.01), y: roundTo(frame.chinY, 0.01) },
|
|
5109
5598
|
bindings: [
|
|
5110
5599
|
// AngleZ is clockwise-positive (Live2D's); a rotation is CCW-positive.
|
|
5111
|
-
{ parameter:
|
|
5600
|
+
{ parameter: P6.AngleZ, channel: "rotate", from: roll, to: -roll },
|
|
5112
5601
|
{
|
|
5113
|
-
parameter:
|
|
5602
|
+
parameter: P6.Breath,
|
|
5114
5603
|
channel: "translateY",
|
|
5115
5604
|
from: 0,
|
|
5116
|
-
to: roundTo(
|
|
5605
|
+
to: roundTo(headOwnBreath(hasBody) * frame.hh, 0.01)
|
|
5117
5606
|
}
|
|
5118
5607
|
]
|
|
5119
5608
|
});
|
|
@@ -5128,8 +5617,8 @@ function deformers(frame, grids, body) {
|
|
|
5128
5617
|
points: g.lattice.points
|
|
5129
5618
|
},
|
|
5130
5619
|
warp2d: {
|
|
5131
|
-
parameter:
|
|
5132
|
-
parameterY:
|
|
5620
|
+
parameter: P6.AngleX,
|
|
5621
|
+
parameterY: P6.AngleY,
|
|
5133
5622
|
valuesX: X_STOPS,
|
|
5134
5623
|
valuesY: Y_STOPS,
|
|
5135
5624
|
keyforms2d: g.keyforms.map((offsets) => ({ offsets }))
|
|
@@ -5142,16 +5631,16 @@ function hairPhysics() {
|
|
|
5142
5631
|
return [
|
|
5143
5632
|
{
|
|
5144
5633
|
id: "hairSway",
|
|
5145
|
-
input: { parameter:
|
|
5146
|
-
output: { parameter:
|
|
5634
|
+
input: { parameter: P6.AngleX, weight: 1 },
|
|
5635
|
+
output: { parameter: P6.HairSwayX, scale: 5 },
|
|
5147
5636
|
mass: 1,
|
|
5148
5637
|
stiffness: 30,
|
|
5149
5638
|
damping: 3
|
|
5150
5639
|
},
|
|
5151
5640
|
{
|
|
5152
5641
|
id: "hairTilt",
|
|
5153
|
-
input: { parameter:
|
|
5154
|
-
output: { parameter:
|
|
5642
|
+
input: { parameter: P6.AngleZ, weight: 1 },
|
|
5643
|
+
output: { parameter: P6.HairSwayZ, scale: 8 },
|
|
5155
5644
|
mass: 1,
|
|
5156
5645
|
stiffness: 25,
|
|
5157
5646
|
damping: 2.5
|
|
@@ -5161,32 +5650,46 @@ function hairPhysics() {
|
|
|
5161
5650
|
function declareParameters(roles) {
|
|
5162
5651
|
const out = [];
|
|
5163
5652
|
const add = (id, name, min, max, def) => out.push({ id, name, min, max, default: def });
|
|
5164
|
-
add(
|
|
5165
|
-
add(
|
|
5166
|
-
add(
|
|
5167
|
-
|
|
5168
|
-
|
|
5653
|
+
add(P6.AngleX, "Head Angle", -30, 30, 0);
|
|
5654
|
+
add(P6.AngleY, "Head Angle Y", -30, 30, 0);
|
|
5655
|
+
add(P6.AngleZ, "Head Angle Z", -30, 30, 0);
|
|
5656
|
+
if (roles.has("body")) {
|
|
5657
|
+
add(P6.BodyAngleX, "Body Angle X", -10, 10, 0);
|
|
5658
|
+
add(P6.BodyAngleY, "Body Angle Y", -10, 10, 0);
|
|
5659
|
+
add(P6.BodyAngleZ, "Body Angle Z", -10, 10, 0);
|
|
5660
|
+
}
|
|
5661
|
+
const [armMin, armMax] = ARM_RANGE;
|
|
5662
|
+
if (roles.has("arm_L")) {
|
|
5663
|
+
add(P6.ArmLeft, "Arm L", armMin, armMax, 0);
|
|
5664
|
+
add(P6.ElbowLeft, "Elbow L", armMin, armMax, 0);
|
|
5665
|
+
}
|
|
5666
|
+
if (roles.has("arm_R")) {
|
|
5667
|
+
add(P6.ArmRight, "Arm R", armMin, armMax, 0);
|
|
5668
|
+
add(P6.ElbowRight, "Elbow R", armMin, armMax, 0);
|
|
5669
|
+
}
|
|
5670
|
+
add(P6.EyeOpenLeft, "Eye L", 0, 1, 1);
|
|
5671
|
+
add(P6.EyeOpenRight, "Eye R", 0, 1, 1);
|
|
5169
5672
|
if ([...roles].some((r) => /^(iris|pupil|highlight)_/.test(r))) {
|
|
5170
|
-
add(
|
|
5171
|
-
add(
|
|
5673
|
+
add(P6.EyeballX, "Gaze X", -1, 1, 0);
|
|
5674
|
+
add(P6.EyeballY, "Gaze Y", -1, 1, 0);
|
|
5172
5675
|
}
|
|
5173
|
-
add(
|
|
5174
|
-
add(
|
|
5676
|
+
add(P6.MouthOpen, "Mouth Open", 0, 1, 0);
|
|
5677
|
+
add(P6.MouthForm, "Mouth Form", -1, 1, 0);
|
|
5175
5678
|
if (roles.has("brow_L")) {
|
|
5176
|
-
add(
|
|
5177
|
-
add(
|
|
5679
|
+
add(P6.BrowLeftY, "Brow L Y", -1, 1, 0);
|
|
5680
|
+
add(P6.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
|
|
5178
5681
|
}
|
|
5179
5682
|
if (roles.has("brow_R")) {
|
|
5180
|
-
add(
|
|
5181
|
-
add(
|
|
5683
|
+
add(P6.BrowRightY, "Brow R Y", -1, 1, 0);
|
|
5684
|
+
add(P6.BrowRightAngle, "Brow R Angle", -1, 1, 0);
|
|
5182
5685
|
}
|
|
5183
5686
|
if (roles.has("blush_L") || roles.has("blush_R")) {
|
|
5184
|
-
add(
|
|
5687
|
+
add(P6.Cheek, "Cheek", 0, 1, 0);
|
|
5185
5688
|
}
|
|
5186
|
-
add(
|
|
5689
|
+
add(P6.Breath, "Breath", 0, 1, 0);
|
|
5187
5690
|
if (roles.has("hair_front")) {
|
|
5188
|
-
add(
|
|
5189
|
-
add(
|
|
5691
|
+
add(P6.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
|
|
5692
|
+
add(P6.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
|
|
5190
5693
|
}
|
|
5191
5694
|
return out;
|
|
5192
5695
|
}
|
|
@@ -5204,6 +5707,7 @@ export {
|
|
|
5204
5707
|
DeletePhysicsRig,
|
|
5205
5708
|
EditorDocument,
|
|
5206
5709
|
HEAD_BAND,
|
|
5710
|
+
LayerGeometryError,
|
|
5207
5711
|
SPECK_CORE_FRACTION,
|
|
5208
5712
|
SetDeformerBindings,
|
|
5209
5713
|
SetDeformerParent,
|
|
@@ -5238,6 +5742,7 @@ export {
|
|
|
5238
5742
|
isSpeckCore,
|
|
5239
5743
|
packAtlas,
|
|
5240
5744
|
parseLayerRoles,
|
|
5745
|
+
partIdsOfRole,
|
|
5241
5746
|
upsertGridKeyform,
|
|
5242
5747
|
uvRectFor,
|
|
5243
5748
|
validateDeformerDelete,
|