reze-engine 0.68.6 → 0.68.8
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/dist/engine.d.ts +2 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +6 -6
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +2 -10
- package/dist/jelly-eyes.d.ts +5 -2
- package/dist/jelly-eyes.d.ts.map +1 -1
- package/dist/jelly-eyes.js +261 -46
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +49 -143
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +13 -2
- package/package.json +1 -1
- package/src/engine.ts +6 -6
- package/src/graph/slots.ts +2 -10
- package/src/jelly-eyes.ts +275 -47
- package/src/shaders/materials/common.ts +49 -143
- package/src/shaders/passes/outline.ts +13 -2
package/dist/jelly-eyes.js
CHANGED
|
@@ -3,36 +3,106 @@
|
|
|
3
3
|
// What every working version of the effect has in common, whether it is keyed
|
|
4
4
|
// by hand in MMD, a pendulum in Live2D, or a jiggle bone in Blender: an INPUT
|
|
5
5
|
// (a blink, a turn), something that REMEMBERS the last frame (a spring), and a
|
|
6
|
-
// cheap OUTPUT. Here the output is the iris's
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// cheap OUTPUT. Here the output is the iris's shape, in place.
|
|
7
|
+
//
|
|
8
|
+
// The blink is the MME 果冻眼's (JellyEye V1.2.2, by 茶叶味香皂): a spring
|
|
9
|
+
// driven by how FAST the lids move. The iris widens while they close and rings
|
|
10
|
+
// out once they stop, kicked again at each change of pace; the lid presses it
|
|
11
|
+
// flatter as it closes; a twist and a drift of a hair ride the spring. On top,
|
|
12
|
+
// her head's motion and an idle tap now and then knock it, as before. A
|
|
13
|
+
// highlight drawn as its own layer over the iris (a separate material, as MME
|
|
14
|
+
// rigs have) follows the iris, then trembles on its own: a new random spot ten
|
|
15
|
+
// times a second, no easing — the jiggle the MME is known for. The inputs are
|
|
16
|
+
// her blinks and her head, never her eyes' own turn: eye tracking turns the
|
|
17
|
+
// eyes after the camera, and the camera must not shake the jelly.
|
|
11
18
|
//
|
|
12
19
|
// The springs run on the CPU, stepped by the engine's frame delta — the same
|
|
13
20
|
// exact delta renderFrame() hands an export, so a blink bounces identically on
|
|
14
|
-
// screen and in the file. Their
|
|
15
|
-
// (jelly[0..
|
|
16
|
-
// the vertices the eye bones carry — about its centre,
|
|
17
|
-
//
|
|
18
|
-
// colour), so every eye graph becomes jelly without knowing it.
|
|
21
|
+
// screen and in the file. Their result rides in the model's ObjectLight
|
|
22
|
+
// (jelly[0..11], see materials/common.ts): the vertex shader moves the iris —
|
|
23
|
+
// the vertices the eye bones carry — about its centre, so every eye graph
|
|
24
|
+
// becomes jelly without knowing it. Nothing changes its colour.
|
|
19
25
|
// The closing morphs, by the names MMD, VRoid and the usual converters give
|
|
20
26
|
// them. Group morphs (a smile that closes the eyes) resolve into these through
|
|
21
27
|
// the effective weights, so only the leaves need naming.
|
|
22
28
|
const BOTH = ["まばたき", "笑い", "はぅ", "blink", "Blink", "eye_close", "Fcl_EYE_Close"];
|
|
23
29
|
const LEFT = ["ウィンク", "ウィンク2", "ウィンク", "ウィンク2", "wink", "Wink", "blink_L", "Fcl_EYE_Close_L"];
|
|
24
30
|
const RIGHT = ["ウィンク右", "ウィンク2右", "ウィンク右", "ウィンク2右", "wink_R", "Wink_R", "blink_R", "Fcl_EYE_Close_R"];
|
|
25
|
-
// The
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
// The blink, as the MME runs it (its JellyEye_Settings.fxsub defaults). The
|
|
32
|
+
// eye's closure (0..1) is differentiated twice. Its speed drives a target the
|
|
33
|
+
// spring chases: CLOSE_DRIVE while closing, OPEN_DRIVE while opening, eased by
|
|
34
|
+
// how fast. Every change between gaining and losing pace kicks the spring's
|
|
35
|
+
// velocity. The spring is an exact under-damped step at SPRING_HZ whose
|
|
36
|
+
// envelope halves every HALF_LIFE.
|
|
37
|
+
const ACCEL_MIN = 0.5;
|
|
38
|
+
const ACCEL_FULL = 2;
|
|
39
|
+
const ACCEL_KICK = 0.32;
|
|
40
|
+
const RELEASE_KICK = 0.2;
|
|
41
|
+
const CLOSE_DRIVE = 0.85;
|
|
42
|
+
const OPEN_DRIVE = 0.2;
|
|
43
|
+
const DRIVE_SPEED_MIN = 0.04;
|
|
44
|
+
const DRIVE_SPEED_FULL = 2.5;
|
|
45
|
+
const DRIVE_CURVE = 0.85;
|
|
46
|
+
const MORPH_SPEED_LIMIT = 30;
|
|
47
|
+
const ACCEL_LIMIT = 240;
|
|
48
|
+
const MAX_STEP = 0.12;
|
|
49
|
+
const SPRING_HZ = 3;
|
|
50
|
+
const HALF_LIFE = 0.28;
|
|
51
|
+
const ELASTIC_S = 2;
|
|
52
|
+
const STOP_EPS = 0.0025;
|
|
53
|
+
const MIN_RELEASE = 0.12;
|
|
54
|
+
const SPRING_LIMIT = 1.25;
|
|
55
|
+
// What the spring does to the iris, per unit of it: wider far more than
|
|
56
|
+
// taller, a little less on the rebound side; the lid's press, flatter by
|
|
57
|
+
// SQUEEZE_Y at full closure; a twist (degrees); and a drift (iris radii: the
|
|
58
|
+
// MME's model units over a typical iris of 0.1). Twist and drift take both the
|
|
59
|
+
// spring's place (_POS) and its velocity (_VEL). The twist is ten and five
|
|
60
|
+
// times the MME's defaults (0.2°, 0.8°): at those it stays under a degree and
|
|
61
|
+
// the iris never visibly turns as it rings.
|
|
62
|
+
const SCALE_GAIN = [0.2, 0.05];
|
|
63
|
+
const REBOUND = [0.7, 0.8];
|
|
64
|
+
const SQUEEZE_Y = 0.18;
|
|
65
|
+
const TWIST_POS = 2;
|
|
66
|
+
const TWIST_VEL = 4;
|
|
67
|
+
const DRIFT_POS = [0.008, 0.015];
|
|
68
|
+
const DRIFT_VEL = [0.012, 0.006];
|
|
69
|
+
// The highlight layer's own motion: a held random spot, re-drawn JITTER_HZ
|
|
70
|
+
// times a second per axis, up to JITTER of an iris radius; and, on the spring,
|
|
71
|
+
// a further sway (iris radii) and turn (degrees) of its own.
|
|
72
|
+
const JITTER_HZ = [9, 11];
|
|
73
|
+
const JITTER = [0.03, 0.02];
|
|
74
|
+
const HL_SWAY = [0.03, 0.045];
|
|
75
|
+
const HL_TURN = 10;
|
|
76
|
+
const HL_SCALE = [0.055, 0.045];
|
|
77
|
+
const HL_SCALE_MIN = 0.85;
|
|
78
|
+
const HL_SCALE_MAX = 1.2;
|
|
79
|
+
// The roll: while a closing expression holds, the iris turns about its eye
|
|
80
|
+
// bone's X axis (左目/右目, the eyeball's centre), rolling down with the lid —
|
|
81
|
+
// the angle at weight 1, and simultaneous expressions add. And it comes
|
|
82
|
+
// forward a little (iris radii) with the expression and with the spring, so
|
|
83
|
+
// the roll does not sink it into the white. The MME rolls with the lid
|
|
84
|
+
// exactly, which the lid hides; here the roll follows on a spring of the same
|
|
85
|
+
// pace and decay as the blink's, so as the eye opens the iris swings up past
|
|
86
|
+
// rest and wobbles back, where it can be seen.
|
|
87
|
+
const ROLL = {
|
|
88
|
+
まばたき: -15, blink: -15, Blink: -15, eye_close: -15, Fcl_EYE_Close: -15, はぅ: -15, 笑い: -5,
|
|
89
|
+
ウィンク: -5, ウィンク: -5, wink: -5, Wink: -5, ウィンク2: -15, ウィンク2: -15, blink_L: -15, Fcl_EYE_Close_L: -15,
|
|
90
|
+
ウィンク右: -5, ウィンク右: -5, wink_R: -5, Wink_R: -5, ウィンク2右: -15, ウィンク2右: -15, blink_R: -15, Fcl_EYE_Close_R: -15,
|
|
91
|
+
};
|
|
92
|
+
const ROLL_HZ = SPRING_HZ;
|
|
93
|
+
const ROLL_ZETA = Math.LN2 / HALF_LIFE / (2 * Math.PI * SPRING_HZ);
|
|
94
|
+
// How much of the spring's swing about the expression angle shows: the whole
|
|
95
|
+
// of it carried the iris up most of its radius after a blink.
|
|
96
|
+
const ROLL_SWING = 0.4;
|
|
97
|
+
const PUSH_EXPRESSION = 0.15;
|
|
98
|
+
const PUSH_ELASTIC = 0.04;
|
|
99
|
+
// A highlight sheet by name, for rigs that keep it on its own material.
|
|
100
|
+
const HIGHLIGHT = /ハイライト|ハイライト|高光|highlight|hilight|(^|[^a-z])hl([^a-z]|$)/i;
|
|
101
|
+
const DEG = Math.PI / 180;
|
|
102
|
+
// The knock's squash (her motion, the idle taps): nearly round, lightly damped
|
|
103
|
+
// so one knock ripples.
|
|
33
104
|
const SQUASH_HZ = 1.8;
|
|
34
105
|
const SQUASH_ZETA = 0.08;
|
|
35
|
-
const CLOSURE_FOLLOW_S = 0.35;
|
|
36
106
|
// The lag: the picture trails the eye's turn and catches up past it. Radians of
|
|
37
107
|
// lag to eye radii of offset; the head's own turn counts for a third, so a
|
|
38
108
|
// sudden stop of the head wobbles the eyes without a dance keeping them
|
|
@@ -94,14 +164,20 @@ const SUBSTEP = 1 / 240;
|
|
|
94
164
|
const SEEK = 0.25;
|
|
95
165
|
const newState = () => ({
|
|
96
166
|
closure: 0,
|
|
97
|
-
|
|
167
|
+
prev: 0,
|
|
168
|
+
speed: 0,
|
|
169
|
+
trend: 0,
|
|
170
|
+
x: 0,
|
|
171
|
+
v: 0,
|
|
172
|
+
age: ELASTIC_S,
|
|
98
173
|
squash: 0,
|
|
99
174
|
squashV: 0,
|
|
100
175
|
lag: [0, 0],
|
|
101
176
|
lagV: [0, 0],
|
|
102
177
|
look: [0, 0],
|
|
103
|
-
shine: 0,
|
|
104
178
|
pulse: 0,
|
|
179
|
+
roll: 0,
|
|
180
|
+
rollV: 0,
|
|
105
181
|
pulseV: 0,
|
|
106
182
|
});
|
|
107
183
|
export class JellyEyes {
|
|
@@ -115,6 +191,7 @@ export class JellyEyes {
|
|
|
115
191
|
this.state = [newState(), newState()];
|
|
116
192
|
this.fresh = true;
|
|
117
193
|
this.inertia = { p: [0, 0, 0], v: [0, 0, 0], head: [0, 0, 0], offset: [0, 0] };
|
|
194
|
+
this.roll = [0, 0];
|
|
118
195
|
this.strength = clampStrength(options.strength);
|
|
119
196
|
this.idle = options.idle ?? true;
|
|
120
197
|
const bones = model.getSkeleton().bones;
|
|
@@ -126,11 +203,14 @@ export class JellyEyes {
|
|
|
126
203
|
const names = model.getMorphing().morphs.map((m) => m.name);
|
|
127
204
|
const find = (list) => list.map((n) => names.indexOf(n)).filter((i) => i >= 0);
|
|
128
205
|
this.morphs = [find(BOTH), find(LEFT), find(RIGHT)];
|
|
206
|
+
const rolls = (list) => list.map((n) => ({ morph: names.indexOf(n), deg: ROLL[n] ?? 0 })).filter((r) => r.morph >= 0 && r.deg !== 0);
|
|
207
|
+
this.rolls = [rolls([...BOTH, ...LEFT]), rolls([...BOTH, ...RIGHT])];
|
|
129
208
|
this.rest = findIrises(model, l, r, both);
|
|
130
209
|
// The bone that carries each iris, which may be under the eye bone.
|
|
131
210
|
this.eyeBones = [this.rest[0].bone, this.rest[1].bone];
|
|
132
211
|
this.shifts = [irisShifts(model, this.rest[0].verts), irisShifts(model, this.rest[1].verts)];
|
|
133
212
|
this.centre = [[...this.rest[0].centre], [...this.rest[1].centre]];
|
|
213
|
+
this.highlights = findHighlights(model, this.rest);
|
|
134
214
|
}
|
|
135
215
|
/** Advance both springs by dt seconds of scene time. */
|
|
136
216
|
step(dt, model) {
|
|
@@ -153,6 +233,8 @@ export class JellyEyes {
|
|
|
153
233
|
const sum = (list) => list.reduce((s, i) => s + (weights[i] ?? 0), 0);
|
|
154
234
|
const both = sum(this.morphs[0]);
|
|
155
235
|
const closure = [Math.min(1, both + sum(this.morphs[1])), Math.min(1, both + sum(this.morphs[2]))];
|
|
236
|
+
for (let e = 0; e < 2; e++)
|
|
237
|
+
this.roll[e] = this.rolls[e].reduce((a, r) => a + r.deg * Math.max(0, Math.min(1, weights[r.morph] ?? 0)), 0);
|
|
156
238
|
const head = this.headBone >= 0 ? model.getBoneWorldMatrixAt(this.headBone) : null;
|
|
157
239
|
const seek = this.fresh || dt > SEEK || dt < 0;
|
|
158
240
|
if (dt > 0)
|
|
@@ -175,24 +257,24 @@ export class JellyEyes {
|
|
|
175
257
|
// Only her motion — the head turning and moving (the inertia) — and her
|
|
176
258
|
// blinks do.
|
|
177
259
|
look(head, null, s.look);
|
|
178
|
-
if (seek)
|
|
179
|
-
s.settled = s.closure;
|
|
180
|
-
else
|
|
181
|
-
s.settled += (s.closure - s.settled) * Math.min(1, dt / CLOSURE_FOLLOW_S);
|
|
182
|
-
// Either way a lid moves squashes — opening too, which is the half of a
|
|
183
|
-
// blink you can see (closing, the lid hides the iris anyway).
|
|
184
260
|
const knock = seek ? 0 : Math.max(this.tapKnock, Math.tanh(Math.hypot(...this.offset(s, e)) / PULSE_REF));
|
|
185
|
-
const squashTarget =
|
|
261
|
+
const squashTarget = MOTION_SQUASH * knock;
|
|
186
262
|
if (seek) {
|
|
263
|
+
// Landing on a pose is no blink: the spring rests where it lands.
|
|
264
|
+
s.prev = s.closure;
|
|
265
|
+
s.speed = s.trend = s.x = s.v = 0;
|
|
266
|
+
s.age = ELASTIC_S;
|
|
187
267
|
s.squash = squashTarget;
|
|
188
268
|
s.squashV = 0;
|
|
189
269
|
s.lag[0] = s.look[0];
|
|
190
270
|
s.lag[1] = s.look[1];
|
|
191
271
|
s.lagV[0] = s.lagV[1] = 0;
|
|
192
|
-
s.shine = 0;
|
|
193
272
|
s.pulse = s.pulseV = 0;
|
|
273
|
+
s.roll = this.roll[e];
|
|
274
|
+
s.rollV = 0;
|
|
194
275
|
continue;
|
|
195
276
|
}
|
|
277
|
+
blink(s, dt);
|
|
196
278
|
const steps = Math.ceil(dt / SUBSTEP);
|
|
197
279
|
const h = steps > 0 ? dt / steps : 0;
|
|
198
280
|
const pulseTarget = PULSE_MAX * knock;
|
|
@@ -201,32 +283,59 @@ export class JellyEyes {
|
|
|
201
283
|
s.pulse += h * s.pulseV;
|
|
202
284
|
s.squashV += h * spring(s.squash - squashTarget, s.squashV, SQUASH_HZ, SQUASH_ZETA);
|
|
203
285
|
s.squash += h * s.squashV;
|
|
286
|
+
s.rollV += h * spring(s.roll - this.roll[e], s.rollV, ROLL_HZ, ROLL_ZETA);
|
|
287
|
+
s.roll += h * s.rollV;
|
|
204
288
|
for (let k = 0; k < 2; k++) {
|
|
205
289
|
s.lagV[k] += h * spring(s.lag[k] - s.look[k], s.lagV[k], LAG_HZ, LAG_ZETA);
|
|
206
290
|
s.lag[k] += h * s.lagV[k];
|
|
207
291
|
}
|
|
208
292
|
}
|
|
209
|
-
// Shine follows the motion: bright while the jelly moves, fading as it
|
|
210
|
-
// settles, eased so a single wobble does not strobe.
|
|
211
|
-
const motion = Math.min(1, 2.5 * Math.abs(s.squash - squashTarget) + 4 * Math.hypot(...this.offset(s, e)));
|
|
212
|
-
s.shine += (motion - s.shine) * Math.min(1, dt * 12);
|
|
213
293
|
}
|
|
214
294
|
this.fresh = false;
|
|
215
295
|
}
|
|
216
|
-
/** Write jelly[0..
|
|
296
|
+
/** Write jelly[0..11] into an ObjectLight copy (floats 48..95). */
|
|
217
297
|
write(o) {
|
|
218
298
|
const [L, R] = this.centre;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
299
|
+
const g = this.strength;
|
|
300
|
+
o.set([L[0], L[1], L[2], g], 48);
|
|
301
|
+
// The highlight layers' draw ids, two packed in one float (exact to 4095).
|
|
302
|
+
const [a = 0, b = 0] = this.highlights;
|
|
303
|
+
o.set([R[0], R[1], R[2], a + b * 4096], 52);
|
|
304
|
+
// One knock swell for both: the mean of the two eyes', so they swell together.
|
|
305
|
+
const pulse = (this.state[0].pulse + this.state[1].pulse) / 2;
|
|
306
|
+
const omega = 2 * Math.PI * SPRING_HZ;
|
|
307
|
+
// Strength scales every departure from rest, then the bounds hold: the
|
|
308
|
+
// swell under 1.08 tall so it cannot rise past the upper lid.
|
|
309
|
+
const by = (k, lo, hi) => Math.max(lo, Math.min(hi, 1 + (k - 1) * g));
|
|
223
310
|
for (let e = 0; e < 2; e++) {
|
|
224
311
|
const s = this.state[e];
|
|
225
312
|
const rest = this.rest[e];
|
|
313
|
+
const r = (rest.radius[0] + rest.radius[1]) / 2;
|
|
314
|
+
// The MME spring's place, and its velocity in the same units.
|
|
315
|
+
const x = s.x;
|
|
316
|
+
const vn = s.v / omega;
|
|
317
|
+
const gain = (k) => SCALE_GAIN[k] * (x >= 0 ? 1 : REBOUND[k]);
|
|
318
|
+
const kx = (1 + x * gain(0)) * (1 - 0.8 * s.squash + pulse);
|
|
319
|
+
const ky = (1 + x * gain(1)) * (1 - SQUEEZE_Y * s.closure) * (1 - s.squash + pulse);
|
|
226
320
|
o.set([rest.radius[0], rest.radius[1], s.closure, this.eyeBones[e]], 56 + e * 4);
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
321
|
+
// [4]/[5]: scale across and down, the iris's twist, the highlight's turn.
|
|
322
|
+
o.set([by(kx, 0.78, 1.25), by(ky, 0.7, 1.08), (x * TWIST_POS + vn * TWIST_VEL) * DEG * g, vn * HL_TURN * DEG * g], 64 + e * 4);
|
|
323
|
+
// [6]/[7]: the iris's drift, then the highlight's on top (bind units).
|
|
324
|
+
// The tremble is held random spots on the scene clock, so an export
|
|
325
|
+
// trembles where the screen did, and a pause holds it.
|
|
326
|
+
const tick = (hz, seed) => (this.idle ? hashSigned(Math.floor(this.clock * hz) + seed + e * 101) : 0);
|
|
327
|
+
o.set([
|
|
328
|
+
(x * DRIFT_POS[0] + vn * DRIFT_VEL[0]) * r * g,
|
|
329
|
+
(x * DRIFT_POS[1] + vn * DRIFT_VEL[1]) * r * g,
|
|
330
|
+
(vn * HL_SWAY[0] + tick(JITTER_HZ[0], 17) * JITTER[0]) * r * g,
|
|
331
|
+
(vn * HL_SWAY[1] + tick(JITTER_HZ[1], 53) * JITTER[1]) * r * g,
|
|
332
|
+
], 72 + e * 4);
|
|
333
|
+
// [8]/[9]: the roll's pivot and angle; [10]/[11]: the highlight's own
|
|
334
|
+
// scale, and how far the iris comes forward (bind units).
|
|
335
|
+
const pivot = rest.pivot;
|
|
336
|
+
o.set(pivot ? [pivot[0], pivot[1], pivot[2], (this.roll[e] + (s.roll - this.roll[e]) * ROLL_SWING) * DEG * g] : [0, 0, 0, 0], 80 + e * 4);
|
|
337
|
+
const hs = (k) => Math.max(HL_SCALE_MIN, Math.min(HL_SCALE_MAX, 1 + x * HL_SCALE[k] * g));
|
|
338
|
+
o.set([hs(0), hs(1), (s.closure * PUSH_EXPRESSION + Math.min(Math.abs(x), SPRING_LIMIT) * PUSH_ELASTIC) * r * g, 0], 88 + e * 4);
|
|
230
339
|
}
|
|
231
340
|
}
|
|
232
341
|
/** Where the iris sits off rest, in iris radii (right, up): the lag through
|
|
@@ -280,9 +389,115 @@ export class JellyEyes {
|
|
|
280
389
|
m.offset[1] = cap((dot(u, up) / Math.hypot(...up) / size) * INERTIA_GAIN);
|
|
281
390
|
}
|
|
282
391
|
}
|
|
283
|
-
/** Zero jelly[0..
|
|
392
|
+
/** Zero jelly[0..11] — strength 0 is what the shader reads as off. */
|
|
284
393
|
export function clearJellyEyes(o) {
|
|
285
|
-
o.fill(0, 48,
|
|
394
|
+
o.fill(0, 48, 96);
|
|
395
|
+
}
|
|
396
|
+
/** A held random value in −1..1 for an integer tick, as the MME hashes it. */
|
|
397
|
+
function hashSigned(n) {
|
|
398
|
+
const x = Math.sin(n * 12.9898 + 78.233) * 43758.5453;
|
|
399
|
+
return (x - Math.floor(x)) * 2 - 1;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* One frame of the MME's blink spring for one eye (JellyEye_StateUpdate.fxsub,
|
|
403
|
+
* JE_ComputeNextState), from its closure this frame.
|
|
404
|
+
*/
|
|
405
|
+
function blink(s, dt) {
|
|
406
|
+
if (!(dt > 0))
|
|
407
|
+
return;
|
|
408
|
+
dt = Math.min(dt, MAX_STEP);
|
|
409
|
+
const omega = 2 * Math.PI * SPRING_HZ;
|
|
410
|
+
const clamp = (x, m) => Math.max(-m, Math.min(m, x));
|
|
411
|
+
const speed = clamp((s.closure - s.prev) / dt, MORPH_SPEED_LIMIT);
|
|
412
|
+
// The change of SPEED, not of signed velocity: gaining pace or losing it.
|
|
413
|
+
const accel = clamp((Math.abs(speed) - Math.abs(s.speed)) / dt, ACCEL_LIMIT);
|
|
414
|
+
const trend = accel > ACCEL_MIN ? 1 : accel < -ACCEL_MIN ? -1 : 0;
|
|
415
|
+
// One run of gaining (or losing) pace is one kick.
|
|
416
|
+
const kicked = trend !== 0 && trend !== s.trend;
|
|
417
|
+
let latched = trend !== 0 ? trend : s.trend;
|
|
418
|
+
if (trend === 0 && Math.abs(speed) < DRIVE_SPEED_MIN && Math.abs(s.speed) < DRIVE_SPEED_MIN)
|
|
419
|
+
latched = 0;
|
|
420
|
+
// While the lids move, a target the spring chases, eased by how fast.
|
|
421
|
+
const amount = Math.max(0, Math.min(1, (Math.abs(speed) - DRIVE_SPEED_MIN) / (DRIVE_SPEED_FULL - DRIVE_SPEED_MIN))) ** DRIVE_CURVE;
|
|
422
|
+
const target = amount * (speed > DRIVE_SPEED_MIN ? CLOSE_DRIVE : speed < -DRIVE_SPEED_MIN ? OPEN_DRIVE : 0);
|
|
423
|
+
const driven = Math.abs(target) > 1e-5;
|
|
424
|
+
const x0 = s.x;
|
|
425
|
+
let v0 = s.v;
|
|
426
|
+
if (kicked)
|
|
427
|
+
v0 += (trend > 0 ? ACCEL_KICK : -RELEASE_KICK) * Math.min(1, Math.abs(accel) / ACCEL_FULL) * omega;
|
|
428
|
+
const age = driven || kicked ? 0 : Math.min(s.age + dt, ELASTIC_S);
|
|
429
|
+
let x = 0;
|
|
430
|
+
let v = 0;
|
|
431
|
+
if (driven || kicked || s.age < ELASTIC_S || Math.abs(x0) > STOP_EPS || Math.abs(v0) / omega > STOP_EPS) {
|
|
432
|
+
// The exact under-damped step about the target.
|
|
433
|
+
const decay = Math.LN2 / HALF_LIFE;
|
|
434
|
+
const envelope = Math.exp(-decay * dt);
|
|
435
|
+
const c = Math.cos(omega * dt);
|
|
436
|
+
const sn = Math.sin(omega * dt);
|
|
437
|
+
const rel = x0 - target;
|
|
438
|
+
x = target + envelope * (rel * c + ((v0 + decay * rel) * sn) / omega);
|
|
439
|
+
v = envelope * (v0 * c - ((decay * v0 + (decay * decay + omega * omega) * rel) * sn) / omega);
|
|
440
|
+
}
|
|
441
|
+
x = clamp(x, SPRING_LIMIT);
|
|
442
|
+
v = clamp(v, omega * SPRING_LIMIT * 2);
|
|
443
|
+
s.prev = s.closure;
|
|
444
|
+
s.speed = speed;
|
|
445
|
+
s.trend = latched;
|
|
446
|
+
const settled = !driven && age >= MIN_RELEASE && Math.abs(x) <= STOP_EPS && Math.abs(v) / omega <= STOP_EPS;
|
|
447
|
+
if (settled || (!driven && age >= ELASTIC_S)) {
|
|
448
|
+
s.x = s.v = 0;
|
|
449
|
+
s.age = ELASTIC_S;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
s.x = x;
|
|
453
|
+
s.v = v;
|
|
454
|
+
s.age = age;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The highlight layers: a material drawn over an iris — after it, on the same
|
|
459
|
+
* disc, made of vertices the eye bones carry — or one named as a highlight.
|
|
460
|
+
* As draw ids (1-based over the materials that draw, as the engine numbers
|
|
461
|
+
* MaterialUniforms.materialId), at most two. A rig whose highlight is painted
|
|
462
|
+
* into the iris has none, and only the iris moves.
|
|
463
|
+
*/
|
|
464
|
+
function findHighlights(model, rest) {
|
|
465
|
+
const materials = model.getMaterials?.() ?? [];
|
|
466
|
+
const { positions, indices } = model.getGeometry();
|
|
467
|
+
const iris = new Set([...rest[0].verts, ...rest[1].verts]);
|
|
468
|
+
if (!materials.length || !indices.length || !iris.size)
|
|
469
|
+
return [];
|
|
470
|
+
const layers = [];
|
|
471
|
+
let start = 0;
|
|
472
|
+
let id = 0;
|
|
473
|
+
for (const m of materials) {
|
|
474
|
+
const count = m.vertexCount;
|
|
475
|
+
if (count === 0)
|
|
476
|
+
continue;
|
|
477
|
+
id++;
|
|
478
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
479
|
+
for (let k = start; k < start + count; k++) {
|
|
480
|
+
const v = indices[k];
|
|
481
|
+
if (!iris.has(v))
|
|
482
|
+
continue;
|
|
483
|
+
minX = Math.min(minX, positions[v * 3]);
|
|
484
|
+
maxX = Math.max(maxX, positions[v * 3]);
|
|
485
|
+
minY = Math.min(minY, positions[v * 3 + 1]);
|
|
486
|
+
maxY = Math.max(maxY, positions[v * 3 + 1]);
|
|
487
|
+
}
|
|
488
|
+
start += count;
|
|
489
|
+
if (minX <= maxX)
|
|
490
|
+
layers.push({ id, name: m.name, minX, maxX, minY, maxY });
|
|
491
|
+
}
|
|
492
|
+
const out = [];
|
|
493
|
+
layers.forEach((l, i) => {
|
|
494
|
+
const cx = (l.minX + l.maxX) / 2;
|
|
495
|
+
const cy = (l.minY + l.maxY) / 2;
|
|
496
|
+
const over = layers.slice(0, i).some((u) => cx >= u.minX && cx <= u.maxX && cy >= u.minY && cy <= u.maxY);
|
|
497
|
+
if ((over || HIGHLIGHT.test(l.name)) && out.length < 2)
|
|
498
|
+
out.push(l.id);
|
|
499
|
+
});
|
|
500
|
+
return out;
|
|
286
501
|
}
|
|
287
502
|
export function clampStrength(s) {
|
|
288
503
|
return Math.max(0, Math.min(2, s ?? 1));
|
|
@@ -487,7 +702,7 @@ function findIrises(model, l, r, both) {
|
|
|
487
702
|
const rest = (set, bone, side, carrierBone) => {
|
|
488
703
|
if (set.length < 3) {
|
|
489
704
|
const c = bone ?? [side * fallbackRadius * 2, 0, 0];
|
|
490
|
-
return { centre: c, radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1 };
|
|
705
|
+
return { centre: c, radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1, pivot: null };
|
|
491
706
|
}
|
|
492
707
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, z = 0;
|
|
493
708
|
for (const v of set) {
|
|
@@ -505,7 +720,7 @@ function findIrises(model, l, r, both) {
|
|
|
505
720
|
const rx = (maxX - minX) / 2;
|
|
506
721
|
const ry = (maxY - minY) / 2;
|
|
507
722
|
if (apart > 0 && (rx > apart * 0.6 || ry > apart * 0.8)) {
|
|
508
|
-
return { centre: bone ?? [side * fallbackRadius * 2, 0, 0], radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1 };
|
|
723
|
+
return { centre: bone ?? [side * fallbackRadius * 2, 0, 0], radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1, pivot: null };
|
|
509
724
|
}
|
|
510
725
|
const centre = [(minX + maxX) / 2, (minY + maxY) / 2, z / set.length];
|
|
511
726
|
const radius = [Math.max(rx, 1e-3), Math.max(ry, 1e-3)];
|
|
@@ -513,7 +728,7 @@ function findIrises(model, l, r, both) {
|
|
|
513
728
|
// still turns its eyes visibly, so read it as a typical eyeball's.
|
|
514
729
|
const r = (radius[0] + radius[1]) / 2;
|
|
515
730
|
const lever = Math.max(bone ? Math.hypot(centre[0] - bone[0], centre[1] - bone[1], centre[2] - bone[2]) : 0, r * 1.5);
|
|
516
|
-
return { centre, radius, lever, verts: set, bone: carrierBone };
|
|
731
|
+
return { centre, radius, lever, verts: set, bone: carrierBone, pivot: bone };
|
|
517
732
|
};
|
|
518
733
|
return [rest(sets[0], lHead, leftSign, carrier(0)), rest(sets[1], rHead, -leftSign, carrier(1))];
|
|
519
734
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,QA2ThC,CAAC;AAwHF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC6H,CAAA"}
|