-
-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathindex.js
More file actions
62 lines (58 loc) · 2.47 KB
/
Copy pathindex.js
File metadata and controls
62 lines (58 loc) · 2.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// @ts-check
// Avatar Composer: public entry point.
//
// One call turns a diversity profile + seed into a rigged, expression-ready GLB
// assembled from modular parts (see README.md). The caller supplies a `loadBase`
// function so this module stays I/O-free and runs the same in a test (read from
// disk), the seed cron (fetch from the site origin), or a worker.
import { composeAvatar } from './compose.js';
import { selectRecipe, basesNeeded } from './select.js';
import { BASES } from './parts.js';
export { composeAvatar } from './compose.js';
export { selectRecipe, basesNeeded } from './select.js';
export { BASES, BASE_BY_ID } from './parts.js';
/**
* Compose one studio avatar end-to-end.
*
* @param {Object} opts
* @param {{ gender?: 'male'|'female', ethnicityKey?: string, ageKey?: string, grayBias?: number, build?: string }} opts.profile
* @param {string} opts.seed
* @param {(baseId: string) => Promise<Uint8Array>} opts.loadBase loads a base GLB's bytes by id
* @returns {Promise<{ bytes: Uint8Array, recipe: object, descriptor: object, meshes: string[], recolored: string[] }>}
*/
export async function composeStudioAvatar({ profile, seed, loadBase }) {
const recipe = selectRecipe(profile, seed);
const ids = basesNeeded(recipe);
/** @type {Record<string, Uint8Array>} */
const bytesByBase = {};
await Promise.all(
ids.map(async (id) => {
bytesByBase[id] = await loadBase(id);
}),
);
const { bytes, meshes, recolored } = await composeAvatar(recipe, bytesByBase);
const { descriptor, ...recipeCore } = recipe;
return { bytes, recipe: recipeCore, descriptor, meshes, recolored };
}
/**
* Rough count of distinct part-mesh combinations the catalog can produce, before
* colorway/scale. Each identity draws swappable parts only from its own rest-pose
* group, so the count sums over identities. Illustrative (docs/tests), not a hard
* guarantee: the colorway (per-part tints across ~10 skin tones, 10 hair tints,
* 10 outfit tints each) multiplies this into the tens of thousands.
* @returns {number}
*/
export function combinationCount() {
let total = 0;
for (const id of BASES) {
let n = 1;
for (const slot of ['hair', 'top', 'bottom', 'footwear']) {
const providers = BASES.filter((b) => b.provides.includes(slot) && b.restGroup === id.restGroup);
n *= Math.max(1, providers.length);
}
const glasses = BASES.filter((b) => b.provides.includes('glasses') && b.restGroup === id.restGroup);
n *= glasses.length + 1; // +none
total += n;
}
return total;
}