humanforge 0.0.0-stage → 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/README.md +230 -2
- package/package.json +108 -4
- package/src/core/build.ts +548 -0
- package/src/core/codec.ts +39 -0
- package/src/core/config.ts +179 -0
- package/src/core/glb.ts +108 -0
- package/src/core/index.ts +6 -0
- package/src/core/scene.ts +85 -0
- package/src/core/schema.ts +79 -0
- package/src/human/export.ts +32 -0
- package/src/human/faceMap.ts +26 -0
- package/src/human/fit.ts +619 -0
- package/src/human/glb.ts +67 -0
- package/src/human/index.ts +3 -0
- package/src/human/runtime.ts +508 -0
- package/src/human/selfie.ts +76 -0
- package/src/human/sliders.ts +145 -0
- package/src/index.ts +3 -0
- package/src/native/AvatarCreator.tsx +161 -0
- package/src/native/AvatarView.tsx +135 -0
- package/src/native/HumanView.tsx +149 -0
- package/src/native/humanExpo.ts +58 -0
- package/src/native/index.ts +7 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Avatar configuration: the single source of truth. Everything (mesh, UI, codec)
|
|
2
|
+
// is derived from this plain JSON object, so it is cheap to store and sync.
|
|
3
|
+
|
|
4
|
+
export const HAIR_STYLES = ['bald', 'buzz', 'short', 'sidepart', 'curly', 'bob', 'long', 'ponytail', 'bun', 'afro'] as const;
|
|
5
|
+
export const FACIAL_HAIR = ['none', 'stubble', 'mustache', 'goatee', 'beard'] as const;
|
|
6
|
+
export const BROWS = ['none', 'thin', 'thick', 'arched'] as const;
|
|
7
|
+
export const NOSES = ['button', 'straight', 'wide'] as const;
|
|
8
|
+
export const MOUTHS = ['smile', 'neutral', 'open'] as const;
|
|
9
|
+
export const TOPS = ['tank', 'tshirt', 'longsleeve', 'hoodie', 'kurta', 'dress'] as const;
|
|
10
|
+
export const BOTTOMS = ['pants', 'shorts', 'skirt', 'longskirt'] as const;
|
|
11
|
+
export const SHOES = ['sneakers', 'boots', 'sandals'] as const;
|
|
12
|
+
export const HEADWEAR = ['none', 'cap', 'beanie', 'kufi', 'hijab'] as const;
|
|
13
|
+
export const GLASSES = ['none', 'round', 'square'] as const;
|
|
14
|
+
|
|
15
|
+
export type HairStyle = (typeof HAIR_STYLES)[number];
|
|
16
|
+
export type FacialHair = (typeof FACIAL_HAIR)[number];
|
|
17
|
+
export type Brow = (typeof BROWS)[number];
|
|
18
|
+
export type Nose = (typeof NOSES)[number];
|
|
19
|
+
export type Mouth = (typeof MOUTHS)[number];
|
|
20
|
+
export type Top = (typeof TOPS)[number];
|
|
21
|
+
export type Bottom = (typeof BOTTOMS)[number];
|
|
22
|
+
export type Shoe = (typeof SHOES)[number];
|
|
23
|
+
export type Headwear = (typeof HEADWEAR)[number];
|
|
24
|
+
export type Glasses = (typeof GLASSES)[number];
|
|
25
|
+
|
|
26
|
+
/** All sliders are normalised 0..1 (0.5 = neutral). Colors are '#rrggbb'. */
|
|
27
|
+
export interface AvatarConfig {
|
|
28
|
+
skin: string;
|
|
29
|
+
// face
|
|
30
|
+
headWidth: number;
|
|
31
|
+
jaw: number;
|
|
32
|
+
chin: number;
|
|
33
|
+
eyeSize: number;
|
|
34
|
+
eyeSpacing: number;
|
|
35
|
+
eyeColor: string;
|
|
36
|
+
brows: Brow;
|
|
37
|
+
nose: Nose;
|
|
38
|
+
noseSize: number;
|
|
39
|
+
mouth: Mouth;
|
|
40
|
+
mouthWidth: number;
|
|
41
|
+
lipColor: string;
|
|
42
|
+
earSize: number;
|
|
43
|
+
// hair
|
|
44
|
+
hair: HairStyle;
|
|
45
|
+
hairColor: string;
|
|
46
|
+
facialHair: FacialHair;
|
|
47
|
+
// body
|
|
48
|
+
height: number;
|
|
49
|
+
build: number;
|
|
50
|
+
chest: number;
|
|
51
|
+
hips: number;
|
|
52
|
+
// outfit
|
|
53
|
+
top: Top;
|
|
54
|
+
topColor: string;
|
|
55
|
+
bottom: Bottom;
|
|
56
|
+
bottomColor: string;
|
|
57
|
+
shoes: Shoe;
|
|
58
|
+
shoesColor: string;
|
|
59
|
+
// extras
|
|
60
|
+
headwear: Headwear;
|
|
61
|
+
headwearColor: string;
|
|
62
|
+
glasses: Glasses;
|
|
63
|
+
glassesColor: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const SKIN_TONES = ['#ffdbc4', '#f1c27d', '#e0ac69', '#c68642', '#a1665e', '#8d5524', '#5c3a21', '#3b2219'];
|
|
67
|
+
export const HAIR_COLORS = ['#0e0b0a', '#2c1b10', '#4a2f1b', '#7a4a26', '#a76b3a', '#d6b370', '#e8d8b0', '#9a9a9a', '#b5452e', '#6b3fa0'];
|
|
68
|
+
export const EYE_COLORS = ['#3b2314', '#5b3a1e', '#8a6a2f', '#4f7a3a', '#3a6f9a', '#6e7f8c', '#1a1a1a'];
|
|
69
|
+
export const LIP_COLORS = ['#c97a6d', '#b3615a', '#9c4f4a', '#d98c8c', '#8a3b3b', '#b0475f'];
|
|
70
|
+
export const CLOTH_COLORS = [
|
|
71
|
+
'#f5f5f0', '#1d1d1f', '#2f3e5c', '#3f6fb5', '#8fb4d9', '#2f6b4f', '#7fae7a', '#c9a227',
|
|
72
|
+
'#e07a2e', '#b23a3a', '#d77fa1', '#6b4c9a', '#8b6b4e', '#c8b89a', '#5a5a5a',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const DEFAULT_AVATAR: AvatarConfig = {
|
|
76
|
+
skin: SKIN_TONES[2],
|
|
77
|
+
headWidth: 0.5,
|
|
78
|
+
jaw: 0.5,
|
|
79
|
+
chin: 0.5,
|
|
80
|
+
eyeSize: 0.5,
|
|
81
|
+
eyeSpacing: 0.5,
|
|
82
|
+
eyeColor: EYE_COLORS[1],
|
|
83
|
+
brows: 'thick',
|
|
84
|
+
nose: 'button',
|
|
85
|
+
noseSize: 0.5,
|
|
86
|
+
mouth: 'smile',
|
|
87
|
+
mouthWidth: 0.5,
|
|
88
|
+
lipColor: LIP_COLORS[0],
|
|
89
|
+
earSize: 0.5,
|
|
90
|
+
hair: 'short',
|
|
91
|
+
hairColor: HAIR_COLORS[1],
|
|
92
|
+
facialHair: 'none',
|
|
93
|
+
height: 0.5,
|
|
94
|
+
build: 0.5,
|
|
95
|
+
chest: 0.5,
|
|
96
|
+
hips: 0.5,
|
|
97
|
+
top: 'tshirt',
|
|
98
|
+
topColor: CLOTH_COLORS[3],
|
|
99
|
+
bottom: 'pants',
|
|
100
|
+
bottomColor: CLOTH_COLORS[2],
|
|
101
|
+
shoes: 'sneakers',
|
|
102
|
+
shoesColor: CLOTH_COLORS[0],
|
|
103
|
+
headwear: 'none',
|
|
104
|
+
headwearColor: CLOTH_COLORS[0],
|
|
105
|
+
glasses: 'none',
|
|
106
|
+
glassesColor: CLOTH_COLORS[1],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Fill missing/invalid fields with defaults (safe to call on untrusted input). */
|
|
110
|
+
export function normalizeConfig(input: Partial<AvatarConfig> | null | undefined): AvatarConfig {
|
|
111
|
+
const out: AvatarConfig = { ...DEFAULT_AVATAR };
|
|
112
|
+
if (!input || typeof input !== 'object') return out;
|
|
113
|
+
for (const k of Object.keys(DEFAULT_AVATAR) as (keyof AvatarConfig)[]) {
|
|
114
|
+
const v = (input as any)[k];
|
|
115
|
+
const d = DEFAULT_AVATAR[k];
|
|
116
|
+
if (typeof d === 'number' && typeof v === 'number' && isFinite(v)) (out as any)[k] = Math.min(1, Math.max(0, v));
|
|
117
|
+
else if (typeof d === 'string' && typeof v === 'string') {
|
|
118
|
+
if (d.startsWith('#')) {
|
|
119
|
+
if (/^#[0-9a-f]{6}$/i.test(v)) (out as any)[k] = v.toLowerCase();
|
|
120
|
+
} else if (ENUMS[k]?.includes(v)) (out as any)[k] = v;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const ENUMS: Partial<Record<keyof AvatarConfig, readonly string[]>> = {
|
|
127
|
+
brows: BROWS,
|
|
128
|
+
nose: NOSES,
|
|
129
|
+
mouth: MOUTHS,
|
|
130
|
+
hair: HAIR_STYLES,
|
|
131
|
+
facialHair: FACIAL_HAIR,
|
|
132
|
+
top: TOPS,
|
|
133
|
+
bottom: BOTTOMS,
|
|
134
|
+
shoes: SHOES,
|
|
135
|
+
headwear: HEADWEAR,
|
|
136
|
+
glasses: GLASSES,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const pick = <T,>(arr: readonly T[], r: () => number) => arr[Math.floor(r() * arr.length)];
|
|
140
|
+
|
|
141
|
+
/** Random avatar. Pass a seeded rng for reproducible results. */
|
|
142
|
+
export function randomAvatar(rng: () => number = Math.random): AvatarConfig {
|
|
143
|
+
const slider = () => 0.2 + rng() * 0.6;
|
|
144
|
+
const hairColor = pick(HAIR_COLORS.slice(0, 8), rng);
|
|
145
|
+
const feminine = rng() < 0.5;
|
|
146
|
+
return {
|
|
147
|
+
skin: pick(SKIN_TONES, rng),
|
|
148
|
+
headWidth: slider(),
|
|
149
|
+
jaw: slider(),
|
|
150
|
+
chin: slider(),
|
|
151
|
+
eyeSize: slider(),
|
|
152
|
+
eyeSpacing: slider(),
|
|
153
|
+
eyeColor: pick(EYE_COLORS, rng),
|
|
154
|
+
brows: pick(feminine ? (['thin', 'arched'] as const) : (['thick', 'thin'] as const), rng),
|
|
155
|
+
nose: pick(NOSES, rng),
|
|
156
|
+
noseSize: slider(),
|
|
157
|
+
mouth: pick(MOUTHS, rng),
|
|
158
|
+
mouthWidth: slider(),
|
|
159
|
+
lipColor: pick(LIP_COLORS, rng),
|
|
160
|
+
earSize: slider(),
|
|
161
|
+
hair: pick(feminine ? (['bob', 'long', 'ponytail', 'bun', 'curly', 'afro'] as const) : (['buzz', 'short', 'sidepart', 'curly', 'bald'] as const), rng),
|
|
162
|
+
hairColor,
|
|
163
|
+
facialHair: feminine ? 'none' : pick(FACIAL_HAIR, rng),
|
|
164
|
+
height: slider(),
|
|
165
|
+
build: slider(),
|
|
166
|
+
chest: feminine ? 0.55 + rng() * 0.3 : 0.3 + rng() * 0.3,
|
|
167
|
+
hips: feminine ? 0.55 + rng() * 0.35 : 0.2 + rng() * 0.3,
|
|
168
|
+
top: pick(feminine ? (['tshirt', 'longsleeve', 'dress', 'kurta'] as const) : (['tshirt', 'longsleeve', 'hoodie', 'kurta', 'tank'] as const), rng),
|
|
169
|
+
topColor: pick(CLOTH_COLORS, rng),
|
|
170
|
+
bottom: pick(feminine ? BOTTOMS : (['pants', 'shorts'] as const), rng),
|
|
171
|
+
bottomColor: pick(CLOTH_COLORS, rng),
|
|
172
|
+
shoes: pick(SHOES, rng),
|
|
173
|
+
shoesColor: pick(CLOTH_COLORS, rng),
|
|
174
|
+
headwear: rng() < 0.75 ? 'none' : pick(feminine ? (['hijab', 'beanie'] as const) : (['cap', 'beanie', 'kufi'] as const), rng),
|
|
175
|
+
headwearColor: pick(CLOTH_COLORS, rng),
|
|
176
|
+
glasses: rng() < 0.7 ? 'none' : pick(['round', 'square'] as const, rng),
|
|
177
|
+
glassesColor: pick(['#1d1d1f', '#8b6b4e', '#c9a227', '#5a5a5a'], rng),
|
|
178
|
+
};
|
|
179
|
+
}
|
package/src/core/glb.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Tiny dependency-free GLB (binary glTF 2.0) writer. Avoids three's GLTFExporter,
|
|
2
|
+
// which depends on Blob/FileReader APIs that React Native doesn't fully provide.
|
|
3
|
+
import { Color } from 'three';
|
|
4
|
+
import { AvatarConfig } from './config';
|
|
5
|
+
import { BuildOptions, buildAvatarGeometry } from './build';
|
|
6
|
+
|
|
7
|
+
export function avatarToGLB(config: Partial<AvatarConfig>, opts?: BuildOptions): Uint8Array {
|
|
8
|
+
const { parts } = buildAvatarGeometry(config, opts);
|
|
9
|
+
const chunks: Uint8Array[] = [];
|
|
10
|
+
let byteLength = 0;
|
|
11
|
+
const bufferViews: object[] = [];
|
|
12
|
+
const accessors: object[] = [];
|
|
13
|
+
const pushView = (data: ArrayBufferView, target: number) => {
|
|
14
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
15
|
+
const pad = (4 - (bytes.byteLength % 4)) % 4;
|
|
16
|
+
bufferViews.push({ buffer: 0, byteOffset: byteLength, byteLength: bytes.byteLength, target });
|
|
17
|
+
chunks.push(bytes, new Uint8Array(pad));
|
|
18
|
+
byteLength += bytes.byteLength + pad;
|
|
19
|
+
return bufferViews.length - 1;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const meshes: object[] = [];
|
|
23
|
+
const materials: object[] = [];
|
|
24
|
+
const nodes: object[] = [{ name: 'avatar', children: [] as number[] }];
|
|
25
|
+
|
|
26
|
+
parts.forEach(({ role, geometry, color }, i) => {
|
|
27
|
+
const pos = geometry.attributes.position.array as Float32Array;
|
|
28
|
+
const nor = geometry.attributes.normal.array as Float32Array;
|
|
29
|
+
const idx = geometry.index!.array as Uint16Array | Uint32Array;
|
|
30
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
31
|
+
for (let j = 0; j < pos.length; j++) {
|
|
32
|
+
const k = j % 3;
|
|
33
|
+
if (pos[j] < min[k]) min[k] = pos[j];
|
|
34
|
+
if (pos[j] > max[k]) max[k] = pos[j];
|
|
35
|
+
}
|
|
36
|
+
const count = pos.length / 3;
|
|
37
|
+
accessors.push({ bufferView: pushView(pos, 34962), componentType: 5126, count, type: 'VEC3', min, max });
|
|
38
|
+
accessors.push({ bufferView: pushView(nor, 34962), componentType: 5126, count, type: 'VEC3' });
|
|
39
|
+
accessors.push({
|
|
40
|
+
bufferView: pushView(idx, 34963),
|
|
41
|
+
componentType: idx instanceof Uint32Array ? 5125 : 5123,
|
|
42
|
+
count: idx.length,
|
|
43
|
+
type: 'SCALAR',
|
|
44
|
+
});
|
|
45
|
+
const lin = new Color(color); // three's Color stores linear values when built from hex
|
|
46
|
+
materials.push({
|
|
47
|
+
name: role,
|
|
48
|
+
pbrMetallicRoughness: { baseColorFactor: [lin.r, lin.g, lin.b, 1], metallicFactor: 0, roughnessFactor: 0.85 },
|
|
49
|
+
doubleSided: true,
|
|
50
|
+
});
|
|
51
|
+
meshes.push({ name: role, primitives: [{ attributes: { POSITION: i * 3, NORMAL: i * 3 + 1 }, indices: i * 3 + 2, material: i }] });
|
|
52
|
+
nodes.push({ name: role, mesh: i });
|
|
53
|
+
(nodes[0] as { children: number[] }).children.push(i + 1);
|
|
54
|
+
geometry.dispose();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const json = {
|
|
58
|
+
asset: { version: '2.0', generator: 'humanforge' },
|
|
59
|
+
scene: 0,
|
|
60
|
+
scenes: [{ nodes: [0] }],
|
|
61
|
+
nodes, meshes, materials, accessors, bufferViews,
|
|
62
|
+
buffers: [{ byteLength }],
|
|
63
|
+
};
|
|
64
|
+
// JSON is pure ASCII here, so a byte-per-char copy avoids needing TextEncoder.
|
|
65
|
+
const str = JSON.stringify(json);
|
|
66
|
+
const jsonBytes = new Uint8Array(str.length);
|
|
67
|
+
for (let i = 0; i < str.length; i++) jsonBytes[i] = str.charCodeAt(i) & 0x7f;
|
|
68
|
+
const jsonPad = (4 - (jsonBytes.length % 4)) % 4;
|
|
69
|
+
const total = 12 + 8 + jsonBytes.length + jsonPad + 8 + byteLength;
|
|
70
|
+
const out = new Uint8Array(total);
|
|
71
|
+
const dv = new DataView(out.buffer);
|
|
72
|
+
dv.setUint32(0, 0x46546c67, true); // 'glTF'
|
|
73
|
+
dv.setUint32(4, 2, true);
|
|
74
|
+
dv.setUint32(8, total, true);
|
|
75
|
+
dv.setUint32(12, jsonBytes.length + jsonPad, true);
|
|
76
|
+
dv.setUint32(16, 0x4e4f534a, true); // 'JSON'
|
|
77
|
+
out.set(jsonBytes, 20);
|
|
78
|
+
out.fill(0x20, 20 + jsonBytes.length, 20 + jsonBytes.length + jsonPad);
|
|
79
|
+
let o = 20 + jsonBytes.length + jsonPad;
|
|
80
|
+
dv.setUint32(o, byteLength, true);
|
|
81
|
+
dv.setUint32(o + 4, 0x004e4942, true); // 'BIN\0'
|
|
82
|
+
o += 8;
|
|
83
|
+
for (const c of chunks) {
|
|
84
|
+
out.set(c, o);
|
|
85
|
+
o += c.byteLength;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
91
|
+
/** Base64 without Buffer/btoa (React Native safe) — e.g. for expo-file-system writes. */
|
|
92
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
93
|
+
let s = '';
|
|
94
|
+
let i = 0;
|
|
95
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
96
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
97
|
+
s += B64[n >> 18] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + B64[n & 63];
|
|
98
|
+
}
|
|
99
|
+
const rest = bytes.length - i;
|
|
100
|
+
if (rest === 1) {
|
|
101
|
+
const n = bytes[i] << 16;
|
|
102
|
+
s += B64[n >> 18] + B64[(n >> 12) & 63] + '==';
|
|
103
|
+
} else if (rest === 2) {
|
|
104
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
105
|
+
s += B64[n >> 18] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + '=';
|
|
106
|
+
}
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Renderer-agnostic stage: lights, camera framing and avatar swapping.
|
|
2
|
+
// Used by both the React Native <AvatarView> and the web demo.
|
|
3
|
+
import { AmbientLight, Color, DirectionalLight, Group, HemisphereLight, PerspectiveCamera, Scene, Vector3 } from 'three';
|
|
4
|
+
import { AvatarConfig } from './config';
|
|
5
|
+
import { AvatarMetrics, BuildOptions, buildAvatar, disposeAvatar } from './build';
|
|
6
|
+
|
|
7
|
+
export type Framing = 'face' | 'bust' | 'body';
|
|
8
|
+
|
|
9
|
+
export class AvatarStage {
|
|
10
|
+
readonly scene = new Scene();
|
|
11
|
+
readonly camera = new PerspectiveCamera(28, 1, 0.05, 50);
|
|
12
|
+
readonly pivot = new Group();
|
|
13
|
+
avatar: Group | null = null;
|
|
14
|
+
framing: Framing = 'body';
|
|
15
|
+
yaw = 0;
|
|
16
|
+
/** Extra zoom multiplier (1 = default framing). */
|
|
17
|
+
zoom = 1;
|
|
18
|
+
|
|
19
|
+
constructor(background: string | null = '#eef1f5') {
|
|
20
|
+
if (background) this.scene.background = new Color(background);
|
|
21
|
+
this.scene.add(new HemisphereLight('#ffffff', '#8a8f99', 1.6));
|
|
22
|
+
this.scene.add(new AmbientLight('#ffffff', 0.35));
|
|
23
|
+
const key = new DirectionalLight('#ffffff', 1.6);
|
|
24
|
+
key.position.set(1.5, 2.5, 3);
|
|
25
|
+
const rim = new DirectionalLight('#cfe0ff', 0.7);
|
|
26
|
+
rim.position.set(-2, 1.5, -2);
|
|
27
|
+
this.scene.add(key, rim, this.pivot);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setAvatar(config: Partial<AvatarConfig>, opts?: BuildOptions) {
|
|
31
|
+
if (this.avatar) {
|
|
32
|
+
this.pivot.remove(this.avatar);
|
|
33
|
+
disposeAvatar(this.avatar);
|
|
34
|
+
}
|
|
35
|
+
this.avatar = buildAvatar(config, opts);
|
|
36
|
+
this.pivot.add(this.avatar);
|
|
37
|
+
this.updateCamera();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get metrics(): AvatarMetrics | undefined {
|
|
41
|
+
return this.avatar?.userData.metrics;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setSize(width: number, height: number) {
|
|
45
|
+
this.camera.aspect = width / Math.max(1, height);
|
|
46
|
+
this.camera.updateProjectionMatrix();
|
|
47
|
+
this.updateCamera();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setFraming(f: Framing) {
|
|
51
|
+
this.framing = f;
|
|
52
|
+
this.updateCamera();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setYaw(yaw: number) {
|
|
56
|
+
this.yaw = yaw;
|
|
57
|
+
this.pivot.rotation.y = yaw;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
updateCamera() {
|
|
61
|
+
const m = this.metrics ?? { headY: 1.55, height: 1.7 };
|
|
62
|
+
const t = Math.tan((this.camera.fov * Math.PI) / 360);
|
|
63
|
+
// distance that fits a box of h x w (metres) in view, whichever side is limiting
|
|
64
|
+
const fit = (h: number, w: number) => Math.max(h / 2 / t, w / 2 / t / this.camera.aspect);
|
|
65
|
+
let target: Vector3, dist: number;
|
|
66
|
+
if (this.framing === 'face') {
|
|
67
|
+
target = new Vector3(0, m.headY + 0.005, 0);
|
|
68
|
+
dist = fit(0.42, 0.4);
|
|
69
|
+
} else if (this.framing === 'bust') {
|
|
70
|
+
target = new Vector3(0, m.headY - 0.1, 0);
|
|
71
|
+
dist = fit(0.7, 0.62);
|
|
72
|
+
} else {
|
|
73
|
+
target = new Vector3(0, m.height / 2, 0);
|
|
74
|
+
dist = fit(m.height * 1.1, 0.75);
|
|
75
|
+
}
|
|
76
|
+
dist /= this.zoom;
|
|
77
|
+
this.camera.position.set(0, target.y + dist * 0.05, dist);
|
|
78
|
+
this.camera.lookAt(target);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dispose() {
|
|
82
|
+
if (this.avatar) disposeAvatar(this.avatar);
|
|
83
|
+
this.avatar = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Editor schema: drives both the React Native creator and the web demo, so adding
|
|
2
|
+
// a field here is all it takes to expose it in every UI.
|
|
3
|
+
import {
|
|
4
|
+
AvatarConfig, BOTTOMS, BROWS, CLOTH_COLORS, EYE_COLORS, FACIAL_HAIR, GLASSES, HAIR_COLORS,
|
|
5
|
+
HAIR_STYLES, HEADWEAR, LIP_COLORS, MOUTHS, NOSES, SHOES, SKIN_TONES, TOPS,
|
|
6
|
+
} from './config';
|
|
7
|
+
|
|
8
|
+
export type Control =
|
|
9
|
+
| { key: keyof AvatarConfig; label: string; type: 'enum'; options: readonly string[] }
|
|
10
|
+
| { key: keyof AvatarConfig; label: string; type: 'color'; palette: readonly string[] }
|
|
11
|
+
| { key: keyof AvatarConfig; label: string; type: 'slider' };
|
|
12
|
+
|
|
13
|
+
export interface Section {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Camera framing that best shows this section. */
|
|
17
|
+
focus: 'face' | 'body';
|
|
18
|
+
controls: Control[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const EDITOR_SCHEMA: Section[] = [
|
|
22
|
+
{
|
|
23
|
+
id: 'face', label: 'Face', focus: 'face', controls: [
|
|
24
|
+
{ key: 'skin', label: 'Skin tone', type: 'color', palette: SKIN_TONES },
|
|
25
|
+
{ key: 'headWidth', label: 'Face width', type: 'slider' },
|
|
26
|
+
{ key: 'jaw', label: 'Jaw', type: 'slider' },
|
|
27
|
+
{ key: 'chin', label: 'Chin', type: 'slider' },
|
|
28
|
+
{ key: 'earSize', label: 'Ears', type: 'slider' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'eyes', label: 'Eyes', focus: 'face', controls: [
|
|
33
|
+
{ key: 'eyeColor', label: 'Eye color', type: 'color', palette: EYE_COLORS },
|
|
34
|
+
{ key: 'eyeSize', label: 'Eye size', type: 'slider' },
|
|
35
|
+
{ key: 'eyeSpacing', label: 'Eye spacing', type: 'slider' },
|
|
36
|
+
{ key: 'brows', label: 'Brows', type: 'enum', options: BROWS },
|
|
37
|
+
{ key: 'glasses', label: 'Glasses', type: 'enum', options: GLASSES },
|
|
38
|
+
{ key: 'glassesColor', label: 'Frame color', type: 'color', palette: ['#1d1d1f', '#8b6b4e', '#c9a227', '#5a5a5a', '#b23a3a', '#3f6fb5'] },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'mouth', label: 'Nose & Mouth', focus: 'face', controls: [
|
|
43
|
+
{ key: 'nose', label: 'Nose', type: 'enum', options: NOSES },
|
|
44
|
+
{ key: 'noseSize', label: 'Nose size', type: 'slider' },
|
|
45
|
+
{ key: 'mouth', label: 'Mouth', type: 'enum', options: MOUTHS },
|
|
46
|
+
{ key: 'mouthWidth', label: 'Mouth width', type: 'slider' },
|
|
47
|
+
{ key: 'lipColor', label: 'Lip color', type: 'color', palette: LIP_COLORS },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'hair', label: 'Hair', focus: 'face', controls: [
|
|
52
|
+
{ key: 'hair', label: 'Style', type: 'enum', options: HAIR_STYLES },
|
|
53
|
+
{ key: 'hairColor', label: 'Color', type: 'color', palette: HAIR_COLORS },
|
|
54
|
+
{ key: 'facialHair', label: 'Facial hair', type: 'enum', options: FACIAL_HAIR },
|
|
55
|
+
{ key: 'headwear', label: 'Headwear', type: 'enum', options: HEADWEAR },
|
|
56
|
+
{ key: 'headwearColor', label: 'Headwear color', type: 'color', palette: CLOTH_COLORS },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'body', label: 'Body', focus: 'body', controls: [
|
|
61
|
+
{ key: 'height', label: 'Height', type: 'slider' },
|
|
62
|
+
{ key: 'build', label: 'Build', type: 'slider' },
|
|
63
|
+
{ key: 'chest', label: 'Chest', type: 'slider' },
|
|
64
|
+
{ key: 'hips', label: 'Hips', type: 'slider' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'outfit', label: 'Outfit', focus: 'body', controls: [
|
|
69
|
+
{ key: 'top', label: 'Top', type: 'enum', options: TOPS },
|
|
70
|
+
{ key: 'topColor', label: 'Top color', type: 'color', palette: CLOTH_COLORS },
|
|
71
|
+
{ key: 'bottom', label: 'Bottom', type: 'enum', options: BOTTOMS },
|
|
72
|
+
{ key: 'bottomColor', label: 'Bottom color', type: 'color', palette: CLOTH_COLORS },
|
|
73
|
+
{ key: 'shoes', label: 'Shoes', type: 'enum', options: SHOES },
|
|
74
|
+
{ key: 'shoesColor', label: 'Shoe color', type: 'color', palette: CLOTH_COLORS },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export const prettyLabel = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Optional GLB export (web). Kept out of runtime.ts so React Native bundles never include gltf-transform.
|
|
2
|
+
import type { MeshStandardMaterial, SkinnedMesh } from 'three';
|
|
3
|
+
import type { HumanAvatar } from './runtime';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Export the current avatar (reshaped mesh, Mixamo skeleton, ARKit-named blendshapes, textures)
|
|
7
|
+
* as a compressed binary glTF: sparse morph targets, quantized vertices (KHR_mesh_quantization),
|
|
8
|
+
* JPEG for opaque textures. Web only: GLTFExporter re-encodes textures through a canvas.
|
|
9
|
+
*/
|
|
10
|
+
export async function exportHumanGLB(avatar: HumanAvatar): Promise<Uint8Array> {
|
|
11
|
+
const [{ GLTFExporter }, { WebIO }, { KHRMeshQuantization }, { sparse, quantize, dedup, prune }] = await Promise.all([
|
|
12
|
+
import('three/examples/jsm/exporters/GLTFExporter.js'),
|
|
13
|
+
import('@gltf-transform/core'),
|
|
14
|
+
import('@gltf-transform/extensions'),
|
|
15
|
+
import('@gltf-transform/functions'),
|
|
16
|
+
]);
|
|
17
|
+
const names = avatar.expressionNames;
|
|
18
|
+
avatar.group.traverse((o) => {
|
|
19
|
+
const m = o as SkinnedMesh;
|
|
20
|
+
if (!m.isMesh) return;
|
|
21
|
+
if (m.morphTargetInfluences) m.morphTargetDictionary = Object.fromEntries(names.map((n, i) => [n, i]));
|
|
22
|
+
const mat = m.material as MeshStandardMaterial;
|
|
23
|
+
const alpha = mat.transparent || mat.alphaTest > 0;
|
|
24
|
+
for (const t of [mat.map, mat.normalMap]) if (t) t.userData.mimeType = alpha ? 'image/png' : 'image/jpeg';
|
|
25
|
+
});
|
|
26
|
+
const raw = (await new GLTFExporter().parseAsync(avatar.group, { binary: true, onlyVisible: true })) as ArrayBuffer;
|
|
27
|
+
const io = new WebIO().registerExtensions([KHRMeshQuantization]);
|
|
28
|
+
const doc = await io.readBinary(new Uint8Array(raw));
|
|
29
|
+
// quantize first: it rewrites accessors, which would drop the sparse flag set by sparse()
|
|
30
|
+
await doc.transform(dedup(), prune(), quantize({ quantizePosition: 14, quantizeNormal: 10, quantizeTexcoord: 12 }), sparse({ ratio: 0.3 }));
|
|
31
|
+
return io.writeBinary(doc);
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by tools/build-facemap.mts from MediaPipe's canonical face model (Apache-2.0)
|
|
2
|
+
// and the CC0 MakeHuman base mesh. Do not edit by hand.
|
|
3
|
+
|
|
4
|
+
export const FACE_MAP = {
|
|
5
|
+
/** MakeHuman vertex index for each of the 468 MediaPipe face-mesh landmarks. */
|
|
6
|
+
vertex: Uint16Array.from([455,297,317,163,294,291,131,1120,136,220,3750,466,509,554,523,494,513,495,5337,302,303,719,5209,141,140,76,5248,47,50,6774,1068,183,5159,1119,5328,196,5067,456,507,5354,375,505,533,3709,296,293,14,5253,5095,5096,5304,290,152,91,3743,167,4873,5144,5240,382,351,3730,528,172,323,174,177,3746,232,230,190,261,461,452,448,396,528,420,528,276,5350,549,556,5336,489,512,487,517,533,516,475,438,372,270,307,525,525,309,344,330,5251,5147,5055,3745,231,176,371,209,236,3748,77,901,5132,31,182,5057,5178,213,192,5059,5058,5086,125,5324,4883,301,5088,5202,5100,340,1119,280,910,81,289,5227,5298,5177,5299,263,5213,306,203,5091,4886,79,436,5153,5220,5223,5295,234,5225,80,142,5084,194,52,4863,47,1069,1068,718,4884,5072,5135,392,5138,133,5170,5210,5214,737,81,5064,5211,5222,5206,515,481,482,483,5335,525,524,524,5145,5302,214,111,99,542,5290,121,721,164,5062,5063,281,5162,724,723,5142,288,730,5332,5321,5286,5157,5093,5163,5160,5155,5153,5173,735,900,5254,5050,287,292,104,58,54,0,9,137,5182,905,908,5249,5303,5306,5307,5201,383,199,5053,300,295,381,301,304,143,909,117,1120,1067,6941,7812,7068,7442,11820,6919,6918,7811,11856,6831,6834,13370,7760,6959,11773,7811,11932,6972,11683,7215,7256,11956,7135,7254,7278,10377,7063,7061,6798,11861,11710,11711,11909,7059,6930,6875,10411,6944,11491,11758,11849,7142,7112,10398,7273,6948,7085,6950,6953,10414,7005,7003,6966,7030,7220,7212,7208,7156,7273,7180,7273,7045,11952,7292,7298,11940,7244,7260,7242,7264,7278,7263,7230,7198,7132,7039,7270,7270,7073,7105,7092,11859,11761,11672,10413,7004,6952,7131,6984,7008,10416,6861,7603,11747,6815,6958,11674,11790,6988,6968,11676,11675,11701,6909,11928,11501,7067,11703,11813,11715,7102,7811,7049,7612,6921,7058,11836,11903,11789,11904,7032,11823,7071,6978,11706,11504,6863,7196,11767,11830,11833,11901,6864,6920,11699,6969,6836,11481,6831,7761,7760,7441,11502,11749,7152,11752,11783,11821,11824,7457,6865,11680,11832,11817,7262,7236,7237,7238,11939,7270,7269,7269,11759,11907,6989,6895,6883,7285,11896,6905,7444,11679,7050,7446,11756,7057,7452,11936,11925,11892,11771,11708,11776,11774,11769,11767,11785,7455,7602,11862,11668,7056,7060,6888,6842,6838,6784,6793,6915,11794,6959,7610,11857,11908,11911,11912,11812,7143,6975,11671,7066,7062,7141,7067,7069,6921,7611,6901,7812,7759]),
|
|
7
|
+
/** Confidence of each correspondence (0.2..1). */
|
|
8
|
+
weight: Float32Array.from([1,1,1,0.85,0.95,0.83,0.95,1,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.95,1,1,1,1,1,1,1,1,1,1,1,1,0.99,1,1,1,1,1,1,1,1,1,0.96,1,1,0.98,0.82,0.99,1,0.52,1,1,1,1,0.94,1,0.93,0.94,0.83,0.93,1,1,1,1,1,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.8,1,1,1,1,1,1,1,0.99,1,0.82,1,1,1,1,1,1,1,1,1,1,1,1,0.89,1,1,1,1,1,1,0.95,1,1,1,1,1,1,1,1,0.56,1,1,1,0.9,0.88,1,1,1,1,1,1,1,1,1,0.61,0.93,1,1,1,1,1,1,1,1,1,1,1,1,1,0.85,1,1,1,1,1,1,1,1,0.94,1,1,1,1,0.93,1,1,1,1,1,0.97,1,1,1,1,1,1,1,1,1,0.86,1,1,0.79,1,0.92,1,1,1,0.97,1,1,1,0.99,1,1,1,1,1,1,1,0.69,1,0.63,1,1,1,1,1,1,0.88,0.94,1,1,1,1,1,1,1,1,1,1,0.21,1,1,1,1,1,1,1,1,1,1,1,1,1,0.85,1,1,1,1,1,1,1,1,1,0.95,1,1,1,1,1,1,1,1,1,1,1,1,0.99,1,1,1,1,1,1,1,1,1,0.96,1,1,0.97,0.83,0.99,1,0.51,1,1,1,1,0.94,1,0.93,0.93,0.82,0.93,1,1,1,1,1,0.98,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.79,1,1,1,1,1,1,0.99,1,0.82,1,1,1,1,1,1,1,1,1,1,1,1,0.89,1,1,1,1,1,1,0.95,1,1,1,1,1,1,1,1,0.57,1,1,1,0.91,0.87,1,1,1,1,1,1,1,1,1,0.6,0.92,1,1,1,1,1,1,1,1,1,1,1,0.84,1,1,1,1,1,1,0.94,1,1,1,0.94,1,1,1,1,1,0.97,1,1,1,1,1,1,1,1,1,0.86,1,1,1,1,0.96,1,1,1,0.98,1,1,1,1,1,1,1,0.7,1,0.63,1,1,1,1,1,1,0.88,0.95,1,1,1,1,1,1,1,1,1,1,0.21,1,1,1,1,1,1,1,1,1,1,1,1,1]),
|
|
9
|
+
/** 1 for silhouette landmarks (depth unreliable). */
|
|
10
|
+
contour: Uint8Array.from([0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0]),
|
|
11
|
+
/** Mean MediaPipe landmarks over 143 public-domain portraits (metres), similarity-aligned to the neutral MakeHuman face at `vertex`. */
|
|
12
|
+
reference: Float32Array.from([0.00001,1.49638,0.15177,-0.00019,1.51282,0.16635,-0.00029,1.50717,0.153,-0.00438,1.5301,0.15818,-0.00014,1.51847,0.16773,-0.0001,1.52551,0.1652,-0.00024,1.54173,0.15053,-0.03443,1.54281,0.12865,-0.00036,1.55338,0.14638,-0.00026,1.56034,0.14706,-0.00033,1.58841,0.14004,0.00001,1.4948,0.15114,0.00005,1.4934,0.1498,-0.00006,1.49228,0.14772,-0.00016,1.48486,0.14565,-0.00019,1.48305,0.14672,-0.0001,1.48069,0.14815,-0.00019,1.4779,0.14768,-0.00038,1.47103,0.14395,-0.00014,1.50984,0.16387,-0.00551,1.50966,0.15669,-0.05452,1.56015,0.10434,-0.02065,1.53929,0.13382,-0.02555,1.5389,0.13362,-0.03045,1.5388,0.13242,-0.03679,1.54078,0.128,-0.01662,1.54001,0.13352,-0.02895,1.55257,0.13503,-0.02303,1.55207,0.13503,-0.03418,1.55133,0.13364,-0.03742,1.54909,0.13174,-0.04141,1.53726,0.12576,-0.02011,1.46273,0.1374,-0.03637,1.54366,0.12698,-0.05657,1.53691,0.10299,-0.04736,1.53968,0.1207,-0.02646,1.51789,0.14062,-0.00753,1.49754,0.15116,-0.00718,1.49386,0.14882,-0.01445,1.49669,0.14757,-0.01946,1.49543,0.1423,-0.01308,1.494,0.14594,-0.0178,1.49352,0.14119,-0.02645,1.48514,0.13302,-0.00448,1.51293,0.16597,-0.00489,1.51859,0.16713,-0.04419,1.55214,0.13111,-0.01498,1.53056,0.14065,-0.01668,1.51528,0.15265,-0.01684,1.5182,0.15074,-0.04197,1.5186,0.13383,-0.00462,1.5249,0.16354,-0.0323,1.558,0.14005,-0.03933,1.55603,0.13624,-0.04935,1.57036,0.11405,-0.01037,1.55388,0.14466,-0.01804,1.54978,0.13356,-0.03081,1.49177,0.13195,-0.05209,1.48408,0.09169,-0.01329,1.51164,0.15083,-0.00914,1.5099,0.15169,-0.02624,1.49248,0.13151,-0.02476,1.49237,0.13276,-0.04211,1.55897,0.1335,-0.01718,1.5133,0.14981,-0.02275,1.55764,0.14292,-0.02371,1.56201,0.14393,-0.02905,1.58462,0.13325,-0.04561,1.5652,0.12482,-0.02603,1.5742,0.13879,-0.04721,1.55413,0.12671,-0.0509,1.55698,0.11593,-0.00746,1.49561,0.15042,-0.01372,1.49525,0.14682,-0.01861,1.4946,0.14207,-0.01219,1.51069,0.15006,-0.02547,1.49246,0.13221,-0.02239,1.48901,0.13532,-0.02406,1.49215,0.13295,-0.01039,1.513,0.15824,-0.01703,1.49287,0.1405,-0.01244,1.49309,0.14401,-0.00677,1.49276,0.14675,-0.00839,1.4716,0.14338,-0.00766,1.47865,0.14689,-0.00751,1.48147,0.14712,-0.00722,1.48362,0.1456,-0.00669,1.48539,0.14461,-0.01676,1.4877,0.13876,-0.01764,1.4872,0.13958,-0.01863,1.48606,0.14017,-0.01937,1.48445,0.13925,-0.0243,1.49997,0.14104,-0.05734,1.51073,0.08329,-0.00034,1.50797,0.15697,-0.02014,1.48929,0.1351,-0.02129,1.48921,0.13551,-0.00766,1.50789,0.1516,-0.01586,1.51013,0.14608,-0.00837,1.50882,0.1519,-0.02043,1.52783,0.13897,-0.02924,1.52469,0.13788,-0.01802,1.51589,0.14784,-0.04119,1.57872,0.12421,-0.03761,1.57097,0.13218,-0.03414,1.56195,0.13891,-0.02187,1.47971,0.13636,-0.01204,1.56075,0.14645,-0.01392,1.5752,0.14247,-0.0158,1.58766,0.13864,-0.03439,1.53935,0.13022,-0.04694,1.53357,0.12305,-0.01413,1.541,0.13281,-0.0421,1.54621,0.12769,-0.0114,1.53365,0.14248,-0.01333,1.51679,0.15784,-0.05255,1.52898,0.11762,-0.04347,1.53082,0.12767,-0.0371,1.52958,0.13236,-0.0274,1.53033,0.13487,-0.02017,1.53226,0.13584,-0.01503,1.53436,0.13735,-0.00494,1.54027,0.1484,-0.05263,1.51858,0.11937,-0.04608,1.54644,0.12551,-0.00283,1.51005,0.16355,-0.01498,1.52551,0.14323,-0.05844,1.53558,0.08615,-0.01122,1.5368,0.13816,-0.01878,1.51534,0.1423,-0.03906,1.54315,0.12609,-0.01328,1.52039,0.15552,-0.05553,1.49771,0.08625,-0.01431,1.54249,0.13135,-0.00898,1.52325,0.1605,-0.04171,1.4747,0.11755,-0.04078,1.46661,0.10976,-0.05684,1.51408,0.10138,-0.04816,1.48129,0.11174,-0.05393,1.54767,0.10797,-0.02085,1.45618,0.13528,-0.00246,1.50811,0.15664,-0.01982,1.52197,0.14147,-0.05156,1.53836,0.11565,-0.02974,1.54183,0.13165,-0.02564,1.54179,0.13284,-0.02319,1.48816,0.13452,-0.05244,1.50726,0.1181,-0.01153,1.44779,0.13519,-0.02678,1.45507,0.1239,-0.0332,1.45988,0.11743,-0.00035,1.57529,0.14372,-0.00082,1.44674,0.13683,-0.02161,1.54216,0.13308,-0.01783,1.54242,0.13238,-0.01534,1.54231,0.13145,-0.04966,1.5471,0.12027,-0.0189,1.54648,0.13277,-0.02309,1.54827,0.13364,-0.02728,1.54856,0.13346,-0.03131,1.54761,0.13232,-0.0338,1.54615,0.13073,-0.05732,1.54895,0.09405,-0.03248,1.54224,0.13006,-0.00025,1.50332,0.15136,-0.0185,1.50264,0.1452,-0.01311,1.51243,0.15301,-0.0081,1.50364,0.15073,-0.00031,1.54757,0.14637,-0.03465,1.46757,0.12256,-0.028,1.46191,0.12853,-0.01163,1.45263,0.14089,-0.04682,1.47381,0.10057,-0.01577,1.54406,0.13197,-0.00812,1.53221,0.15011,-0.00068,1.45132,0.14223,-0.01986,1.45089,0.13059,-0.05546,1.50202,0.10266,-0.01217,1.48626,0.14202,-0.01303,1.48524,0.14292,-0.01371,1.48337,0.14371,-0.01423,1.48076,0.14315,-0.01607,1.47481,0.14029,-0.02171,1.49305,0.13674,-0.02261,1.4935,0.13652,-0.0233,1.49402,0.1366,-0.02881,1.49649,0.13621,-0.04656,1.506,0.12862,-0.00808,1.53719,0.14505,-0.01076,1.54626,0.13517,-0.01383,1.54575,0.13296,-0.02084,1.49249,0.13631,-0.04713,1.49131,0.12086,-0.00629,1.54645,0.14249,-0.01816,1.46916,0.13858,-0.00013,1.53098,0.15997,-0.00462,1.53501,0.15388,-0.00028,1.53605,0.15507,-0.01179,1.52445,0.15047,-0.00065,1.4576,0.14448,-0.00048,1.46475,0.14406,-0.00965,1.46565,0.14271,-0.02981,1.48178,0.13095,-0.02309,1.51123,0.14085,-0.02444,1.47457,0.13434,-0.03531,1.51161,0.1386,-0.0284,1.50583,0.13904,-0.03993,1.50279,0.13386,-0.01112,1.45867,0.1429,-0.01504,1.52178,0.14671,-0.03355,1.47566,0.12731,-0.0268,1.46867,0.1316,-0.0345,1.48954,0.1306,-0.0514,1.49735,0.11653,-0.04046,1.48671,0.12659,-0.05291,1.49069,0.10506,-0.03313,1.49982,0.13586,-0.01137,1.52886,0.14633,-0.01182,1.51419,0.15925,-0.01528,1.51331,0.15349,-0.00935,1.51782,0.16283,-0.01462,1.55062,0.13638,-0.02286,1.55379,0.13735,-0.03022,1.55441,0.13707,-0.03625,1.55306,0.13507,-0.04027,1.55036,0.13201,-0.0426,1.54154,0.12425,-0.05729,1.52555,0.10183,-0.03843,1.53562,0.12831,-0.03326,1.53457,0.13147,-0.0265,1.53481,0.13352,-0.02024,1.53587,0.13418,-0.01554,1.53732,0.13472,-0.01243,1.53866,0.13499,-0.05802,1.52317,0.08295,-0.0153,1.5121,0.15091,-0.00818,1.52765,0.15453,-0.00813,1.51373,0.16338,-0.00561,1.5109,0.16143,-0.0083,1.51272,0.16088,-0.01416,1.51052,0.1492,-0.00459,1.5104,0.16281,-0.00423,1.50884,0.15664,-0.01263,1.54229,0.13236,-0.01002,1.54134,0.13573,-0.0084,1.54059,0.1398,-0.03524,1.54488,0.12914,-0.03927,1.54637,0.12892,0.00398,1.53014,0.15826,0.03427,1.5429,0.12878,0.00503,1.50961,0.1565,0.05516,1.56088,0.10429,0.02069,1.53935,0.13413,0.02552,1.53894,0.13389,0.03049,1.53892,0.13258,0.03674,1.54093,0.12813,0.01638,1.54007,0.13373,0.02896,1.55285,0.13502,0.02294,1.55222,0.13499,0.03417,1.55161,0.13348,0.03745,1.54935,0.13152,0.04156,1.53749,0.12568,0.01934,1.46224,0.13715,0.03614,1.54388,0.12709,0.05706,1.53725,0.10276,0.04773,1.53986,0.12048,0.02642,1.51789,0.14031,0.00783,1.49756,0.15105,0.00748,1.49385,0.14872,0.01507,1.49665,0.14745,0.02026,1.49535,0.14217,0.01368,1.49383,0.14586,0.01857,1.49349,0.14095,0.02652,1.4848,0.13274,0.00411,1.51292,0.16582,0.00468,1.51847,0.16704,0.04439,1.55236,0.1309,0.0147,1.53065,0.14073,0.01682,1.5153,0.15235,0.01692,1.51819,0.15049,0.04226,1.51873,0.13352,0.00429,1.52503,0.16359,0.03207,1.5584,0.13967,0.0393,1.55631,0.13596,0.04979,1.57117,0.11392,0.00986,1.55406,0.14458,0.01781,1.54995,0.13361,0.03112,1.49164,0.13165,0.05305,1.48354,0.09164,0.01297,1.51167,0.15055,0.00873,1.50994,0.15126,0.02677,1.49229,0.13134,0.02547,1.49219,0.13247,0.04206,1.55923,0.1332,0.01739,1.51333,0.14943,0.02237,1.55793,0.1426,0.02334,1.56233,0.14354,0.02887,1.58523,0.13307,0.04563,1.5658,0.12452,0.02552,1.57465,0.13846,0.04742,1.55434,0.1264,0.05122,1.55751,0.11571,0.0078,1.49556,0.15032,0.01436,1.49518,0.14671,0.01946,1.49444,0.1419,0.01193,1.51077,0.14961,0.02628,1.49227,0.13193,0.02305,1.48867,0.13503,0.02489,1.49197,0.13266,0.01031,1.5131,0.15814,0.01771,1.49285,0.14027,0.01296,1.49296,0.1439,0.00703,1.49264,0.14672,0.00775,1.47165,0.14314,0.00764,1.47858,0.14686,0.00751,1.48133,0.14714,0.00716,1.48364,0.14564,0.00658,1.48514,0.14469,0.01725,1.48751,0.13859,0.01816,1.48701,0.13933,0.01917,1.48573,0.13989,0.01988,1.48415,0.13896,0.02437,1.49993,0.14062,0.05806,1.51063,0.08336,0.02082,1.4891,0.13494,0.02184,1.489,0.13526,0.00719,1.50792,0.15133,0.0157,1.51005,0.14553,0.00792,1.50879,0.15155,0.02029,1.52804,0.13899,0.02925,1.52484,0.13776,0.01814,1.51592,0.14751,0.04139,1.57953,0.12397,0.03738,1.57157,0.13182,0.03383,1.56229,0.13851,0.02166,1.4794,0.13613,0.01155,1.56097,0.14622,0.01336,1.57544,0.14229,0.01543,1.58803,0.13836,0.0345,1.53958,0.13035,0.04725,1.53366,0.12283,0.01378,1.5409,0.13312,0.04219,1.54635,0.12753,0.01095,1.53373,0.14264,0.01325,1.51675,0.15763,0.05318,1.52918,0.11737,0.04368,1.53102,0.12756,0.03715,1.52977,0.13231,0.02734,1.53052,0.13495,0.01995,1.53237,0.13601,0.01463,1.5345,0.13749,0.00439,1.54037,0.14848,0.05321,1.51853,0.11919,0.04635,1.54661,0.12535,0.00251,1.5101,0.16352,0.01479,1.52558,0.14321,0.05885,1.5361,0.08626,0.01074,1.53693,0.13834,0.01858,1.51532,0.1419,0.0389,1.54333,0.12608,0.01336,1.52042,0.15534,0.05651,1.49729,0.08617,0.01386,1.54238,0.1317,0.00884,1.52325,0.16045,0.04201,1.4738,0.11729,0.04121,1.46558,0.10969,0.05751,1.51405,0.1012,0.04876,1.48063,0.11142,0.05435,1.54815,0.10778,0.02005,1.45552,0.13515,0.00179,1.5082,0.15656,0.01977,1.52202,0.1413,0.05207,1.53855,0.1154,0.02948,1.542,0.13183,0.02554,1.54193,0.1331,0.02388,1.48801,0.13429,0.05324,1.5071,0.11781,0.01005,1.44734,0.13523,0.0261,1.45397,0.12392,0.03306,1.45875,0.11744,0.02138,1.54222,0.13335,0.01759,1.54239,0.13272,0.01505,1.54231,0.13182,0.05025,1.54733,0.12017,0.01886,1.54647,0.13301,0.02325,1.54835,0.13378,0.02754,1.54867,0.13357,0.03135,1.54781,0.13237,0.03383,1.54628,0.13075,0.05782,1.54971,0.09404,0.03224,1.54235,0.13022,0.01844,1.50263,0.14487,0.01284,1.51245,0.15266,0.0078,1.50364,0.15057,0.03436,1.46673,0.12228,0.02753,1.46093,0.12831,0.01044,1.45219,0.14086,0.04758,1.473,0.10065,0.01554,1.54406,0.13235,0.00766,1.53223,0.15016,0.01874,1.45007,0.13069,0.05637,1.50162,0.10243,0.01247,1.48616,0.14199,0.01322,1.48497,0.14284,0.01405,1.48303,0.14367,0.01441,1.48063,0.14304,0.01573,1.47465,0.14015,0.02262,1.49283,0.13652,0.02336,1.49341,0.13638,0.02411,1.49385,0.1365,0.02912,1.49627,0.13586,0.04719,1.50585,0.12815,0.00747,1.53729,0.14516,0.01014,1.5463,0.13521,0.0134,1.54578,0.13321,0.02163,1.49242,0.13609,0.04762,1.49086,0.12041,0.00561,1.54648,0.14253,0.01765,1.46884,0.13835,0.0042,1.53503,0.15387,0.01161,1.52458,0.15035,0.00883,1.46538,0.14257,0.02988,1.48136,0.13063,0.02306,1.5112,0.14042,0.02423,1.47426,0.13401,0.03556,1.51152,0.13808,0.02863,1.5058,0.13855,0.04033,1.50267,0.13331,0.01013,1.45838,0.14283,0.01503,1.52184,0.14654,0.03326,1.47506,0.127,0.02644,1.46809,0.13129,0.03468,1.48942,0.13034,0.05216,1.49706,0.1161,0.04081,1.4862,0.12622,0.05381,1.49021,0.10486,0.03348,1.4997,0.13546,0.01104,1.52896,0.14639,0.01166,1.51427,0.15907,0.01518,1.51325,0.15313,0.00916,1.51783,0.16279,0.01418,1.5507,0.13641,0.02258,1.55403,0.13718,0.03002,1.55465,0.13684,0.03611,1.55332,0.13483,0.04021,1.55062,0.13176,0.04263,1.54166,0.12414,0.05773,1.5257,0.10172,0.03855,1.53576,0.12832,0.03324,1.53477,0.13151,0.02644,1.53492,0.13366,0.02005,1.53598,0.13437,0.01527,1.53738,0.13481,0.01184,1.53865,0.13514,0.05859,1.52345,0.0831,0.01528,1.51214,0.15052,0.0078,1.52779,0.15455,0.00791,1.51373,0.16323,0.00544,1.5109,0.16121,0.0081,1.51263,0.16074,0.01414,1.5105,0.1487,0.00428,1.51044,0.16266,0.00366,1.50869,0.15653,0.01203,1.54224,0.13255,0.00942,1.54133,0.13582,0.0079,1.54059,0.1399,0.03519,1.54503,0.12918,0.03938,1.54654,0.12874]),
|
|
13
|
+
/** Landmarks spanning the face height (forehead, chin), for error normalization. */
|
|
14
|
+
heightPair: [10, 152] as [number, number],
|
|
15
|
+
/** Landmarks around which skin color is sampled (cheeks, forehead). */
|
|
16
|
+
skinLandmarks: [50,280,205,425,151,108,337],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Median sRGB color of each skin texture at the skin landmarks. */
|
|
20
|
+
export const SKIN_COLORS: Record<string, number[]> = {"middleage_african_female":[111,62,42],"middleage_african_male":[107,66,44],"middleage_asian_female":[237,193,168],"middleage_asian_male":[225,177,145],"middleage_caucasian_female":[225,179,150],"middleage_caucasian_male":[223,176,144],"old_african_female":[108,46,25],"old_african_male":[117,65,40],"old_asian_female":[236,176,141],"old_asian_male":[211,141,101],"old_caucasian_female":[224,179,154],"old_caucasian_male":[241,187,150],"young_african_female":[108,64,43],"young_african_male":[91,60,41],"young_asian_female":[243,199,172],"young_asian_male":[217,172,144],"young_caucasian_female":[232,194,169],"young_caucasian_female2":[243,196,173],"young_caucasian_male":[242,189,156],"young_caucasian_male2":[217,172,144]};
|
|
21
|
+
|
|
22
|
+
/** Median sRGB sclera color of the eye textures (white reference for skin matching). */
|
|
23
|
+
export const SCLERA_COLOR: number[] = [168,165,159];
|
|
24
|
+
|
|
25
|
+
/** Median sRGB iris color of each eye texture. */
|
|
26
|
+
export const EYE_COLORS: Record<string, number[]> = {"blue":[47,87,98],"bluegreen":[117,175,159],"brown":[70,14,2],"brownlight":[107,80,38],"deepblue":[56,55,88],"green":[57,93,53],"grey":[134,149,138],"ice":[124,160,135],"lightblue":[101,159,176]};
|