@ikijs/editor 0.1.0 → 0.2.1
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 +297 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +297 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @ikijs/editor
|
|
2
2
|
|
|
3
|
-
**Headless** editing core for [`.iki`](
|
|
3
|
+
**Headless** editing core for [`.iki`](https://github.com/zeikar/iki/tree/main/packages/format) models — a document with
|
|
4
4
|
undo/redo, invertible edit commands, atlas layout + UV math, and the auto-rigger.
|
|
5
5
|
|
|
6
6
|
This package ships no UI. It is the model and command layer an editor is built
|
|
7
7
|
_on_, not an editor you can mount.
|
|
8
8
|
|
|
9
9
|
Everything here is pure logic — no DOM, no canvas, no WebGL. It depends only on
|
|
10
|
-
[`@ikijs/format`](
|
|
10
|
+
[`@ikijs/format`](https://github.com/zeikar/iki/tree/main/packages/format), so the same core backs this repo's
|
|
11
11
|
browser editor app (`examples/editor`), the Node MCP server
|
|
12
|
-
([`@ikijs/mcp`](
|
|
12
|
+
([`@ikijs/mcp`](https://github.com/zeikar/iki/tree/main/packages/mcp)), and tests.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
package/dist/index.js
CHANGED
|
@@ -1663,14 +1663,25 @@ function createDefaultWarpDeformer(model) {
|
|
|
1663
1663
|
// src/auto-rig.ts
|
|
1664
1664
|
var import_format3 = require("@ikijs/format");
|
|
1665
1665
|
var ROLE_TABLE = {
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1668
|
-
|
|
1666
|
+
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1667
|
+
// whose grid it would stretch across the shoulders) as a MESH: it bends on
|
|
1668
|
+
// the turn through its own part warp (bakeHairBackTurnWarp) and swings its
|
|
1669
|
+
// ends on the hair-sway warps.
|
|
1670
|
+
hair_back: { deformer: "headDeformer", order: 0, mesh: true },
|
|
1671
|
+
// The torso. Alone among the roles it hangs from NO deformer: the head turns
|
|
1672
|
+
// about the neck pivot while the shoulders stay put, which is what keeps the
|
|
1673
|
+
// character from reading as a floating head. Drawn over the back hair so long
|
|
1674
|
+
// hair falls behind the shoulders.
|
|
1675
|
+
body: { deformer: "none", order: 5, mesh: false },
|
|
1669
1676
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1670
1677
|
nose: { deformer: "faceWarp", order: 15, mesh: true },
|
|
1671
1678
|
blush_L: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1672
1679
|
blush_R: { deformer: "faceWarp", order: 20, mesh: true },
|
|
1673
1680
|
mouth: { deformer: "faceWarp", order: 25, mesh: true },
|
|
1681
|
+
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1682
|
+
// MouthOpen instead of the closed one being stretched, which is the
|
|
1683
|
+
// difference between a portrait rig and one that can lip-sync.
|
|
1684
|
+
mouth_open: { deformer: "faceWarp", order: 26, mesh: true },
|
|
1674
1685
|
eye_L: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "L" },
|
|
1675
1686
|
eye_R: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "R" },
|
|
1676
1687
|
iris_L: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "L" },
|
|
@@ -1822,29 +1833,62 @@ function createPixelGridMesh(cols, rows, w, h) {
|
|
|
1822
1833
|
}
|
|
1823
1834
|
return { vertices, uvs, indices };
|
|
1824
1835
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1836
|
+
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1837
|
+
var HEAD_TURN_MAX_DEG = 30;
|
|
1838
|
+
function headTurnParallaxUnit(gridHalfWidth) {
|
|
1839
|
+
return gridHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1840
|
+
}
|
|
1841
|
+
function gridReach(grid, axis, center) {
|
|
1842
|
+
let reach = 0;
|
|
1843
|
+
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1844
|
+
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1845
|
+
}
|
|
1846
|
+
return reach;
|
|
1847
|
+
}
|
|
1848
|
+
function pinnedCylinderBend(local, radius, theta) {
|
|
1849
|
+
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1850
|
+
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
1851
|
+
}
|
|
1852
|
+
function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, centerY) {
|
|
1853
|
+
const STOPS = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG];
|
|
1854
|
+
const RADIUS_X = gridReach(grid, 0, centerX) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1855
|
+
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1829
1856
|
const pointCount = grid.points.length / 2;
|
|
1830
1857
|
const DEG_TO_RAD = Math.PI / 180;
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
const
|
|
1834
|
-
for (
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1858
|
+
const keyforms2d = [];
|
|
1859
|
+
for (const angleY of STOPS) {
|
|
1860
|
+
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1861
|
+
for (const angleX of STOPS) {
|
|
1862
|
+
const thetaX = angleX * DEG_TO_RAD;
|
|
1863
|
+
const offsets = [];
|
|
1864
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1865
|
+
offsets.push(
|
|
1866
|
+
pinnedCylinderBend(grid.points[i * 2] - centerX, RADIUS_X, thetaX),
|
|
1867
|
+
pinnedCylinderBend(
|
|
1868
|
+
grid.points[i * 2 + 1] - centerY,
|
|
1869
|
+
RADIUS_Y,
|
|
1870
|
+
thetaY
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
keyforms2d.push({ offsets });
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
parameter: parameterX,
|
|
1879
|
+
parameterY,
|
|
1880
|
+
valuesX: STOPS,
|
|
1881
|
+
valuesY: STOPS,
|
|
1882
|
+
keyforms2d
|
|
1883
|
+
};
|
|
1845
1884
|
}
|
|
1846
1885
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
1847
|
-
|
|
1886
|
+
var HAIR_FRONT_DEPTH = 0.1;
|
|
1887
|
+
var HAIR_BACK_DEPTH = -0.08;
|
|
1888
|
+
var NOD_TRAVEL = 30;
|
|
1889
|
+
var NOD_BEND = 0.5;
|
|
1890
|
+
var HAIR_BACK_NOD_DEPTH = -0.07;
|
|
1891
|
+
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
1848
1892
|
const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));
|
|
1849
1893
|
if (isEyeStack && spec.eyeSide !== void 0) {
|
|
1850
1894
|
const isGazeRole = role.startsWith("iris_") || role.startsWith("pupil_") || role.startsWith("highlight_");
|
|
@@ -1866,22 +1910,47 @@ function bindingsForRole(spec, role, cropW, cropH) {
|
|
|
1866
1910
|
}
|
|
1867
1911
|
];
|
|
1868
1912
|
}
|
|
1913
|
+
const mouthForm = {
|
|
1914
|
+
// Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
|
|
1915
|
+
parameter: import_format3.StandardParameter.MouthForm,
|
|
1916
|
+
channel: "scaleX",
|
|
1917
|
+
from: -0.2,
|
|
1918
|
+
to: 0.4
|
|
1919
|
+
};
|
|
1869
1920
|
if (role === "mouth") {
|
|
1921
|
+
if (options.hasMouthOpen) {
|
|
1922
|
+
return [
|
|
1923
|
+
{
|
|
1924
|
+
parameter: import_format3.StandardParameter.MouthOpen,
|
|
1925
|
+
channel: "opacity",
|
|
1926
|
+
from: 1,
|
|
1927
|
+
to: 0
|
|
1928
|
+
},
|
|
1929
|
+
mouthForm
|
|
1930
|
+
];
|
|
1931
|
+
}
|
|
1870
1932
|
return [
|
|
1871
1933
|
// Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).
|
|
1934
|
+
// Only a fallback — it distorts, but it is better than a mouth that
|
|
1935
|
+
// cannot open at all when the layer set has no open drawing.
|
|
1872
1936
|
{
|
|
1873
1937
|
parameter: import_format3.StandardParameter.MouthOpen,
|
|
1874
1938
|
channel: "scaleY",
|
|
1875
1939
|
from: 0,
|
|
1876
1940
|
to: 3
|
|
1877
1941
|
},
|
|
1878
|
-
|
|
1942
|
+
mouthForm
|
|
1943
|
+
];
|
|
1944
|
+
}
|
|
1945
|
+
if (role === "mouth_open") {
|
|
1946
|
+
return [
|
|
1879
1947
|
{
|
|
1880
|
-
parameter: import_format3.StandardParameter.
|
|
1881
|
-
channel: "
|
|
1882
|
-
from:
|
|
1883
|
-
to:
|
|
1884
|
-
}
|
|
1948
|
+
parameter: import_format3.StandardParameter.MouthOpen,
|
|
1949
|
+
channel: "opacity",
|
|
1950
|
+
from: 0,
|
|
1951
|
+
to: 1
|
|
1952
|
+
},
|
|
1953
|
+
mouthForm
|
|
1885
1954
|
];
|
|
1886
1955
|
}
|
|
1887
1956
|
if (role === "brow_L" || role === "brow_R") {
|
|
@@ -1919,21 +1988,29 @@ function bindingsForRole(spec, role, cropW, cropH) {
|
|
|
1919
1988
|
];
|
|
1920
1989
|
}
|
|
1921
1990
|
}
|
|
1922
|
-
if (role === "hair_front") {
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
{
|
|
1931
|
-
parameter: import_format3.StandardParameter.HairSwayX,
|
|
1991
|
+
if (role === "hair_front" || role === "hair_back") {
|
|
1992
|
+
const isFront = role === "hair_front";
|
|
1993
|
+
const depth = isFront ? HAIR_FRONT_DEPTH : HAIR_BACK_DEPTH;
|
|
1994
|
+
const parallax = [];
|
|
1995
|
+
const shiftX = depth * (options.parallaxUnit ?? 0);
|
|
1996
|
+
if (shiftX !== 0) {
|
|
1997
|
+
parallax.push({
|
|
1998
|
+
parameter: import_format3.StandardParameter.AngleX,
|
|
1932
1999
|
channel: "translateX",
|
|
1933
|
-
from: -
|
|
1934
|
-
to:
|
|
1935
|
-
}
|
|
1936
|
-
|
|
2000
|
+
from: -shiftX,
|
|
2001
|
+
to: shiftX
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
const shiftY = (isFront ? 0 : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
|
|
2005
|
+
if (shiftY !== 0) {
|
|
2006
|
+
parallax.push({
|
|
2007
|
+
parameter: import_format3.StandardParameter.AngleY,
|
|
2008
|
+
channel: "translateY",
|
|
2009
|
+
from: -shiftY,
|
|
2010
|
+
to: shiftY
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
return parallax;
|
|
1937
2014
|
}
|
|
1938
2015
|
return [];
|
|
1939
2016
|
}
|
|
@@ -1956,9 +2033,62 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
1956
2033
|
]
|
|
1957
2034
|
};
|
|
1958
2035
|
}
|
|
2036
|
+
var HAIR_BACK_BEND_RADIUS_FACTOR = 2.5;
|
|
2037
|
+
var HAIR_BACK_FAR_BULGE = 0.22;
|
|
2038
|
+
function bakeHairBackTurnWarp(mesh, parameter) {
|
|
2039
|
+
let left = Infinity;
|
|
2040
|
+
let right = -Infinity;
|
|
2041
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2042
|
+
left = Math.min(left, mesh.vertices[i]);
|
|
2043
|
+
right = Math.max(right, mesh.vertices[i]);
|
|
2044
|
+
}
|
|
2045
|
+
const halfWidth = (right - left) / 2;
|
|
2046
|
+
const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;
|
|
2047
|
+
const bulge = HAIR_BACK_FAR_BULGE * halfWidth;
|
|
2048
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
2049
|
+
const keyforms = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG].map((deg) => {
|
|
2050
|
+
const theta = deg * DEG_TO_RAD;
|
|
2051
|
+
const s = Math.sign(deg);
|
|
2052
|
+
const offsets = [];
|
|
2053
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2054
|
+
const x = mesh.vertices[i];
|
|
2055
|
+
const far = Math.max(0, -s * x / halfWidth);
|
|
2056
|
+
offsets.push(pinnedCylinderBend(x, radius, theta) - s * bulge * far, 0);
|
|
2057
|
+
}
|
|
2058
|
+
return { value: deg, offsets };
|
|
2059
|
+
});
|
|
2060
|
+
return { parameter, keyforms };
|
|
2061
|
+
}
|
|
2062
|
+
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2063
|
+
var HAIR_SWAY_CURL = 1.5;
|
|
2064
|
+
var HAIR_SWAY_RANGE = 20;
|
|
2065
|
+
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2066
|
+
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2067
|
+
let top = -Infinity;
|
|
2068
|
+
let bottom = Infinity;
|
|
2069
|
+
for (let i = 1; i < mesh.vertices.length; i += 2) {
|
|
2070
|
+
top = Math.max(top, mesh.vertices[i]);
|
|
2071
|
+
bottom = Math.min(bottom, mesh.vertices[i]);
|
|
2072
|
+
}
|
|
2073
|
+
const height = top - bottom;
|
|
2074
|
+
const swing = [];
|
|
2075
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2076
|
+
const u = height > 0 ? (top - mesh.vertices[i + 1]) / height : 0;
|
|
2077
|
+
swing.push(tipShift * Math.pow(u, HAIR_SWAY_CURL), 0);
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
parameter,
|
|
2081
|
+
keyforms: [
|
|
2082
|
+
// `0 - v`, not `-v`: the pinned root row must stay a plain +0.
|
|
2083
|
+
{ value: -range, offsets: swing.map((v) => 0 - v) },
|
|
2084
|
+
{ value: range, offsets: swing }
|
|
2085
|
+
]
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
1959
2088
|
function generateIkiFromLayerSet(layers, canvas) {
|
|
1960
2089
|
validateLayerInputs(layers, canvas);
|
|
1961
2090
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2091
|
+
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
1962
2092
|
const parameters = [
|
|
1963
2093
|
{
|
|
1964
2094
|
id: import_format3.StandardParameter.MouthOpen,
|
|
@@ -2009,6 +2139,20 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2009
2139
|
max: 30,
|
|
2010
2140
|
default: 0
|
|
2011
2141
|
},
|
|
2142
|
+
{
|
|
2143
|
+
id: import_format3.StandardParameter.AngleY,
|
|
2144
|
+
name: "Head Angle Y",
|
|
2145
|
+
min: -30,
|
|
2146
|
+
max: 30,
|
|
2147
|
+
default: 0
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
id: import_format3.StandardParameter.AngleZ,
|
|
2151
|
+
name: "Head Angle Z",
|
|
2152
|
+
min: -30,
|
|
2153
|
+
max: 30,
|
|
2154
|
+
default: 0
|
|
2155
|
+
},
|
|
2012
2156
|
{
|
|
2013
2157
|
id: import_format3.StandardParameter.Breath,
|
|
2014
2158
|
name: "Breath",
|
|
@@ -2046,13 +2190,22 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2046
2190
|
}
|
|
2047
2191
|
];
|
|
2048
2192
|
if (hasHair) {
|
|
2049
|
-
parameters.push(
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2193
|
+
parameters.push(
|
|
2194
|
+
{
|
|
2195
|
+
id: import_format3.StandardParameter.HairSwayX,
|
|
2196
|
+
name: "Hair Sway X",
|
|
2197
|
+
min: -HAIR_SWAY_RANGE,
|
|
2198
|
+
max: HAIR_SWAY_RANGE,
|
|
2199
|
+
default: 0
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
id: import_format3.StandardParameter.HairSwayZ,
|
|
2203
|
+
name: "Hair Sway Z",
|
|
2204
|
+
min: -HAIR_SWAY_RANGE,
|
|
2205
|
+
max: HAIR_SWAY_RANGE,
|
|
2206
|
+
default: 0
|
|
2207
|
+
}
|
|
2208
|
+
);
|
|
2056
2209
|
}
|
|
2057
2210
|
const faceLayers = layers.filter((l) => l.role === "face");
|
|
2058
2211
|
const faceLayer = faceLayers[0];
|
|
@@ -2110,16 +2263,22 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2110
2263
|
unionMaxY
|
|
2111
2264
|
)
|
|
2112
2265
|
};
|
|
2266
|
+
const faceCenterY = faceTransform.y;
|
|
2267
|
+
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
2268
|
+
const parallaxUnit = headTurnParallaxUnit(halfW);
|
|
2269
|
+
const parallaxUnitY = headTurnParallaxUnit(halfH);
|
|
2113
2270
|
const faceBottom = faceTransform.y - faceCropH / 2;
|
|
2114
2271
|
const neckPivot = {
|
|
2115
2272
|
x: faceCenterX,
|
|
2116
2273
|
y: faceBottom - faceCropH * 0.15
|
|
2117
2274
|
// 15% below face bottom = neck
|
|
2118
2275
|
};
|
|
2119
|
-
const
|
|
2276
|
+
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
2120
2277
|
faceGrid,
|
|
2121
2278
|
import_format3.StandardParameter.AngleX,
|
|
2122
|
-
|
|
2279
|
+
import_format3.StandardParameter.AngleY,
|
|
2280
|
+
faceCenterX,
|
|
2281
|
+
faceCenterY
|
|
2123
2282
|
);
|
|
2124
2283
|
const deformers = [
|
|
2125
2284
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
@@ -2140,6 +2299,27 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2140
2299
|
from: -50,
|
|
2141
2300
|
to: 50
|
|
2142
2301
|
},
|
|
2302
|
+
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
2303
|
+
// rigid rotation would sum with the roll below at diagonal poses,
|
|
2304
|
+
// collapsing pitch into roll.
|
|
2305
|
+
{
|
|
2306
|
+
parameter: import_format3.StandardParameter.AngleY,
|
|
2307
|
+
channel: "translateY",
|
|
2308
|
+
from: -NOD_TRAVEL,
|
|
2309
|
+
to: NOD_TRAVEL
|
|
2310
|
+
},
|
|
2311
|
+
// Tilt: the whole head rolls about the neck pivot, one degree per
|
|
2312
|
+
// degree. Positive AngleZ is clockwise on screen (engine rotate is
|
|
2313
|
+
// CCW-positive, hence the flipped from/to) — Live2D's convention, and
|
|
2314
|
+
// the same sense as the lean above: in Live2D's own sample motions
|
|
2315
|
+
// AngleZ carries the sign of AngleX 214 times out of 222. The two
|
|
2316
|
+
// rotations sum, which is correct for two rolls about one pivot.
|
|
2317
|
+
{
|
|
2318
|
+
parameter: import_format3.StandardParameter.AngleZ,
|
|
2319
|
+
channel: "rotate",
|
|
2320
|
+
from: 30,
|
|
2321
|
+
to: -30
|
|
2322
|
+
},
|
|
2143
2323
|
{
|
|
2144
2324
|
parameter: import_format3.StandardParameter.Breath,
|
|
2145
2325
|
channel: "translateY",
|
|
@@ -2150,12 +2330,14 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2150
2330
|
},
|
|
2151
2331
|
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
2152
2332
|
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
2333
|
+
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
2334
|
+
// `warps` or `warp2d`, never both).
|
|
2153
2335
|
{
|
|
2154
2336
|
kind: "warp",
|
|
2155
2337
|
id: "faceWarp",
|
|
2156
2338
|
parent: "headDeformer",
|
|
2157
2339
|
grid: faceGrid,
|
|
2158
|
-
|
|
2340
|
+
warp2d: faceWarp2d
|
|
2159
2341
|
}
|
|
2160
2342
|
];
|
|
2161
2343
|
const eyeCreaseBySide = {};
|
|
@@ -2175,7 +2357,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2175
2357
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
2176
2358
|
const spec = ROLE_TABLE[role];
|
|
2177
2359
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
2178
|
-
const roleBindings = bindingsForRole(spec, role, cropW, cropH
|
|
2360
|
+
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
2361
|
+
hasMouthOpen,
|
|
2362
|
+
parallaxUnit,
|
|
2363
|
+
parallaxUnitY
|
|
2364
|
+
});
|
|
2365
|
+
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
2179
2366
|
if (spec.mesh) {
|
|
2180
2367
|
const mesh = createPixelGridMesh(4, 4, cropW, cropH);
|
|
2181
2368
|
const part = {
|
|
@@ -2185,23 +2372,58 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2185
2372
|
height: 1,
|
|
2186
2373
|
order: spec.order,
|
|
2187
2374
|
transform: t,
|
|
2188
|
-
deformer:
|
|
2375
|
+
deformer: deformerId,
|
|
2189
2376
|
mesh
|
|
2190
2377
|
};
|
|
2191
2378
|
if (roleBindings.length > 0) {
|
|
2192
2379
|
part.bindings = roleBindings;
|
|
2193
2380
|
}
|
|
2381
|
+
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
2382
|
+
let tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
2383
|
+
if (role === "hair_front") {
|
|
2384
|
+
const shift = HAIR_FRONT_DEPTH * parallaxUnit;
|
|
2385
|
+
const headroom = Math.min(
|
|
2386
|
+
faceGridMaxX - (t.x + cropW / 2),
|
|
2387
|
+
t.x - cropW / 2 - faceGridMinX
|
|
2388
|
+
) - shift;
|
|
2389
|
+
tipShift = Math.min(
|
|
2390
|
+
tipShift,
|
|
2391
|
+
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
part.warps = [
|
|
2395
|
+
bakeHairSwayWarp(
|
|
2396
|
+
mesh,
|
|
2397
|
+
import_format3.StandardParameter.HairSwayX,
|
|
2398
|
+
tipShift,
|
|
2399
|
+
HAIR_SWAY_RANGE
|
|
2400
|
+
),
|
|
2401
|
+
bakeHairSwayWarp(
|
|
2402
|
+
mesh,
|
|
2403
|
+
import_format3.StandardParameter.HairSwayZ,
|
|
2404
|
+
tipShift,
|
|
2405
|
+
HAIR_SWAY_RANGE
|
|
2406
|
+
)
|
|
2407
|
+
];
|
|
2408
|
+
}
|
|
2409
|
+
if (role === "hair_back") {
|
|
2410
|
+
part.warps = [
|
|
2411
|
+
...part.warps ?? [],
|
|
2412
|
+
bakeHairBackTurnWarp(mesh, import_format3.StandardParameter.AngleX)
|
|
2413
|
+
];
|
|
2414
|
+
}
|
|
2194
2415
|
if (spec.eyeSide !== void 0) {
|
|
2195
2416
|
const isLash = role.startsWith("lash_");
|
|
2196
2417
|
if (role.startsWith("eye_") || isLash) {
|
|
2197
2418
|
const openParam = spec.eyeSide === "L" ? import_format3.StandardParameter.EyeOpenLeft : import_format3.StandardParameter.EyeOpenRight;
|
|
2198
2419
|
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
2420
|
+
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
2199
2421
|
part.warps = [
|
|
2200
2422
|
bakeEyelidFoldWarp(
|
|
2201
2423
|
mesh,
|
|
2202
2424
|
openParam,
|
|
2203
2425
|
creaseWorldY - t.y,
|
|
2204
|
-
isLash ? LASH_FOLD_K : EYELID_FOLD_K
|
|
2426
|
+
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
2205
2427
|
)
|
|
2206
2428
|
];
|
|
2207
2429
|
} else {
|
|
@@ -2210,15 +2432,19 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2210
2432
|
}
|
|
2211
2433
|
return part;
|
|
2212
2434
|
} else {
|
|
2213
|
-
|
|
2435
|
+
const part = {
|
|
2214
2436
|
id: role,
|
|
2215
2437
|
color: [1, 1, 1, 1],
|
|
2216
2438
|
width: cropW,
|
|
2217
2439
|
height: cropH,
|
|
2218
2440
|
order: spec.order,
|
|
2219
2441
|
transform: t,
|
|
2220
|
-
deformer:
|
|
2442
|
+
deformer: deformerId
|
|
2221
2443
|
};
|
|
2444
|
+
if (roleBindings.length > 0) {
|
|
2445
|
+
part.bindings = roleBindings;
|
|
2446
|
+
}
|
|
2447
|
+
return part;
|
|
2222
2448
|
}
|
|
2223
2449
|
});
|
|
2224
2450
|
const model = {
|
|
@@ -2229,9 +2455,12 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2229
2455
|
parameters,
|
|
2230
2456
|
deformers,
|
|
2231
2457
|
parts,
|
|
2232
|
-
// Secondary motion:
|
|
2233
|
-
// behind the head turn (same constants as the hand-authored sample)
|
|
2234
|
-
//
|
|
2458
|
+
// Secondary motion: one spring lags AngleX onto HairSwayX so front hair
|
|
2459
|
+
// sways behind the head turn (same constants as the hand-authored sample),
|
|
2460
|
+
// and a second lags AngleZ onto HairSwayZ so it swings behind a tilt. Two
|
|
2461
|
+
// rigs because a rig has one input and one output (validator-enforced).
|
|
2462
|
+
// Both hair parts read the outputs through root-pinned sway warps.
|
|
2463
|
+
// Omitted when there is no front hair to drive.
|
|
2235
2464
|
physics: hasHair ? [
|
|
2236
2465
|
{
|
|
2237
2466
|
id: "hairSway",
|
|
@@ -2240,6 +2469,14 @@ function generateIkiFromLayerSet(layers, canvas) {
|
|
|
2240
2469
|
mass: 1,
|
|
2241
2470
|
stiffness: 80,
|
|
2242
2471
|
damping: 10
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
id: "hairTilt",
|
|
2475
|
+
input: { parameter: import_format3.StandardParameter.AngleZ, weight: 1 },
|
|
2476
|
+
output: { parameter: import_format3.StandardParameter.HairSwayZ, scale: -10 },
|
|
2477
|
+
mass: 1,
|
|
2478
|
+
stiffness: 80,
|
|
2479
|
+
damping: 10
|
|
2243
2480
|
}
|
|
2244
2481
|
] : void 0
|
|
2245
2482
|
};
|