humanforge 0.2.1 → 0.3.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.ASSETS.md +53 -0
- package/README.md +42 -13
- package/package.json +2 -1
- package/src/human/catalog.ts +59 -2
- package/src/human/editor.ts +37 -19
- package/src/human/runtime.ts +41 -5
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Asset licenses
|
|
2
|
+
|
|
3
|
+
All human assets in `assets/human` are CC0 1.0 (public domain dedication). Credit is appreciated but not required.
|
|
4
|
+
|
|
5
|
+
- Base mesh, targets, rig, weights, expression units: [MPFB2](https://github.com/makehumancommunity/mpfb2) (`src/mpfb/data`), CC0.
|
|
6
|
+
- System skins, eyes, brows, lashes, teeth, 10 hairstyles, 12 outfits, shoes, hats: [MakeHuman system assets](https://files2.makehumancommunity.org/asset_packs/makehuman_system_assets/makehuman_system_assets_cc0.zip), CC0.
|
|
7
|
+
|
|
8
|
+
## Community assets
|
|
9
|
+
|
|
10
|
+
Baked by `tools/build-human.ts --extra <pack>`. The list and provenance live in `tools/extra-assets.json`. Each item's license was checked against the pack's `packs/<pack>.json` and its source page (all marked "Creative Commons Zero", checked 2026-10-18). Only the CC0 variants of the MakeHuman community packs are used; the CC-BY packs (`hair02`, `hair03`, `dress02`, ...) are not.
|
|
11
|
+
|
|
12
|
+
| Pack | Download |
|
|
13
|
+
|---|---|
|
|
14
|
+
| hair01 | https://files2.makehumancommunity.org/asset_packs/hair01/hair01_cc0.zip |
|
|
15
|
+
| dress01 | https://files2.makehumancommunity.org/asset_packs/dress01/dress01_cc0.zip |
|
|
16
|
+
| suits01 | https://files2.makehumancommunity.org/asset_packs/suits01/suits01_cc0.zip |
|
|
17
|
+
|
|
18
|
+
### Hair
|
|
19
|
+
|
|
20
|
+
| Id | Label | Author | License | Source |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| `toigo_blunt_bob` | Blunt bob | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1669 |
|
|
23
|
+
| `toigo_blunt_bob_with_bangs` | Blunt bob with bangs | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1681 |
|
|
24
|
+
| `toigo_curled_under_bob` | Curled bob | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1677 |
|
|
25
|
+
| `toigo_inverted_bob_with_bangs` | Inverted bob with bangs | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1685 |
|
|
26
|
+
| `cortu_straight_bangs` | Straight with bangs | Cortu | CC0-1.0 | http://www.makehumancommunity.org/node/2810 |
|
|
27
|
+
| `rehmanpolanski_hair_bun_brown` | Bun | RehmanPolanski | CC0-1.0 | http://www.makehumancommunity.org/node/2477 |
|
|
28
|
+
| `culturalibre_hair_01` | Long sleek | culturalibre | CC0-1.0 | http://www.makehumancommunity.org/node/2893 |
|
|
29
|
+
| `sonntag78_junglebook_hair` | Shoulder layers | sonntag78 | CC0-1.0 | http://www.makehumancommunity.org/node/174 |
|
|
30
|
+
| `culturalibre_hair_02` | Short side part | culturalibre | CC0-1.0 | http://www.makehumancommunity.org/node/2892 |
|
|
31
|
+
|
|
32
|
+
### Clothes
|
|
33
|
+
|
|
34
|
+
| Id | Label | Author | License | Source |
|
|
35
|
+
|---|---|---|---|---|
|
|
36
|
+
| `toigo_camisole_dress_with_full_skirt` | Camisole dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1730 |
|
|
37
|
+
| `toigo_halter_dress_knee_length` | Halter dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1853 |
|
|
38
|
+
| `toigo_halter_dress_midi` | Halter midi | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1854 |
|
|
39
|
+
| `toigo_halter_dress_with_fluted_skirt` | Halter gown | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1855 |
|
|
40
|
+
| `toigo_shift_dress` | Shift dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1724 |
|
|
41
|
+
| `toigo_keyhole_neck_dress` | Keyhole dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1613 |
|
|
42
|
+
| `toigo_dress_with_tiered_skirt` | Tiered dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1640 |
|
|
43
|
+
| `toigo_bodice_dress_with_lace_ruffle_skirt` | Lace ruffle dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1706 |
|
|
44
|
+
| `toigo_strapless_ruffle_top_dress` | Strapless dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1705 |
|
|
45
|
+
| `toigo_cut_out_dress` | Cut-out dress | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1704 |
|
|
46
|
+
| `toigo_female_suit` | Skirt suit | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1324 |
|
|
47
|
+
| `toigo_female_double-breasted_suit` | Double-breasted suit | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1742 |
|
|
48
|
+
| `toigo_female_suit_2` | Trouser suit | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1739 |
|
|
49
|
+
| `toigo_male_suit_3` | Suit and tie | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1733 |
|
|
50
|
+
| `toigo_male_double-breasted_suit` | Double-breasted suit | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1740 |
|
|
51
|
+
| `toigo_suit_with_dinner_jacket` | Dinner jacket | MargaretToigo | CC0-1.0 | http://www.makehumancommunity.org/node/1746 |
|
|
52
|
+
|
|
53
|
+
Notes from the asset pages: `culturalibre_hair_01` and `culturalibre_hair_02` are re-uploads of 2015 MakeHuman community hair, and `rehmanpolanski_hair_bun_brown` is adapted from the system `ponytail01`. All of them are published as CC0.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ avatar builders such as MetaPerson Creator. It has two modes:
|
|
|
7
7
|
| --- | --- | --- |
|
|
8
8
|
| Look | Photoreal skin, eyes, hair cards, clothes | Flat-colored, procedural |
|
|
9
9
|
| Base | MakeHuman / MPFB2 human (CC0), 26.7k-triangle body | three.js primitives, 7–11k triangles |
|
|
10
|
-
| Customization | 11 macro + 57 detail morph sliders, 20 skins, 9 eye colors, 12 brows, 4 lashes,
|
|
10
|
+
| Customization | 11 macro + 57 detail morph sliders, 20 skins, 9 eye colors, 12 brows, 4 lashes, 19 hairstyles, 28 outfits, 6 shoes | 30+ options |
|
|
11
11
|
| Rig | 52-bone **Mixamo** skeleton with skin weights (clothes and hair follow) | none |
|
|
12
12
|
| Face animation | 34 FACS blendshapes with **ARKit names** (`jawOpen`, `mouthSmile`, `eyeBlinkLeft`, …) | none |
|
|
13
13
|
| Download | ~1.4 MB one-time core + ~0.3–0.8 MB per look (gzipped, cached on device) | 0 (code only) |
|
|
@@ -25,9 +25,31 @@ git clone --depth 1 --filter=blob:none --sparse https://github.com/makehumancomm
|
|
|
25
25
|
cd mpfb2 && git sparse-checkout set src/mpfb/data && cd ..
|
|
26
26
|
curl -LO https://files2.makehumancommunity.org/asset_packs/makehuman_system_assets/makehuman_system_assets_cc0.zip
|
|
27
27
|
unzip makehuman_system_assets_cc0.zip -d mh_assets
|
|
28
|
-
|
|
28
|
+
# CC0 community packs: extra hairstyles, dresses and suits (the items listed in tools/extra-assets.json)
|
|
29
|
+
for p in hair01 dress01 suits01; do
|
|
30
|
+
curl -LO https://files2.makehumancommunity.org/asset_packs/$p/${p}_cc0.zip && unzip -q ${p}_cc0.zip -d mh_$p
|
|
31
|
+
done
|
|
32
|
+
npm run build:human -- mpfb2/src/mpfb/data mh_assets \
|
|
33
|
+
--extra mh_hair01 --extra mh_dress01 --extra mh_suits01 # -> assets/human (~26 MB, all options)
|
|
34
|
+
npm run build:garments # + modest wear and dresses (below)
|
|
29
35
|
```
|
|
30
36
|
|
|
37
|
+
Leave out the `--extra` packs for the system set only (~14 MB). Every item loads on demand, so
|
|
38
|
+
the size is what you host, not what each user downloads. To add other community items, put
|
|
39
|
+
their ids in `tools/extra-assets.json` (with author, license and source) and their labels in
|
|
40
|
+
`src/human/catalog.ts`. `demo/wardrobe.html` renders a contact sheet of hairstyles or outfits
|
|
41
|
+
for comparing them.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
`build:garments` (`tools/build-garments.mts`) adds garments the MakeHuman packs don't have, generated
|
|
45
|
+
in code from the human package itself (no third-party assets): **hijab**, **al-amira**, **abaya**,
|
|
46
|
+
**jalabiya**, a **day dress**, a **maxi dress** and an **evening gown**. They are regular proxies, so
|
|
47
|
+
they follow every body and face slider; they are tintable (`garmentColors`, palette in
|
|
48
|
+
`GARMENT_COLORS`), and headwear hides the hair while worn. Run it again after `build:human`.
|
|
49
|
+
`demo/garments.html` renders a contact sheet (`?variants=1` adds extreme body shapes).
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
31
53
|
Host `assets/human/` on any static host or CDN (S3, Cloudflare R2, GitHub Pages…). No server
|
|
32
54
|
compression is needed: binaries are pre-gzipped and decoded in JS by `fflate`.
|
|
33
55
|
|
|
@@ -79,7 +101,7 @@ const glb = await exportHumanGLB(avatar); // Mixamo-rigged, ARKit blendshapes
|
|
|
79
101
|
### Avatar editor: one config, web and React Native
|
|
80
102
|
|
|
81
103
|
`<HumanEditor>` is a drop-in customization screen with live 3D preview, section tabs (face,
|
|
82
|
-
skin, eyes, brows, nose, mouth, ears, hair, body, outfit, shoes, hat), randomize and save. The
|
|
104
|
+
skin, eyes, brows, nose, mouth, ears, hair, headwear, body, outfit, shoes, hat), randomize and save. The
|
|
83
105
|
same `options` object decides what your users may change on both platforms:
|
|
84
106
|
|
|
85
107
|
```tsx
|
|
@@ -112,9 +134,9 @@ const { pkg } = useHumanPackage(ASSETS);
|
|
|
112
134
|
| `gender` | `'female'` or `'male'` locks the gender, hides the gender slider, and filters hair, clothes and skins to ones that suit it. `'any'` shows everything. |
|
|
113
135
|
| `sections` | Which tabs to show, in order. |
|
|
114
136
|
| `exclude` / `include` | Hide (or whitelist) whole sections or single controls: `'ears'`, `'hair.color'`, `'nose.noseWidth'`, `'body.weight'`… |
|
|
115
|
-
| `items` | Allowed ids per slot: `hair`, `outfit`, `shoes`, `hat`, `skin`, `eyes`, `eyebrows`, `eyelashes`. |
|
|
116
|
-
| `colors` | Palettes for `hair`, `eyebrows` and `
|
|
117
|
-
| `optional` | Slots that offer "None" (`hair`, `hat`, `eyebrows`, `shoes`). |
|
|
137
|
+
| `items` | Allowed ids per slot: `hair`, `headwear`, `outfit`, `shoes`, `hat`, `skin`, `eyes`, `eyebrows`, `eyelashes`. |
|
|
138
|
+
| `colors` | Palettes for `hair`, `eyebrows`, `skinTint` and `garments` (outfit/headwear fabric). |
|
|
139
|
+
| `optional` | Slots that offer "None" (`hair`, `hat`, `headwear`, `eyebrows`, `shoes`). |
|
|
118
140
|
| `detailRange` | Caps the face/body detail sliders (0..1). |
|
|
119
141
|
| `defaults` / `locked` | Starting values / values that are always enforced. |
|
|
120
142
|
| `labels` | Rename any section, control or item (for translation). |
|
|
@@ -210,7 +232,8 @@ avatar picks up the same bias, such as a wider mouth.
|
|
|
210
232
|
- **Detail:** `detail: { [id]: -1..1 }`, with the ids listed in `DETAIL_SLIDERS` (head shape, jaw,
|
|
211
233
|
chin, cheeks, eyes, brows, nose ×9, mouth ×8, ears, neck, shoulders, chest, waist, hips…).
|
|
212
234
|
- **Style:** `skin`, `skinTint`, `eyes`, `eyebrows`, `eyebrowColor`, `eyelashes`, `hair`,
|
|
213
|
-
`hairColor`,
|
|
235
|
+
`hairColor`, `clothes: string[]` and `garmentColors: { outfit?, headwear? }` (fabric colors of
|
|
236
|
+
tintable garments). The available ids are listed in `pkg.manifest`.
|
|
214
237
|
|
|
215
238
|
### How it works
|
|
216
239
|
|
|
@@ -263,11 +286,15 @@ npm run demo # http://localhost:5173 (stylized) and /human.html (realistic)
|
|
|
263
286
|
shipping.
|
|
264
287
|
- **Selfie fit:** verified in Chrome with public-domain portraits (web path). The React Native
|
|
265
288
|
landmark path is documented but not implemented. See "Selfie to avatar" for its limits.
|
|
266
|
-
- **Wardrobe:**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
289
|
+
- **Wardrobe:** the CC0 MakeHuman system packs (10 hairstyles, 12 outfits, 6 shoes) plus CC0
|
|
290
|
+
community items: 8 women's hairstyles (bobs, bangs, a bun, long and layered cuts), 1 men's style,
|
|
291
|
+
10 dresses and 3 women's and 3 men's suits (see `LICENSE.ASSETS.md`,
|
|
292
|
+
[before/after](demo/hair-compare.jpg)). They are still behind commercial avatar SDKs: hair is
|
|
293
|
+
rendered with soft two-pass cards on 2014–2021-era community meshes. Separate tops, skirts and
|
|
294
|
+
trousers aren't offered because the editor has a single outfit slot. More CC0 packs, or your
|
|
295
|
+
own `.mhclo` assets, can be added through `tools/build-human.ts --extra`. Modest wear and
|
|
296
|
+
dresses come from `tools/build-garments.mts`: they're smooth, procedurally draped shapes (no
|
|
297
|
+
simulated folds), shaped for the rest pose; there's no kufi or niqab yet.
|
|
271
298
|
- **Hair quality:** hair uses alpha-tested cards, so the edges are crisper with MSAA (`antialias: true`).
|
|
272
299
|
|
|
273
300
|
## Licenses
|
|
@@ -275,7 +302,9 @@ npm run demo # http://localhost:5173 (stylized) and /human.html (realistic)
|
|
|
275
302
|
- Code: MIT.
|
|
276
303
|
- Human assets (base mesh, targets, skins, hair, clothes, rig, weights, expression units): CC0 1.0,
|
|
277
304
|
from the [MakeHuman](http://www.makehumancommunity.org) project and
|
|
278
|
-
[MPFB2](https://github.com/makehumancommunity/mpfb2)
|
|
305
|
+
[MPFB2](https://github.com/makehumancommunity/mpfb2), plus CC0 community hair and clothes listed
|
|
306
|
+
per item (author, source) in [LICENSE.ASSETS.md](LICENSE.ASSETS.md). Credit is appreciated but not required.
|
|
307
|
+
- Generated garments and fabric textures (`tools/build-garments.mts` output): original, CC0 1.0.
|
|
279
308
|
- Selfie fitting: MediaPipe Face Landmarker and its canonical face mesh are Apache-2.0 (Google).
|
|
280
309
|
`src/human/faceMap.ts` contains vertex indices and averaged landmark positions derived from them.
|
|
281
310
|
The model and WASM are downloaded at runtime and aren't bundled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "humanforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Open-source avatar SDK for React and React Native: realistic MakeHuman-based humans (CC0) plus a tiny procedural low-poly mode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "abdulrehman043",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"demo": "vite",
|
|
50
50
|
"build:human": "tsx tools/build-human.ts",
|
|
51
|
+
"build:garments": "tsx tools/build-garments.mts",
|
|
51
52
|
"typecheck": "tsc --noEmit",
|
|
52
53
|
"test": "tsx scripts/smoke.ts && tsx scripts/human-smoke.mts && tsx scripts/fit-test.mts && tsx scripts/editor-test.mts",
|
|
53
54
|
"build:facemap": "tsx tools/build-facemap.mts"
|
package/src/human/catalog.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// they get a generated label and are treated as unisex.
|
|
4
4
|
|
|
5
5
|
export type Fit = 'female' | 'male' | 'unisex';
|
|
6
|
-
export type ClothingSlot = 'outfit' | 'shoes' | 'hat';
|
|
6
|
+
export type ClothingSlot = 'outfit' | 'shoes' | 'hat' | 'headwear';
|
|
7
7
|
|
|
8
8
|
export interface ItemInfo { label: string; fit: Fit }
|
|
9
9
|
|
|
@@ -18,6 +18,16 @@ export const HAIR: Record<string, ItemInfo> = {
|
|
|
18
18
|
short02: { label: 'Short tousled', fit: 'male' },
|
|
19
19
|
short03: { label: 'Side fringe', fit: 'unisex' },
|
|
20
20
|
short04: { label: 'Slicked back', fit: 'male' },
|
|
21
|
+
// MakeHuman community hair (CC0, see LICENSE.ASSETS.md)
|
|
22
|
+
toigo_blunt_bob: { label: 'Blunt bob', fit: 'female' },
|
|
23
|
+
toigo_blunt_bob_with_bangs: { label: 'Blunt bob with bangs', fit: 'female' },
|
|
24
|
+
toigo_curled_under_bob: { label: 'Curled bob', fit: 'female' },
|
|
25
|
+
toigo_inverted_bob_with_bangs: { label: 'Inverted bob with bangs', fit: 'female' },
|
|
26
|
+
cortu_straight_bangs: { label: 'Straight with bangs', fit: 'female' },
|
|
27
|
+
culturalibre_hair_01: { label: 'Long sleek', fit: 'female' },
|
|
28
|
+
sonntag78_junglebook_hair: { label: 'Shoulder layers', fit: 'female' },
|
|
29
|
+
rehmanpolanski_hair_bun_brown: { label: 'Bun', fit: 'female' },
|
|
30
|
+
culturalibre_hair_02: { label: 'Short side part', fit: 'male' },
|
|
21
31
|
};
|
|
22
32
|
|
|
23
33
|
export const CLOTHES: Record<string, ItemInfo & { slot: ClothingSlot }> = {
|
|
@@ -33,6 +43,23 @@ export const CLOTHES: Record<string, ItemInfo & { slot: ClothingSlot }> = {
|
|
|
33
43
|
male_casualsuit06: { label: 'Casual 6', fit: 'male', slot: 'outfit' },
|
|
34
44
|
male_elegantsuit01: { label: 'Suit', fit: 'male', slot: 'outfit' },
|
|
35
45
|
male_worksuit01: { label: 'Workwear', fit: 'male', slot: 'outfit' },
|
|
46
|
+
// MakeHuman community clothes (CC0, see LICENSE.ASSETS.md)
|
|
47
|
+
toigo_camisole_dress_with_full_skirt: { label: 'Camisole dress', fit: 'female', slot: 'outfit' },
|
|
48
|
+
toigo_halter_dress_knee_length: { label: 'Halter dress', fit: 'female', slot: 'outfit' },
|
|
49
|
+
toigo_halter_dress_midi: { label: 'Halter midi', fit: 'female', slot: 'outfit' },
|
|
50
|
+
toigo_halter_dress_with_fluted_skirt: { label: 'Halter gown', fit: 'female', slot: 'outfit' },
|
|
51
|
+
toigo_shift_dress: { label: 'Shift dress', fit: 'female', slot: 'outfit' },
|
|
52
|
+
toigo_keyhole_neck_dress: { label: 'Keyhole dress', fit: 'female', slot: 'outfit' },
|
|
53
|
+
toigo_dress_with_tiered_skirt: { label: 'Tiered dress', fit: 'female', slot: 'outfit' },
|
|
54
|
+
toigo_bodice_dress_with_lace_ruffle_skirt: { label: 'Lace ruffle dress', fit: 'female', slot: 'outfit' },
|
|
55
|
+
toigo_strapless_ruffle_top_dress: { label: 'Strapless dress', fit: 'female', slot: 'outfit' },
|
|
56
|
+
toigo_cut_out_dress: { label: 'Cut-out dress', fit: 'female', slot: 'outfit' },
|
|
57
|
+
toigo_female_suit: { label: 'Skirt suit', fit: 'female', slot: 'outfit' },
|
|
58
|
+
toigo_female_suit_2: { label: 'Trouser suit', fit: 'female', slot: 'outfit' },
|
|
59
|
+
'toigo_female_double-breasted_suit': { label: 'Double-breasted suit', fit: 'female', slot: 'outfit' },
|
|
60
|
+
toigo_male_suit_3: { label: 'Suit and tie', fit: 'male', slot: 'outfit' },
|
|
61
|
+
'toigo_male_double-breasted_suit': { label: 'Double-breasted suit', fit: 'male', slot: 'outfit' },
|
|
62
|
+
toigo_suit_with_dinner_jacket: { label: 'Dinner jacket', fit: 'male', slot: 'outfit' },
|
|
36
63
|
shoes01: { label: 'Shoes 1', fit: 'unisex', slot: 'shoes' },
|
|
37
64
|
shoes02: { label: 'Shoes 2', fit: 'unisex', slot: 'shoes' },
|
|
38
65
|
shoes03: { label: 'Shoes 3', fit: 'unisex', slot: 'shoes' },
|
|
@@ -41,8 +68,38 @@ export const CLOTHES: Record<string, ItemInfo & { slot: ClothingSlot }> = {
|
|
|
41
68
|
shoes06: { label: 'Shoes 6', fit: 'unisex', slot: 'shoes' },
|
|
42
69
|
fedora01: { label: 'Fedora', fit: 'unisex', slot: 'hat' },
|
|
43
70
|
fedora_cocked: { label: 'Fedora (tilted)', fit: 'unisex', slot: 'hat' },
|
|
71
|
+
// Generated by tools/build-garments.ts (original, CC0): tintable, see GARMENT_COLORS.
|
|
72
|
+
abaya01: { label: 'Abaya', fit: 'female', slot: 'outfit' },
|
|
73
|
+
jalabiya01: { label: 'Jalabiya', fit: 'female', slot: 'outfit' },
|
|
74
|
+
dress_knee01: { label: 'Day dress', fit: 'female', slot: 'outfit' },
|
|
75
|
+
dress_maxi01: { label: 'Maxi dress', fit: 'female', slot: 'outfit' },
|
|
76
|
+
gown01: { label: 'Evening gown', fit: 'female', slot: 'outfit' },
|
|
77
|
+
hijab01: { label: 'Hijab', fit: 'female', slot: 'headwear' },
|
|
78
|
+
amira01: { label: 'Al-amira', fit: 'female', slot: 'headwear' },
|
|
44
79
|
};
|
|
45
80
|
|
|
81
|
+
/** Fabric colors for tintable garments (multiplied over a light fabric texture). */
|
|
82
|
+
export const GARMENT_COLORS: { id: string; label: string; color: string }[] = [
|
|
83
|
+
{ id: 'black', label: 'Black', color: '#1b1b1f' },
|
|
84
|
+
{ id: 'charcoal', label: 'Charcoal', color: '#3a3c42' },
|
|
85
|
+
{ id: 'navy', label: 'Navy', color: '#1f3a5f' },
|
|
86
|
+
{ id: 'slate', label: 'Slate blue', color: '#3f5e8c' },
|
|
87
|
+
{ id: 'teal', label: 'Teal', color: '#1f6f6b' },
|
|
88
|
+
{ id: 'emerald', label: 'Emerald', color: '#1e6b45' },
|
|
89
|
+
{ id: 'olive', label: 'Olive', color: '#6b6a3a' },
|
|
90
|
+
{ id: 'mocha', label: 'Mocha', color: '#6b4f3f' },
|
|
91
|
+
{ id: 'sand', label: 'Sand', color: '#cbb89a' },
|
|
92
|
+
{ id: 'ivory', label: 'Ivory', color: '#e9e2d6' },
|
|
93
|
+
{ id: 'white', label: 'White', color: '#f4f3ef' },
|
|
94
|
+
{ id: 'blush', label: 'Blush', color: '#d9a7a0' },
|
|
95
|
+
{ id: 'rose', label: 'Dusty rose', color: '#b56f7b' },
|
|
96
|
+
{ id: 'terracotta', label: 'Terracotta', color: '#b85c4a' },
|
|
97
|
+
{ id: 'burgundy', label: 'Burgundy', color: '#7c2945' },
|
|
98
|
+
{ id: 'plum', label: 'Plum', color: '#5b3a5e' },
|
|
99
|
+
{ id: 'lavender', label: 'Lavender', color: '#a79bc4' },
|
|
100
|
+
{ id: 'sky', label: 'Sky', color: '#8fb3d1' },
|
|
101
|
+
];
|
|
102
|
+
|
|
46
103
|
export const EYE_LABELS: Record<string, string> = {
|
|
47
104
|
brown: 'Dark brown', brownlight: 'Brown', green: 'Green', blue: 'Blue', deepblue: 'Deep blue',
|
|
48
105
|
lightblue: 'Light blue', bluegreen: 'Blue-green', grey: 'Grey', ice: 'Ice',
|
|
@@ -81,7 +138,7 @@ const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
|
81
138
|
export function hairInfo(id: string): ItemInfo { return HAIR[id] ?? { label: cap(words(id)), fit: 'unisex' }; }
|
|
82
139
|
export function clothingInfo(id: string): ItemInfo & { slot: ClothingSlot } {
|
|
83
140
|
if (CLOTHES[id]) return CLOTHES[id];
|
|
84
|
-
const slot: ClothingSlot = id.startsWith('shoes') ? 'shoes' : /hat|fedora|cap|helmet/.test(id) ? 'hat' : 'outfit';
|
|
141
|
+
const slot: ClothingSlot = id.startsWith('shoes') ? 'shoes' : /hijab|amira|shayla|scarf|turban/.test(id) ? 'headwear' : /hat|fedora|cap|helmet/.test(id) ? 'hat' : 'outfit';
|
|
85
142
|
const fit: Fit = id.startsWith('female') ? 'female' : id.startsWith('male') ? 'male' : 'unisex';
|
|
86
143
|
return { label: cap(words(id.replace(/^(fe)?male_/, ''))), fit, slot };
|
|
87
144
|
}
|
package/src/human/editor.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
// editor.normalize(savedConfig) -> clamps/repairs any config to the options (use on load)
|
|
9
9
|
import { DEFAULT_HUMAN, HumanConfig, HumanManifest } from './runtime';
|
|
10
10
|
import { DETAIL_SLIDERS, MACRO_SLIDERS } from './sliders';
|
|
11
|
-
import { EYE_LABELS, EYE_SWATCH, HUMAN_HAIR_COLORS, SKIN_TINTS, clothingInfo, fits, hairInfo, skinInfo } from './catalog';
|
|
11
|
+
import { EYE_LABELS, EYE_SWATCH, GARMENT_COLORS, HUMAN_HAIR_COLORS, SKIN_TINTS, clothingInfo, fits, hairInfo, skinInfo } from './catalog';
|
|
12
12
|
|
|
13
13
|
export type EditorGender = 'female' | 'male' | 'any';
|
|
14
|
-
export type SectionId = 'body' | 'face' | 'skin' | 'eyes' | 'brows' | 'nose' | 'mouth' | 'ears' | 'hair' | 'outfit' | 'shoes' | 'hat';
|
|
15
|
-
export type ItemSlot = 'hair' | 'outfit' | 'shoes' | 'hat' | 'skin' | 'eyes' | 'eyebrows' | 'eyelashes';
|
|
14
|
+
export type SectionId = 'body' | 'face' | 'skin' | 'eyes' | 'brows' | 'nose' | 'mouth' | 'ears' | 'hair' | 'headwear' | 'outfit' | 'shoes' | 'hat';
|
|
15
|
+
export type ItemSlot = 'hair' | 'headwear' | 'outfit' | 'shoes' | 'hat' | 'skin' | 'eyes' | 'eyebrows' | 'eyelashes';
|
|
16
16
|
|
|
17
|
-
export const ALL_SECTIONS: SectionId[] = ['face', 'skin', 'eyes', 'brows', 'nose', 'mouth', 'ears', 'hair', 'body', 'outfit', 'shoes', 'hat'];
|
|
17
|
+
export const ALL_SECTIONS: SectionId[] = ['face', 'skin', 'eyes', 'brows', 'nose', 'mouth', 'ears', 'hair', 'headwear', 'body', 'outfit', 'shoes', 'hat'];
|
|
18
18
|
|
|
19
19
|
export interface HumanEditorOptions {
|
|
20
20
|
/** Lock the avatar to one gender: hides the gender slider and filters hair, clothes and skins. Default 'any'. */
|
|
@@ -28,9 +28,9 @@ export interface HumanEditorOptions {
|
|
|
28
28
|
/** Allowed item ids per slot (default: everything that fits the gender). */
|
|
29
29
|
items?: Partial<Record<ItemSlot, string[]>>;
|
|
30
30
|
/** Color palettes. */
|
|
31
|
-
colors?: { hair?: string[]; eyebrows?: string[]; skinTint?: string[] };
|
|
32
|
-
/** Slots that may be empty ("None" option). Default: hat and eyebrows. */
|
|
33
|
-
optional?: Partial<Record<'hair' | 'hat' | 'eyebrows' | 'shoes', boolean>>;
|
|
31
|
+
colors?: { hair?: string[]; eyebrows?: string[]; skinTint?: string[]; garments?: string[] };
|
|
32
|
+
/** Slots that may be empty ("None" option). Default: hat, headwear and eyebrows. */
|
|
33
|
+
optional?: Partial<Record<'hair' | 'hat' | 'headwear' | 'eyebrows' | 'shoes', boolean>>;
|
|
34
34
|
/** Max magnitude of face/body detail sliders (0..1). Lower keeps faces more natural. Default 1. */
|
|
35
35
|
detailRange?: number;
|
|
36
36
|
/** Starting values (merged over the gender's default look). */
|
|
@@ -64,9 +64,12 @@ export interface HumanEditorModel {
|
|
|
64
64
|
|
|
65
65
|
const SECTION_LABELS: Record<SectionId, string> = {
|
|
66
66
|
body: 'Body', face: 'Face', skin: 'Skin', eyes: 'Eyes', brows: 'Brows', nose: 'Nose', mouth: 'Mouth', ears: 'Ears',
|
|
67
|
-
hair: 'Hair', outfit: 'Outfit', shoes: 'Shoes', hat: 'Hat',
|
|
67
|
+
hair: 'Hair', headwear: 'Headwear', outfit: 'Outfit', shoes: 'Shoes', hat: 'Hat',
|
|
68
68
|
};
|
|
69
|
-
const FACE_SECTIONS: SectionId[] = ['face', 'skin', 'eyes', 'brows', 'nose', 'mouth', 'ears', 'hair', 'hat'];
|
|
69
|
+
const FACE_SECTIONS: SectionId[] = ['face', 'skin', 'eyes', 'brows', 'nose', 'mouth', 'ears', 'hair', 'headwear', 'hat'];
|
|
70
|
+
type WearSlot = 'outfit' | 'shoes' | 'hat' | 'headwear';
|
|
71
|
+
/** Slots that can't be worn together: choosing one takes off the other. */
|
|
72
|
+
const CONFLICTS: Partial<Record<WearSlot, WearSlot>> = { hat: 'headwear', headwear: 'hat' };
|
|
70
73
|
const BROW_SLIDERS = ['browHeight', 'browAngle', 'browDepth'];
|
|
71
74
|
|
|
72
75
|
const GENDER_DEFAULTS: Record<'female' | 'male', Partial<HumanConfig>> = {
|
|
@@ -88,7 +91,7 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
88
91
|
const include = options.include ? new Set(options.include) : null;
|
|
89
92
|
const range = Math.min(1, Math.max(0, options.detailRange ?? 1));
|
|
90
93
|
const label = (id: string, fallback: string) => options.labels?.[id] ?? fallback;
|
|
91
|
-
const optional = { hat: true, eyebrows: true, hair: false, shoes: false, ...options.optional };
|
|
94
|
+
const optional = { hat: true, headwear: true, eyebrows: true, hair: false, shoes: false, ...options.optional };
|
|
92
95
|
|
|
93
96
|
// ---- allowed items
|
|
94
97
|
const restrict = (slot: ItemSlot, ids: string[]) => {
|
|
@@ -96,9 +99,10 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
96
99
|
return only ? ids.filter((id) => only.includes(id)) : ids;
|
|
97
100
|
};
|
|
98
101
|
const clothes = manifest.proxies.filter((p) => p.kind === 'clothes').map((p) => p.id);
|
|
99
|
-
const bySlot = (slot:
|
|
102
|
+
const bySlot = (slot: WearSlot) => clothes.filter((id) => clothingInfo(id).slot === slot && fits(clothingInfo(id).fit, gender));
|
|
100
103
|
const allowed: Record<ItemSlot, string[]> = {
|
|
101
104
|
hair: restrict('hair', manifest.proxies.filter((p) => p.kind === 'hair').map((p) => p.id).filter((id) => fits(hairInfo(id).fit, gender))),
|
|
105
|
+
headwear: restrict('headwear', bySlot('headwear')),
|
|
102
106
|
outfit: restrict('outfit', bySlot('outfit')),
|
|
103
107
|
shoes: restrict('shoes', bySlot('shoes')),
|
|
104
108
|
hat: restrict('hat', bySlot('hat')),
|
|
@@ -113,6 +117,9 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
113
117
|
const hairColors = options.colors?.hair ?? HUMAN_HAIR_COLORS.map((h) => h.color);
|
|
114
118
|
const browColors = options.colors?.eyebrows ?? hairColors;
|
|
115
119
|
const tints = options.colors?.skinTint ?? SKIN_TINTS;
|
|
120
|
+
const garmentColors = options.colors?.garments ?? GARMENT_COLORS.map((g) => g.color);
|
|
121
|
+
const tintable = (id: string) => !!manifest.proxies.find((p) => p.kind === 'clothes' && p.id === id)?.tintable;
|
|
122
|
+
const defaultColor = (id: string | undefined) => manifest.proxies.find((p) => p.kind === 'clothes' && p.id === id)?.color;
|
|
116
123
|
|
|
117
124
|
// ---- controls
|
|
118
125
|
const shown = (section: SectionId, id: string) => !exclude.has(section) && !exclude.has(`${section}.${id}`) && !exclude.has(id) && (!include || include.has(`${section}.${id}`) || include.has(id));
|
|
@@ -132,10 +139,16 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
132
139
|
const color = (id: string, lbl: string, colors: string[], value: ColorControl['value'], apply: ColorControl['apply']): ColorControl =>
|
|
133
140
|
({ kind: 'color', id, label: label(id, lbl), colors, value, apply });
|
|
134
141
|
const none = (on: boolean): ChoiceOption[] => (on ? [{ id: null, label: label('none', 'None') }] : []);
|
|
135
|
-
const
|
|
142
|
+
const worn = (c: HumanConfig, slot: WearSlot) => c.clothes.find((id) => clothingInfo(id).slot === slot);
|
|
143
|
+
const clothingChoice = (slot: WearSlot, lbl: string, canBeEmpty: boolean) => choice('style', lbl,
|
|
136
144
|
[...none(canBeEmpty), ...allowed[slot].map((id) => ({ id, label: label(id, clothingInfo(id).label) }))],
|
|
137
|
-
(c) => c
|
|
138
|
-
(c, v) => ({ ...c, clothes: [...c.clothes.filter((id) => clothingInfo(id).slot !== slot), ...(v ? [v] : [])] }));
|
|
145
|
+
(c) => worn(c, slot) ?? null,
|
|
146
|
+
(c, v) => ({ ...c, clothes: [...c.clothes.filter((id) => clothingInfo(id).slot !== slot && !(v && clothingInfo(id).slot === CONFLICTS[slot])), ...(v ? [v] : [])] }));
|
|
147
|
+
/** Fabric color of the tintable garment in a slot (only shown when the slot offers one). */
|
|
148
|
+
const garmentColor = (slot: WearSlot): EditorControl[] => (allowed[slot].some(tintable)
|
|
149
|
+
? [color('color', 'Color', garmentColors, (c) => c.garmentColors?.[slot] ?? defaultColor(worn(c, slot)) ?? garmentColors[0],
|
|
150
|
+
(c, v) => ({ ...c, garmentColors: { ...c.garmentColors, [slot]: v } }))]
|
|
151
|
+
: []);
|
|
139
152
|
|
|
140
153
|
const build: Record<SectionId, () => EditorControl[]> = {
|
|
141
154
|
face: () => [
|
|
@@ -174,7 +187,8 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
174
187
|
...(gender !== 'male' ? [macro('cupSize', 'Bust'), macro('firmness', 'Firmness')] : []),
|
|
175
188
|
...DETAIL_SLIDERS.filter((d) => d.group === 'body').map((d) => detail(d.id)),
|
|
176
189
|
],
|
|
177
|
-
|
|
190
|
+
headwear: () => [clothingChoice('headwear', 'Headwear', optional.headwear), ...garmentColor('headwear')],
|
|
191
|
+
outfit: () => [clothingChoice('outfit', 'Outfit', false), ...garmentColor('outfit')],
|
|
178
192
|
shoes: () => [clothingChoice('shoes', 'Shoes', optional.shoes)],
|
|
179
193
|
hat: () => [clothingChoice('hat', 'Hat', optional.hat)],
|
|
180
194
|
};
|
|
@@ -183,7 +197,7 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
183
197
|
for (const id of options.sections ?? ALL_SECTIONS) {
|
|
184
198
|
if (exclude.has(id)) continue;
|
|
185
199
|
const controls = build[id]().filter((ctl) => shown(id, ctl.id)).filter((ctl) => ctl.kind !== 'choice' || ctl.options.length > 1 || ctl.options[0]?.id !== null);
|
|
186
|
-
if ((id === 'outfit' || id === 'shoes' || id === 'hat') && !allowed[id].length) continue;
|
|
200
|
+
if ((id === 'outfit' || id === 'shoes' || id === 'hat' || id === 'headwear') && !allowed[id].length) continue;
|
|
187
201
|
if (controls.length) sections.push({ id, label: label(id, SECTION_LABELS[id]), framing: FACE_SECTIONS.includes(id) ? 'face' : 'body', controls });
|
|
188
202
|
}
|
|
189
203
|
|
|
@@ -202,14 +216,16 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
202
216
|
c.hair = c.hair === null && optional.hair ? null : pick(c.hair, allowed.hair, base.hair);
|
|
203
217
|
c.eyebrows = c.eyebrows === null && optional.eyebrows ? null : pick(c.eyebrows, allowed.eyebrows, base.eyebrows);
|
|
204
218
|
c.eyelashes = c.eyelashes === null ? null : pick(c.eyelashes, allowed.eyelashes, base.eyelashes);
|
|
205
|
-
const wear = (slot:
|
|
219
|
+
const wear = (slot: WearSlot, canBeEmpty: boolean) => {
|
|
206
220
|
const cur = c.clothes.find((id) => clothingInfo(id).slot === slot);
|
|
207
221
|
const fallback = base.clothes.find((id) => clothingInfo(id).slot === slot);
|
|
208
222
|
if (!cur && canBeEmpty) return [];
|
|
209
223
|
const v = pick(cur, allowed[slot], fallback);
|
|
210
224
|
return v ? [v] : [];
|
|
211
225
|
};
|
|
212
|
-
|
|
226
|
+
const headwear = wear('headwear', true);
|
|
227
|
+
c.clothes = [...wear('outfit', false), ...wear('shoes', optional.shoes), ...headwear, ...(headwear.length ? [] : wear('hat', true))];
|
|
228
|
+
if (c.garmentColors) c.garmentColors = Object.fromEntries(Object.entries(c.garmentColors).filter(([, v]) => typeof v === 'string'));
|
|
213
229
|
return c;
|
|
214
230
|
}
|
|
215
231
|
|
|
@@ -233,7 +249,9 @@ export function createHumanEditor(manifest: HumanManifest, options: HumanEditorO
|
|
|
233
249
|
return { options, sections, initial: normalize(), normalize, randomize, allowed };
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
const HAIR_ORDER = ['long01', '
|
|
252
|
+
const HAIR_ORDER = ['long01', 'culturalibre_hair_01', 'cortu_straight_bangs', 'sonntag78_junglebook_hair', 'ponytail01', 'rehmanpolanski_hair_bun_brown',
|
|
253
|
+
'toigo_blunt_bob', 'toigo_blunt_bob_with_bangs', 'toigo_curled_under_bob', 'toigo_inverted_bob_with_bangs', 'bob02', 'afro01',
|
|
254
|
+
'short02', 'culturalibre_hair_02', 'short01', 'short04', 'bob01', 'braid01', 'short03'];
|
|
237
255
|
const order = (id: string) => { const i = HAIR_ORDER.indexOf(id); return i < 0 ? 100 : i; };
|
|
238
256
|
|
|
239
257
|
/** Same age/ethnicity skin among the allowed ones (used when gender filtering removes the current skin). */
|
package/src/human/runtime.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// (TextureLoader on the web by default; see src/native/humanTextures.ts for React Native).
|
|
7
7
|
import {
|
|
8
8
|
Bone, BufferAttribute, BufferGeometry, DoubleSide, FrontSide, Group, Material, Matrix4, MeshStandardMaterial,
|
|
9
|
-
Quaternion, SRGBColorSpace, Skeleton, SkinnedMesh, Texture, TextureLoader, Vector3,
|
|
9
|
+
Quaternion, RepeatWrapping, SRGBColorSpace, Skeleton, SkinnedMesh, Texture, TextureLoader, Vector3,
|
|
10
10
|
} from 'three';
|
|
11
11
|
import { gunzipSync } from 'fflate';
|
|
12
12
|
import { DETAIL_SLIDERS } from './sliders';
|
|
13
13
|
import { GlbPrimitive, readGlb, toFloat } from './glb';
|
|
14
|
+
import { clothingInfo } from './catalog';
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------- config
|
|
16
17
|
export interface HumanConfig {
|
|
@@ -39,6 +40,8 @@ export interface HumanConfig {
|
|
|
39
40
|
hair: string | null;
|
|
40
41
|
hairColor: string;
|
|
41
42
|
clothes: string[];
|
|
43
|
+
/** Colors of tintable garments by clothing slot ('outfit', 'headwear'...); unset = the garment's default. */
|
|
44
|
+
garmentColors?: Record<string, string>;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export const DEFAULT_HUMAN: HumanConfig = {
|
|
@@ -53,6 +56,13 @@ export interface ProxyMeta {
|
|
|
53
56
|
id: string; kind: string; file: string; triangles: number; transparent: boolean; tintable: boolean; zDepth: number;
|
|
54
57
|
deleteVerts?: [number, number][]; scale: [number, number, number][];
|
|
55
58
|
texture?: string; normalTexture?: string; textureSize: number; doubleSided: boolean;
|
|
59
|
+
/** Default tint for tintable garments. */
|
|
60
|
+
color?: string;
|
|
61
|
+
/** Headwear that covers the hair: the hair proxy is not shown while it is worn. */
|
|
62
|
+
hidesHair?: boolean;
|
|
63
|
+
roughness?: number;
|
|
64
|
+
/** Texture coordinates are in tiles (repeat wrapping). */
|
|
65
|
+
tileable?: boolean;
|
|
56
66
|
}
|
|
57
67
|
export interface HumanManifest {
|
|
58
68
|
vertexCount: number;
|
|
@@ -233,9 +243,25 @@ function geometryFrom(prim: GlbPrimitive, joints: string, weights: string) {
|
|
|
233
243
|
g.setAttribute('skinIndex', new BufferAttribute(asU16(prim.attributes[joints]), 4));
|
|
234
244
|
g.setAttribute('skinWeight', new BufferAttribute(toFloat(prim.attributes[weights]), 4));
|
|
235
245
|
g.setIndex(new BufferAttribute(prim.index, 1));
|
|
246
|
+
if (prim.attributes.COLOR_0) g.setAttribute('color', new BufferAttribute(toFloat(prim.attributes.COLOR_0), prim.attributes.COLOR_0.itemSize));
|
|
236
247
|
return g;
|
|
237
248
|
}
|
|
238
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Vertices split only for texture seams share refs/weights/offsets. Pairs (dup, first) so
|
|
252
|
+
* normals can be averaged across the seam after computeVertexNormals.
|
|
253
|
+
*/
|
|
254
|
+
function seamPairs(refs: Uint16Array, refw: Float32Array, offs: Float32Array) {
|
|
255
|
+
const first = new Map<string, number>(), pairs: number[] = [];
|
|
256
|
+
for (let i = 0; i < refs.length / 3; i++) {
|
|
257
|
+
let k = '';
|
|
258
|
+
for (let c = 0; c < 3; c++) k += `${refs[i * 3 + c]},${Math.round(refw[i * 3 + c] * 1e4)},${Math.round(offs[i * 3 + c] * 1e5)},`;
|
|
259
|
+
const f = first.get(k);
|
|
260
|
+
if (f === undefined) first.set(k, i); else pairs.push(i, f);
|
|
261
|
+
}
|
|
262
|
+
return Uint32Array.from(pairs);
|
|
263
|
+
}
|
|
264
|
+
|
|
239
265
|
/**
|
|
240
266
|
* Hair cards: the texture's transparent texels are near-white, which shows as a light halo
|
|
241
267
|
* around strands once alpha is blended. Darken texels by their coverage so tips fade naturally.
|
|
@@ -248,7 +274,7 @@ function hairShading(m: MeshStandardMaterial) {
|
|
|
248
274
|
m.customProgramCacheKey = () => 'humanforge-hair';
|
|
249
275
|
}
|
|
250
276
|
|
|
251
|
-
interface ProxyInstance { meta: ProxyMeta; mesh: SkinnedMesh; soft?: SkinnedMesh; refs: Uint16Array; refw: Float32Array; offs: Float32Array }
|
|
277
|
+
interface ProxyInstance { meta: ProxyMeta; mesh: SkinnedMesh; soft?: SkinnedMesh; refs: Uint16Array; refw: Float32Array; offs: Float32Array; seams?: Uint32Array }
|
|
252
278
|
|
|
253
279
|
export class HumanAvatar {
|
|
254
280
|
readonly group = new Group();
|
|
@@ -318,7 +344,8 @@ export class HumanAvatar {
|
|
|
318
344
|
if (c.eyelashes) wanted.set('eyelashes', `eyelashes-${c.eyelashes}`);
|
|
319
345
|
wanted.set('teeth', 'teeth-teeth_base');
|
|
320
346
|
wanted.set('tongue', 'tongue-tongue01');
|
|
321
|
-
|
|
347
|
+
const meta = (id: string) => this.pkg.manifest.proxies.find((p) => p.kind === 'clothes' && p.id === id);
|
|
348
|
+
if (c.hair && !c.clothes.some((id) => meta(id)?.hidesHair)) wanted.set('hair', `hair-${c.hair}`);
|
|
322
349
|
c.clothes.forEach((id) => wanted.set(`clothes:${id}`, `clothes-${id}`));
|
|
323
350
|
const loads = [...wanted.entries()].map(async ([slot, id]) => {
|
|
324
351
|
const cur = this.proxies.get(slot);
|
|
@@ -347,6 +374,7 @@ export class HumanAvatar {
|
|
|
347
374
|
if (slot === 'hair') { m.color.set(c.hairColor); (p.soft?.material as MeshStandardMaterial | undefined)?.color.set(c.hairColor); }
|
|
348
375
|
if (slot === 'eyebrows') m.color.set(c.eyebrowColor);
|
|
349
376
|
if (slot === 'eyes' && m.map !== eyeTex) { m.map = eyeTex; m.needsUpdate = true; }
|
|
377
|
+
if (p.meta.kind === 'clothes' && p.meta.tintable) m.color.set(c.garmentColors?.[clothingInfo(p.meta.id).slot] ?? p.meta.color ?? '#ffffff');
|
|
350
378
|
}
|
|
351
379
|
this.reshape();
|
|
352
380
|
}
|
|
@@ -396,9 +424,11 @@ export class HumanAvatar {
|
|
|
396
424
|
const refs = asU16(prim.attributes._REFS), refw = toFloat(prim.attributes._REFW), offs = toFloat(prim.attributes._OFFS);
|
|
397
425
|
const mat = new MeshStandardMaterial({
|
|
398
426
|
map, normalMap, metalness: 0,
|
|
399
|
-
roughness: meta.kind === 'eyes' ? 0.15 : meta.kind === 'hair' ? 0.55 : 0.8,
|
|
427
|
+
roughness: meta.roughness ?? (meta.kind === 'eyes' ? 0.15 : meta.kind === 'hair' ? 0.55 : 0.8),
|
|
400
428
|
side: meta.doubleSided ? DoubleSide : FrontSide,
|
|
429
|
+
vertexColors: !!geo.getAttribute('color'),
|
|
401
430
|
});
|
|
431
|
+
if (meta.tileable) for (const t of [map, normalMap]) if (t && t.wrapS !== RepeatWrapping) { t.wrapS = t.wrapT = RepeatWrapping; t.needsUpdate = true; }
|
|
402
432
|
let soft: SkinnedMesh | undefined;
|
|
403
433
|
if (meta.kind === 'hair') {
|
|
404
434
|
// Two passes: a solid core (alpha-tested, writes depth, so strands sort correctly) and a
|
|
@@ -432,7 +462,8 @@ export class HumanAvatar {
|
|
|
432
462
|
mesh.morphTargetDictionary = this.body.morphTargetDictionary;
|
|
433
463
|
mesh.morphTargetInfluences = [...(this.body.morphTargetInfluences ?? [])];
|
|
434
464
|
}
|
|
435
|
-
|
|
465
|
+
const seams = meta.kind === 'clothes' ? seamPairs(refs, refw, offs) : undefined;
|
|
466
|
+
return { meta, mesh, soft, refs, refw, offs, seams: seams?.length ? seams : undefined };
|
|
436
467
|
}
|
|
437
468
|
|
|
438
469
|
private removeProxy(slot: string) {
|
|
@@ -497,6 +528,11 @@ export class HumanAvatar {
|
|
|
497
528
|
}
|
|
498
529
|
pp.needsUpdate = true;
|
|
499
530
|
g.computeVertexNormals();
|
|
531
|
+
if (p.seams) {
|
|
532
|
+
const nn = g.getAttribute('normal') as BufferAttribute, q = p.seams;
|
|
533
|
+
for (let i = 0; i < q.length; i += 2) { tmpA.fromBufferAttribute(nn, q[i]); tmpB.fromBufferAttribute(nn, q[i + 1]).add(tmpA); nn.setXYZ(q[i + 1], tmpB.x, tmpB.y, tmpB.z); }
|
|
534
|
+
for (let i = q.length - 2; i >= 0; i -= 2) { tmpA.fromBufferAttribute(nn, q[i + 1]).normalize(); nn.setXYZ(q[i + 1], tmpA.x, tmpA.y, tmpA.z); nn.setXYZ(q[i], tmpA.x, tmpA.y, tmpA.z); }
|
|
535
|
+
}
|
|
500
536
|
g.computeBoundingSphere();
|
|
501
537
|
}
|
|
502
538
|
|