@pieai/swimmer-avatar-kit 0.1.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/LICENSE +21 -0
- package/NOTICE.md +23 -0
- package/README.md +125 -0
- package/UPSTREAM.md +47 -0
- package/dist/avatar.d.ts +2 -0
- package/dist/avatar.js +69 -0
- package/dist/avatar.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/materials.d.ts +20 -0
- package/dist/materials.js +61 -0
- package/dist/materials.js.map +1 -0
- package/dist/react-three-fiber.d.ts +16 -0
- package/dist/react-three-fiber.js +52 -0
- package/dist/react-three-fiber.js.map +1 -0
- package/dist/recipe.d.ts +18 -0
- package/dist/recipe.js +118 -0
- package/dist/recipe.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +102 -0
- package/upstream-lock.json +38 -0
- package/vendor/kindergrimm/LICENSE +24 -0
- package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
- package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
- package/vendor/kindergrimm/src/gloss/gface.js +226 -0
- package/vendor/kindergrimm/src/gloss/gform.js +201 -0
- package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
- package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
- package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
- package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
- package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
- package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
- package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
- package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
- package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
- package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
- package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
- package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
- package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
- package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
- package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
- package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
- package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
- package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
- package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
- package/vendor/kindergrimm/src/gloss/grig.js +286 -0
- package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
- package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
- package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
- package/vendor/kindergrimm/src/rng.d.ts +11 -0
- package/vendor/kindergrimm/src/rng.js +27 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// ---------------------------------------------------------------
|
|
2
|
+
// THE HAND — `sketch.js` draws, `carve.js` carves, this one CUTS AND
|
|
3
|
+
// STAMPS. Everything in this lab is solid geometry: no fields, no
|
|
4
|
+
// sampling grid, nothing to resolve. Two things get made here.
|
|
5
|
+
//
|
|
6
|
+
// A SOLID — the body. A sphere, scaled.
|
|
7
|
+
// A PLATE — every face feature. An outline, extruded, bevelled.
|
|
8
|
+
//
|
|
9
|
+
// The plate is the whole face system, and it is one shape of thing on
|
|
10
|
+
// purpose: a flat front with a rounded rim. That rim is the cartoon
|
|
11
|
+
// read — it is what catches the studio highlight around the edge of an
|
|
12
|
+
// eye, and it is why a feature looks moulded into the character rather than
|
|
13
|
+
// printed on it.
|
|
14
|
+
//
|
|
15
|
+
// AUTHORING RULE: a plate's front crest sits at z = 0 and its body
|
|
16
|
+
// runs BACKWARD. Placed on the skin it is therefore flush, and `proud`
|
|
17
|
+
// is the single number that lifts it out. Because the body runs
|
|
18
|
+
// backward and the sphere curves away underneath, a plate can never
|
|
19
|
+
// float off the silhouette.
|
|
20
|
+
//
|
|
21
|
+
// Parts never import this file — they never touch three.js. They name
|
|
22
|
+
// an OUTLINE and hand over numbers, and the rig stamps them here.
|
|
23
|
+
// ---------------------------------------------------------------
|
|
24
|
+
import * as THREE from 'three';
|
|
25
|
+
|
|
26
|
+
const SEG = 44;
|
|
27
|
+
|
|
28
|
+
// THE DETAIL KNOB — one number, set once by the page, that every
|
|
29
|
+
// segment count here is scaled by.
|
|
30
|
+
//
|
|
31
|
+
// It exists because the lab and a GAME want opposite things and both
|
|
32
|
+
// are right. On `gloss.html` a character is the subject, there is exactly
|
|
33
|
+
// one of it, and it is four hundred pixels across: every segment is
|
|
34
|
+
// worth having. In `marbles.html` there are a dozen on a table at
|
|
35
|
+
// sixty pixels each, and at full detail one of them was a quarter of a
|
|
36
|
+
// million vertices and twenty-eight milliseconds to build — which is
|
|
37
|
+
// half a frame's budget, per marble, for a resolution that lands
|
|
38
|
+
// entirely between two pixels.
|
|
39
|
+
//
|
|
40
|
+
// It is a SCALE rather than a set of alternative numbers, so the lab's
|
|
41
|
+
// proportions survive it, and every count keeps a floor: a body below
|
|
42
|
+
// about fourteen segments stops being round, and a bevel below two
|
|
43
|
+
// stops being a bevel, which is the whole cartoon read.
|
|
44
|
+
let DETAIL = 1;
|
|
45
|
+
|
|
46
|
+
/** 1 is the lab. A game asks for less; nothing else should. */
|
|
47
|
+
export function setGlossDetail(k) { DETAIL = Math.max(.2, Math.min(1, k)); }
|
|
48
|
+
export const glossDetail = () => DETAIL;
|
|
49
|
+
const seg = (n, min) => Math.max(min, Math.round(n * DETAIL));
|
|
50
|
+
|
|
51
|
+
// ---- outlines ---------------------------------------------------------
|
|
52
|
+
// Every one is centred on the origin and takes HALF-extents.
|
|
53
|
+
|
|
54
|
+
function ellipse(w, h) {
|
|
55
|
+
const s = new THREE.Shape();
|
|
56
|
+
s.absellipse(0, 0, w, h, 0, Math.PI * 2, false, 0);
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ring(w, h, thick) {
|
|
61
|
+
const s = ellipse(w, h);
|
|
62
|
+
const hole = new THREE.Path();
|
|
63
|
+
hole.absellipse(0, 0, w * (1 - thick), h * (1 - thick), 0, Math.PI * 2, true, 0);
|
|
64
|
+
s.holes.push(hole);
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function roundedRect(w, h, r) {
|
|
69
|
+
const s = new THREE.Shape();
|
|
70
|
+
r = Math.min(r, w, h);
|
|
71
|
+
s.moveTo(-w + r, -h);
|
|
72
|
+
s.lineTo(w - r, -h); s.absarc(w - r, -h + r, r, -Math.PI / 2, 0);
|
|
73
|
+
s.lineTo(w, h - r); s.absarc(w - r, h - r, r, 0, Math.PI / 2);
|
|
74
|
+
s.lineTo(-w + r, h); s.absarc(-w + r, h - r, r, Math.PI / 2, Math.PI);
|
|
75
|
+
s.lineTo(-w, -h + r); s.absarc(-w + r, -h + r, r, Math.PI, Math.PI * 1.5);
|
|
76
|
+
return s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** a rounded rectangle with a rounded rectangle punched out of it —
|
|
80
|
+
* square spectacles, and anything else that is a frame rather than a
|
|
81
|
+
* filled shape. `ring` is its elliptical twin. */
|
|
82
|
+
function rectRing(w, h, r, thick) {
|
|
83
|
+
const s = roundedRect(w, h, r);
|
|
84
|
+
const iw = w * (1 - thick), ih = h * (1 - thick);
|
|
85
|
+
const ir = Math.max(1e-4, Math.min(r * (1 - thick), iw, ih));
|
|
86
|
+
const hole = new THREE.Path();
|
|
87
|
+
// wound the OTHER way round from `roundedRect`: a hole has to run
|
|
88
|
+
// against its shape or three fills it in as solid
|
|
89
|
+
hole.moveTo(-iw + ir, -ih);
|
|
90
|
+
hole.absarc(-iw + ir, -ih + ir, ir, Math.PI * 1.5, Math.PI, true);
|
|
91
|
+
hole.lineTo(-iw, ih - ir);
|
|
92
|
+
hole.absarc(-iw + ir, ih - ir, ir, Math.PI, Math.PI / 2, true);
|
|
93
|
+
hole.lineTo(iw - ir, ih);
|
|
94
|
+
hole.absarc(iw - ir, ih - ir, ir, Math.PI / 2, 0, true);
|
|
95
|
+
hole.lineTo(iw, -ih + ir);
|
|
96
|
+
hole.absarc(iw - ir, -ih + ir, ir, 0, -Math.PI / 2, true);
|
|
97
|
+
s.holes.push(hole);
|
|
98
|
+
return s;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** the four-point sparkle. The sides are pulled IN toward the centre —
|
|
102
|
+
* that concavity is the entire reason it reads as a star and not a
|
|
103
|
+
* diamond, and `pinch` is how deep the waist cuts. */
|
|
104
|
+
function sparkle(w, h, pinch) {
|
|
105
|
+
const s = new THREE.Shape();
|
|
106
|
+
s.moveTo(0, h);
|
|
107
|
+
s.quadraticCurveTo(w * pinch, h * pinch, w, 0);
|
|
108
|
+
s.quadraticCurveTo(w * pinch, -h * pinch, 0, -h);
|
|
109
|
+
s.quadraticCurveTo(-w * pinch, -h * pinch, -w, 0);
|
|
110
|
+
s.quadraticCurveTo(-w * pinch, h * pinch, 0, h);
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function star5(w, h) {
|
|
115
|
+
const s = new THREE.Shape();
|
|
116
|
+
for (let i = 0; i < 10; i++) {
|
|
117
|
+
const a = -Math.PI / 2 + i * Math.PI / 5;
|
|
118
|
+
const r = i % 2 ? .46 : 1;
|
|
119
|
+
const x = Math.cos(a) * w * r, y = -Math.sin(a) * h * r;
|
|
120
|
+
i === 0 ? s.moveTo(x, y) : s.lineTo(x, y);
|
|
121
|
+
}
|
|
122
|
+
s.closePath();
|
|
123
|
+
return s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function heart(w, h) {
|
|
127
|
+
const s = new THREE.Shape();
|
|
128
|
+
s.moveTo(0, -h);
|
|
129
|
+
s.bezierCurveTo(-w * 1.25, h * .18, -w * .62, h * 1.28, 0, h * .46);
|
|
130
|
+
s.bezierCurveTo(w * .62, h * 1.28, w * 1.25, h * .18, 0, -h);
|
|
131
|
+
return s;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** an X: a plus polygon turned 45°. The bevel rounds its corners. */
|
|
135
|
+
function cross(w, arm) {
|
|
136
|
+
const c = Math.SQRT1_2;
|
|
137
|
+
const pts = [[arm, arm], [w, arm], [w, -arm], [arm, -arm], [arm, -w], [-arm, -w],
|
|
138
|
+
[-arm, -arm], [-w, -arm], [-w, arm], [-arm, arm], [-arm, w], [arm, w]];
|
|
139
|
+
const s = new THREE.Shape();
|
|
140
|
+
pts.forEach(([x, y], i) => {
|
|
141
|
+
const px = (x - y) * c, py = (x + y) * c;
|
|
142
|
+
i === 0 ? s.moveTo(px, py) : s.lineTo(px, py);
|
|
143
|
+
});
|
|
144
|
+
s.closePath();
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** a polar polygon: `r(θ)` sampled round once. Every lobed outline in
|
|
149
|
+
* the catalogue is one of these with a different harmonic. */
|
|
150
|
+
function polar(w, h, n, fn) {
|
|
151
|
+
const s = new THREE.Shape();
|
|
152
|
+
for (let i = 0; i <= n; i++) {
|
|
153
|
+
const a = i / n * Math.PI * 2, r = fn(a);
|
|
154
|
+
const x = Math.cos(a) * w * r, y = Math.sin(a) * h * r;
|
|
155
|
+
i === 0 ? s.moveTo(x, y) : s.lineTo(x, y);
|
|
156
|
+
}
|
|
157
|
+
s.closePath();
|
|
158
|
+
return s;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** a daisy. `petals` lobes from one absolute cosine. */
|
|
162
|
+
function flower(w, h, petals, amp) {
|
|
163
|
+
return polar(w, h, 132, a => 1 - amp + amp * Math.abs(Math.cos(petals * a / 2)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** an organic lump — fixed harmonics, so it is the SAME lump every
|
|
167
|
+
* rebuild. A wobble rolled per frame is the drawn rig's line boil,
|
|
168
|
+
* and a solid has no business shimmering. */
|
|
169
|
+
function wobble(w, h) {
|
|
170
|
+
return polar(w, h, 108, a => 1 + .13 * Math.cos(3 * a + .7) + .07 * Math.sin(5 * a));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** a moon: one bulging arc out and a shallower one back. `bite` is how
|
|
174
|
+
* deep the inner curve cuts, so 0 is a disc and 1 is a hair. */
|
|
175
|
+
function crescent(w, h, bite) {
|
|
176
|
+
const s = new THREE.Shape();
|
|
177
|
+
const o = w * 1.33, i = o * (1 - bite);
|
|
178
|
+
s.moveTo(0, h);
|
|
179
|
+
s.bezierCurveTo(-o, h * .55, -o, -h * .55, 0, -h);
|
|
180
|
+
s.bezierCurveTo(-i, -h * .5, -i, h * .5, 0, h);
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function triangle(w, h, flip) {
|
|
185
|
+
const s = new THREE.Shape();
|
|
186
|
+
const d = flip ? -1 : 1;
|
|
187
|
+
s.moveTo(0, -h * d); s.lineTo(w, h * d); s.lineTo(-w, h * d);
|
|
188
|
+
s.closePath();
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- bands ------------------------------------------------------------
|
|
193
|
+
// A mouth, a brow and a closed lid are all the SAME object: a stroke of
|
|
194
|
+
// constant width along a centreline. One ribbon function and a table of
|
|
195
|
+
// centrelines gives the whole family, so adding a mouth shape is adding
|
|
196
|
+
// a curve, not a polygon.
|
|
197
|
+
|
|
198
|
+
function ribbon(pts, tube) {
|
|
199
|
+
const n = pts.length, nor = [];
|
|
200
|
+
for (let i = 0; i < n; i++) {
|
|
201
|
+
const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
|
|
202
|
+
let dx = b[0] - a[0], dy = b[1] - a[1];
|
|
203
|
+
const l = Math.hypot(dx, dy) || 1;
|
|
204
|
+
nor.push([-dy / l, dx / l]);
|
|
205
|
+
}
|
|
206
|
+
const s = new THREE.Shape();
|
|
207
|
+
s.moveTo(pts[0][0] + nor[0][0] * tube, pts[0][1] + nor[0][1] * tube);
|
|
208
|
+
for (let i = 1; i < n; i++) s.lineTo(pts[i][0] + nor[i][0] * tube, pts[i][1] + nor[i][1] * tube);
|
|
209
|
+
for (let i = n - 1; i >= 0; i--) s.lineTo(pts[i][0] - nor[i][0] * tube, pts[i][1] - nor[i][1] * tube);
|
|
210
|
+
s.closePath();
|
|
211
|
+
return s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const STEPS = 16;
|
|
215
|
+
const span = f => Array.from({ length: STEPS }, (_, i) => f(-1 + 2 * i / (STEPS - 1)));
|
|
216
|
+
|
|
217
|
+
// `sag > 0` curves the middle DOWN — which is a smile, because the
|
|
218
|
+
// ends are what lift. Negate it and the same curve is a frown.
|
|
219
|
+
const CURVE = {
|
|
220
|
+
line: (w, sag) => [[-w, 0], [w, 0]],
|
|
221
|
+
arc: (w, sag) => span(t => [t * w, sag * (t * t - 1)]),
|
|
222
|
+
// ω — two dips meeting at a peak in the middle. The cat mouth.
|
|
223
|
+
wave: (w, sag) => span(t => [t * w, sag * (Math.cos(t * Math.PI * 2) * .5 - .5)]),
|
|
224
|
+
zig: (w, sag) => Array.from({ length: 7 }, (_, i) =>
|
|
225
|
+
[-w + 2 * w * i / 6, (i % 2 ? -sag : sag) * .6]),
|
|
226
|
+
// dizzy. A spiral is not a shape to be cut, it is a STROKE — so it
|
|
227
|
+
// costs one centreline here rather than a new outline.
|
|
228
|
+
spiral: w => Array.from({ length: 40 }, (_, i) => {
|
|
229
|
+
const t = i / 39, a = t * Math.PI * 4.3, r = w * (1 - t * .84);
|
|
230
|
+
return [Math.cos(a) * r, Math.sin(a) * r];
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// ---- the dispatch -----------------------------------------------------
|
|
235
|
+
// A part names one of these and passes numbers. Nothing else.
|
|
236
|
+
|
|
237
|
+
const OUTLINE = {
|
|
238
|
+
ellipse: s => ellipse(s.w, s.h),
|
|
239
|
+
ring: s => ring(s.w, s.h, s.thick ?? .36),
|
|
240
|
+
rring: s => rectRing(s.w, s.h, s.r ?? s.h * .4, s.thick ?? .3),
|
|
241
|
+
rect: s => roundedRect(s.w, s.h, s.r ?? s.h * .92),
|
|
242
|
+
sparkle: s => sparkle(s.w, s.h, s.pinch ?? .22),
|
|
243
|
+
star: s => star5(s.w, s.h),
|
|
244
|
+
heart: s => heart(s.w, s.h),
|
|
245
|
+
cross: s => cross(Math.max(s.w, s.h), Math.min(s.w, s.h) * .38),
|
|
246
|
+
tri: s => triangle(s.w, s.h, s.flip),
|
|
247
|
+
flower: s => flower(s.w, s.h, s.petals ?? 5, s.amp ?? .3),
|
|
248
|
+
wobble: s => wobble(s.w, s.h),
|
|
249
|
+
crescent: s => crescent(s.w, s.h, s.bite ?? .55),
|
|
250
|
+
band: s => ribbon(CURVE[s.curve ?? 'arc'](s.w, s.sag ?? 0), s.tube ?? s.h),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const OUTLINES = Object.keys(OUTLINE);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* plate spec → BufferGeometry, front crest at z = 0, body running
|
|
257
|
+
* backward, centred in u/v.
|
|
258
|
+
* { outline, w, h, d, bevel?, + whatever that outline reads }
|
|
259
|
+
*/
|
|
260
|
+
export function plateGeometry(spec) {
|
|
261
|
+
const shape = OUTLINE[spec.outline](spec);
|
|
262
|
+
const depth = Math.max(spec.d, 1e-4);
|
|
263
|
+
// the bevel cannot exceed what the thinnest part of the outline has
|
|
264
|
+
// to give up, or ExtrudeGeometry folds the shape inside out
|
|
265
|
+
const bevel = Math.max(1e-5, Math.min(spec.bevel ?? depth * .55, depth * .9));
|
|
266
|
+
|
|
267
|
+
// A POLYLINE NEEDS NO CURVE SEGMENTS. `ExtrudeGeometry` subdivides
|
|
268
|
+
// EVERY curve of an outline by `curveSegments`, and a `LineCurve`
|
|
269
|
+
// subdivided is the same straight line carrying forty times the
|
|
270
|
+
// vertices. The whole BAND family — every mouth, brow, closed lid and
|
|
271
|
+
// blush stripe — is built by `ribbon` out of straight segments, so it
|
|
272
|
+
// was paying that toll for nothing: one mouth came to seven thousand
|
|
273
|
+
// vertices, four times what the body cost. Detecting it here rather
|
|
274
|
+
// than flagging it per outline means a new polyline outline gets the
|
|
275
|
+
// saving without knowing this function exists.
|
|
276
|
+
const straight = shape.curves.every(c => c.isLineCurve)
|
|
277
|
+
&& shape.holes.every(h => h.curves.every(c => c.isLineCurve));
|
|
278
|
+
|
|
279
|
+
const g = new THREE.ExtrudeGeometry(shape, {
|
|
280
|
+
depth, steps: 1, curveSegments: straight ? 1 : seg(SEG, 8),
|
|
281
|
+
bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: seg(6, 2),
|
|
282
|
+
});
|
|
283
|
+
g.translate(0, 0, -(depth + bevel)); // front crest to z = 0
|
|
284
|
+
g.computeBoundingBox();
|
|
285
|
+
const b = g.boundingBox;
|
|
286
|
+
g.translate(-(b.min.x + b.max.x) / 2, -(b.min.y + b.max.y) / 2, 0);
|
|
287
|
+
g.computeVertexNormals();
|
|
288
|
+
return g;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- THE SURFACE, shared with the layout ------------------------------
|
|
292
|
+
// A superellipsoid: |x/rx|ⁿ + |y/ry|ⁿ + |z/rz|ⁿ = 1.
|
|
293
|
+
//
|
|
294
|
+
// These two functions ARE the surface: `solidGeometry` builds with
|
|
295
|
+
// them and `L.at` places with them, imported from here, so the two can
|
|
296
|
+
// never disagree about where the skin is — which used to be guaranteed
|
|
297
|
+
// by careful duplication and is now guaranteed by there being one copy.
|
|
298
|
+
//
|
|
299
|
+
// A chin-tuck knob lived here for a day, trying to bend this formula
|
|
300
|
+
// into a chibi skull, and it could not: one implicit surface cannot
|
|
301
|
+
// say "cheeks full HERE, chin THIS wide, face flat in front". The
|
|
302
|
+
// humanoid's head is MODELED instead — a control cage + subdivision,
|
|
303
|
+
// in gskull.js — and this family stays what it is: a ball with a
|
|
304
|
+
// squareness knob.
|
|
305
|
+
|
|
306
|
+
// Two shapes the exponent cannot reach — a rock that sits FLAT and a
|
|
307
|
+
// slime that comes to a POINT — are not here at all. They were tried
|
|
308
|
+
// twice in this file, first as a radial displacement on the ball and
|
|
309
|
+
// then as a surface of revolution over a silhouette, and both are gone:
|
|
310
|
+
// see `gform.js`, which models them from a control cage. A radial
|
|
311
|
+
// scale moves a ball's surface but keeps its topology of extents, so
|
|
312
|
+
// nothing can sit flat; a profile is only an outline, and everything a
|
|
313
|
+
// shape does that is not its outline then has to be smuggled in on
|
|
314
|
+
// top. A shape you can name the parts of wants a cage.
|
|
315
|
+
|
|
316
|
+
/** how far along direction (dx,dy,dz) the surface sits. */
|
|
317
|
+
export function surfT(dx, dy, dz, rx, ry, rz, n) {
|
|
318
|
+
return 1 / Math.pow(Math.pow(Math.abs(dx / rx), n)
|
|
319
|
+
+ Math.pow(Math.abs(dy / ry), n)
|
|
320
|
+
+ Math.pow(Math.abs(dz / rz), n), 1 / n);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** the outward normal at a surface point — the GRADIENT, never the
|
|
324
|
+
* direction from the centre. */
|
|
325
|
+
export function surfN(x, y, z, rx, ry, rz, n) {
|
|
326
|
+
const gx = Math.pow(Math.abs(x / rx), n - 1) * Math.sign(x) / rx;
|
|
327
|
+
const gy = Math.pow(Math.abs(y / ry), n - 1) * Math.sign(y) / ry;
|
|
328
|
+
const gz = Math.pow(Math.abs(z / rz), n - 1) * Math.sign(z) / rz;
|
|
329
|
+
const l = Math.hypot(gx, gy, gz) || 1;
|
|
330
|
+
return [gx / l, gy / l, gz / l];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The body, and any other solid lump (a button nose).
|
|
335
|
+
*
|
|
336
|
+
* A SUPERELLIPSOID: `|x/rx|ⁿ + |y/ry|ⁿ + |z/rz|ⁿ = 1`. One number is
|
|
337
|
+
* the whole shape family — n=2 is a ball, n≈4 a rounded cube, n=8 a
|
|
338
|
+
* box with the corners knocked off — which is the same lever the voxel
|
|
339
|
+
* hand uses for its discs and the same reason: a form should be a knob
|
|
340
|
+
* you can scrub, not a second primitive to maintain.
|
|
341
|
+
*
|
|
342
|
+
* It is built by pushing a sphere's vertices out to the surface, so the
|
|
343
|
+
* topology (and therefore the UV-free smooth shading) is unchanged. The
|
|
344
|
+
* normals are ANALYTIC — the gradient of the implicit surface, not
|
|
345
|
+
* averaged from triangles — because a rounded cube's flat faces meet
|
|
346
|
+
* its corners at exactly the place where averaged normals go lumpy, and
|
|
347
|
+
* a lumpy highlight on a clearcoat is the first thing you notice.
|
|
348
|
+
*
|
|
349
|
+
* `dome` cuts the solid short: a fraction of π swept down from the top
|
|
350
|
+
* pole, so `.5` is a hemisphere. It exists for exactly one thing — the
|
|
351
|
+
* ball eye's lid, a cap that sits OVER another solid — which is why the
|
|
352
|
+
* open rim is fine: whatever a dome caps is filling it from inside.
|
|
353
|
+
*/
|
|
354
|
+
export function solidGeometry(rx, ry, rz, n = 2, dome = 0, domeFrom = 0) {
|
|
355
|
+
const g = dome
|
|
356
|
+
? new THREE.SphereGeometry(1, seg(72, 16), seg(40, 10), 0, Math.PI * 2,
|
|
357
|
+
Math.PI * domeFrom, Math.PI * (dome - domeFrom))
|
|
358
|
+
: new THREE.SphereGeometry(1, seg(72, 16), seg(54, 12));
|
|
359
|
+
const pos = g.attributes.position, nor = g.attributes.normal;
|
|
360
|
+
for (let i = 0; i < pos.count; i++) {
|
|
361
|
+
// a sphere vertex is already a unit direction
|
|
362
|
+
const dx = pos.getX(i), dy = pos.getY(i), dz = pos.getZ(i);
|
|
363
|
+
const t = surfT(dx, dy, dz, rx, ry, rz, n);
|
|
364
|
+
const x = dx * t, y = dy * t, z = dz * t;
|
|
365
|
+
pos.setXYZ(i, x, y, z);
|
|
366
|
+
const nn = surfN(x, y, z, rx, ry, rz, n);
|
|
367
|
+
nor.setXYZ(i, nn[0], nn[1], nn[2]);
|
|
368
|
+
}
|
|
369
|
+
pos.needsUpdate = true; nor.needsUpdate = true;
|
|
370
|
+
g.computeBoundingSphere();
|
|
371
|
+
return g;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* A modeled mesh (`gform.js`, `ghair.js`) → BufferGeometry. The arrays arrive
|
|
376
|
+
* ALREADY BUILT from the layout's surface — the same vertices the
|
|
377
|
+
* features were placed against — so this is packing, not modeling.
|
|
378
|
+
* All vertices are shared, so computeVertexNormals is seam-free.
|
|
379
|
+
*/
|
|
380
|
+
export function skullGeometry({ verts, faces }) {
|
|
381
|
+
const position = new Float32Array(verts.length * 3);
|
|
382
|
+
for (let i = 0; i < verts.length; i++) {
|
|
383
|
+
position[i * 3] = verts[i][0];
|
|
384
|
+
position[i * 3 + 1] = verts[i][1];
|
|
385
|
+
position[i * 3 + 2] = verts[i][2];
|
|
386
|
+
}
|
|
387
|
+
const indices = [];
|
|
388
|
+
for (const f of faces)
|
|
389
|
+
for (let i = 2; i < f.length; i++) indices.push(f[0], f[i - 1], f[i]);
|
|
390
|
+
|
|
391
|
+
const g = new THREE.BufferGeometry();
|
|
392
|
+
g.setAttribute('position', new THREE.BufferAttribute(position, 3));
|
|
393
|
+
g.setIndex(indices);
|
|
394
|
+
g.computeVertexNormals();
|
|
395
|
+
g.computeBoundingSphere();
|
|
396
|
+
return g;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* surface point + normal → plate transform. v is held to world-up so a
|
|
401
|
+
* feature never rolls with the surface's whim, and `proud` lifts it out
|
|
402
|
+
* along the normal. `roll` tilts it in its own plane — which is all a
|
|
403
|
+
* slanted eyebrow is.
|
|
404
|
+
*/
|
|
405
|
+
export function basisAt(p, n, proud = 0, roll = 0) {
|
|
406
|
+
const N = new THREE.Vector3(n[0], n[1], n[2]).normalize();
|
|
407
|
+
const T = new THREE.Vector3(0, 1, 0).cross(N);
|
|
408
|
+
if (T.lengthSq() < 1e-6) T.set(1, 0, 0); else T.normalize();
|
|
409
|
+
const B = new THREE.Vector3().crossVectors(N, T);
|
|
410
|
+
const q = new THREE.Quaternion().setFromRotationMatrix(
|
|
411
|
+
new THREE.Matrix4().makeBasis(T, B, N));
|
|
412
|
+
if (roll) q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), roll));
|
|
413
|
+
return {
|
|
414
|
+
position: new THREE.Vector3(p[0], p[1], p[2]).addScaledVector(N, proud),
|
|
415
|
+
quaternion: q,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Animating the face is NOT here — `gface.js` owns every write to a
|
|
420
|
+
// feature mesh, so blink, gaze and expression can never fight each
|
|
421
|
+
// other over the same transform.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GlossSpecies {
|
|
2
|
+
label: string;
|
|
3
|
+
material?: string;
|
|
4
|
+
palette?: string;
|
|
5
|
+
cast: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const GSPECIES: Readonly<Record<string, GlossSpecies>>;
|
|
9
|
+
export const GSPECIES_IDS: readonly string[];
|
|
10
|
+
export const GSPECIES_WEIGHTS: readonly [string, number][];
|