@ikijs/editor 0.1.0 → 0.2.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 +3 -3
- package/dist/index.js +285 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +285 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -3
package/dist/index.mjs
CHANGED
|
@@ -1604,14 +1604,25 @@ import {
|
|
|
1604
1604
|
parseIkiModel as parseIkiModel3
|
|
1605
1605
|
} from "@ikijs/format";
|
|
1606
1606
|
var ROLE_TABLE = {
|
|
1607
|
-
//
|
|
1608
|
-
//
|
|
1609
|
-
|
|
1607
|
+
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1608
|
+
// whose grid it would stretch across the shoulders) as a MESH: it bends on
|
|
1609
|
+
// the turn through its own part warp (bakeHairBackTurnWarp) and swings its
|
|
1610
|
+
// ends on the hair-sway warps.
|
|
1611
|
+
hair_back: { deformer: "headDeformer", order: 0, mesh: true },
|
|
1612
|
+
// The torso. Alone among the roles it hangs from NO deformer: the head turns
|
|
1613
|
+
// about the neck pivot while the shoulders stay put, which is what keeps the
|
|
1614
|
+
// character from reading as a floating head. Drawn over the back hair so long
|
|
1615
|
+
// hair falls behind the shoulders.
|
|
1616
|
+
body: { deformer: "none", order: 5, mesh: false },
|
|
1610
1617
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1611
1618
|
nose: { deformer: "faceWarp", order: 15, mesh: true },
|
|
1612
1619
|
blush_L: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1613
1620
|
blush_R: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1614
1621
|
mouth: { deformer: "faceWarp", order: 25, mesh: true },
|
|
1622
|
+
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1623
|
+
// MouthOpen instead of the closed one being stretched, which is the
|
|
1624
|
+
// difference between a portrait rig and one that can lip-sync.
|
|
1625
|
+
mouth_open: { deformer: "faceWarp", order: 26, mesh: true },
|
|
1615
1626
|
eye_L: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "L" },
|
|
1616
1627
|
eye_R: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "R" },
|
|
1617
1628
|
iris_L: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "L" },
|
|
@@ -1763,29 +1774,63 @@ function createPixelGridMesh(cols, rows, w, h) {
|
|
|
1763
1774
|
}
|
|
1764
1775
|
return { vertices, uvs, indices };
|
|
1765
1776
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1777
|
+
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1778
|
+
var HEAD_TURN_MAX_DEG = 30;
|
|
1779
|
+
function headTurnParallaxUnit(gridHalfWidth) {
|
|
1780
|
+
return gridHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1781
|
+
}
|
|
1782
|
+
function pinnedCylinderBend(local, radius, theta) {
|
|
1783
|
+
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1784
|
+
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
1785
|
+
}
|
|
1786
|
+
function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, centerY) {
|
|
1787
|
+
const STOPS = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG];
|
|
1768
1788
|
const halfWidth = (grid.points[grid.cols * 2] - grid.points[0]) / 2;
|
|
1769
|
-
const
|
|
1789
|
+
const RADIUS_X = halfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1770
1790
|
const pointCount = grid.points.length / 2;
|
|
1791
|
+
let halfHeight = 0;
|
|
1792
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1793
|
+
halfHeight = Math.max(
|
|
1794
|
+
halfHeight,
|
|
1795
|
+
Math.abs(grid.points[i * 2 + 1] - centerY)
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
const RADIUS_Y = halfHeight * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1771
1799
|
const DEG_TO_RAD = Math.PI / 180;
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
const
|
|
1775
|
-
for (
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1800
|
+
const keyforms2d = [];
|
|
1801
|
+
for (const angleY of STOPS) {
|
|
1802
|
+
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1803
|
+
for (const angleX of STOPS) {
|
|
1804
|
+
const thetaX = angleX * DEG_TO_RAD;
|
|
1805
|
+
const offsets = [];
|
|
1806
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1807
|
+
offsets.push(
|
|
1808
|
+
pinnedCylinderBend(grid.points[i * 2] - centerX, RADIUS_X, thetaX),
|
|
1809
|
+
pinnedCylinderBend(
|
|
1810
|
+
grid.points[i * 2 + 1] - centerY,
|
|
1811
|
+
RADIUS_Y,
|
|
1812
|
+
thetaY
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
keyforms2d.push({ offsets });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return {
|
|
1820
|
+
parameter: parameterX,
|
|
1821
|
+
parameterY,
|
|
1822
|
+
valuesX: STOPS,
|
|
1823
|
+
valuesY: STOPS,
|
|
1824
|
+
keyforms2d
|
|
1825
|
+
};
|
|
1786
1826
|
}
|
|
1787
1827
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
1788
|
-
|
|
1828
|
+
var HAIR_FRONT_DEPTH = 0.1;
|
|
1829
|
+
var HAIR_BACK_DEPTH = -0.08;
|
|
1830
|
+
var NOD_TRAVEL = 30;
|
|
1831
|
+
var NOD_BEND = 0.5;
|
|
1832
|
+
var HAIR_BACK_NOD_DEPTH = -0.07;
|
|
1833
|
+
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
1789
1834
|
const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));
|
|
1790
1835
|
if (isEyeStack && spec.eyeSide !== void 0) {
|
|
1791
1836
|
const isGazeRole = role.startsWith("iris_") || role.startsWith("pupil_") || role.startsWith("highlight_");
|
|
@@ -1807,22 +1852,47 @@ function bindingsForRole(spec, role, cropW, cropH) {
|
|
|
1807
1852
|
}
|
|
1808
1853
|
];
|
|
1809
1854
|
}
|
|
1855
|
+
const mouthForm = {
|
|
1856
|
+
// Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
|
|
1857
|
+
parameter: StandardParameter.MouthForm,
|
|
1858
|
+
channel: "scaleX",
|
|
1859
|
+
from: -0.2,
|
|
1860
|
+
to: 0.4
|
|
1861
|
+
};
|
|
1810
1862
|
if (role === "mouth") {
|
|
1863
|
+
if (options.hasMouthOpen) {
|
|
1864
|
+
return [
|
|
1865
|
+
{
|
|
1866
|
+
parameter: StandardParameter.MouthOpen,
|
|
1867
|
+
channel: "opacity",
|
|
1868
|
+
from: 1,
|
|
1869
|
+
to: 0
|
|
1870
|
+
},
|
|
1871
|
+
mouthForm
|
|
1872
|
+
];
|
|
1873
|
+
}
|
|
1811
1874
|
return [
|
|
1812
1875
|
// Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).
|
|
1876
|
+
// Only a fallback — it distorts, but it is better than a mouth that
|
|
1877
|
+
// cannot open at all when the layer set has no open drawing.
|
|
1813
1878
|
{
|
|
1814
1879
|
parameter: StandardParameter.MouthOpen,
|
|
1815
1880
|
channel: "scaleY",
|
|
1816
1881
|
from: 0,
|
|
1817
1882
|
to: 3
|
|
1818
1883
|
},
|
|
1819
|
-
|
|
1884
|
+
mouthForm
|
|
1885
|
+
];
|
|
1886
|
+
}
|
|
1887
|
+
if (role === "mouth_open") {
|
|
1888
|
+
return [
|
|
1820
1889
|
{
|
|
1821
|
-
parameter: StandardParameter.
|
|
1822
|
-
channel: "
|
|
1823
|
-
from:
|
|
1824
|
-
to:
|
|
1825
|
-
}
|
|
1890
|
+
parameter: StandardParameter.MouthOpen,
|
|
1891
|
+
channel: "opacity",
|
|
1892
|
+
from: 0,
|
|
1893
|
+
to: 1
|
|
1894
|
+
},
|
|
1895
|
+
mouthForm
|
|
1826
1896
|
];
|
|
1827
1897
|
}
|
|
1828
1898
|
if (role === "brow_L" || role === "brow_R") {
|
|
@@ -1860,21 +1930,29 @@ function bindingsForRole(spec, role, cropW, cropH) {
|
|
|
1860
1930
|
];
|
|
1861
1931
|
}
|
|
1862
1932
|
}
|
|
1863
|
-
if (role === "hair_front") {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
{
|
|
1872
|
-
parameter: StandardParameter.HairSwayX,
|
|
1933
|
+
if (role === "hair_front" || role === "hair_back") {
|
|
1934
|
+
const isFront = role === "hair_front";
|
|
1935
|
+
const depth = isFront ? HAIR_FRONT_DEPTH : HAIR_BACK_DEPTH;
|
|
1936
|
+
const parallax = [];
|
|
1937
|
+
const shiftX = depth * (options.parallaxUnit ?? 0);
|
|
1938
|
+
if (shiftX !== 0) {
|
|
1939
|
+
parallax.push({
|
|
1940
|
+
parameter: StandardParameter.AngleX,
|
|
1873
1941
|
channel: "translateX",
|
|
1874
|
-
from: -
|
|
1875
|
-
to:
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1942
|
+
from: -shiftX,
|
|
1943
|
+
to: shiftX
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
const shiftY = (isFront ? 0 : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
|
|
1947
|
+
if (shiftY !== 0) {
|
|
1948
|
+
parallax.push({
|
|
1949
|
+
parameter: StandardParameter.AngleY,
|
|
1950
|
+
channel: "translateY",
|
|
1951
|
+
from: -shiftY,
|
|
1952
|
+
to: shiftY
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
return parallax;
|
|
1878
1956
|
}
|
|
1879
1957
|
return [];
|
|
1880
1958
|
}
|
|
@@ -1897,9 +1975,61 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
1897
1975
|
]
|
|
1898
1976
|
};
|
|
1899
1977
|
}
|
|
1978
|
+
var HAIR_BACK_BEND_RADIUS_FACTOR = 2.5;
|
|
1979
|
+
var HAIR_BACK_FAR_BULGE = 0.22;
|
|
1980
|
+
function bakeHairBackTurnWarp(mesh, parameter) {
|
|
1981
|
+
let left = Infinity;
|
|
1982
|
+
let right = -Infinity;
|
|
1983
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
1984
|
+
left = Math.min(left, mesh.vertices[i]);
|
|
1985
|
+
right = Math.max(right, mesh.vertices[i]);
|
|
1986
|
+
}
|
|
1987
|
+
const halfWidth = (right - left) / 2;
|
|
1988
|
+
const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;
|
|
1989
|
+
const bulge = HAIR_BACK_FAR_BULGE * halfWidth;
|
|
1990
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1991
|
+
const keyforms = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG].map((deg) => {
|
|
1992
|
+
const theta = deg * DEG_TO_RAD;
|
|
1993
|
+
const s = Math.sign(deg);
|
|
1994
|
+
const offsets = [];
|
|
1995
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
1996
|
+
const x = mesh.vertices[i];
|
|
1997
|
+
const far = Math.max(0, -s * x / halfWidth);
|
|
1998
|
+
offsets.push(pinnedCylinderBend(x, radius, theta) - s * bulge * far, 0);
|
|
1999
|
+
}
|
|
2000
|
+
return { value: deg, offsets };
|
|
2001
|
+
});
|
|
2002
|
+
return { parameter, keyforms };
|
|
2003
|
+
}
|
|
2004
|
+
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2005
|
+
var HAIR_SWAY_CURL = 1.5;
|
|
2006
|
+
var HAIR_SWAY_RANGE = 20;
|
|
2007
|
+
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2008
|
+
let top = -Infinity;
|
|
2009
|
+
let bottom = Infinity;
|
|
2010
|
+
for (let i = 1; i < mesh.vertices.length; i += 2) {
|
|
2011
|
+
top = Math.max(top, mesh.vertices[i]);
|
|
2012
|
+
bottom = Math.min(bottom, mesh.vertices[i]);
|
|
2013
|
+
}
|
|
2014
|
+
const height = top - bottom;
|
|
2015
|
+
const swing = [];
|
|
2016
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2017
|
+
const u = height > 0 ? (top - mesh.vertices[i + 1]) / height : 0;
|
|
2018
|
+
swing.push(tipShift * Math.pow(u, HAIR_SWAY_CURL), 0);
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
parameter,
|
|
2022
|
+
keyforms: [
|
|
2023
|
+
// `0 - v`, not `-v`: the pinned root row must stay a plain +0.
|
|
2024
|
+
{ value: -range, offsets: swing.map((v) => 0 - v) },
|
|
2025
|
+
{ value: range, offsets: swing }
|
|
2026
|
+
]
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
1900
2029
|
function generateIkiFromLayerSet(layers, canvas) {
|
|
1901
2030
|
validateLayerInputs(layers, canvas);
|
|
1902
2031
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2032
|
+
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
1903
2033
|
const parameters = [
|
|
1904
2034
|
{
|
|
1905
2035
|
id: StandardParameter.MouthOpen,
|
|
@@ -1950,6 +2080,20 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
1950
2080
|
max: 30,
|
|
1951
2081
|
default: 0
|
|
1952
2082
|
},
|
|
2083
|
+
{
|
|
2084
|
+
id: StandardParameter.AngleY,
|
|
2085
|
+
name: "Head Angle Y",
|
|
2086
|
+
min: -30,
|
|
2087
|
+
max: 30,
|
|
2088
|
+
default: 0
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
id: StandardParameter.AngleZ,
|
|
2092
|
+
name: "Head Angle Z",
|
|
2093
|
+
min: -30,
|
|
2094
|
+
max: 30,
|
|
2095
|
+
default: 0
|
|
2096
|
+
},
|
|
1953
2097
|
{
|
|
1954
2098
|
id: StandardParameter.Breath,
|
|
1955
2099
|
name: "Breath",
|
|
@@ -1987,13 +2131,22 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
1987
2131
|
}
|
|
1988
2132
|
];
|
|
1989
2133
|
if (hasHair) {
|
|
1990
|
-
parameters.push(
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2134
|
+
parameters.push(
|
|
2135
|
+
{
|
|
2136
|
+
id: StandardParameter.HairSwayX,
|
|
2137
|
+
name: "Hair Sway X",
|
|
2138
|
+
min: -HAIR_SWAY_RANGE,
|
|
2139
|
+
max: HAIR_SWAY_RANGE,
|
|
2140
|
+
default: 0
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
id: StandardParameter.HairSwayZ,
|
|
2144
|
+
name: "Hair Sway Z",
|
|
2145
|
+
min: -HAIR_SWAY_RANGE,
|
|
2146
|
+
max: HAIR_SWAY_RANGE,
|
|
2147
|
+
default: 0
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
1997
2150
|
}
|
|
1998
2151
|
const faceLayers = layers.filter((l) => l.role === "face");
|
|
1999
2152
|
const faceLayer = faceLayers[0];
|
|
@@ -2051,16 +2204,22 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2051
2204
|
unionMaxY
|
|
2052
2205
|
)
|
|
2053
2206
|
};
|
|
2207
|
+
const faceCenterY = faceTransform.y;
|
|
2208
|
+
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
2209
|
+
const parallaxUnit = headTurnParallaxUnit(halfW);
|
|
2210
|
+
const parallaxUnitY = headTurnParallaxUnit(halfH);
|
|
2054
2211
|
const faceBottom = faceTransform.y - faceCropH / 2;
|
|
2055
2212
|
const neckPivot = {
|
|
2056
2213
|
x: faceCenterX,
|
|
2057
2214
|
y: faceBottom - faceCropH * 0.15
|
|
2058
2215
|
// 15% below face bottom = neck
|
|
2059
2216
|
};
|
|
2060
|
-
const
|
|
2217
|
+
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
2061
2218
|
faceGrid,
|
|
2062
2219
|
StandardParameter.AngleX,
|
|
2063
|
-
|
|
2220
|
+
StandardParameter.AngleY,
|
|
2221
|
+
faceCenterX,
|
|
2222
|
+
faceCenterY
|
|
2064
2223
|
);
|
|
2065
2224
|
const deformers = [
|
|
2066
2225
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
@@ -2081,6 +2240,27 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2081
2240
|
from: -50,
|
|
2082
2241
|
to: 50
|
|
2083
2242
|
},
|
|
2243
|
+
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
2244
|
+
// rigid rotation would sum with the roll below at diagonal poses,
|
|
2245
|
+
// collapsing pitch into roll.
|
|
2246
|
+
{
|
|
2247
|
+
parameter: StandardParameter.AngleY,
|
|
2248
|
+
channel: "translateY",
|
|
2249
|
+
from: -NOD_TRAVEL,
|
|
2250
|
+
to: NOD_TRAVEL
|
|
2251
|
+
},
|
|
2252
|
+
// Tilt: the whole head rolls about the neck pivot, one degree per
|
|
2253
|
+
// degree. Positive AngleZ is clockwise on screen (engine rotate is
|
|
2254
|
+
// CCW-positive, hence the flipped from/to) — Live2D's convention, and
|
|
2255
|
+
// the same sense as the lean above: in Live2D's own sample motions
|
|
2256
|
+
// AngleZ carries the sign of AngleX 214 times out of 222. The two
|
|
2257
|
+
// rotations sum, which is correct for two rolls about one pivot.
|
|
2258
|
+
{
|
|
2259
|
+
parameter: StandardParameter.AngleZ,
|
|
2260
|
+
channel: "rotate",
|
|
2261
|
+
from: 30,
|
|
2262
|
+
to: -30
|
|
2263
|
+
},
|
|
2084
2264
|
{
|
|
2085
2265
|
parameter: StandardParameter.Breath,
|
|
2086
2266
|
channel: "translateY",
|
|
@@ -2091,12 +2271,14 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2091
2271
|
},
|
|
2092
2272
|
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
2093
2273
|
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
2274
|
+
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
2275
|
+
// `warps` or `warp2d`, never both).
|
|
2094
2276
|
{
|
|
2095
2277
|
kind: "warp",
|
|
2096
2278
|
id: "faceWarp",
|
|
2097
2279
|
parent: "headDeformer",
|
|
2098
2280
|
grid: faceGrid,
|
|
2099
|
-
|
|
2281
|
+
warp2d: faceWarp2d
|
|
2100
2282
|
}
|
|
2101
2283
|
];
|
|
2102
2284
|
const eyeCreaseBySide = {};
|
|
@@ -2116,7 +2298,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2116
2298
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
2117
2299
|
const spec = ROLE_TABLE[role];
|
|
2118
2300
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
2119
|
-
const roleBindings = bindingsForRole(spec, role, cropW, cropH
|
|
2301
|
+
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
2302
|
+
hasMouthOpen,
|
|
2303
|
+
parallaxUnit,
|
|
2304
|
+
parallaxUnitY
|
|
2305
|
+
});
|
|
2306
|
+
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
2120
2307
|
if (spec.mesh) {
|
|
2121
2308
|
const mesh = createPixelGridMesh(4, 4, cropW, cropH);
|
|
2122
2309
|
const part = {
|
|
@@ -2126,23 +2313,47 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2126
2313
|
height: 1,
|
|
2127
2314
|
order: spec.order,
|
|
2128
2315
|
transform: t,
|
|
2129
|
-
deformer:
|
|
2316
|
+
deformer: deformerId,
|
|
2130
2317
|
mesh
|
|
2131
2318
|
};
|
|
2132
2319
|
if (roleBindings.length > 0) {
|
|
2133
2320
|
part.bindings = roleBindings;
|
|
2134
2321
|
}
|
|
2322
|
+
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
2323
|
+
const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
2324
|
+
part.warps = [
|
|
2325
|
+
bakeHairSwayWarp(
|
|
2326
|
+
mesh,
|
|
2327
|
+
StandardParameter.HairSwayX,
|
|
2328
|
+
tipShift,
|
|
2329
|
+
HAIR_SWAY_RANGE
|
|
2330
|
+
),
|
|
2331
|
+
bakeHairSwayWarp(
|
|
2332
|
+
mesh,
|
|
2333
|
+
StandardParameter.HairSwayZ,
|
|
2334
|
+
tipShift,
|
|
2335
|
+
HAIR_SWAY_RANGE
|
|
2336
|
+
)
|
|
2337
|
+
];
|
|
2338
|
+
}
|
|
2339
|
+
if (role === "hair_back") {
|
|
2340
|
+
part.warps = [
|
|
2341
|
+
...part.warps ?? [],
|
|
2342
|
+
bakeHairBackTurnWarp(mesh, StandardParameter.AngleX)
|
|
2343
|
+
];
|
|
2344
|
+
}
|
|
2135
2345
|
if (spec.eyeSide !== void 0) {
|
|
2136
2346
|
const isLash = role.startsWith("lash_");
|
|
2137
2347
|
if (role.startsWith("eye_") || isLash) {
|
|
2138
2348
|
const openParam = spec.eyeSide === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight;
|
|
2139
2349
|
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
2350
|
+
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
2140
2351
|
part.warps = [
|
|
2141
2352
|
bakeEyelidFoldWarp(
|
|
2142
2353
|
mesh,
|
|
2143
2354
|
openParam,
|
|
2144
2355
|
creaseWorldY - t.y,
|
|
2145
|
-
isLash ? LASH_FOLD_K : EYELID_FOLD_K
|
|
2356
|
+
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
2146
2357
|
)
|
|
2147
2358
|
];
|
|
2148
2359
|
} else {
|
|
@@ -2151,15 +2362,19 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2151
2362
|
}
|
|
2152
2363
|
return part;
|
|
2153
2364
|
} else {
|
|
2154
|
-
|
|
2365
|
+
const part = {
|
|
2155
2366
|
id: role,
|
|
2156
2367
|
color: [1, 1, 1, 1],
|
|
2157
2368
|
width: cropW,
|
|
2158
2369
|
height: cropH,
|
|
2159
2370
|
order: spec.order,
|
|
2160
2371
|
transform: t,
|
|
2161
|
-
deformer:
|
|
2372
|
+
deformer: deformerId
|
|
2162
2373
|
};
|
|
2374
|
+
if (roleBindings.length > 0) {
|
|
2375
|
+
part.bindings = roleBindings;
|
|
2376
|
+
}
|
|
2377
|
+
return part;
|
|
2163
2378
|
}
|
|
2164
2379
|
});
|
|
2165
2380
|
const model = {
|
|
@@ -2170,9 +2385,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2170
2385
|
parameters,
|
|
2171
2386
|
deformers,
|
|
2172
2387
|
parts,
|
|
2173
|
-
// Secondary motion:
|
|
2174
|
-
// behind the head turn (same constants as the hand-authored sample)
|
|
2175
|
-
//
|
|
2388
|
+
// Secondary motion: one spring lags AngleX onto HairSwayX so front hair
|
|
2389
|
+
// sways behind the head turn (same constants as the hand-authored sample),
|
|
2390
|
+
// and a second lags AngleZ onto HairSwayZ so it swings behind a tilt. Two
|
|
2391
|
+
// rigs because a rig has one input and one output (validator-enforced).
|
|
2392
|
+
// Both hair parts read the outputs through root-pinned sway warps.
|
|
2393
|
+
// Omitted when there is no front hair to drive.
|
|
2176
2394
|
physics: hasHair ? [
|
|
2177
2395
|
{
|
|
2178
2396
|
id: "hairSway",
|
|
@@ -2181,6 +2399,14 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2181
2399
|
mass: 1,
|
|
2182
2400
|
stiffness: 80,
|
|
2183
2401
|
damping: 10
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
id: "hairTilt",
|
|
2405
|
+
input: { parameter: StandardParameter.AngleZ, weight: 1 },
|
|
2406
|
+
output: { parameter: StandardParameter.HairSwayZ, scale: -10 },
|
|
2407
|
+
mass: 1,
|
|
2408
|
+
stiffness: 80,
|
|
2409
|
+
damping: 10
|
|
2184
2410
|
}
|
|
2185
2411
|
] : void 0
|
|
2186
2412
|
};
|