reze-engine 0.68.4 → 0.68.6
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 +24 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +77 -6
- package/dist/jelly-eyes.d.ts +10 -0
- package/dist/jelly-eyes.d.ts.map +1 -1
- package/dist/jelly-eyes.js +225 -46
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +71 -20
- package/package.json +1 -1
- package/src/engine.ts +77 -6
- package/src/jelly-eyes.ts +224 -40
- package/src/shaders/materials/common.ts +71 -20
package/dist/jelly-eyes.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
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 SCALE, in place: it
|
|
7
|
-
//
|
|
6
|
+
// cheap OUTPUT. Here the output is the iris's SCALE, in place: it shrinks and
|
|
7
|
+
// springs back as a lid moves (opening too), swells as her head turns or moves, and
|
|
8
8
|
// wobbles back. It never slides — tried, it read as the iris coming loose. The
|
|
9
9
|
// inputs are her blinks and her head, never her eyes' own turn: eye tracking
|
|
10
10
|
// turns the eyes after the camera, and the camera must not shake the jelly.
|
|
@@ -23,14 +23,15 @@ const BOTH = ["まばたき", "笑い", "はぅ", "blink", "Blink", "eye_close",
|
|
|
23
23
|
const LEFT = ["ウィンク", "ウィンク2", "ウィンク", "ウィンク2", "wink", "Wink", "blink_L", "Fcl_EYE_Close_L"];
|
|
24
24
|
const RIGHT = ["ウィンク右", "ウィンク2右", "ウィンク右", "ウィンク2右", "wink_R", "Wink_R", "blink_R", "Fcl_EYE_Close_R"];
|
|
25
25
|
// The squash: how far a closed eye presses the picture, and the spring that
|
|
26
|
-
// follows it —
|
|
27
|
-
//
|
|
26
|
+
// follows it — lightly damped, so one knock ripples: the squash, a rebound past
|
|
27
|
+
// rest, and several smaller wobbles before it settles. One dip and done read as
|
|
28
|
+
// a scale, not a jelly; the ripple is the "Duang" the effect is named for.
|
|
28
29
|
// Only a CHANGE of closure squashes: the eye's closure less a slow follower of
|
|
29
30
|
// it, so a blink squeezes then bounces, while a held half-close (a smile)
|
|
30
31
|
// leaves the iris at rest instead of squashed for as long as she smiles.
|
|
31
32
|
const SQUASH_CLOSED = 0.35;
|
|
32
|
-
const SQUASH_HZ = 1.
|
|
33
|
-
const SQUASH_ZETA = 0.
|
|
33
|
+
const SQUASH_HZ = 1.8;
|
|
34
|
+
const SQUASH_ZETA = 0.08;
|
|
34
35
|
const CLOSURE_FOLLOW_S = 0.35;
|
|
35
36
|
// The lag: the picture trails the eye's turn and catches up past it. Radians of
|
|
36
37
|
// lag to eye radii of offset; the head's own turn counts for a third, so a
|
|
@@ -46,7 +47,7 @@ const LAG_ZETA = 0.4;
|
|
|
46
47
|
const LAG_GAIN = 0.5;
|
|
47
48
|
const LAG_MAX = (3 * Math.PI) / 180;
|
|
48
49
|
// And never past this many iris radii, whatever the lever, at strength 1.
|
|
49
|
-
const OFFSET_MAX = 0.
|
|
50
|
+
const OFFSET_MAX = 0.2;
|
|
50
51
|
const HEAD_SHARE = 0.35;
|
|
51
52
|
// The inertia: the iris has a little mass, hung on a spring from the head, so
|
|
52
53
|
// her own motion — a bob, a step, a sway of the dance — leaves it behind and
|
|
@@ -56,11 +57,38 @@ const INERTIA_ZETA = 0.4;
|
|
|
56
57
|
const INERTIA_GAIN = 0.04;
|
|
57
58
|
// The jelly's give: the iris never moves, it swells. The lag and the inertia
|
|
58
59
|
// are how hard it is being knocked (in iris radii, as if it could move); that
|
|
59
|
-
// knock grows it a little and it
|
|
60
|
-
// of the blink's squash.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// knock grows it a little and it ripples back, as the squash does — the bounce on top
|
|
61
|
+
// of the blink's squash. Eased, not linear: the swell is PULSE_MAX·tanh(knock /
|
|
62
|
+
// PULSE_REF), so the small steady knocks of a dance still show while a jolt
|
|
63
|
+
// (play from a pause: the head going from still to moving at once) cannot
|
|
64
|
+
// swell it past the cap.
|
|
65
|
+
const PULSE_MAX = 0.07;
|
|
66
|
+
const PULSE_REF = 0.006;
|
|
67
|
+
// And the same knock squashes it as a blink does — nearly round, on
|
|
68
|
+
// the blink's spring — so her motion reads as plainly as a blink. Eased by the
|
|
69
|
+
// same curve, up to MOTION_SQUASH (a blink's opening gives about 0.2).
|
|
70
|
+
const MOTION_SQUASH = 0.18;
|
|
71
|
+
const PULSE_HZ = 2.0;
|
|
72
|
+
const PULSE_ZETA = 0.08;
|
|
73
|
+
// The idle taps: every 4–8 s, at uneven times, a knock into the
|
|
74
|
+
// same springs her motion drives — the iris dips, bounces and settles, then
|
|
75
|
+
// rests — the way a jelly on a table is never quite still. A steady sine was
|
|
76
|
+
// tried, as size (a zoom) and as wider-then-taller (queasy); a settling tap is
|
|
77
|
+
// what reads as jelly. A tap is a full KNOCK, exactly what her motion gives at
|
|
78
|
+
// its strongest — a gentler nudge was tried and went unnoticed — held for a
|
|
79
|
+
// moment and let go, so it rings through the same springs. The gaps come from
|
|
80
|
+
// a hash of the tap's count, so the scene clock alone decides them and an
|
|
81
|
+
// export taps where the screen did.
|
|
82
|
+
const IDLE_TAP_S = 0.12;
|
|
83
|
+
// How hard a tap knocks (motion's strongest is 1) and how far apart they fall:
|
|
84
|
+
// a full knock every 2–4 s read as the iris zooming constantly; rarer, a touch
|
|
85
|
+
// softer, each one reads as a ripple and the eye rests between.
|
|
86
|
+
const IDLE_TAP = 0.6;
|
|
87
|
+
const IDLE_GAP = [4, 8];
|
|
88
|
+
const idleGap = (n) => {
|
|
89
|
+
const x = Math.sin(n * 12.9898 + 78.233) * 43758.5453;
|
|
90
|
+
return IDLE_GAP[0] + (x - Math.floor(x)) * (IDLE_GAP[1] - IDLE_GAP[0]);
|
|
91
|
+
};
|
|
64
92
|
// Integration step, and the jump past which a delta is a seek, not time.
|
|
65
93
|
const SUBSTEP = 1 / 240;
|
|
66
94
|
const SEEK = 0.25;
|
|
@@ -78,30 +106,66 @@ const newState = () => ({
|
|
|
78
106
|
});
|
|
79
107
|
export class JellyEyes {
|
|
80
108
|
constructor(model, options) {
|
|
109
|
+
// Scene time stepped so far, and the idle taps: how many, and when the next.
|
|
110
|
+
this.clock = 0;
|
|
111
|
+
this.taps = 0;
|
|
112
|
+
this.nextTap = idleGap(0);
|
|
113
|
+
// The current tap's knock, 1 at the tap and fading over IDLE_TAP_S.
|
|
114
|
+
this.tapKnock = 0;
|
|
81
115
|
this.state = [newState(), newState()];
|
|
82
116
|
this.fresh = true;
|
|
83
117
|
this.inertia = { p: [0, 0, 0], v: [0, 0, 0], head: [0, 0, 0], offset: [0, 0] };
|
|
84
118
|
this.strength = clampStrength(options.strength);
|
|
119
|
+
this.idle = options.idle ?? true;
|
|
85
120
|
const bones = model.getSkeleton().bones;
|
|
86
121
|
const index = (name) => bones.findIndex((b) => b.name === name);
|
|
87
122
|
const both = index("両目");
|
|
88
123
|
const l = index("左目");
|
|
89
124
|
const r = index("右目");
|
|
90
|
-
this.eyeBones = [l >= 0 ? l : both, r >= 0 ? r : both];
|
|
91
125
|
this.headBone = index("頭");
|
|
92
126
|
const names = model.getMorphing().morphs.map((m) => m.name);
|
|
93
127
|
const find = (list) => list.map((n) => names.indexOf(n)).filter((i) => i >= 0);
|
|
94
128
|
this.morphs = [find(BOTH), find(LEFT), find(RIGHT)];
|
|
95
129
|
this.rest = findIrises(model, l, r, both);
|
|
130
|
+
// The bone that carries each iris, which may be under the eye bone.
|
|
131
|
+
this.eyeBones = [this.rest[0].bone, this.rest[1].bone];
|
|
132
|
+
this.shifts = [irisShifts(model, this.rest[0].verts), irisShifts(model, this.rest[1].verts)];
|
|
133
|
+
this.centre = [[...this.rest[0].centre], [...this.rest[1].centre]];
|
|
96
134
|
}
|
|
97
135
|
/** Advance both springs by dt seconds of scene time. */
|
|
98
136
|
step(dt, model) {
|
|
99
137
|
const weights = model.getEffectiveMorphWeights();
|
|
138
|
+
for (let e = 0; e < 2; e++) {
|
|
139
|
+
const c = this.centre[e];
|
|
140
|
+
const r = this.rest[e].centre;
|
|
141
|
+
c[0] = r[0];
|
|
142
|
+
c[1] = r[1];
|
|
143
|
+
c[2] = r[2];
|
|
144
|
+
for (const { morph, d } of this.shifts[e]) {
|
|
145
|
+
const w = weights[morph] ?? 0;
|
|
146
|
+
if (w === 0)
|
|
147
|
+
continue;
|
|
148
|
+
c[0] += d[0] * w;
|
|
149
|
+
c[1] += d[1] * w;
|
|
150
|
+
c[2] += d[2] * w;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
100
153
|
const sum = (list) => list.reduce((s, i) => s + (weights[i] ?? 0), 0);
|
|
101
154
|
const both = sum(this.morphs[0]);
|
|
102
155
|
const closure = [Math.min(1, both + sum(this.morphs[1])), Math.min(1, both + sum(this.morphs[2]))];
|
|
103
156
|
const head = this.headBone >= 0 ? model.getBoneWorldMatrixAt(this.headBone) : null;
|
|
104
157
|
const seek = this.fresh || dt > SEEK || dt < 0;
|
|
158
|
+
if (dt > 0)
|
|
159
|
+
this.clock += dt;
|
|
160
|
+
if (dt > 0)
|
|
161
|
+
this.tapKnock *= Math.exp(-dt / IDLE_TAP_S);
|
|
162
|
+
if (seek)
|
|
163
|
+
this.tapKnock = 0;
|
|
164
|
+
else if (this.idle && this.clock >= this.nextTap) {
|
|
165
|
+
this.tapKnock = IDLE_TAP;
|
|
166
|
+
this.taps++;
|
|
167
|
+
this.nextTap = this.clock + idleGap(this.taps);
|
|
168
|
+
}
|
|
105
169
|
this.stepInertia(dt, head, seek);
|
|
106
170
|
for (let e = 0; e < 2; e++) {
|
|
107
171
|
const s = this.state[e];
|
|
@@ -117,7 +181,8 @@ export class JellyEyes {
|
|
|
117
181
|
s.settled += (s.closure - s.settled) * Math.min(1, dt / CLOSURE_FOLLOW_S);
|
|
118
182
|
// Either way a lid moves squashes — opening too, which is the half of a
|
|
119
183
|
// blink you can see (closing, the lid hides the iris anyway).
|
|
120
|
-
const
|
|
184
|
+
const knock = seek ? 0 : Math.max(this.tapKnock, Math.tanh(Math.hypot(...this.offset(s, e)) / PULSE_REF));
|
|
185
|
+
const squashTarget = SQUASH_CLOSED * Math.abs(s.closure - s.settled) + MOTION_SQUASH * knock;
|
|
121
186
|
if (seek) {
|
|
122
187
|
s.squash = squashTarget;
|
|
123
188
|
s.squashV = 0;
|
|
@@ -130,7 +195,7 @@ export class JellyEyes {
|
|
|
130
195
|
}
|
|
131
196
|
const steps = Math.ceil(dt / SUBSTEP);
|
|
132
197
|
const h = steps > 0 ? dt / steps : 0;
|
|
133
|
-
const pulseTarget =
|
|
198
|
+
const pulseTarget = PULSE_MAX * knock;
|
|
134
199
|
for (let i = 0; i < steps; i++) {
|
|
135
200
|
s.pulseV += h * spring(s.pulse - pulseTarget, s.pulseV, PULSE_HZ, PULSE_ZETA);
|
|
136
201
|
s.pulse += h * s.pulseV;
|
|
@@ -150,17 +215,18 @@ export class JellyEyes {
|
|
|
150
215
|
}
|
|
151
216
|
/** Write jelly[0..5] into an ObjectLight copy (floats 48..71). */
|
|
152
217
|
write(o) {
|
|
153
|
-
const [L, R] = this.
|
|
154
|
-
o.set([L
|
|
218
|
+
const [L, R] = this.centre;
|
|
219
|
+
o.set([L[0], L[1], L[2], this.strength], 48);
|
|
155
220
|
// One scale bounce for both: the mean of the two eyes', so they swell together.
|
|
156
221
|
const pulse = ((this.state[0].pulse + this.state[1].pulse) / 2) * this.strength;
|
|
157
|
-
o.set([R
|
|
222
|
+
o.set([R[0], R[1], R[2], pulse], 52);
|
|
158
223
|
for (let e = 0; e < 2; e++) {
|
|
159
224
|
const s = this.state[e];
|
|
160
225
|
const rest = this.rest[e];
|
|
161
226
|
o.set([rest.radius[0], rest.radius[1], s.closure, this.eyeBones[e]], 56 + e * 4);
|
|
162
|
-
//
|
|
163
|
-
|
|
227
|
+
// Word 65 stays 0 (the iris holds its place; only scale); 66 carries the
|
|
228
|
+
// scene clock for the gem's twinkles, wrapped so a float keeps it exact.
|
|
229
|
+
o.set([s.squash * this.strength, 0, this.clock % 1000, s.shine], 64 + e * 4);
|
|
164
230
|
}
|
|
165
231
|
}
|
|
166
232
|
/** Where the iris sits off rest, in iris radii (right, up): the lag through
|
|
@@ -254,6 +320,71 @@ function look(eye, head, out) {
|
|
|
254
320
|
}
|
|
255
321
|
}
|
|
256
322
|
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
323
|
+
/**
|
|
324
|
+
* The iris out of everything one eye carries: grouped by material, the group
|
|
325
|
+
* whose centre is nearest the eye bone, and any other group centred on it
|
|
326
|
+
* (within its own size) — layers of the same eye. Without materials (or a
|
|
327
|
+
* bone), the set as it came.
|
|
328
|
+
*/
|
|
329
|
+
function irisOnly(model, set, bone) {
|
|
330
|
+
const materials = model.getMaterials?.() ?? [];
|
|
331
|
+
const { positions, indices } = model.getGeometry();
|
|
332
|
+
if (set.length < 3 || !bone || !materials.length || !indices.length)
|
|
333
|
+
return set;
|
|
334
|
+
const material = new Int32Array(positions.length / 3).fill(-1);
|
|
335
|
+
let start = 0;
|
|
336
|
+
materials.forEach((m, i) => {
|
|
337
|
+
for (let k = start; k < start + m.vertexCount; k++)
|
|
338
|
+
if (material[indices[k]] < 0)
|
|
339
|
+
material[indices[k]] = i;
|
|
340
|
+
start += m.vertexCount;
|
|
341
|
+
});
|
|
342
|
+
const groups = new Map();
|
|
343
|
+
for (const v of set) {
|
|
344
|
+
const g = groups.get(material[v]);
|
|
345
|
+
if (g)
|
|
346
|
+
g.push(v);
|
|
347
|
+
else
|
|
348
|
+
groups.set(material[v], [v]);
|
|
349
|
+
}
|
|
350
|
+
if (groups.size < 2)
|
|
351
|
+
return set;
|
|
352
|
+
const boxes = [...groups.values()].map((vs) => {
|
|
353
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
354
|
+
for (const v of vs) {
|
|
355
|
+
minX = Math.min(minX, positions[v * 3]);
|
|
356
|
+
maxX = Math.max(maxX, positions[v * 3]);
|
|
357
|
+
minY = Math.min(minY, positions[v * 3 + 1]);
|
|
358
|
+
maxY = Math.max(maxY, positions[v * 3 + 1]);
|
|
359
|
+
}
|
|
360
|
+
return { vs, x: (minX + maxX) / 2, y: (minY + maxY) / 2, size: Math.max(maxX - minX, maxY - minY) / 2 };
|
|
361
|
+
});
|
|
362
|
+
const near = boxes.reduce((a, b) => (Math.hypot(b.x - bone[0], b.y - bone[1]) < Math.hypot(a.x - bone[0], a.y - bone[1]) ? b : a));
|
|
363
|
+
return boxes.filter((b) => Math.hypot(b.x - near.x, b.y - near.y) <= near.size).flatMap((b) => b.vs);
|
|
364
|
+
}
|
|
365
|
+
/** Every vertex morph that moves this iris, with the mean offset it gives it. */
|
|
366
|
+
function irisShifts(model, verts) {
|
|
367
|
+
if (!verts.length)
|
|
368
|
+
return [];
|
|
369
|
+
const inIris = new Set(verts);
|
|
370
|
+
const out = [];
|
|
371
|
+
model.getMorphing().morphs.forEach((m, morph) => {
|
|
372
|
+
const d = [0, 0, 0];
|
|
373
|
+
for (const o of m.vertexOffsets ?? []) {
|
|
374
|
+
if (!inIris.has(o.vertexIndex))
|
|
375
|
+
continue;
|
|
376
|
+
d[0] += o.positionOffset[0];
|
|
377
|
+
d[1] += o.positionOffset[1];
|
|
378
|
+
d[2] += o.positionOffset[2];
|
|
379
|
+
}
|
|
380
|
+
d[0] /= verts.length;
|
|
381
|
+
d[1] /= verts.length;
|
|
382
|
+
d[2] /= verts.length;
|
|
383
|
+
if (Math.hypot(...d) > 1e-6)
|
|
384
|
+
out.push({ morph, d });
|
|
385
|
+
});
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
257
388
|
/**
|
|
258
389
|
* Each iris in bind space: the vertices the eye bones carry are exactly the
|
|
259
390
|
* part of the eye that turns to look — the iris and pupil — so their centroid
|
|
@@ -280,35 +411,83 @@ function findIrises(model, l, r, both) {
|
|
|
280
411
|
const leftSign = lHead && rHead ? Math.sign(lHead[0] - rHead[0]) || 1 : 1;
|
|
281
412
|
const { positions } = model.getGeometry();
|
|
282
413
|
const { joints, weights } = model.getSkinning();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
414
|
+
// Which eye each bone turns with: an eye bone, or any bone under one. Rigs
|
|
415
|
+
// differ in which carries the iris — some hang it on the eye bone itself,
|
|
416
|
+
// some on its tip (左目先/右目先), which turns with it all the same.
|
|
417
|
+
const owner = bones.map((_, i) => {
|
|
418
|
+
for (let b = i; b >= 0; b = bones[b].parentIndex) {
|
|
419
|
+
if (b === l)
|
|
420
|
+
return 0;
|
|
421
|
+
if (b === r)
|
|
422
|
+
return 1;
|
|
423
|
+
if (b === both)
|
|
424
|
+
return 2;
|
|
425
|
+
}
|
|
426
|
+
return -1;
|
|
427
|
+
});
|
|
428
|
+
// Per side, the vertices and how much iris weight each bone carries: the
|
|
429
|
+
// heaviest is the one the vertex shader scales by (one bone per eye).
|
|
430
|
+
//
|
|
431
|
+
// The per-eye trees first. 両目 is a fallback for rigs that carry both irises
|
|
432
|
+
// on it alone: where 左目/右目 do carry them, anything hung on 両目 is
|
|
433
|
+
// something else that turns with the eyes — an expression overlay above them
|
|
434
|
+
// (blush lines, sparkles) — and split by side it stretches each "iris" past
|
|
435
|
+
// any iris, so the size check rejects both and the eyes never move.
|
|
436
|
+
const gather = (useBoth) => {
|
|
437
|
+
const sets = [[], []];
|
|
438
|
+
const n = positions.length / 3;
|
|
439
|
+
for (let v = 0; v < n; v++) {
|
|
440
|
+
const w3 = [0, 0, 0];
|
|
441
|
+
for (let k = 0; k < 4; k++) {
|
|
442
|
+
const o = owner[joints[v * 4 + k]];
|
|
443
|
+
if (o >= 0)
|
|
444
|
+
w3[o] += weights[v * 4 + k];
|
|
445
|
+
}
|
|
446
|
+
let side;
|
|
447
|
+
if (w3[0] >= 128)
|
|
448
|
+
side = 0;
|
|
449
|
+
else if (w3[1] >= 128)
|
|
450
|
+
side = 1;
|
|
451
|
+
else if (useBoth && w3[2] >= 128)
|
|
452
|
+
side = Math.sign(positions[v * 3]) === leftSign ? 0 : 1;
|
|
453
|
+
else
|
|
454
|
+
continue;
|
|
455
|
+
sets[side].push(v);
|
|
456
|
+
}
|
|
457
|
+
return sets;
|
|
458
|
+
};
|
|
459
|
+
let gathered = gather(false);
|
|
460
|
+
if (gathered[0].length < 3 && gathered[1].length < 3)
|
|
461
|
+
gathered = gather(true);
|
|
462
|
+
// Of what turns with an eye, the iris is the mesh nearest its bone, with the
|
|
463
|
+
// layers stacked on it (an eye white, a highlight sheet over the same disc).
|
|
464
|
+
// A mesh elsewhere that the eye bones also carry — an expression overlay
|
|
465
|
+
// above the eyes — is left out, or it stretches the "iris" past any iris.
|
|
466
|
+
const sets = [irisOnly(model, gathered[0], lHead), irisOnly(model, gathered[1], rHead)];
|
|
467
|
+
const carried = [new Map(), new Map()];
|
|
468
|
+
for (const side of [0, 1]) {
|
|
469
|
+
for (const v of sets[side]) {
|
|
470
|
+
for (let k = 0; k < 4; k++) {
|
|
471
|
+
const j = joints[v * 4 + k];
|
|
472
|
+
if (owner[j] >= 0)
|
|
473
|
+
carried[side].set(j, (carried[side].get(j) ?? 0) + weights[v * 4 + k]);
|
|
474
|
+
}
|
|
298
475
|
}
|
|
299
|
-
if (wl >= 128)
|
|
300
|
-
sets[0].push(v);
|
|
301
|
-
else if (wr >= 128)
|
|
302
|
-
sets[1].push(v);
|
|
303
|
-
else if (wb >= 128)
|
|
304
|
-
sets[Math.sign(positions[v * 3]) === leftSign ? 0 : 1].push(v);
|
|
305
476
|
}
|
|
477
|
+
const carrier = (side) => {
|
|
478
|
+
let best = -1;
|
|
479
|
+
let most = 0;
|
|
480
|
+
for (const [j, w] of carried[side])
|
|
481
|
+
if (w > most)
|
|
482
|
+
[best, most] = [j, w];
|
|
483
|
+
return best;
|
|
484
|
+
};
|
|
306
485
|
const apart = lHead && rHead ? Math.hypot(lHead[0] - rHead[0], lHead[1] - rHead[1]) : 0;
|
|
307
486
|
const fallbackRadius = apart > 0 ? apart * 0.22 : 0.3;
|
|
308
|
-
const rest = (set, bone, side) => {
|
|
487
|
+
const rest = (set, bone, side, carrierBone) => {
|
|
309
488
|
if (set.length < 3) {
|
|
310
489
|
const c = bone ?? [side * fallbackRadius * 2, 0, 0];
|
|
311
|
-
return { centre: c, radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2 };
|
|
490
|
+
return { centre: c, radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1 };
|
|
312
491
|
}
|
|
313
492
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, z = 0;
|
|
314
493
|
for (const v of set) {
|
|
@@ -326,7 +505,7 @@ function findIrises(model, l, r, both) {
|
|
|
326
505
|
const rx = (maxX - minX) / 2;
|
|
327
506
|
const ry = (maxY - minY) / 2;
|
|
328
507
|
if (apart > 0 && (rx > apart * 0.6 || ry > apart * 0.8)) {
|
|
329
|
-
return { centre: bone ?? [side * fallbackRadius * 2, 0, 0], radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2 };
|
|
508
|
+
return { centre: bone ?? [side * fallbackRadius * 2, 0, 0], radius: [fallbackRadius, fallbackRadius], lever: fallbackRadius * 2, verts: [], bone: -1 };
|
|
330
509
|
}
|
|
331
510
|
const centre = [(minX + maxX) / 2, (minY + maxY) / 2, z / set.length];
|
|
332
511
|
const radius = [Math.max(rx, 1e-3), Math.max(ry, 1e-3)];
|
|
@@ -334,7 +513,7 @@ function findIrises(model, l, r, both) {
|
|
|
334
513
|
// still turns its eyes visibly, so read it as a typical eyeball's.
|
|
335
514
|
const r = (radius[0] + radius[1]) / 2;
|
|
336
515
|
const lever = Math.max(bone ? Math.hypot(centre[0] - bone[0], centre[1] - bone[1], centre[2] - bone[2]) : 0, r * 1.5);
|
|
337
|
-
return { centre, radius, lever };
|
|
516
|
+
return { centre, radius, lever, verts: set, bone: carrierBone };
|
|
338
517
|
};
|
|
339
|
-
return [rest(sets[0], lHead, leftSign), rest(sets[1], rHead, -leftSign)];
|
|
518
|
+
return [rest(sets[0], lHead, leftSign, carrier(0)), rest(sets[1], rHead, -leftSign, carrier(1))];
|
|
340
519
|
}
|
|
@@ -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,QAyZhC,CAAC;AAwHF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC6H,CAAA"}
|
|
@@ -219,7 +219,7 @@ fn _rzCastShadow(wp: vec3f) -> f32 {
|
|
|
219
219
|
// [1] right iris centre, scale bounce (both eyes)
|
|
220
220
|
// [2] left iris half extents, closure, eye bone (−1 none)
|
|
221
221
|
// [3] the right's
|
|
222
|
-
// [4] left squash, 0,
|
|
222
|
+
// [4] left squash, 0, clock, shine [5] the right's
|
|
223
223
|
struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 6> }
|
|
224
224
|
@group(1) @binding(1) var<uniform> objectLight: ObjectLight;
|
|
225
225
|
|
|
@@ -231,12 +231,14 @@ struct RzJelly {
|
|
|
231
231
|
w: f32,
|
|
232
232
|
closure: f32,
|
|
233
233
|
shine: f32,
|
|
234
|
+
/** Scene seconds (wrapped), for the twinkles. */
|
|
235
|
+
clock: f32,
|
|
234
236
|
};
|
|
235
237
|
|
|
236
238
|
/**
|
|
237
239
|
* Where the jelly puts an iris vertex. The iris is the part of the eye the eye
|
|
238
240
|
* bones carry, so it is moved as a whole, in bind space before skinning:
|
|
239
|
-
*
|
|
241
|
+
* shrunk about its centre while a lid moves, a little more down than across,
|
|
240
242
|
* grown past rest as it springs back, and swelling as a turn or her motion
|
|
241
243
|
* knocks it — scale only, in place, inside the white, which stays where it is. Vertices take it by
|
|
242
244
|
* their weight on the eye bones (jelly[2].w, jelly[3].w), so nothing else on
|
|
@@ -256,10 +258,11 @@ fn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {
|
|
|
256
258
|
if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }
|
|
257
259
|
if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }
|
|
258
260
|
let st = j[4u + e];
|
|
259
|
-
// The
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
261
|
+
// The squash, nearly round: it shrinks both ways, a little more down than
|
|
262
|
+
// across, so the iris keeps its circle and only leans oval; and the bounce
|
|
263
|
+
// both eyes share (jelly[1].w). Held under 1.08 so a swell cannot rise
|
|
264
|
+
// past the upper lid.
|
|
265
|
+
let k = clamp(vec2f(1.0 - 0.8 * st.x, 1.0 - st.x) + j[1].w, vec2f(0.78, 0.75), vec2f(1.08, 1.08));
|
|
263
266
|
let moved = c.xy + (p.xy - c.xy) * k;
|
|
264
267
|
out = vec3f(mix(p.xy, moved, w), p.z);
|
|
265
268
|
}
|
|
@@ -279,7 +282,7 @@ fn rzJellyEye(rp: vec3f) -> RzJelly {
|
|
|
279
282
|
let a = select(qL, qR, right);
|
|
280
283
|
let st = select(j[4], j[5], right);
|
|
281
284
|
let w = (1.0 - smoothstep(0.85, 1.0, length(a))) * select(0.0, 1.0, j[0].w > 0.0);
|
|
282
|
-
return RzJelly(a, w, select(j[2].z, j[3].z, right), st.w);
|
|
285
|
+
return RzJelly(a, w, select(j[2].z, j[3].z, right), st.w, j[4].z);
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
/**
|
|
@@ -301,37 +304,85 @@ fn rzJellyGlow(jj: RzJelly, tex: vec3f, alpha: f32, v: vec3f, l: vec3f, sun: vec
|
|
|
301
304
|
fwd = -normalize(hm[2].xyz);
|
|
302
305
|
}
|
|
303
306
|
let h = normalize(l + v);
|
|
304
|
-
// Held
|
|
305
|
-
//
|
|
307
|
+
// Held in the middle and lower iris: the top of an iris sits under the upper
|
|
308
|
+
// lid, and a glint there — or its bloom halo — pokes out over it.
|
|
306
309
|
var g = vec2f(dot(h, right), dot(h, up)) * 0.6;
|
|
307
|
-
g = g * min(1.0, 0.
|
|
310
|
+
g = g * min(1.0, 0.25 / max(length(g), 1e-6)) - vec2f(0.0, 0.1);
|
|
311
|
+
g.y = min(g.y, 0.0);
|
|
308
312
|
let aa = max(length(fwidth(jj.src)), 1e-4);
|
|
309
|
-
|
|
313
|
+
// A star, not only a disc: the disc with two thin rays through it, the
|
|
314
|
+
// anime sparkle.
|
|
315
|
+
let dg = jj.src - g;
|
|
316
|
+
let rays = max(0.0, 1.0 - abs(dg.y) / 0.018) * (1.0 - smoothstep(0.0, 0.34, abs(dg.x)))
|
|
317
|
+
+ max(0.0, 1.0 - abs(dg.x) / 0.018) * (1.0 - smoothstep(0.0, 0.26, abs(dg.y)));
|
|
318
|
+
let glint = max(1.0 - smoothstep(0.13 - aa, 0.13 + aa, length(dg)), rays * 0.8);
|
|
310
319
|
let second = 1.0 - smoothstep(0.07 - aa, 0.07 + aa, distance(jj.src, -0.55 * g + vec2f(0.1, -0.12)));
|
|
311
320
|
let facing = saturate(dot(fwd, v) * 2.0);
|
|
312
321
|
let key = clamp(dot(sun, vec3f(0.2126, 0.7152, 0.0722)), 0.3, 2.0);
|
|
313
322
|
let lit = facing * key * (0.35 + 0.65 * shadow);
|
|
314
323
|
// HDR, well past 1, so bloom takes them as the hard sparkle of a gem.
|
|
315
|
-
|
|
316
|
-
|
|
324
|
+
// Bright enough for bloom to catch, not so bright its halo spreads past the
|
|
325
|
+
// iris onto the lid.
|
|
326
|
+
let glints = (glint + 0.8 * second) * lit * (1.8 + 0.8 * jj.shine);
|
|
327
|
+
// Gone as the lid starts down, not only once shut: a half-closed smile puts
|
|
328
|
+
// the lid right over where the glint sits.
|
|
329
|
+
let open = 1.0 - smoothstep(0.1, 0.45, jj.closure);
|
|
317
330
|
// Only where the eye's own picture is (its alpha), never on the iris's top
|
|
318
331
|
// rim, and gone as the lid comes down.
|
|
319
|
-
let
|
|
332
|
+
let solid = smoothstep(0.5, 0.9, alpha);
|
|
333
|
+
let onEye = solid * (1.0 - smoothstep(0.05, 0.35, jj.src.y));
|
|
320
334
|
// Light through the jelly: the light enters where the glint is and comes out
|
|
321
335
|
// the far side, so the iris glows in its own colour in a crescent opposite
|
|
322
336
|
// the glint — the lit-from-within look of a gem, or a jelly.
|
|
323
337
|
let r = length(jj.src);
|
|
324
|
-
|
|
338
|
+
// Out the LOWER iris, always — the anime gem's bright underside — leaning
|
|
339
|
+
// away from the glint; never up, where its bloom would spill on the lid.
|
|
340
|
+
let away = normalize(vec2f(-g.x * 0.6, -1.0));
|
|
325
341
|
let crescent = smoothstep(0.15, 0.7, dot(jj.src, away)) * (1.0 - smoothstep(0.8, 1.0, r));
|
|
326
|
-
let through = tex * crescent * (
|
|
342
|
+
let through = tex * crescent * (2.2 + 1.0 * jj.shine) * lit;
|
|
327
343
|
// And the colour runs deeper: the iris's own hue pushed, never its greys.
|
|
328
344
|
let lum = dot(tex, vec3f(0.2126, 0.7152, 0.0722));
|
|
329
|
-
let vivid = max(tex - vec3f(lum), vec3f(0.0)) *
|
|
330
|
-
|
|
345
|
+
let vivid = max(tex - vec3f(lum), vec3f(0.0)) * 1.2;
|
|
346
|
+
// The glass over it: a broad soft sheen across the upper iris, under the
|
|
347
|
+
// lid line, the dome of a bead catching the room.
|
|
348
|
+
let dome = (1.0 - smoothstep(0.0, 0.5, distance(jj.src * vec2f(0.8, 1.4), vec2f(0.0, 0.2)))) * 0.45 * lit;
|
|
349
|
+
// Everything that blooms keeps to the lower iris, so no halo reaches the lid.
|
|
350
|
+
let lower = 1.0 - smoothstep(-0.05, 0.3, jj.src.y);
|
|
351
|
+
// The cut: soft facets fanning across the lower iris, light split as a cut
|
|
352
|
+
// stone splits it, turning slowly.
|
|
353
|
+
let ang = atan2(jj.src.y, jj.src.x);
|
|
354
|
+
let facets = pow(0.5 + 0.5 * cos(ang * 7.0 + jj.clock * 0.35), 6.0)
|
|
355
|
+
* smoothstep(0.3, 0.75, r) * (1.0 - smoothstep(0.82, 0.97, r)) * lower;
|
|
356
|
+
// The rim: a band of its own colour just inside the iris edge, HDR, which
|
|
357
|
+
// bloom spreads into a glow around the stone.
|
|
358
|
+
let rim = smoothstep(0.7, 0.84, r) * (1.0 - smoothstep(0.86, 0.97, r)) * lower;
|
|
359
|
+
// Twinkles: three specks in the lower iris, each flashing now and then on
|
|
360
|
+
// the scene clock.
|
|
361
|
+
var twinkle = 0.0;
|
|
362
|
+
for (var i = 0; i < 3; i++) {
|
|
363
|
+
let fi = f32(i);
|
|
364
|
+
let at = vec2f(0.42 * cos(3.6 + fi * 1.1), -0.25 - 0.18 * fi + 0.1 * sin(fi * 2.3));
|
|
365
|
+
let flash = pow(max(0.0, sin(jj.clock * (1.3 + 0.37 * fi) + fi * 2.1)), 12.0);
|
|
366
|
+
twinkle += (1.0 - smoothstep(0.0, 0.05 + aa, distance(jj.src, at))) * flash;
|
|
367
|
+
}
|
|
368
|
+
let sparkle = (vec3f(glints + twinkle * 2.0) * onEye
|
|
369
|
+
+ tex * facets * 2.5 * lit * solid * (1.0 + 0.6 * jj.shine));
|
|
370
|
+
// The soft light (sheen, glow through, colour) needs only the eye's picture.
|
|
371
|
+
let gem = (sparkle + (vec3f(dome) + through + vivid) * solid) * open * jj.w;
|
|
372
|
+
// The edge, which jj.w (fading out from 0.85) would hide: the glowing rim
|
|
373
|
+
// band, and a thin glossy line right at the border — light running round
|
|
374
|
+
// the stone's edge — all the way round, bright below and dim above, where a
|
|
375
|
+
// halo would reach the lid. Up to the mesh's edge and no further.
|
|
376
|
+
let on = select(0.0, 1.0, strength > 0.0) * (1.0 - smoothstep(1.0 - aa, 1.0, r));
|
|
377
|
+
let edgeLine = smoothstep(0.92, 0.955, r) * (1.0 - smoothstep(0.965, 0.995, r));
|
|
378
|
+
let edgeLit = mix(0.3, 1.0, lower) * lit * (1.0 + 0.6 * jj.shine);
|
|
379
|
+
let edge = (tex * rim * 2.0 + vec3f(rim * 0.25)) * lit * (1.0 + 0.6 * jj.shine)
|
|
380
|
+
+ (vec3f(0.55) + tex * 0.8) * edgeLine * edgeLit;
|
|
381
|
+
let border = edge * solid * open * on;
|
|
331
382
|
// The whole eye shines a little brighter, more while it wobbles — not just
|
|
332
383
|
// the iris, so this one is not held to it (jj.w).
|
|
333
|
-
let lift = tex * (0.
|
|
334
|
-
return (gem + lift) * min(strength, 1.0);
|
|
384
|
+
let lift = tex * (0.25 + 0.35 * jj.shine) * select(0.0, 1.0, strength > 0.0);
|
|
385
|
+
return (gem + border + lift) * min(strength, 1.0);
|
|
335
386
|
}
|
|
336
387
|
|
|
337
388
|
/** The light arriving at a surface of this object facing n: its own ambient
|