@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +23 -0
  3. package/README.md +125 -0
  4. package/UPSTREAM.md +47 -0
  5. package/dist/avatar.d.ts +2 -0
  6. package/dist/avatar.js +69 -0
  7. package/dist/avatar.js.map +1 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/materials.d.ts +20 -0
  12. package/dist/materials.js +61 -0
  13. package/dist/materials.js.map +1 -0
  14. package/dist/react-three-fiber.d.ts +16 -0
  15. package/dist/react-three-fiber.js +52 -0
  16. package/dist/react-three-fiber.js.map +1 -0
  17. package/dist/recipe.d.ts +18 -0
  18. package/dist/recipe.js +118 -0
  19. package/dist/recipe.js.map +1 -0
  20. package/dist/types.d.ts +62 -0
  21. package/dist/types.js +2 -0
  22. package/dist/types.js.map +1 -0
  23. package/package.json +102 -0
  24. package/upstream-lock.json +38 -0
  25. package/vendor/kindergrimm/LICENSE +24 -0
  26. package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
  27. package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
  28. package/vendor/kindergrimm/src/gloss/gface.js +226 -0
  29. package/vendor/kindergrimm/src/gloss/gform.js +201 -0
  30. package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
  31. package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
  32. package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
  33. package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
  34. package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
  35. package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
  36. package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
  37. package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
  38. package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
  39. package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
  40. package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
  41. package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
  42. package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
  43. package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
  44. package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
  45. package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
  46. package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
  47. package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
  48. package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
  49. package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
  50. package/vendor/kindergrimm/src/gloss/grig.js +286 -0
  51. package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
  52. package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
  53. package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
  54. package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
  55. package/vendor/kindergrimm/src/rng.d.ts +11 -0
  56. package/vendor/kindergrimm/src/rng.js +27 -0
@@ -0,0 +1,470 @@
1
+ // ---------------------------------------------------------------
2
+ // THE STUDIO — one shared light rig and one material factory.
3
+ //
4
+ // Half of the gloss look is not in the geometry at all: it is
5
+ // a big soft studio reflected in a clearcoat. So the environment here
6
+ // is hand-built — three overbright softbox planes in a grey room,
7
+ // prefiltered once — and every material takes it. Parts and the rig
8
+ // never construct a material; they name a RANK and a colour and this
9
+ // factory pours it. Same rule as `F.media.*` for the pencil and
10
+ // `V.pal.*` for the cubes: one place owns the finish, so the whole
11
+ // sheet reads as one cast.
12
+ // ---------------------------------------------------------------
13
+ import * as THREE from 'three';
14
+ // for the reachability check at the bottom only — a species may NAME a
15
+ // material instead of it being dealt. `gspecies.js` imports nothing,
16
+ // so this cannot cycle.
17
+ import { GSPECIES } from './gspecies.js';
18
+ import { knitNormal, woodRough, woodNormal, layerNormal, crazeNormal,
19
+ pearlThickness } from './gtexture.js';
20
+
21
+ /** three softboxes in a grey room → PMREM. The gloss lives here. */
22
+ export function studioEnv(renderer) {
23
+ const room = new THREE.Scene();
24
+ const box = (w, h, cr, cg, cb, x, y, z, ry = 0, rx = 0) => {
25
+ const m = new THREE.Mesh(
26
+ new THREE.PlaneGeometry(w, h),
27
+ new THREE.MeshBasicMaterial({ color: new THREE.Color(cr, cg, cb), side: THREE.DoubleSide }));
28
+ m.position.set(x, y, z); m.rotation.set(rx, ry, 0);
29
+ room.add(m);
30
+ };
31
+ room.add(new THREE.Mesh(
32
+ new THREE.BoxGeometry(24, 24, 24),
33
+ new THREE.MeshBasicMaterial({ color: new THREE.Color(.38, .36, .33), side: THREE.BackSide })));
34
+ box(9, 5, 5.2, 5.1, 4.9, 0, 8.5, 2, 0, Math.PI / 2); // key: big overhead softbox
35
+ box(4, 7, 2.4, 2.3, 2.2, -8, 3, 4, Math.PI / 3); // warm-ish fill, left
36
+ box(3, 6, 1.4, 1.5, 1.7, 8, 2, 3, -Math.PI / 3); // cool kicker, right
37
+
38
+ // Structure for a MIRROR to find. A rough lobe averages the room and
39
+ // sees a grey box; a polished one reflects it point for point, and a
40
+ // grey box reflected point for point is still a grey box — chrome
41
+ // with nothing to reflect reads as flat plastic. So: two dark
42
+ // stanchions behind the camera and one bright strip beside it.
43
+ //
44
+ // They are deliberately NARROW. Each subtends about a fortieth of the
45
+ // sphere, so the total irradiance moves by well under a percent and
46
+ // the finishes that were already signed off do not shift — but a
47
+ // mirror gets verticals to bend around, which is the whole read.
48
+ box(1.2, 12, .05, .05, .06, -3.4, 2, 9); // stanchion, left
49
+ box(1.2, 12, .05, .05, .06, 3.4, 2, 9); // stanchion, right
50
+ box(.6, 10, 2.6, 2.55, 2.4, -6.5, 3, 5, Math.PI / 2.6); // a streak to travel
51
+ const pmrem = new THREE.PMREMGenerator(renderer);
52
+ const env = pmrem.fromScene(room, .04).texture;
53
+ pmrem.dispose();
54
+ return env;
55
+ }
56
+
57
+ // ELEVEN finishes, and the rule that decides whether one earns its
58
+ // place is the SHEET, not the turntable: a finish that only reads
59
+ // while a character is spinning at 400px is a parameter, not a variety.
60
+ // Every one below changes something you can see in a 1/35th cell —
61
+ // the size of the highlight, whether there is one at all, or what the
62
+ // silhouette's edge does.
63
+ //
64
+ // FREQUENCY IS ART DIRECTION, the same as a style table in `eyes.js`.
65
+ // Glossy and rubber carry the sheet; a chrome turns up about
66
+ // once a sheet, which is what keeps it worth catching. Dealt with
67
+ // `wpick` in `grig.js`, never a uniform pick.
68
+ export const MATERIALS = [
69
+ { id: 'glossy', label: 'glossy' },
70
+ { id: 'rubber', label: 'rubber' },
71
+ { id: 'ceramic', label: 'ceramic' },
72
+ { id: 'pearl', label: 'pearl' },
73
+ { id: 'flocked', label: 'flocked' },
74
+ { id: 'wood', label: 'wood' },
75
+ { id: 'wool', label: 'wool' },
76
+ { id: 'resin', label: 'resin' },
77
+ { id: 'chrome', label: 'chrome' },
78
+ { id: 'crazed', label: 'crazed' },
79
+ { id: 'skin', label: 'skin' },
80
+ ];
81
+ export const MATERIAL_IDS = MATERIALS.map(m => m.id);
82
+
83
+ /** how often each turns up. Sums to 100 so a weight reads as a percent
84
+ * of the sheet, which is the number you actually want when deciding
85
+ * whether something has become common enough to stop being a treat.
86
+ *
87
+ * `skin` is NOT dealt: it is the humanoid's material, asked for by
88
+ * name, and a skin-finished bear is just a beige bear. It stays in
89
+ * `MATERIALS` so the filter bar and the editor can still pin it. */
90
+ export const MATERIAL_WEIGHTS = [
91
+ ['glossy', 25], ['rubber', 19], ['pearl', 12], ['ceramic', 11], ['flocked', 8],
92
+ ['wood', 7], ['wool', 6], ['resin', 5], ['chrome', 4], ['crazed', 3],
93
+ ];
94
+
95
+ /** a lightened version of a colour — for a sheen tint only, never for a
96
+ * shadow. Flock and wool catch their own colour at the rim rather than
97
+ * a white one, which is the difference between fuzz and frost. */
98
+ const lift = (c, k) => c.clone().lerp(new THREE.Color(1, 1, 1), k);
99
+
100
+ const FINISH = {
101
+ // a hard clearcoat over smooth plastic: one tight, travelling highlight
102
+ glossy: c => new THREE.MeshPhysicalMaterial({
103
+ color: c, roughness: .26, metalness: 0,
104
+ clearcoat: 1, clearcoatRoughness: .07, envMapIntensity: 1,
105
+ }),
106
+
107
+ // no coat at all and a broad rough lobe, so it takes the same studio
108
+ // and returns a soft even glow with no hotspot — and it leans harder
109
+ // on the environment, which is what keeps it from going dead flat
110
+ rubber: c => new THREE.MeshPhysicalMaterial({
111
+ color: c, roughness: .82, metalness: 0,
112
+ clearcoat: 0, sheen: .5, sheenRoughness: .8, sheenColor: new THREE.Color('#ffffff'),
113
+ envMapIntensity: .7,
114
+ }),
115
+
116
+ // Glazed porcelain. It is the nearest neighbour glossy has, and it
117
+ // stays legible next to it for one reason: the coat is four times
118
+ // smoother, so the highlight is a hard small dot where glossy's is a
119
+ // soft patch. Dull that coat and the two collapse into each other.
120
+ ceramic: c => new THREE.MeshPhysicalMaterial({
121
+ color: c, roughness: .3, metalness: 0,
122
+ clearcoat: 1, clearcoatRoughness: .015, envMapIntensity: 1.25,
123
+ }),
124
+
125
+ // Soft-vinyl pearl. The best value on the list: iridescence is a thin
126
+ // film whose colour depends on the ANGLE, so the rim shifts green to
127
+ // pink as the character turns and the finish animates for free on a page
128
+ // where nothing else moves but a face.
129
+ //
130
+ // THREE NUMBERS HERE ARE NOT FREE CHOICES.
131
+ //
132
+ // `clearcoat` is LOW because iridescence sits UNDER the coat: the coat
133
+ // attenuates the tinted base by (1 - clearcoat·F) and stacks a plain
134
+ // white highlight on top, so at clearcoat 1 the rainbow washes out to
135
+ // exactly the glossy finish. This is the one place in the lab where a
136
+ // hard coat is the wrong answer.
137
+ //
138
+ // `ior` is high because the film's strength is proportional to F0, and
139
+ // a default dielectric's F0 is .04 — enough to be invisible. At 1.9 it
140
+ // is about .1 and the colour actually arrives.
141
+ //
142
+ // And the thickness range only means anything because of the MAP: with
143
+ // no map the shader takes the maximum and ignores the minimum, so the
144
+ // character would be one flat film. See `pearlThickness` in `gtexture.js`.
145
+ pearl: c => new THREE.MeshPhysicalMaterial({
146
+ color: c, roughness: .3, metalness: 0, ior: 1.9,
147
+ clearcoat: .35, clearcoatRoughness: .1, envMapIntensity: 1.05,
148
+ iridescence: .85, iridescenceIOR: 1.5, iridescenceThicknessRange: [180, 520],
149
+ }),
150
+
151
+ // Flocked — the fuzz on a nodding-dog. Sheen is a retroreflective
152
+ // lobe at grazing angles, so what it actually buys is a lit RIM on
153
+ // the silhouette, which is the one thing that reads at sheet scale.
154
+ // Tighter and stronger than rubber's, or the two would be one finish.
155
+ flocked: c => new THREE.MeshPhysicalMaterial({
156
+ color: c, roughness: 1, metalness: 0,
157
+ clearcoat: 0, sheen: 1, sheenRoughness: .35, sheenColor: lift(c, .5),
158
+ envMapIntensity: .55,
159
+ }),
160
+
161
+ // A turned character. The COLOUR stays the palette's — a wood albedo would
162
+ // quietly overrule `gpalette.js`, and painted beech is a real object
163
+ // anyway. What says wood is the grain, and the grain arrives as
164
+ // roughness and a whisper of relief, never as hue.
165
+ //
166
+ // `anisotropy` is the lathe. It smears the highlight PERPENDICULAR to
167
+ // the tangent, and at rotation 0 the tangent is the UV's u — which on
168
+ // this body runs around the equator, exactly where a tool leaves its
169
+ // marks. So the streak comes out vertical, off a rotation of zero, for
170
+ // free. It needs no tangent attribute: with none present three derives
171
+ // the frame from the screen-space derivatives of the UVs, which is all
172
+ // a sphere would have given it anyway.
173
+ wood: c => new THREE.MeshPhysicalMaterial({
174
+ color: c, roughness: .62, metalness: 0,
175
+ clearcoat: .35, clearcoatRoughness: .3, envMapIntensity: .85,
176
+ anisotropy: .5, anisotropyRotation: 0,
177
+ }),
178
+
179
+ // Knitted, with the stitch in the normal map. Rough, unglazed, and
180
+ // leaning on sheen for the fuzz — a jumper's read is its silhouette
181
+ // catching light, not a highlight on its front.
182
+ wool: c => new THREE.MeshPhysicalMaterial({
183
+ color: c, roughness: .95, metalness: 0,
184
+ clearcoat: 0, sheen: 1, sheenRoughness: .6, sheenColor: lift(c, .35),
185
+ envMapIntensity: .6,
186
+ }),
187
+
188
+ // The lab printing its own output: layer lines, a half-hearted coat,
189
+ // the slightly chalky look of unpolished resin.
190
+ resin: c => new THREE.MeshPhysicalMaterial({
191
+ color: c, roughness: .55, metalness: 0,
192
+ clearcoat: .25, clearcoatRoughness: .4, envMapIntensity: .8,
193
+ }),
194
+
195
+ // Anodized metal. A metal has no diffuse term at all — everything you
196
+ // see is the room, tinted by the colour — which is why `studioEnv`
197
+ // grew two dark stanchions above. Without them this is grey plastic.
198
+ chrome: c => new THREE.MeshPhysicalMaterial({
199
+ color: c, roughness: .16, metalness: 1,
200
+ clearcoat: 0, envMapIntensity: 1.4,
201
+ }),
202
+
203
+ // Ceramic that has been around: the same glaze, a shade duller, with
204
+ // the hairline map opened up across it.
205
+ crazed: c => new THREE.MeshPhysicalMaterial({
206
+ color: c, roughness: .34, metalness: 0,
207
+ clearcoat: 1, clearcoatRoughness: .04, envMapIntensity: 1.15,
208
+ }),
209
+
210
+ // SKIN — the humanoid's, ported from the chibi-skull study. Cheap
211
+ // fake subsurface: a broad warm SHEEN does the rim glow light gets
212
+ // when it passes through the shallow edge of a face, and a weak,
213
+ // rough CLEARCOAT gives the soft vinyl-doll sheen without the tight
214
+ // travelling hotspot that makes `glossy` read as plastic. The sheen
215
+ // colour is a fixed warm pink on purpose — flesh scatters red
216
+ // whatever shade the skin is — so it is NOT lifted from `c`.
217
+ skin: c => new THREE.MeshPhysicalMaterial({
218
+ color: c, roughness: .62, metalness: 0,
219
+ sheen: 1, sheenRoughness: .85, sheenColor: new THREE.Color('#ff9d8e'),
220
+ clearcoat: .25, clearcoatRoughness: .5, envMapIntensity: .9,
221
+ }),
222
+
223
+ // ACCESSORIES — a hat, a bow, a plaster. Soft plastic, and the one
224
+ // number that matters is that the SHEEN IS SELF-COLOURED. Worn on
225
+ // `rubber`, whose sheen is white at .5, a brick-red beanie under the
226
+ // overhead key washed out to pale pink and read as a bald head — the
227
+ // colour was right in the material and wrong on the screen. Exactly
228
+ // the mistake the hair block records, made a second time.
229
+ acc: c => new THREE.MeshPhysicalMaterial({
230
+ color: c, roughness: .55, metalness: 0,
231
+ clearcoat: .22, clearcoatRoughness: .4,
232
+ sheen: .15, sheenRoughness: .7, sheenColor: lift(c, .2),
233
+ envMapIntensity: .9,
234
+ }),
235
+
236
+ // HAIR, and it is dead-matte VINYL. Two failures are recorded in this
237
+ // block. A hard clearcoat came first — one travelling hotspot on a
238
+ // shape whose whole job is many soft clumps: a plastic wig. Then a
239
+ // strong whitened sheen — and sheen is a rim lobe, so on a grooved
240
+ // surface it lit every groove edge as a glassy STREAK, which read as
241
+ // cellophane. The reference figures have neither: their hair is the
242
+ // flattest thing on the character, and the grooves read by OCCLUSION and
243
+ // the soft studio gradient, not by any highlight. So: rough, no
244
+ // coat, the faintest self-coloured sheen so the silhouette does not
245
+ // go dead, and matte from ROUGHNESS — never from dimming the
246
+ // environment, which just makes every colour darker than its swatch.
247
+ hair: c => new THREE.MeshPhysicalMaterial({
248
+ color: c, roughness: .97, metalness: 0,
249
+ clearcoat: 0,
250
+ sheen: .12, sheenRoughness: .8, sheenColor: lift(c, .12),
251
+ envMapIntensity: .85,
252
+ }),
253
+
254
+ };
255
+
256
+ /**
257
+ * WHAT A FEATURE TAKES WHEN THE SHELL IS WEARING SOMETHING ELSE.
258
+ *
259
+ * A face feature is an `ExtrudeGeometry` plate, and its UVs are three's
260
+ * world-space default — which is to say a texture laid on one is noise.
261
+ * That is the mechanical reason a map is shell-only. The good reason is
262
+ * that it is also how the real objects are built: a knitted doll has
263
+ * PLASTIC safety eyes, a turned one has painted pupils. Nobody knits an
264
+ * eye.
265
+ *
266
+ * Only the finishes whose PARAMETERS are wrong on a feature appear
267
+ * here. `resin` and `crazed` are absent on purpose: their numbers are
268
+ * fine at 3mm, it is only the tile that has nowhere to land, and the
269
+ * factory drops that on its own.
270
+ */
271
+ const FEATURE_OF = { wool: 'glossy', wood: 'glossy' };
272
+
273
+ /** the tile each finish lays on its shell, and how hard. Baked once,
274
+ * in `gtexture.js`, and shared by every character on the page. */
275
+ const MAPS = {
276
+ // the swirl in the film — see the note on `pearl` above for why this
277
+ // is load-bearing rather than decoration
278
+ pearl: m => { m.iridescenceThicknessMap = pearlThickness(); },
279
+ wool: m => { m.normalMap = knitNormal(); m.normalScale.set(1, 1); },
280
+ wood: m => {
281
+ m.normalMap = woodNormal(); m.normalScale.set(.35, .35);
282
+ // roughness MULTIPLIES the map, so the base is the ceiling
283
+ m.roughnessMap = woodRough();
284
+ },
285
+ resin: m => { m.normalMap = layerNormal(); m.normalScale.set(.7, .7); },
286
+ crazed: m => { m.normalMap = crazeNormal(); m.normalScale.set(.45, .45); },
287
+ };
288
+
289
+ /**
290
+ * `(finish, colour, shell) -> material`, cached for the whole page.
291
+ *
292
+ * `shell` is true for the character's BODY and nothing else. Everything else
293
+ * is a feature set into it, and takes `FEATURE_OF` if the shell is
294
+ * wearing something a feature cannot wear, and never takes a tile.
295
+ *
296
+ * The cache key is the RESOLVED finish, not the requested one, so a
297
+ * wool character's glossy eyes share one material with a glossy character's —
298
+ * thirty-five characters on the sheet still come out with a couple of dozen
299
+ * materials between them.
300
+ */
301
+ // THE CROSS-CHECK. `FINISH[id] || FINISH.glossy` fails SILENTLY: add a
302
+ // finish to `MATERIALS` and forget its recipe and you get a glossy character
303
+ // with the right chip lit, the right filter pinned and the right id in
304
+ // the recipe. That is precisely the bug `CLAUDE.md` records against
305
+ // `eyes.js` — "a style in the table but not in the wpick list is
306
+ // unreachable and nothing will tell you; that has already happened
307
+ // once". Two parallel tables with no cross-check is the pattern, so
308
+ // here is the check, at module load, where it costs nothing.
309
+ // A material may reach a character TWO ways now: dealt by weight, or asked
310
+ // for BY NAME by a species (`skin`). Both count as reachable; anything
311
+ // with neither is the silent bug this check exists for.
312
+ const NAMED_MATERIALS = new Set(
313
+ Object.values(GSPECIES).map(s => s.material).filter(Boolean));
314
+
315
+ // `hair` and `acc` are deliberately NOT in `MATERIALS`. They are not
316
+ // finishes a CHARACTER can be poured in — you cannot make a chrome character out of
317
+ // haircut — they are what two particular parts are always made of, named
318
+ // per-spec via `spec.finish`. So they are exempt from the reachability
319
+ // check below, which is about the finishes the sheet deals.
320
+
321
+ for (const id of MATERIAL_IDS) {
322
+ if (!FINISH[id]) throw new Error(`gmedia: material '${id}' has no FINISH recipe`);
323
+ if (!MATERIAL_WEIGHTS.some(w => w[0] === id) && !NAMED_MATERIALS.has(id))
324
+ throw new Error(`gmedia: material '${id}' is neither dealt nor named by a species, `
325
+ + `so it can never reach a character`);
326
+ }
327
+ for (const [id] of MATERIAL_WEIGHTS) {
328
+ if (!MATERIAL_IDS.includes(id))
329
+ throw new Error(`gmedia: weight for '${id}', which is not a material`);
330
+ }
331
+
332
+ export function makeMaterialFactory(env) {
333
+ const cache = new Map();
334
+ const materialFor = (finish, color, shell = false, print = null) => {
335
+ const id = shell ? finish : (FEATURE_OF[finish] ?? finish);
336
+ const tiled = shell && MAPS[id] ? 1 : 0;
337
+ const key = print ? `${id}:print:${print.key}` : `${id}:${tiled}:${color}`;
338
+ if (cache.has(key)) return cache.get(key);
339
+ const mat = (FINISH[id] || FINISH.glossy)(new THREE.Color(color));
340
+ mat.envMap = env;
341
+ if (tiled) MAPS[id](mat);
342
+ // A PRINT carries the whole diffuse — cloth ground and motif ink —
343
+ // so the base colour steps aside to white and the map decides. The
344
+ // finish was still built from the CLOTH colour first, because that
345
+ // is what keeps the sheen self-coloured: a white-based sheen on
346
+ // dark cloth is the beanie bug a third time.
347
+ if (print) { mat.map = print.tex; mat.color.set('#ffffff'); }
348
+ cache.set(key, mat);
349
+ return mat;
350
+ };
351
+
352
+ return materialFor;
353
+ }
354
+
355
+ /**
356
+ * The seamless cyc: one warm taupe sweep, darker up in the corners and
357
+ * lifting toward the floor line, painted into a texture rather than
358
+ * built as geometry. A real curved wall would need lighting of its own
359
+ * and would fight the studio env for the same job.
360
+ */
361
+ function cycTexture() {
362
+ const c = document.createElement('canvas');
363
+ c.width = 16; c.height = 512;
364
+ const g = c.getContext('2d');
365
+ const grad = g.createLinearGradient(0, 0, 0, 512);
366
+ grad.addColorStop(0, '#b9b2a6'); // up in the roll
367
+ grad.addColorStop(.42, '#cdc6ba');
368
+ grad.addColorStop(.72, '#dcd5c9'); // where the wall meets the floor
369
+ grad.addColorStop(1, '#d3ccc0'); // floor coming toward camera
370
+ g.fillStyle = grad;
371
+ g.fillRect(0, 0, 16, 512);
372
+ const tex = new THREE.CanvasTexture(c);
373
+ tex.colorSpace = THREE.SRGBColorSpace;
374
+ return tex;
375
+ }
376
+
377
+ /**
378
+ * the cyc, one steep key with a genuinely blurred shadow, soft fill.
379
+ *
380
+ * `span` is how wide a piece of floor has to catch shadow: one character on
381
+ * the lab's turntable needs 6.4 units, a sheet of twenty needs the
382
+ * whole grid. Everything else scales off it — the catcher, the key's
383
+ * shadow frustum, and how far the key stands back — so a crowd gets
384
+ * the same studio, only bigger. Returns the two handles a scene might
385
+ * want to move (`key`, `floor`); the lab uses neither.
386
+ */
387
+ export function dressScene(scene, renderer,
388
+ { span = 6.4, pool = .19, shadows = true, wallZ = 1.7 } = {}) {
389
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
390
+ renderer.toneMappingExposure = 1.06;
391
+
392
+ // A page of characters hung on a grid has no ground to fall on — so it
393
+ // gets a WALL instead, close behind, and the shadows land on that.
394
+ // The key is nearly head-on and only a little up and to the left, so
395
+ // each character throws a short shadow down-right onto the wall just
396
+ // behind it. Rake it any harder and every shadow lands on the
397
+ // neighbour instead of reading as depth.
398
+ if (shadows === 'wall') {
399
+ renderer.shadowMap.enabled = true;
400
+ renderer.shadowMap.type = THREE.VSMShadowMap;
401
+ scene.background = new THREE.Color('#d6cfc3');
402
+
403
+ const key = new THREE.DirectionalLight('#fff6e8', 1.25);
404
+ // nearly head-on: the further the wall goes back, the further a
405
+ // raking light throws the shadow, and past about a third of a cell
406
+ // it stops reading as depth and starts landing on the neighbour
407
+ key.position.set(-1.1, 1.5, 9);
408
+ key.castShadow = true;
409
+ key.shadow.mapSize.set(2048, 2048);
410
+ const e = span * .62;
411
+ key.shadow.camera.left = -e; key.shadow.camera.right = e;
412
+ key.shadow.camera.top = e; key.shadow.camera.bottom = -e;
413
+ key.shadow.camera.near = 1; key.shadow.camera.far = 30;
414
+ // a wide blur, and it has to BE wide: VSM's radius is in shadow-map
415
+ // texels, so over a frustum this size a single-digit radius is a
416
+ // hard edge no matter how soft the number looks
417
+ key.shadow.radius = 24; key.shadow.blurSamples = 26; key.shadow.bias = 0;
418
+ scene.add(key);
419
+ // and lift the ambient — a shadow reads soft when it is shallow as
420
+ // well as blurred
421
+ scene.add(new THREE.HemisphereLight('#fff4e0', '#c3b6a2', .78));
422
+
423
+ const wall = new THREE.Mesh(
424
+ new THREE.PlaneGeometry(span * 3, span * 3),
425
+ new THREE.MeshStandardMaterial({ color: '#d6cfc3', roughness: .96, metalness: 0 }));
426
+ wall.position.z = -wallZ;
427
+ wall.receiveShadow = true;
428
+ scene.add(wall);
429
+ return { key, wall, floor: null };
430
+ }
431
+
432
+ renderer.shadowMap.enabled = true;
433
+ // VSM, not PCF: `radius` is a real blur here, and a molded character wants
434
+ // a shadow that reads as a pool rather than an outline
435
+ renderer.shadowMap.type = THREE.VSMShadowMap;
436
+ scene.background = cycTexture();
437
+
438
+ // steep and slightly to the side, so the pool sits under the character
439
+ const k = span / 6.4;
440
+ const key = new THREE.DirectionalLight('#fff6e8', 1.45);
441
+ key.position.set(1.6 * k, 6 * k, 2.6 * k);
442
+ key.castShadow = true;
443
+ key.shadow.mapSize.set(span > 8 ? 2048 : 1024, span > 8 ? 2048 : 1024);
444
+ key.shadow.camera.left = -3.6 * k; key.shadow.camera.right = 3.6 * k;
445
+ key.shadow.camera.top = 3.6 * k; key.shadow.camera.bottom = -3.6 * k;
446
+ key.shadow.camera.near = 1 * k; key.shadow.camera.far = 16 * k;
447
+ key.shadow.radius = 14; key.shadow.blurSamples = 24; key.shadow.bias = 0;
448
+ scene.add(key);
449
+ scene.add(new THREE.HemisphereLight('#fff4e0', '#b8a890', .4));
450
+
451
+ // The catcher has to disappear at its own edges. It must not reach
452
+ // past the shadow camera (VSM bleeds outside the frustum, and that
453
+ // bleed reads as a grey band ruled across the backdrop), but a plane
454
+ // cut to fit then shows its own rim — so fade the shadow radially and
455
+ // the pool just stops existing before the geometry does.
456
+ const mat = new THREE.ShadowMaterial({ opacity: pool });
457
+ mat.onBeforeCompile = sh => {
458
+ sh.vertexShader = 'varying vec2 vPool;\n' + sh.vertexShader
459
+ .replace('void main() {', 'void main() {\n vPool = uv;');
460
+ sh.fragmentShader = 'varying vec2 vPool;\n' + sh.fragmentShader
461
+ .replace('gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );',
462
+ 'gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) * ' +
463
+ '( 1.0 - smoothstep( 0.28, 0.48, length( vPool - 0.5 ) ) ) );');
464
+ };
465
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(span, span), mat);
466
+ floor.rotation.x = -Math.PI / 2;
467
+ floor.receiveShadow = true;
468
+ scene.add(floor);
469
+ return { key, floor };
470
+ }
@@ -0,0 +1,10 @@
1
+ export interface GlossPalette {
2
+ id: string;
3
+ label: string;
4
+ colors: readonly string[];
5
+ }
6
+
7
+ export const PALETTES: readonly GlossPalette[];
8
+ export const PALETTE_IDS: readonly string[];
9
+ export const PALETTE_DEAL_IDS: readonly string[];
10
+ export const PALETTE_BY_ID: Readonly<Record<string, GlossPalette>>;